mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 15:42:11 +01:00
Add comprehensive unit tests for services and main package
- 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.
This commit is contained in:
440
internal/services/parser_test.go
Normal file
440
internal/services/parser_test.go
Normal file
@ -0,0 +1,440 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user