diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 49894b8..ef94164 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -33,11 +33,10 @@ func TestLoad(t *testing.T) { func TestLoad_WithEnvironmentVariables(t *testing.T) { // Set environment variables - os.Setenv("ARTICULATE_BASE_URL", "https://test.example.com") - os.Setenv("ARTICULATE_REQUEST_TIMEOUT", "60") - os.Setenv("LOG_LEVEL", "debug") - os.Setenv("LOG_FORMAT", "json") - defer os.Clearenv() + t.Setenv("ARTICULATE_BASE_URL", "https://test.example.com") + t.Setenv("ARTICULATE_REQUEST_TIMEOUT", "60") + t.Setenv("LOG_LEVEL", "debug") + t.Setenv("LOG_FORMAT", "json") cfg := Load() @@ -81,7 +80,7 @@ func TestGetLogLevelEnv(t *testing.T) { t.Run(tt.name, func(t *testing.T) { os.Clearenv() if tt.value != "" { - os.Setenv("TEST_LOG_LEVEL", tt.value) + t.Setenv("TEST_LOG_LEVEL", tt.value) } result := getLogLevelEnv("TEST_LOG_LEVEL", slog.LevelInfo) if result != tt.expected { @@ -107,7 +106,7 @@ func TestGetDurationEnv(t *testing.T) { t.Run(tt.name, func(t *testing.T) { os.Clearenv() if tt.value != "" { - os.Setenv("TEST_DURATION", tt.value) + t.Setenv("TEST_DURATION", tt.value) } result := getDurationEnv("TEST_DURATION", 30*time.Second) if result != tt.expected { diff --git a/internal/exporters/bench_test.go b/internal/exporters/bench_test.go index f60f353..5f91aab 100644 --- a/internal/exporters/bench_test.go +++ b/internal/exporters/bench_test.go @@ -146,7 +146,7 @@ func createBenchmarkCourse() *models.Course { // createLargeBenchmarkCourse creates a large course for stress testing. func createLargeBenchmarkCourse() *models.Course { lessons := make([]models.Lesson, 50) - for i := 0; i < 50; i++ { + for i := range 50 { lessons[i] = models.Lesson{ ID: string(rune(i)), Title: "Lesson " + string(rune(i)), diff --git a/internal/exporters/docx.go b/internal/exporters/docx.go index 3e5285f..8c87114 100644 --- a/internal/exporters/docx.go +++ b/internal/exporters/docx.go @@ -72,6 +72,7 @@ func (e *DocxExporter) Export(course *models.Course, outputPath string) error { } // Create the file + // #nosec G304 - Output path is provided by user via CLI argument, which is expected behavior file, err := os.Create(outputPath) if err != nil { return fmt.Errorf("failed to create output file: %w", err) diff --git a/internal/exporters/html.go b/internal/exporters/html.go index 63cb628..eea187f 100644 --- a/internal/exporters/html.go +++ b/internal/exporters/html.go @@ -110,6 +110,7 @@ func (e *HTMLExporter) Export(course *models.Course, outputPath string) error { buf.WriteString("\n") buf.WriteString("\n") + // #nosec G306 - 0644 is appropriate for export files that should be readable by others return os.WriteFile(outputPath, buf.Bytes(), 0644) } diff --git a/internal/exporters/markdown.go b/internal/exporters/markdown.go index 091637d..02dd18c 100644 --- a/internal/exporters/markdown.go +++ b/internal/exporters/markdown.go @@ -80,6 +80,7 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro buf.WriteString("\n---\n\n") } + // #nosec G306 - 0644 is appropriate for export files that should be readable by others return os.WriteFile(outputPath, buf.Bytes(), 0644) } diff --git a/internal/exporters/output.docx b/internal/exporters/output.docx index db1d20c..3bc4c87 100644 Binary files a/internal/exporters/output.docx and b/internal/exporters/output.docx differ diff --git a/internal/services/html_cleaner.go b/internal/services/html_cleaner.go index 7facd42..63817f6 100644 --- a/internal/services/html_cleaner.go +++ b/internal/services/html_cleaner.go @@ -58,7 +58,9 @@ func extractText(w io.Writer, n *html.Node) { // If this is a text node, write its content if n.Type == html.TextNode { - w.Write([]byte(n.Data)) + // Write errors are ignored because we're writing to an in-memory buffer + // which cannot fail in normal circumstances + _, _ = w.Write([]byte(n.Data)) } // Recursively process all child nodes diff --git a/internal/services/parser.go b/internal/services/parser.go index 31fbfba..cd9e57d 100644 --- a/internal/services/parser.go +++ b/internal/services/parser.go @@ -99,6 +99,7 @@ func (p *ArticulateParser) FetchCourse(ctx context.Context, uri string) (*models // LoadCourseFromFile loads an Articulate Rise course from a local JSON file. func (p *ArticulateParser) LoadCourseFromFile(filePath string) (*models.Course, error) { + // #nosec G304 - File path is provided by user via CLI argument, which is expected behavior data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) diff --git a/internal/services/parser_bench_test.go b/internal/services/parser_bench_test.go index 5834d5d..74b150b 100644 --- a/internal/services/parser_bench_test.go +++ b/internal/services/parser_bench_test.go @@ -34,7 +34,9 @@ func BenchmarkArticulateParser_FetchCourse(b *testing.B) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in benchmarks; the test server's ResponseWriter + // writes are reliable and any encoding error would be a test setup issue + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -57,7 +59,7 @@ func BenchmarkArticulateParser_FetchCourse(b *testing.B) { func BenchmarkArticulateParser_FetchCourse_LargeCourse(b *testing.B) { // Create a large course with many lessons lessons := make([]models.Lesson, 100) - for i := 0; i < 100; i++ { + for i := range 100 { lessons[i] = models.Lesson{ ID: string(rune(i)), Title: "Lesson " + string(rune(i)), @@ -90,7 +92,9 @@ func BenchmarkArticulateParser_FetchCourse_LargeCourse(b *testing.B) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in benchmarks; the test server's ResponseWriter + // writes are reliable and any encoding error would be a test setup issue + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -145,7 +149,7 @@ func BenchmarkArticulateParser_LoadCourseFromFile(b *testing.B) { func BenchmarkArticulateParser_LoadCourseFromFile_Large(b *testing.B) { // Create a large course lessons := make([]models.Lesson, 200) - for i := 0; i < 200; i++ { + for i := range 200 { lessons[i] = models.Lesson{ ID: string(rune(i)), Title: "Lesson " + string(rune(i)), diff --git a/internal/services/parser_context_test.go b/internal/services/parser_context_test.go index 4e0ee45..2d312b6 100644 --- a/internal/services/parser_context_test.go +++ b/internal/services/parser_context_test.go @@ -27,7 +27,8 @@ func TestArticulateParser_FetchCourse_ContextCancellation(t *testing.T) { Title: "Test Course", }, } - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -69,7 +70,8 @@ func TestArticulateParser_FetchCourse_ContextTimeout(t *testing.T) { Title: "Test Course", }, } - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -111,7 +113,8 @@ func TestArticulateParser_FetchCourse_ContextDeadline(t *testing.T) { Title: "Test Course", }, } - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -152,7 +155,8 @@ func TestArticulateParser_FetchCourse_ContextSuccess(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Respond quickly - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -196,7 +200,8 @@ func TestArticulateParser_FetchCourse_CancellationDuringRequest(t *testing.T) { testCourse := &models.Course{ ShareID: "test-id", } - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() @@ -242,7 +247,8 @@ func TestArticulateParser_FetchCourse_MultipleTimeouts(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(100 * time.Millisecond) testCourse := &models.Course{ShareID: "test"} - json.NewEncoder(w).Encode(testCourse) + // Encode errors are ignored in test setup; httptest.ResponseWriter is reliable + _ = json.NewEncoder(w).Encode(testCourse) })) defer server.Close() diff --git a/internal/version/version.go b/internal/version/version.go index 7bcef44..e3e780d 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -5,7 +5,17 @@ package version // Version information. var ( // Version is the current version of the application. - Version = "0.4.1" + // Breaking changes from 0.4.x: + // - Renamed GetSupportedFormat() -> SupportedFormat() + // - Renamed GetSupportedFormats() -> SupportedFormats() + // - FetchCourse now requires context.Context parameter + // - NewArticulateParser now accepts logger, baseURL, timeout + // New features: + // - Structured logging with slog + // - Configuration via environment variables + // - Context-aware HTTP requests + // - Comprehensive benchmarks and examples + Version = "1.0.0" // BuildTime is the time the binary was built. BuildTime = "unknown"