package site import ( "os" "path/filepath" "testing" "time" json "github.com/goccy/go-json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/lukaszraczylo/git-velocity/internal/config" "github.com/lukaszraczylo/git-velocity/internal/domain/models" ) func TestNewGenerator(t *testing.T) { t.Parallel() cfg := config.DefaultConfig() gen, err := NewGenerator("/tmp/output", cfg) require.NoError(t, err) assert.NotNil(t, gen) assert.Equal(t, "/tmp/output", gen.outputDir) assert.Equal(t, cfg, gen.config) } func TestGenerator_GenerateCreatesOutputDir(t *testing.T) { tempDir := t.TempDir() outputDir := filepath.Join(tempDir, "new-output") cfg := config.DefaultConfig() gen, err := NewGenerator(outputDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Period: models.Period{ Start: time.Now().Add(-24 * time.Hour), End: time.Now(), }, } err = gen.Generate(metrics) require.NoError(t, err) // Verify output directory was created info, err := os.Stat(outputDir) require.NoError(t, err) assert.True(t, info.IsDir()) } func TestGenerator_GenerateCreatesDataDir(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{} err = gen.Generate(metrics) require.NoError(t, err) // Verify data directory was created dataDir := filepath.Join(tempDir, "data") info, err := os.Stat(dataDir) require.NoError(t, err) assert.True(t, info.IsDir()) } func TestGenerator_GenerateGlobalJSON(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ TotalContributors: 5, TotalCommits: 100, TotalPRs: 50, TotalReviews: 75, TotalLinesAdded: 10000, TotalLinesDeleted: 5000, } err = gen.Generate(metrics) require.NoError(t, err) // Read and verify global.json globalPath := filepath.Join(tempDir, "data", "global.json") data, err := os.ReadFile(globalPath) require.NoError(t, err) var result struct { TotalContributors int `json:"total_contributors"` TotalCommits int `json:"total_commits"` TotalPRs int `json:"total_prs"` TotalReviews int `json:"total_reviews"` TotalLinesAdded int `json:"total_lines_added"` TotalLinesDeleted int `json:"total_lines_deleted"` GeneratedAt time.Time `json:"generated_at"` } err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, 5, result.TotalContributors) assert.Equal(t, 100, result.TotalCommits) assert.Equal(t, 50, result.TotalPRs) assert.Equal(t, 75, result.TotalReviews) assert.Equal(t, 10000, result.TotalLinesAdded) assert.Equal(t, 5000, result.TotalLinesDeleted) assert.False(t, result.GeneratedAt.IsZero()) } func TestGenerator_GenerateLeaderboardJSON(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Leaderboard: []models.LeaderboardEntry{ {Rank: 1, Login: "user1", Score: 1000}, {Rank: 2, Login: "user2", Score: 800}, {Rank: 3, Login: "user3", Score: 600}, }, } err = gen.Generate(metrics) require.NoError(t, err) // Read and verify leaderboard.json leaderboardPath := filepath.Join(tempDir, "data", "leaderboard.json") data, err := os.ReadFile(leaderboardPath) require.NoError(t, err) var result []models.LeaderboardEntry err = json.Unmarshal(data, &result) require.NoError(t, err) require.Len(t, result, 3) assert.Equal(t, "user1", result[0].Login) assert.Equal(t, 1000, result[0].Score) assert.Equal(t, "user2", result[1].Login) assert.Equal(t, 800, result[1].Score) } func TestGenerator_GenerateRepositoryJSON(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Repositories: []models.RepositoryMetrics{ { Owner: "myorg", Name: "myrepo", TotalCommits: 42, TotalPRs: 10, }, }, } err = gen.Generate(metrics) require.NoError(t, err) // Read and verify repository metrics repoPath := filepath.Join(tempDir, "data", "repos", "myorg", "myrepo", "metrics.json") data, err := os.ReadFile(repoPath) require.NoError(t, err) var result models.RepositoryMetrics err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, "myorg", result.Owner) assert.Equal(t, "myrepo", result.Name) assert.Equal(t, 42, result.TotalCommits) assert.Equal(t, 10, result.TotalPRs) } func TestGenerator_GenerateMultipleRepositories(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Repositories: []models.RepositoryMetrics{ {Owner: "org1", Name: "repo1", TotalCommits: 100}, {Owner: "org1", Name: "repo2", TotalCommits: 200}, {Owner: "org2", Name: "repo3", TotalCommits: 300}, }, } err = gen.Generate(metrics) require.NoError(t, err) // Verify all repository files exist _, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo1", "metrics.json")) assert.NoError(t, err) _, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo2", "metrics.json")) assert.NoError(t, err) _, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org2", "repo3", "metrics.json")) assert.NoError(t, err) } func TestGenerator_GenerateTeamJSON(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Teams: []models.TeamMetrics{ { Name: "Backend Team", Color: "#ff0000", Members: []string{"user1", "user2"}, TotalScore: 1500, }, }, } err = gen.Generate(metrics) require.NoError(t, err) // Read and verify team JSON (slugified name) teamPath := filepath.Join(tempDir, "data", "teams", "backend-team.json") data, err := os.ReadFile(teamPath) require.NoError(t, err) var result models.TeamMetrics err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, "Backend Team", result.Name) assert.Equal(t, "#ff0000", result.Color) assert.Equal(t, 1500, result.TotalScore) assert.Len(t, result.Members, 2) } func TestGenerator_GenerateContributorJSON(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) // Generator now uses global Contributors, not per-repo Contributors metrics := &models.GlobalMetrics{ Contributors: []models.ContributorMetrics{ { Login: "john-doe", Name: "John Doe", CommitCount: 50, PRsOpened: 10, }, }, } err = gen.Generate(metrics) require.NoError(t, err) // Read and verify contributor JSON contributorPath := filepath.Join(tempDir, "data", "contributors", "john-doe.json") data, err := os.ReadFile(contributorPath) require.NoError(t, err) var result models.ContributorMetrics err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, "john-doe", result.Login) assert.Equal(t, "John Doe", result.Name) assert.Equal(t, 50, result.CommitCount) assert.Equal(t, 10, result.PRsOpened) } func TestGenerator_UsesGlobalContributorsNotPerRepo(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) // Same contributor in multiple repos with different per-repo stats // But GlobalMetrics.Contributors should have AGGREGATED stats metrics := &models.GlobalMetrics{ // Per-repo data (used for repository-specific pages) Repositories: []models.RepositoryMetrics{ { Owner: "org", Name: "repo1", Contributors: []models.ContributorMetrics{ {Login: "user1", CommitCount: 50, PRsOpened: 5}, }, }, { Owner: "org", Name: "repo2", Contributors: []models.ContributorMetrics{ {Login: "user1", CommitCount: 75, PRsOpened: 10}, // Same user, different count }, }, }, // Global aggregated data (this is what the generator should use for contributor files) Contributors: []models.ContributorMetrics{ {Login: "user1", CommitCount: 125, PRsOpened: 15}, // Sum: 50+75=125, 5+10=15 }, } err = gen.Generate(metrics) require.NoError(t, err) // Contributor file should have AGGREGATED data from GlobalMetrics.Contributors contributorPath := filepath.Join(tempDir, "data", "contributors", "user1.json") data, err := os.ReadFile(contributorPath) require.NoError(t, err) var result models.ContributorMetrics err = json.Unmarshal(data, &result) require.NoError(t, err) // Should be the aggregated count (125 commits, 15 PRs), NOT 50 or 75 assert.Equal(t, 125, result.CommitCount, "Should use aggregated commits from GlobalMetrics.Contributors") assert.Equal(t, 15, result.PRsOpened, "Should use aggregated PRs from GlobalMetrics.Contributors") } func TestGenerator_MultipleContributorsAcrossRepos(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) // Multiple contributors across multiple repos with aggregated global data metrics := &models.GlobalMetrics{ Repositories: []models.RepositoryMetrics{ { Owner: "org", Name: "repo1", Contributors: []models.ContributorMetrics{ {Login: "alice", CommitCount: 100, LinesAdded: 5000}, {Login: "bob", CommitCount: 50, LinesAdded: 2000}, }, }, { Owner: "org", Name: "repo2", Contributors: []models.ContributorMetrics{ {Login: "alice", CommitCount: 50, LinesAdded: 3000}, {Login: "charlie", CommitCount: 75, LinesAdded: 4000}, }, }, }, // Aggregated global contributors Contributors: []models.ContributorMetrics{ {Login: "alice", CommitCount: 150, LinesAdded: 8000}, // 100+50, 5000+3000 {Login: "bob", CommitCount: 50, LinesAdded: 2000}, // Only in repo1 {Login: "charlie", CommitCount: 75, LinesAdded: 4000}, // Only in repo2 }, } err = gen.Generate(metrics) require.NoError(t, err) // Verify alice has aggregated data alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json") aliceData, err := os.ReadFile(alicePath) require.NoError(t, err) var aliceResult models.ContributorMetrics err = json.Unmarshal(aliceData, &aliceResult) require.NoError(t, err) assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits") assert.Equal(t, 8000, aliceResult.LinesAdded, "Alice should have aggregated lines added") // Verify bob exists with his data bobPath := filepath.Join(tempDir, "data", "contributors", "bob.json") bobData, err := os.ReadFile(bobPath) require.NoError(t, err) var bobResult models.ContributorMetrics err = json.Unmarshal(bobData, &bobResult) require.NoError(t, err) assert.Equal(t, 50, bobResult.CommitCount) assert.Equal(t, 2000, bobResult.LinesAdded) // Verify charlie exists with his data charliePath := filepath.Join(tempDir, "data", "contributors", "charlie.json") charlieData, err := os.ReadFile(charliePath) require.NoError(t, err) var charlieResult models.ContributorMetrics err = json.Unmarshal(charlieData, &charlieResult) require.NoError(t, err) assert.Equal(t, 75, charlieResult.CommitCount) assert.Equal(t, 4000, charlieResult.LinesAdded) } func TestGenerator_NoTeamsDoesNotCreateTeamDir(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) metrics := &models.GlobalMetrics{ Teams: []models.TeamMetrics{}, // Empty teams } err = gen.Generate(metrics) require.NoError(t, err) // Team directory should not exist teamDir := filepath.Join(tempDir, "data", "teams") _, err = os.Stat(teamDir) assert.True(t, os.IsNotExist(err)) } func TestSlugify(t *testing.T) { t.Parallel() tests := []struct { input string expected string }{ {"Backend Team", "backend-team"}, {"Frontend_Team", "frontend-team"}, {"UPPER CASE", "upper-case"}, {"already-slug", "already-slug"}, {"Multiple Spaces", "multiple---spaces"}, {"Mixed_And Spaced", "mixed-and-spaced"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { t.Parallel() result := slugify(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestWriteJSON(t *testing.T) { tempDir := t.TempDir() testData := map[string]interface{}{ "key": "value", "number": 42, "nested": map[string]string{ "inner": "data", }, } path := filepath.Join(tempDir, "test.json") err := writeJSON(path, testData) require.NoError(t, err) // Verify file was created and is valid JSON data, err := os.ReadFile(path) require.NoError(t, err) var result map[string]interface{} err = json.Unmarshal(data, &result) require.NoError(t, err) assert.Equal(t, "value", result["key"]) assert.Equal(t, float64(42), result["number"]) // JSON numbers are float64 } func TestWriteJSON_Indented(t *testing.T) { tempDir := t.TempDir() testData := map[string]string{"key": "value"} path := filepath.Join(tempDir, "test.json") err := writeJSON(path, testData) require.NoError(t, err) data, err := os.ReadFile(path) require.NoError(t, err) // Should be formatted with indentation assert.Contains(t, string(data), "\n") assert.Contains(t, string(data), " ") // 2-space indent } func TestWriteJSON_ErrorOnInvalidPath(t *testing.T) { // Try to write to a path that doesn't exist path := "/nonexistent/directory/test.json" err := writeJSON(path, "data") assert.Error(t, err) } func TestGenerator_GenerateWithFullMetrics(t *testing.T) { tempDir := t.TempDir() cfg := config.DefaultConfig() gen, err := NewGenerator(tempDir, cfg) require.NoError(t, err) // Create comprehensive metrics metrics := &models.GlobalMetrics{ Period: models.Period{ Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), End: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), Granularity: "monthly", Label: "2024", }, TotalContributors: 10, TotalCommits: 500, TotalPRs: 100, TotalReviews: 200, TotalLinesAdded: 50000, TotalLinesDeleted: 25000, Repositories: []models.RepositoryMetrics{ { Owner: "org", Name: "repo1", TotalCommits: 300, TotalPRs: 60, ActiveContributors: 5, Contributors: []models.ContributorMetrics{ {Login: "alice", Name: "Alice", CommitCount: 100}, {Login: "bob", Name: "Bob", CommitCount: 200}, }, }, { Owner: "org", Name: "repo2", TotalCommits: 200, TotalPRs: 40, ActiveContributors: 5, Contributors: []models.ContributorMetrics{ {Login: "alice", Name: "Alice", CommitCount: 50}, {Login: "charlie", Name: "Charlie", CommitCount: 150}, }, }, }, // Global aggregated contributors (used for individual contributor files) Contributors: []models.ContributorMetrics{ {Login: "alice", Name: "Alice", CommitCount: 150}, // 100+50 aggregated {Login: "bob", Name: "Bob", CommitCount: 200}, // Only in repo1 {Login: "charlie", Name: "Charlie", CommitCount: 150}, // Only in repo2 }, Teams: []models.TeamMetrics{ { Name: "Core Team", Members: []string{"alice", "bob"}, TotalScore: 5000, }, }, Leaderboard: []models.LeaderboardEntry{ {Rank: 1, Login: "alice", Score: 3000}, {Rank: 2, Login: "bob", Score: 2000}, {Rank: 3, Login: "charlie", Score: 1500}, }, } err = gen.Generate(metrics) require.NoError(t, err) // Verify all expected files exist expectedPaths := []string{ filepath.Join(tempDir, "data", "global.json"), filepath.Join(tempDir, "data", "leaderboard.json"), filepath.Join(tempDir, "data", "repos", "org", "repo1", "metrics.json"), filepath.Join(tempDir, "data", "repos", "org", "repo2", "metrics.json"), filepath.Join(tempDir, "data", "teams", "core-team.json"), filepath.Join(tempDir, "data", "contributors", "alice.json"), filepath.Join(tempDir, "data", "contributors", "bob.json"), filepath.Join(tempDir, "data", "contributors", "charlie.json"), } for _, path := range expectedPaths { _, err := os.Stat(path) assert.NoError(t, err, "Expected file to exist: %s", path) } // Verify alice's file has aggregated data (150 commits, not 100 from first repo) alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json") aliceData, err := os.ReadFile(alicePath) require.NoError(t, err) var aliceResult models.ContributorMetrics err = json.Unmarshal(aliceData, &aliceResult) require.NoError(t, err) assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits from global Contributors") }