mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 18:22:08 +01:00
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.
292 lines
8.2 KiB
Go
292 lines
8.2 KiB
Go
// Package services_test provides context-aware tests for the parser service.
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kjanat/articulate-parser/internal/models"
|
|
)
|
|
|
|
// TestArticulateParser_FetchCourse_ContextCancellation tests that FetchCourse
|
|
// respects context cancellation.
|
|
func TestArticulateParser_FetchCourse_ContextCancellation(t *testing.T) {
|
|
// Create a server that delays response
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Sleep to give time for context cancellation
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
testCourse := &models.Course{
|
|
ShareID: "test-id",
|
|
Course: models.CourseInfo{
|
|
Title: "Test Course",
|
|
},
|
|
}
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
// Create a context that we'll cancel immediately
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
_, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
|
|
// Should get a context cancellation error
|
|
if err == nil {
|
|
t.Fatal("Expected error due to context cancellation, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "context canceled") {
|
|
t.Errorf("Expected context cancellation error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_ContextTimeout tests that FetchCourse
|
|
// respects context timeout.
|
|
func TestArticulateParser_FetchCourse_ContextTimeout(t *testing.T) {
|
|
// Create a server that delays response longer than timeout
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Sleep longer than the context timeout
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
testCourse := &models.Course{
|
|
ShareID: "test-id",
|
|
Course: models.CourseInfo{
|
|
Title: "Test Course",
|
|
},
|
|
}
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
// Create a context with a very short timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
|
|
// Should get a context deadline exceeded error
|
|
if err == nil {
|
|
t.Fatal("Expected error due to context timeout, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "deadline exceeded") &&
|
|
!strings.Contains(err.Error(), "context deadline exceeded") {
|
|
t.Errorf("Expected context timeout error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_ContextDeadline tests that FetchCourse
|
|
// respects context deadline.
|
|
func TestArticulateParser_FetchCourse_ContextDeadline(t *testing.T) {
|
|
// Create a server that delays response
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
testCourse := &models.Course{
|
|
ShareID: "test-id",
|
|
Course: models.CourseInfo{
|
|
Title: "Test Course",
|
|
},
|
|
}
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
// Create a context with a deadline in the past
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Millisecond))
|
|
defer cancel()
|
|
|
|
_, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
|
|
// Should get a deadline exceeded error
|
|
if err == nil {
|
|
t.Fatal("Expected error due to context deadline, got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "deadline exceeded") &&
|
|
!strings.Contains(err.Error(), "context deadline exceeded") {
|
|
t.Errorf("Expected deadline exceeded error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_ContextSuccess tests that FetchCourse
|
|
// succeeds when context is not cancelled.
|
|
func TestArticulateParser_FetchCourse_ContextSuccess(t *testing.T) {
|
|
testCourse := &models.Course{
|
|
ShareID: "test-id",
|
|
Course: models.CourseInfo{
|
|
Title: "Test Course",
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Respond quickly
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
// Create a context with generous timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
course, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Expected no error, got: %v", err)
|
|
}
|
|
|
|
if course == nil {
|
|
t.Fatal("Expected course, got nil")
|
|
}
|
|
|
|
if course.Course.Title != testCourse.Course.Title {
|
|
t.Errorf("Expected title '%s', got '%s'", testCourse.Course.Title, course.Course.Title)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_CancellationDuringRequest tests cancellation
|
|
// during an in-flight request.
|
|
func TestArticulateParser_FetchCourse_CancellationDuringRequest(t *testing.T) {
|
|
requestStarted := make(chan bool)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestStarted <- true
|
|
// Keep the handler running to simulate slow response
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
testCourse := &models.Course{
|
|
ShareID: "test-id",
|
|
}
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Start the request in a goroutine
|
|
errChan := make(chan error, 1)
|
|
go func() {
|
|
_, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
errChan <- err
|
|
}()
|
|
|
|
// Wait for request to start
|
|
<-requestStarted
|
|
|
|
// Cancel after request has started
|
|
cancel()
|
|
|
|
// Get the error
|
|
err := <-errChan
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error due to context cancellation, got nil")
|
|
}
|
|
|
|
// Should contain context canceled somewhere in the error chain
|
|
if !strings.Contains(err.Error(), "context canceled") {
|
|
t.Errorf("Expected context canceled error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestArticulateParser_FetchCourse_MultipleTimeouts tests behavior with
|
|
// multiple concurrent requests and timeouts.
|
|
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"}
|
|
// Encode errors are ignored in test setup; httptest.ResponseWriter is reliable
|
|
_ = json.NewEncoder(w).Encode(testCourse)
|
|
}))
|
|
defer server.Close()
|
|
|
|
parser := &ArticulateParser{
|
|
BaseURL: server.URL,
|
|
Client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
},
|
|
Logger: NewNoOpLogger(),
|
|
}
|
|
|
|
// Launch multiple requests with different timeouts
|
|
tests := []struct {
|
|
name string
|
|
timeout time.Duration
|
|
shouldSucceed bool
|
|
}{
|
|
{"very short timeout", 10 * time.Millisecond, false},
|
|
{"short timeout", 50 * time.Millisecond, false},
|
|
{"adequate timeout", 500 * time.Millisecond, true},
|
|
{"long timeout", 2 * time.Second, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), tt.timeout)
|
|
defer cancel()
|
|
|
|
_, err := parser.FetchCourse(ctx, "https://rise.articulate.com/share/test-id")
|
|
|
|
if tt.shouldSucceed && err != nil {
|
|
t.Errorf("Expected success with timeout %v, got error: %v", tt.timeout, err)
|
|
}
|
|
|
|
if !tt.shouldSucceed && err == nil {
|
|
t.Errorf("Expected timeout error with timeout %v, got success", tt.timeout)
|
|
}
|
|
})
|
|
}
|
|
}
|