mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 06:22:09 +01:00
Renames the `OriginalUrl` field to `OriginalURL` across media models to adhere to Go's common initialisms convention. The `json` tag is unchanged to maintain API compatibility. Introduces constants for exporter formats (e.g., `FormatMarkdown`, `FormatDocx`) to eliminate the use of magic strings, enhancing type safety and making the code easier to maintain. Additionally, this commit includes several minor code quality improvements: - Wraps file-writing errors in exporters to provide more context. - Removes redundant package-level comments from test files. - Applies various minor linting fixes throughout the codebase.
350 lines
9.8 KiB
Go
350 lines
9.8 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
|
"github.com/kjanat/articulate-parser/internal/models"
|
|
)
|
|
|
|
// MockCourseParser is a mock implementation of interfaces.CourseParser for testing.
|
|
type MockCourseParser struct {
|
|
mockFetchCourse func(ctx context.Context, uri string) (*models.Course, error)
|
|
mockLoadCourseFromFile func(filePath string) (*models.Course, error)
|
|
}
|
|
|
|
func (m *MockCourseParser) FetchCourse(ctx context.Context, uri string) (*models.Course, error) {
|
|
if m.mockFetchCourse != nil {
|
|
return m.mockFetchCourse(ctx, uri)
|
|
}
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
func (m *MockCourseParser) LoadCourseFromFile(filePath string) (*models.Course, error) {
|
|
if m.mockLoadCourseFromFile != nil {
|
|
return m.mockLoadCourseFromFile(filePath)
|
|
}
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
// MockExporter is a mock implementation of interfaces.Exporter for testing.
|
|
type MockExporter struct {
|
|
mockExport func(course *models.Course, outputPath string) error
|
|
mockSupportedFormat func() string
|
|
}
|
|
|
|
func (m *MockExporter) Export(course *models.Course, outputPath string) error {
|
|
if m.mockExport != nil {
|
|
return m.mockExport(course, outputPath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockExporter) SupportedFormat() string {
|
|
if m.mockSupportedFormat != nil {
|
|
return m.mockSupportedFormat()
|
|
}
|
|
return "mock"
|
|
}
|
|
|
|
// MockExporterFactory is a mock implementation of interfaces.ExporterFactory for testing.
|
|
type MockExporterFactory struct {
|
|
mockCreateExporter func(format string) (*MockExporter, error)
|
|
mockSupportedFormats func() []string
|
|
}
|
|
|
|
func (m *MockExporterFactory) CreateExporter(format string) (interfaces.Exporter, error) {
|
|
if m.mockCreateExporter != nil {
|
|
exporter, err := m.mockCreateExporter(format)
|
|
return exporter, err
|
|
}
|
|
return &MockExporter{}, nil
|
|
}
|
|
|
|
func (m *MockExporterFactory) SupportedFormats() []string {
|
|
if m.mockSupportedFormats != nil {
|
|
return m.mockSupportedFormats()
|
|
}
|
|
return []string{"mock"}
|
|
}
|
|
|
|
// createTestCourse creates a sample course for testing purposes.
|
|
func createTestCourse() *models.Course {
|
|
return &models.Course{
|
|
ShareID: "test-share-id",
|
|
Author: "Test Author",
|
|
Course: models.CourseInfo{
|
|
ID: "test-course-id",
|
|
Title: "Test Course",
|
|
Description: "This is a test course",
|
|
Lessons: []models.Lesson{
|
|
{
|
|
ID: "lesson-1",
|
|
Title: "Test Lesson",
|
|
Type: "lesson",
|
|
Items: []models.Item{
|
|
{
|
|
ID: "item-1",
|
|
Type: "text",
|
|
Items: []models.SubItem{
|
|
{
|
|
ID: "subitem-1",
|
|
Title: "Test Title",
|
|
Paragraph: "Test paragraph content",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// TestNewApp tests the NewApp constructor.
|
|
func TestNewApp(t *testing.T) {
|
|
parser := &MockCourseParser{}
|
|
factory := &MockExporterFactory{}
|
|
|
|
app := NewApp(parser, factory)
|
|
|
|
if app == nil {
|
|
t.Fatal("NewApp() returned nil")
|
|
}
|
|
|
|
if app.parser != parser {
|
|
t.Error("App parser was not set correctly")
|
|
}
|
|
|
|
// Test that the factory is set (we can't directly compare interface values)
|
|
formats := app.SupportedFormats()
|
|
if len(formats) == 0 {
|
|
t.Error("App exporterFactory was not set correctly - no supported formats")
|
|
}
|
|
}
|
|
|
|
// TestApp_ProcessCourseFromFile tests the ProcessCourseFromFile method.
|
|
func TestApp_ProcessCourseFromFile(t *testing.T) {
|
|
testCourse := createTestCourse()
|
|
|
|
tests := []struct {
|
|
name string
|
|
filePath string
|
|
format string
|
|
outputPath string
|
|
setupMocks func(*MockCourseParser, *MockExporterFactory, *MockExporter)
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "successful processing",
|
|
filePath: "test.json",
|
|
format: "markdown",
|
|
outputPath: "output.md",
|
|
setupMocks: func(parser *MockCourseParser, factory *MockExporterFactory, exporter *MockExporter) {
|
|
parser.mockLoadCourseFromFile = func(filePath string) (*models.Course, error) {
|
|
if filePath != "test.json" {
|
|
t.Errorf("Expected filePath 'test.json', got '%s'", filePath)
|
|
}
|
|
return testCourse, nil
|
|
}
|
|
|
|
factory.mockCreateExporter = func(format string) (*MockExporter, error) {
|
|
if format != "markdown" {
|
|
t.Errorf("Expected format 'markdown', got '%s'", format)
|
|
}
|
|
return exporter, nil
|
|
}
|
|
|
|
exporter.mockExport = func(course *models.Course, outputPath string) error {
|
|
if outputPath != "output.md" {
|
|
t.Errorf("Expected outputPath 'output.md', got '%s'", outputPath)
|
|
}
|
|
if course != testCourse {
|
|
t.Error("Expected course to match testCourse")
|
|
}
|
|
return nil
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "file loading error",
|
|
filePath: "nonexistent.json",
|
|
format: "markdown",
|
|
outputPath: "output.md",
|
|
setupMocks: func(parser *MockCourseParser, factory *MockExporterFactory, exporter *MockExporter) {
|
|
parser.mockLoadCourseFromFile = func(filePath string) (*models.Course, error) {
|
|
return nil, errors.New("file not found")
|
|
}
|
|
},
|
|
expectedError: "failed to load course from file",
|
|
},
|
|
{
|
|
name: "exporter creation error",
|
|
filePath: "test.json",
|
|
format: "unsupported",
|
|
outputPath: "output.txt",
|
|
setupMocks: func(parser *MockCourseParser, factory *MockExporterFactory, exporter *MockExporter) {
|
|
parser.mockLoadCourseFromFile = func(filePath string) (*models.Course, error) {
|
|
return testCourse, nil
|
|
}
|
|
|
|
factory.mockCreateExporter = func(format string) (*MockExporter, error) {
|
|
return nil, errors.New("unsupported format")
|
|
}
|
|
},
|
|
expectedError: "failed to create exporter",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &MockCourseParser{}
|
|
exporter := &MockExporter{}
|
|
factory := &MockExporterFactory{}
|
|
|
|
tt.setupMocks(parser, factory, exporter)
|
|
|
|
app := NewApp(parser, factory)
|
|
err := app.ProcessCourseFromFile(tt.filePath, tt.format, tt.outputPath)
|
|
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("Expected error containing '%s', got nil", tt.expectedError)
|
|
}
|
|
if !contains(err.Error(), tt.expectedError) {
|
|
t.Errorf("Expected error containing '%s', got '%s'", tt.expectedError, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Expected no error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApp_ProcessCourseFromURI tests the ProcessCourseFromURI method.
|
|
func TestApp_ProcessCourseFromURI(t *testing.T) {
|
|
testCourse := createTestCourse()
|
|
|
|
tests := []struct {
|
|
name string
|
|
uri string
|
|
format string
|
|
outputPath string
|
|
setupMocks func(*MockCourseParser, *MockExporterFactory, *MockExporter)
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "successful processing",
|
|
uri: "https://rise.articulate.com/share/test123",
|
|
format: "docx",
|
|
outputPath: "output.docx",
|
|
setupMocks: func(parser *MockCourseParser, factory *MockExporterFactory, exporter *MockExporter) {
|
|
parser.mockFetchCourse = func(ctx context.Context, uri string) (*models.Course, error) {
|
|
if uri != "https://rise.articulate.com/share/test123" {
|
|
t.Errorf("Expected uri 'https://rise.articulate.com/share/test123', got '%s'", uri)
|
|
}
|
|
return testCourse, nil
|
|
}
|
|
|
|
factory.mockCreateExporter = func(format string) (*MockExporter, error) {
|
|
if format != "docx" {
|
|
t.Errorf("Expected format 'docx', got '%s'", format)
|
|
}
|
|
return exporter, nil
|
|
}
|
|
|
|
exporter.mockExport = func(course *models.Course, outputPath string) error {
|
|
if outputPath != "output.docx" {
|
|
t.Errorf("Expected outputPath 'output.docx', got '%s'", outputPath)
|
|
}
|
|
return nil
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "fetch error",
|
|
uri: "invalid-uri",
|
|
format: "docx",
|
|
outputPath: "output.docx",
|
|
setupMocks: func(parser *MockCourseParser, factory *MockExporterFactory, exporter *MockExporter) {
|
|
parser.mockFetchCourse = func(ctx context.Context, uri string) (*models.Course, error) {
|
|
return nil, errors.New("network error")
|
|
}
|
|
},
|
|
expectedError: "failed to fetch course",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := &MockCourseParser{}
|
|
exporter := &MockExporter{}
|
|
factory := &MockExporterFactory{}
|
|
|
|
tt.setupMocks(parser, factory, exporter)
|
|
|
|
app := NewApp(parser, factory)
|
|
err := app.ProcessCourseFromURI(context.Background(), tt.uri, tt.format, tt.outputPath)
|
|
|
|
if tt.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("Expected error containing '%s', got nil", tt.expectedError)
|
|
}
|
|
if !contains(err.Error(), tt.expectedError) {
|
|
t.Errorf("Expected error containing '%s', got '%s'", tt.expectedError, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("Expected no error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApp_SupportedFormats tests the SupportedFormats method.
|
|
func TestApp_SupportedFormats(t *testing.T) {
|
|
expectedFormats := []string{"markdown", "docx", "pdf"}
|
|
|
|
parser := &MockCourseParser{}
|
|
factory := &MockExporterFactory{
|
|
mockSupportedFormats: func() []string {
|
|
return expectedFormats
|
|
},
|
|
}
|
|
|
|
app := NewApp(parser, factory)
|
|
formats := app.SupportedFormats()
|
|
|
|
if len(formats) != len(expectedFormats) {
|
|
t.Errorf("Expected %d formats, got %d", len(expectedFormats), len(formats))
|
|
}
|
|
|
|
for i, format := range formats {
|
|
if format != expectedFormats[i] {
|
|
t.Errorf("Expected format '%s' at index %d, got '%s'", expectedFormats[i], i, format)
|
|
}
|
|
}
|
|
}
|
|
|
|
// contains checks if a string contains a substring.
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) &&
|
|
(substr == "" ||
|
|
s == substr ||
|
|
(len(s) > len(substr) &&
|
|
(s[:len(substr)] == substr ||
|
|
s[len(s)-len(substr):] == substr ||
|
|
containsSubstring(s, substr))))
|
|
}
|
|
|
|
// containsSubstring checks if s contains substr as a substring.
|
|
func containsSubstring(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|