From 68c6f4e40890a45d84698be7c5cfa62b27421388 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 6 Nov 2025 05:59:52 +0100 Subject: [PATCH] chore!: prepare for v1.0.0 release Bumps the application version to 1.0.0, signaling the first stable release. This version consolidates several new features and breaking API changes. This commit also includes various code quality improvements: - Modernizes tests to use t.Setenv for safer environment variable handling. - Addresses various linter warnings (gosec, errcheck). - Updates loop syntax to use Go 1.22's range-over-integer feature. BREAKING CHANGE: The public API has been updated for consistency and to introduce new features like context support and structured logging. - `GetSupportedFormat()` is renamed to `SupportedFormat()`. - `GetSupportedFormats()` is renamed to `SupportedFormats()`. - `FetchCourse()` now requires a `context.Context` parameter. - `NewArticulateParser()` constructor signature has been updated. --- internal/config/config_test.go | 13 ++++++------- internal/exporters/bench_test.go | 2 +- internal/exporters/docx.go | 1 + internal/exporters/html.go | 1 + internal/exporters/markdown.go | 1 + internal/exporters/output.docx | Bin 775 -> 775 bytes internal/services/html_cleaner.go | 4 +++- internal/services/parser.go | 1 + internal/services/parser_bench_test.go | 12 ++++++++---- internal/services/parser_context_test.go | 18 ++++++++++++------ internal/version/version.go | 12 +++++++++++- 11 files changed, 45 insertions(+), 20 deletions(-) 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 db1d20cfc76239cd4d25138f5922bf0da606cde2..3bc4c8769773046d55fbc36db8c43f443586e60a 100644 GIT binary patch delta 53 zcmZo?YiHXWz$nGIktu+2@=7KRMuy4RjPjE+n6yMC7#SGK^NUjSQ}UBbb5rw5^eS?5 JCVylS0|0x+4}Aat delta 55 zcmZo?YiFAvwb_W#f^j301mol_Od6sLKv14vl%k)KpIn-onpdJ%k()C)gGq}ifN`RX J+~ki;VgRa{58wa* 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"