mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 08:22:09 +01:00
- Implement tests for the app service, including course processing from file and URI. - Create mock implementations for CourseParser and Exporter to facilitate testing. - Add tests for HTML cleaner service to validate HTML content cleaning functionality. - Develop tests for the parser service, covering course fetching and loading from files. - Introduce tests for utility functions in the main package, ensuring URI validation and string joining. - Include benchmarks for performance evaluation of key functions.
441 lines
12 KiB
Go
441 lines
12 KiB
Go
// Package services_test provides tests for the parser service.
|
|
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kjanat/articulate-parser/internal/models"
|
|
)
|
|
|
|
// TestNewArticulateParser tests the NewArticulateParser constructor.
|
|
func TestNewArticulateParser(t *testing.T) {
|
|
parser := NewArticulateParser()
|
|
|
|
if parser == nil {
|
|
t.Fatal("NewArticulateParser() returned nil")
|
|
}
|
|
|
|
// Type assertion to check internal structure
|
|
articulateParser, ok := parser.(*ArticulateParser)
|
|
if !ok {
|
|
t.Fatal("NewArticulateParser() returned wrong type")
|
|
}
|
|
|
|
expectedBaseURL := "https://rise.articulate.com"
|
|
if articulateParser.BaseURL != expectedBaseURL {
|
|
t.Errorf("Expected BaseURL '%s', got '%s'", expectedBaseURL, articulateParser.BaseURL)
|
|
}
|
|
|
|
if articulateParser.Client == nil {
|
|
t.Error("Client should not be nil")
|
|
}
|
|
|
|
expectedTimeout := 30 * time.Second
|
|
if articulateParser.Client.Timeout != expectedTimeout {
|
|
t.Errorf("Expected timeout %v, got %v", expectedTimeout, articulateParser.Client.Timeout)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse tests the FetchCourse method.
|
|
func TestArticulateParser_FetchCourse(t *testing.T) {
|
|
// Create a test course object
|
|
testCourse := &models.Course{
|
|
ShareID: "test-share-id",
|
|
Author: "Test Author",
|
|
Course: models.CourseInfo{
|
|
ID: "test-course-id",
|
|
Title: "Test Course",
|
|
Description: "Test Description",
|
|
},
|
|
}
|
|
|
|
// Create test server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Check request path
|
|
expectedPath := "/api/rise-runtime/boot/share/test-share-id"
|
|
if r.URL.Path != expectedPath {
|
|
t.Errorf("Expected path '%s', got '%s'", expectedPath, r.URL.Path)
|
|
}
|
|
|
|
// Check request method
|
|
if r.Method != http.MethodGet {
|
|
t.Errorf("Expected method GET, got %s", r.Method)
|
|
}
|
|
|
|
// Return mock response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(testCourse); err != nil {
|
|
t.Fatalf("Failed to encode test course: %v", err)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Create parser with test server URL
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
uri string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "valid articulate rise URI",
|
|
uri: "https://rise.articulate.com/share/test-share-id#/",
|
|
},
|
|
{
|
|
name: "valid articulate rise URI without fragment",
|
|
uri: "https://rise.articulate.com/share/test-share-id",
|
|
},
|
|
{
|
|
name: "invalid URI format",
|
|
uri: "invalid-uri",
|
|
expectedError: "invalid domain for Articulate Rise URI:",
|
|
},
|
|
{
|
|
name: "empty URI",
|
|
uri: "",
|
|
expectedError: "invalid domain for Articulate Rise URI:",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
course, err := parser.FetchCourse(tt.uri)
|
|
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("Expected error containing '%s', got nil", tt.expectedError)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.expectedError) {
|
|
t.Errorf("Expected error containing '%s', got '%s'", tt.expectedError, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
if course == nil {
|
|
t.Fatal("Expected course, got nil")
|
|
}
|
|
if course.ShareID != testCourse.ShareID {
|
|
t.Errorf("Expected ShareID '%s', got '%s'", testCourse.ShareID, course.ShareID)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_NetworkError tests network error handling.
|
|
func TestArticulateParser_FetchCourse_NetworkError(t *testing.T) {
|
|
// Create parser with invalid URL to simulate network error
|
|
parser := &ArticulateParser{
|
|
BaseURL: "http://localhost:99999", // Invalid port
|
|
Client: &http.Client{
|
|
Timeout: 1 * time.Millisecond, // Very short timeout
|
|
},
|
|
}
|
|
|
|
_, err := parser.FetchCourse("https://rise.articulate.com/share/test-share-id")
|
|
if err == nil {
|
|
t.Fatal("Expected network error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "failed to fetch course data") {
|
|
t.Errorf("Expected error to contain 'failed to fetch course data', got '%s'", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_InvalidJSON tests invalid JSON response handling.
|
|
func TestArticulateParser_FetchCourse_InvalidJSON(t *testing.T) {
|
|
// Create test server that returns invalid JSON
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte("invalid json"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
}
|
|
|
|
_, err := parser.FetchCourse("https://rise.articulate.com/share/test-share-id")
|
|
if err == nil {
|
|
t.Fatal("Expected JSON parsing error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "failed to unmarshal JSON") {
|
|
t.Errorf("Expected error to contain 'failed to unmarshal JSON', got '%s'", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_LoadCourseFromFile tests the LoadCourseFromFile method.
|
|
func TestArticulateParser_LoadCourseFromFile(t *testing.T) {
|
|
// Create a temporary test file
|
|
testCourse := &models.Course{
|
|
ShareID: "file-test-share-id",
|
|
Author: "File Test Author",
|
|
Course: models.CourseInfo{
|
|
ID: "file-test-course-id",
|
|
Title: "File Test Course",
|
|
Description: "File Test Description",
|
|
},
|
|
}
|
|
|
|
// Create temporary directory and file
|
|
tempDir := t.TempDir()
|
|
tempFile := filepath.Join(tempDir, "test-course.json")
|
|
|
|
// Write test data to file
|
|
data, err := json.Marshal(testCourse)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal test course: %v", err)
|
|
}
|
|
|
|
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
|
t.Fatalf("Failed to write test file: %v", err)
|
|
}
|
|
|
|
parser := NewArticulateParser()
|
|
|
|
tests := []struct {
|
|
name string
|
|
filePath string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "valid file",
|
|
filePath: tempFile,
|
|
},
|
|
{
|
|
name: "nonexistent file",
|
|
filePath: filepath.Join(tempDir, "nonexistent.json"),
|
|
expectedError: "failed to read file",
|
|
},
|
|
{
|
|
name: "empty path",
|
|
filePath: "",
|
|
expectedError: "failed to read file",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
course, err := parser.LoadCourseFromFile(tt.filePath)
|
|
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("Expected error containing '%s', got nil", tt.expectedError)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.expectedError) {
|
|
t.Errorf("Expected error containing '%s', got '%s'", tt.expectedError, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
if course == nil {
|
|
t.Fatal("Expected course, got nil")
|
|
}
|
|
if course.ShareID != testCourse.ShareID {
|
|
t.Errorf("Expected ShareID '%s', got '%s'", testCourse.ShareID, course.ShareID)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_LoadCourseFromFile_InvalidJSON tests invalid JSON file handling.
|
|
func TestArticulateParser_LoadCourseFromFile_InvalidJSON(t *testing.T) {
|
|
// Create temporary file with invalid JSON
|
|
tempDir := t.TempDir()
|
|
tempFile := filepath.Join(tempDir, "invalid.json")
|
|
|
|
if err := os.WriteFile(tempFile, []byte("invalid json content"), 0644); err != nil {
|
|
t.Fatalf("Failed to write test file: %v", err)
|
|
}
|
|
|
|
parser := NewArticulateParser()
|
|
_, err := parser.LoadCourseFromFile(tempFile)
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected JSON parsing error, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "failed to unmarshal JSON") {
|
|
t.Errorf("Expected error to contain 'failed to unmarshal JSON', got '%s'", err.Error())
|
|
}
|
|
}
|
|
|
|
// TestExtractShareID tests the extractShareID method.
|
|
func TestExtractShareID(t *testing.T) {
|
|
parser := &ArticulateParser{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
uri string
|
|
expected string
|
|
hasError bool
|
|
}{
|
|
{
|
|
name: "standard articulate rise URI with fragment",
|
|
uri: "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/",
|
|
expected: "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
},
|
|
{
|
|
name: "standard articulate rise URI without fragment",
|
|
uri: "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
expected: "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
},
|
|
{
|
|
name: "URI with trailing slash",
|
|
uri: "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO/",
|
|
expected: "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
},
|
|
{
|
|
name: "short share ID",
|
|
uri: "https://rise.articulate.com/share/abc123",
|
|
expected: "abc123",
|
|
},
|
|
{
|
|
name: "share ID with hyphens and underscores",
|
|
uri: "https://rise.articulate.com/share/test_ID-123_abc",
|
|
expected: "test_ID-123_abc",
|
|
},
|
|
{
|
|
name: "invalid URI - no share path",
|
|
uri: "https://rise.articulate.com/",
|
|
hasError: true,
|
|
},
|
|
{
|
|
name: "invalid URI - wrong domain",
|
|
uri: "https://example.com/share/test123",
|
|
hasError: true,
|
|
},
|
|
{
|
|
name: "invalid URI - no share ID",
|
|
uri: "https://rise.articulate.com/share/",
|
|
hasError: true,
|
|
},
|
|
{
|
|
name: "empty URI",
|
|
uri: "",
|
|
hasError: true,
|
|
},
|
|
{
|
|
name: "malformed URI",
|
|
uri: "not-a-uri",
|
|
hasError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parser.extractShareID(tt.uri)
|
|
|
|
if tt.hasError {
|
|
if err == nil {
|
|
t.Fatalf("Expected error for URI '%s', got nil", tt.uri)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Expected no error for URI '%s', got: %v", tt.uri, err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("Expected share ID '%s', got '%s'", tt.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildAPIURL tests the buildAPIURL method.
|
|
func TestBuildAPIURL(t *testing.T) {
|
|
parser := &ArticulateParser{
|
|
BaseURL: "https://rise.articulate.com",
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
shareID string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "standard share ID",
|
|
shareID: "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
expected: "https://rise.articulate.com/api/rise-runtime/boot/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO",
|
|
},
|
|
{
|
|
name: "short share ID",
|
|
shareID: "abc123",
|
|
expected: "https://rise.articulate.com/api/rise-runtime/boot/share/abc123",
|
|
},
|
|
{
|
|
name: "share ID with special characters",
|
|
shareID: "test_ID-123_abc",
|
|
expected: "https://rise.articulate.com/api/rise-runtime/boot/share/test_ID-123_abc",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := parser.buildAPIURL(tt.shareID)
|
|
if result != tt.expected {
|
|
t.Errorf("Expected URL '%s', got '%s'", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildAPIURL_DifferentBaseURL tests buildAPIURL with different base URLs.
|
|
func TestBuildAPIURL_DifferentBaseURL(t *testing.T) {
|
|
parser := &ArticulateParser{
|
|
BaseURL: "https://custom.domain.com",
|
|
}
|
|
|
|
shareID := "test123"
|
|
expected := "https://custom.domain.com/api/rise-runtime/boot/share/test123"
|
|
result := parser.buildAPIURL(shareID)
|
|
|
|
if result != expected {
|
|
t.Errorf("Expected URL '%s', got '%s'", expected, result)
|
|
}
|
|
}
|
|
|
|
// BenchmarkExtractShareID benchmarks the extractShareID method.
|
|
func BenchmarkExtractShareID(b *testing.B) {
|
|
parser := &ArticulateParser{}
|
|
uri := "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/"
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = parser.extractShareID(uri)
|
|
}
|
|
}
|
|
|
|
// BenchmarkBuildAPIURL benchmarks the buildAPIURL method.
|
|
func BenchmarkBuildAPIURL(b *testing.B) {
|
|
parser := &ArticulateParser{
|
|
BaseURL: "https://rise.articulate.com",
|
|
}
|
|
shareID := "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO"
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = parser.buildAPIURL(shareID)
|
|
}
|
|
}
|