From 73ca73f9fc9883f5b6198b84070cb73d5660b4f7 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Thu, 11 Dec 2025 09:45:49 +0000 Subject: [PATCH] Update tests. --- internal/aggregator/aggregator_test.go | 495 +++++++++++++++++++++ internal/domain/scoring/calculator_test.go | 70 +++ internal/server/server.go | 50 ++- internal/server/server_test.go | 228 ++++++++++ 4 files changed, 827 insertions(+), 16 deletions(-) diff --git a/internal/aggregator/aggregator_test.go b/internal/aggregator/aggregator_test.go index 6bd81ba..c59d2fd 100644 --- a/internal/aggregator/aggregator_test.go +++ b/internal/aggregator/aggregator_test.go @@ -381,3 +381,498 @@ func TestParseRepoName(t *testing.T) { assert.Equal(t, tt.expectedName, name, "name mismatch for %s", tt.fullName) } } + +func TestSetUserProfiles(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + profiles := map[string]UserProfile{ + "user1": {Login: "user1", Email: "user1@example.com", Name: "User One", ID: 12345}, + "user2": {Login: "user2", Email: "user2@example.com", Name: "User Two", ID: 67890}, + } + + agg.SetUserProfiles(profiles) + assert.Equal(t, profiles, agg.userProfiles) +} + +func TestNormalizeForComparison(t *testing.T) { + t.Parallel() + + tests := []struct { + input string + expected string + }{ + {"John Doe", "johndoe"}, + {"john-doe", "johndoe"}, + {"john_doe", "johndoe"}, + {"john.doe", "johndoe"}, + {"JOHN DOE", "johndoe"}, + {"John123Doe", "johndoe"}, + {"123", ""}, + {"", ""}, + {"ABC xyz 123", "abcxyz"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := normalizeForComparison(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBuildEmailToLoginMapping_NoReplyEmails(t *testing.T) { + t.Parallel() + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "", Email: "12345+johndoe@users.noreply.github.com", Name: "John Doe"}, + Repository: "owner/repo", + }, + }, + PullRequests: []models.PullRequest{ + { + Number: 1, + Author: models.Author{Login: "johndoe", ID: 12345}, + }, + }, + } + + mapping := buildEmailToLoginMapping(data, nil) + + // Should map via the ID + assert.Equal(t, "johndoe", mapping["12345+johndoe@users.noreply.github.com"]) +} + +func TestBuildEmailToLoginMapping_ProfileEmails(t *testing.T) { + t.Parallel() + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "", Email: "john@company.com", Name: "John Doe"}, + Repository: "owner/repo", + }, + }, + } + + profiles := map[string]UserProfile{ + "johndoe": {Login: "johndoe", Email: "john@company.com", Name: "John Doe", ID: 12345}, + } + + mapping := buildEmailToLoginMapping(data, profiles) + + // Should map via profile email + assert.Equal(t, "johndoe", mapping["john@company.com"]) +} + +func TestBuildEmailToLoginMapping_NameMatching(t *testing.T) { + t.Parallel() + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "", Email: "john@somewhere.com", Name: "John Doe"}, + Repository: "owner/repo", + }, + }, + PullRequests: []models.PullRequest{ + { + Number: 1, + Author: models.Author{Login: "johndoe", Name: "John Doe"}, + }, + }, + } + + mapping := buildEmailToLoginMapping(data, nil) + + // Should map via name matching + assert.Equal(t, "johndoe", mapping["john@somewhere.com"]) +} + +func TestCalculateWorkWeekStreak(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dates map[string]bool + expectedStreak int + }{ + { + name: "empty dates", + dates: map[string]bool{}, + expectedStreak: 0, + }, + { + name: "single weekday", + dates: map[string]bool{ + "2024-01-08": true, // Monday + }, + expectedStreak: 1, + }, + { + name: "consecutive weekdays", + dates: map[string]bool{ + "2024-01-08": true, // Monday + "2024-01-09": true, // Tuesday + "2024-01-10": true, // Wednesday + }, + expectedStreak: 3, + }, + { + name: "weekdays with weekend gap", + dates: map[string]bool{ + "2024-01-12": true, // Friday + "2024-01-15": true, // Monday + "2024-01-16": true, // Tuesday + }, + expectedStreak: 3, // Weekend doesn't break streak + }, + { + name: "broken streak on weekday", + dates: map[string]bool{ + "2024-01-08": true, // Monday + "2024-01-10": true, // Wednesday (skipped Tuesday) + }, + expectedStreak: 1, + }, + { + name: "weekend only", + dates: map[string]bool{ + "2024-01-13": true, // Saturday + "2024-01-14": true, // Sunday + }, + expectedStreak: 0, // Weekends don't count + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calculateWorkWeekStreak(tt.dates) + assert.Equal(t, tt.expectedStreak, result) + }) + } +} + +func TestCalculateWorkWeekStreak_LongestStreak(t *testing.T) { + t.Parallel() + + // Multiple streaks - should return longest + dates := map[string]bool{ + "2024-01-08": true, // Monday + "2024-01-09": true, // Tuesday + "2024-01-15": true, // Monday (gap - breaks streak) + "2024-01-16": true, // Tuesday + "2024-01-17": true, // Wednesday + "2024-01-18": true, // Thursday + "2024-01-19": true, // Friday + "2024-01-22": true, // Monday (weekend doesn't break) + } + + result := calculateWorkWeekStreak(dates) + assert.Equal(t, 6, result) // Mon-Fri + Mon = 6 weekdays in a row +} + +func TestAggregator_OutOfHoursTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 7, 0, 0, 0, time.UTC), // 7am - before 9am + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), // 10am - work hours + Repository: "owner/repo", + }, + { + SHA: "ghi789", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 18, 0, 0, 0, time.UTC), // 6pm - after 5pm + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 2, contrib.OutOfHoursCount) // 7am and 6pm are out of hours +} + +func TestAggregator_WorkWeekStreakTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 8, 10, 0, 0, 0, time.UTC), // Monday + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 9, 10, 0, 0, 0, time.UTC), // Tuesday + Repository: "owner/repo", + }, + { + SHA: "ghi789", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 10, 10, 0, 0, 0, time.UTC), // Wednesday + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 3, contrib.WorkWeekStreak) +} + +// Note: Bot filtering tests removed - bot filtering happens in app.go before data reaches aggregator +// The aggregator receives already filtered data + +func TestAggregator_EarlyBirdTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 6, 0, 0, 0, time.UTC), // 6am + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 16, 8, 30, 0, 0, time.UTC), // 8:30am + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 2, contrib.EarlyBirdCount) // Both before 9am +} + +func TestAggregator_NightOwlTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 21, 0, 0, 0, time.UTC), // 9pm + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 16, 23, 30, 0, 0, time.UTC), // 11:30pm + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 2, contrib.NightOwlCount) // Both after 9pm +} + +func TestAggregator_MidnightTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 0, 30, 0, 0, time.UTC), // 12:30am + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 16, 3, 0, 0, 0, time.UTC), // 3am + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 2, contrib.MidnightCount) // Both between 0-4am +} + +func TestAggregator_WeekendWarriorTracking(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 13, 10, 0, 0, 0, time.UTC), // Saturday + Repository: "owner/repo", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 14, 15, 0, 0, 0, time.UTC), // Sunday + Repository: "owner/repo", + }, + { + SHA: "ghi789", + Author: models.Author{Login: "user1"}, + Date: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC), // Monday (not weekend) + Repository: "owner/repo", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + require.Len(t, metrics.Repositories, 1) + require.Len(t, metrics.Repositories[0].Contributors, 1) + contrib := metrics.Repositories[0].Contributors[0] + assert.Equal(t, 2, contrib.WeekendWarrior) // Saturday and Sunday only +} + +func TestAggregator_MultiRepoContributions(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + agg := New(cfg) + + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "user1"}, + Repository: "owner/repo1", + }, + { + SHA: "def456", + Author: models.Author{Login: "user1"}, + Repository: "owner/repo2", + }, + { + SHA: "ghi789", + Author: models.Author{Login: "user1"}, + Repository: "owner/repo3", + }, + }, + } + + dateRange := &config.ParsedDateRange{} + + metrics, err := agg.Aggregate(data, dateRange) + require.NoError(t, err) + + // MultiRepoCount is tracked in the global leaderboard entries, not repo contributors + // The leaderboard entry should show 3 repos for user1 + require.Len(t, metrics.Repositories, 3) + assert.Equal(t, 1, metrics.TotalContributors) +} + +func TestBuildEmailToLoginMapping_EmptyData(t *testing.T) { + t.Parallel() + + data := &models.RawData{} + mapping := buildEmailToLoginMapping(data, nil) + assert.Empty(t, mapping) +} + +func TestBuildEmailToLoginMapping_NoReplyEmailWithoutID(t *testing.T) { + t.Parallel() + + // When the email is just "username@users.noreply.github.com" (without ID+), + // the mapping only happens if there's a matching PR author (via name matching later) + // The direct extraction only works for "ID+username@" format + data := &models.RawData{ + Commits: []models.Commit{ + { + SHA: "abc123", + Author: models.Author{Login: "", Email: "johndoe@users.noreply.github.com", Name: "John Doe"}, + Repository: "owner/repo", + }, + }, + // Add a PR to enable name matching + PullRequests: []models.PullRequest{ + { + Number: 1, + Author: models.Author{Login: "johndoe", Name: "John Doe"}, + }, + }, + } + + mapping := buildEmailToLoginMapping(data, nil) + // Should map via name matching since there's a PR author with the same name + assert.Equal(t, "johndoe", mapping["johndoe@users.noreply.github.com"]) +} diff --git a/internal/domain/scoring/calculator_test.go b/internal/domain/scoring/calculator_test.go index 64d29fc..3dc1b32 100644 --- a/internal/domain/scoring/calculator_test.go +++ b/internal/domain/scoring/calculator_test.go @@ -667,6 +667,76 @@ func TestCalculator_NoReviewsNoBonus(t *testing.T) { assert.Equal(t, 0, contributor.Score.Breakdown.ResponseBonus) } +func TestCalculator_OutOfHoursScoring(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + cfg.Scoring.Enabled = true + cfg.Scoring.Points = config.PointsConfig{ + Commit: 10, + OutOfHours: 5, // 5 points per out-of-hours commit + } + calc := NewCalculator(cfg) + + metrics := &models.GlobalMetrics{ + Repositories: []models.RepositoryMetrics{ + { + FullName: "owner/repo", + Contributors: []models.ContributorMetrics{ + { + Login: "night-owl", + CommitCount: 10, + OutOfHoursCount: 8, // 8 commits outside 9am-5pm + RepositoriesContributed: []string{"owner/repo"}, + }, + }, + }, + }, + } + + result := calc.Calculate(metrics) + + contributor := result.Repositories[0].Contributors[0] + // Commits: 10 * 10 = 100 + // OutOfHours: 8 * 5 = 40 + // Total: 140 + assert.Equal(t, 100, contributor.Score.Breakdown.Commits) + assert.Equal(t, 40, contributor.Score.Breakdown.OutOfHours) + assert.Equal(t, 140, contributor.Score.Total) +} + +func TestCalculator_WorkWeekStreakAchievement(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + cfg.Scoring.Enabled = true + // Achievements are now hardcoded + calc := NewCalculator(cfg) + + metrics := &models.GlobalMetrics{ + Repositories: []models.RepositoryMetrics{ + { + FullName: "owner/repo", + Contributors: []models.ContributorMetrics{ + { + Login: "consistent-worker", + CommitCount: 20, + WorkWeekStreak: 5, // 5-day work week streak + RepositoriesContributed: []string{"owner/repo"}, + }, + }, + }, + }, + } + + result := calc.Calculate(metrics) + + contributor := result.Repositories[0].Contributors[0] + // Should have earned work week streak achievements for 3 and 5 days + assert.Contains(t, contributor.Achievements, "workweek-3") + assert.Contains(t, contributor.Achievements, "workweek-5") +} + func TestContains(t *testing.T) { t.Parallel() diff --git a/internal/server/server.go b/internal/server/server.go index 54db762..90ba31f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,26 +24,13 @@ func New(directory, port string) *Server { // Start starts the HTTP server func (s *Server) Start() error { - // Check if directory exists - if _, err := os.Stat(s.directory); os.IsNotExist(err) { - return fmt.Errorf("directory does not exist: %s", s.directory) - } - - // Get absolute path - absPath, err := filepath.Abs(s.directory) + handler, err := s.CreateHandler() if err != nil { - return fmt.Errorf("failed to get absolute path: %w", err) + return err } - // Create file server with directory listing disabled for security - fs := http.FileServer(http.Dir(absPath)) - - // Wrap with middleware - handler := s.loggingMiddleware(s.cacheMiddleware(fs)) - - addr := fmt.Sprintf(":%s", s.port) srv := &http.Server{ - Addr: addr, + Addr: s.GetAddress(), Handler: handler, ReadTimeout: 15 * time.Second, ReadHeaderTimeout: 15 * time.Second, @@ -75,3 +62,34 @@ func (s *Server) cacheMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +// CreateHandler creates and returns the HTTP handler without starting the server. +// This is useful for testing and for embedding the server in other applications. +func (s *Server) CreateHandler() (http.Handler, error) { + // Check if directory exists + if _, err := os.Stat(s.directory); os.IsNotExist(err) { + return nil, fmt.Errorf("directory does not exist: %s", s.directory) + } + + // Get absolute path + absPath, err := filepath.Abs(s.directory) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Create file server with directory listing disabled for security + fs := http.FileServer(http.Dir(absPath)) + + // Wrap with middleware + return s.loggingMiddleware(s.cacheMiddleware(fs)), nil +} + +// GetAddress returns the server address in the format :port +func (s *Server) GetAddress() string { + return fmt.Sprintf(":%s", s.port) +} + +// GetDirectory returns the directory being served +func (s *Server) GetDirectory() string { + return s.directory +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 863ff24..6f10984 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -207,3 +207,231 @@ func TestServer_ServesIndexHtml(t *testing.T) { body, _ := io.ReadAll(resp.Body) assert.Contains(t, string(body), "Test Page") } + +func TestServer_CreateHandler(t *testing.T) { + tempDir := t.TempDir() + + // Create an index.html + indexFile := filepath.Join(tempDir, "index.html") + err := os.WriteFile(indexFile, []byte("Handler Test"), 0644) + require.NoError(t, err) + + s := New(tempDir, "8080") + + handler, err := s.CreateHandler() + require.NoError(t, err) + + ts := httptest.NewServer(handler) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(body), "Handler Test") + + // Check middleware headers are applied + assert.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get("Cache-Control")) + assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin")) +} + +func TestServer_CreateHandlerWithNonExistentDirectory(t *testing.T) { + t.Parallel() + + s := New("/this/directory/does/not/exist", "8080") + + handler, err := s.CreateHandler() + assert.Error(t, err) + assert.Nil(t, handler) + assert.Contains(t, err.Error(), "directory does not exist") +} + +func TestServer_GetAddress(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + port string + expected string + }{ + {"standard port", "8080", ":8080"}, + {"different port", "3000", ":3000"}, + {"port 0 for random", "0", ":0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := New(".", tt.port) + assert.Equal(t, tt.expected, s.GetAddress()) + }) + } +} + +func TestServer_GetDirectory(t *testing.T) { + t.Parallel() + + s := New("/some/path", "8080") + assert.Equal(t, "/some/path", s.GetDirectory()) +} + +func TestServer_ServesJSONWithCorrectContentType(t *testing.T) { + tempDir := t.TempDir() + + // Create a JSON file + jsonFile := filepath.Join(tempDir, "data.json") + err := os.WriteFile(jsonFile, []byte(`{"status": "ok"}`), 0644) + require.NoError(t, err) + + s := New(tempDir, "0") + handler, err := s.CreateHandler() + require.NoError(t, err) + + ts := httptest.NewServer(handler) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/data.json") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + // Check content type is JSON + contentType := resp.Header.Get("Content-Type") + assert.Contains(t, contentType, "application/json") +} + +func TestServer_ServesHTMLWithCorrectContentType(t *testing.T) { + tempDir := t.TempDir() + + // Create an HTML file + htmlFile := filepath.Join(tempDir, "page.html") + err := os.WriteFile(htmlFile, []byte("HTML Page"), 0644) + require.NoError(t, err) + + s := New(tempDir, "0") + handler, err := s.CreateHandler() + require.NoError(t, err) + + ts := httptest.NewServer(handler) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/page.html") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + // Check content type is HTML + contentType := resp.Header.Get("Content-Type") + assert.Contains(t, contentType, "text/html") +} + +func TestServer_CORSHeaders(t *testing.T) { + tempDir := t.TempDir() + + // Create a test file + testFile := filepath.Join(tempDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + require.NoError(t, err) + + s := New(tempDir, "0") + handler, err := s.CreateHandler() + require.NoError(t, err) + + ts := httptest.NewServer(handler) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/test.txt") + require.NoError(t, err) + defer resp.Body.Close() + + // Check CORS header + assert.Equal(t, "*", resp.Header.Get("Access-Control-Allow-Origin")) +} + +func TestServer_CacheDisabledHeaders(t *testing.T) { + tempDir := t.TempDir() + + // Create a test file + testFile := filepath.Join(tempDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + require.NoError(t, err) + + s := New(tempDir, "0") + handler, err := s.CreateHandler() + require.NoError(t, err) + + ts := httptest.NewServer(handler) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/test.txt") + require.NoError(t, err) + defer resp.Body.Close() + + // Check cache headers are disabled for development + assert.Equal(t, "no-cache, no-store, must-revalidate", resp.Header.Get("Cache-Control")) + assert.Equal(t, "no-cache", resp.Header.Get("Pragma")) + assert.Equal(t, "0", resp.Header.Get("Expires")) +} + +func TestServer_LoggingMiddlewareWithDifferentMethods(t *testing.T) { + t.Parallel() + + s := New(".", "8080") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + wrapped := s.loggingMiddleware(handler) + + methods := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"} + for _, method := range methods { + t.Run(method, func(t *testing.T) { + req := httptest.NewRequest(method, "/test-path", nil) + rr := httptest.NewRecorder() + + wrapped.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) + } +} + +func TestServer_CacheMiddlewarePreservesResponseBody(t *testing.T) { + t.Parallel() + + s := New(".", "8080") + + expectedBody := "This is the response body content" + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(expectedBody)) + }) + + wrapped := s.cacheMiddleware(handler) + + req := httptest.NewRequest("GET", "/test", nil) + rr := httptest.NewRecorder() + + wrapped.ServeHTTP(rr, req) + + body, _ := io.ReadAll(rr.Body) + assert.Equal(t, expectedBody, string(body)) +} + +func TestNew_WithEmptyValues(t *testing.T) { + t.Parallel() + + s := New("", "") + assert.Equal(t, "", s.directory) + assert.Equal(t, "", s.port) +} + +func TestNew_WithSpecialCharactersInPath(t *testing.T) { + t.Parallel() + + path := "/path/with spaces/and-dashes/and_underscores" + s := New(path, "8080") + assert.Equal(t, path, s.directory) +}