mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-07-05 06:05:03 +00:00
Additional checks on issues.
This commit is contained in:
@@ -408,6 +408,63 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
|
||||
}
|
||||
}
|
||||
|
||||
// Process issue comments
|
||||
for _, comment := range data.IssueComments {
|
||||
login := comment.Author.Login
|
||||
if login == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Initialize contributor if needed
|
||||
if _, ok := contributorMap[login]; !ok {
|
||||
contributorMap[login] = &models.ContributorMetrics{
|
||||
Login: login,
|
||||
Period: period,
|
||||
}
|
||||
}
|
||||
|
||||
cm := contributorMap[login]
|
||||
cm.IssueComments++
|
||||
|
||||
// Track repository participation
|
||||
if !contains(cm.RepositoriesContributed, comment.Repository) {
|
||||
cm.RepositoriesContributed = append(cm.RepositoriesContributed, comment.Repository)
|
||||
}
|
||||
|
||||
// Update per-repo contributor metrics
|
||||
rcm := getRepoContributor(comment.Repository, login, cm.Name, cm.AvatarURL)
|
||||
rcm.IssueComments++
|
||||
}
|
||||
|
||||
// Count issue references in commits (e.g., "fixes #123", "closes #456", "refs #789")
|
||||
for _, commit := range data.Commits {
|
||||
login := commit.Author.Login
|
||||
if login == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Normalize login
|
||||
if mappedLogin, ok := emailToLogin[commit.Author.Email]; ok {
|
||||
login = mappedLogin
|
||||
}
|
||||
if mappedLogin, ok := loginToLogin[login]; ok {
|
||||
login = mappedLogin
|
||||
}
|
||||
|
||||
// Count issue references in commit message
|
||||
issueRefCount := countIssueReferences(commit.Message)
|
||||
if issueRefCount > 0 {
|
||||
if cm, ok := contributorMap[login]; ok {
|
||||
cm.IssueReferencesInCommits += issueRefCount
|
||||
}
|
||||
|
||||
// Update per-repo contributor metrics
|
||||
if rcm, ok := repoContributorMap[commit.Repository][login]; ok {
|
||||
rcm.IssueReferencesInCommits += issueRefCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate averages and finalize contributor metrics
|
||||
for login, cm := range contributorMap {
|
||||
// Calculate average time to merge
|
||||
@@ -1272,7 +1329,6 @@ func calculateStreaks(days map[string]bool) (longest, current int) {
|
||||
|
||||
// Calculate streaks
|
||||
longest = 1
|
||||
current = 1
|
||||
streak := 1
|
||||
|
||||
for i := 1; i < len(dates); i++ {
|
||||
@@ -1300,3 +1356,32 @@ func calculateStreaks(days map[string]bool) (longest, current int) {
|
||||
|
||||
return longest, current
|
||||
}
|
||||
|
||||
// countIssueReferences counts the number of issue references in a commit message
|
||||
// Detects patterns like: fixes #123, closes #456, resolves #789, refs #12, etc.
|
||||
func countIssueReferences(message string) int {
|
||||
count := 0
|
||||
|
||||
// Count all #<number> patterns in the message
|
||||
// This covers both keyword-prefixed references (fixes #123, closes #456)
|
||||
// and standalone mentions (see #123, just #123)
|
||||
// We only count each unique position once
|
||||
for i := 0; i < len(message); i++ {
|
||||
if message[i] == '#' && i+1 < len(message) {
|
||||
// Check for digits after #
|
||||
hasDigits := false
|
||||
for j := i + 1; j < len(message); j++ {
|
||||
if message[j] >= '0' && message[j] <= '9' {
|
||||
hasDigits = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasDigits {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -876,3 +876,248 @@ func TestBuildEmailToLoginMapping_NoReplyEmailWithoutID(t *testing.T) {
|
||||
// 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"])
|
||||
}
|
||||
|
||||
func TestCountIssueReferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
message string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "no references",
|
||||
message: "Just a regular commit message",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "fixes issue",
|
||||
message: "fixes #123",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "Fixes issue uppercase",
|
||||
message: "Fixes #456",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "closes issue",
|
||||
message: "closes #789",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "resolves issue",
|
||||
message: "resolves #101",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "refs issue",
|
||||
message: "refs #202",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "ref issue",
|
||||
message: "ref #303",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple fixes",
|
||||
message: "fixes #1, fixes #2, fixes #3",
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "mixed keywords",
|
||||
message: "fixes #1 and closes #2",
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "standalone issue reference",
|
||||
message: "Related to #123",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "multiple standalone references",
|
||||
message: "See #1 and #2 for context",
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "fix with extra whitespace",
|
||||
message: "fix #123",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "closed past tense",
|
||||
message: "closed #123",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "fixed past tense",
|
||||
message: "fixed #456",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "resolved past tense",
|
||||
message: "resolved #789",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "close without s",
|
||||
message: "close #123",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "fix without es",
|
||||
message: "fix #456",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "resolve without s",
|
||||
message: "resolve #789",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "hash without number",
|
||||
message: "This is about # something",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "complex commit message",
|
||||
message: "feat: Add new feature\n\nThis implements the feature requested in #123.\nAlso fixes #456 and closes #789.",
|
||||
expected: 3,
|
||||
},
|
||||
{
|
||||
name: "PR style reference",
|
||||
message: "Merge pull request #100 from feature-branch",
|
||||
expected: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := countIssueReferences(tt.message)
|
||||
assert.Equal(t, tt.expected, result, "message: %s", tt.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregator_IssueComments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
agg := New(cfg)
|
||||
|
||||
data := &models.RawData{
|
||||
// Need a commit to create the repository
|
||||
Commits: []models.Commit{
|
||||
{
|
||||
SHA: "abc123",
|
||||
Author: models.Author{Login: "user1"},
|
||||
Repository: "owner/repo",
|
||||
},
|
||||
},
|
||||
IssueComments: []models.IssueComment{
|
||||
{
|
||||
ID: 1,
|
||||
Issue: 1,
|
||||
Repository: "owner/repo",
|
||||
Author: models.Author{Login: "user1"},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Issue: 1,
|
||||
Repository: "owner/repo",
|
||||
Author: models.Author{Login: "user1"},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Issue: 2,
|
||||
Repository: "owner/repo",
|
||||
Author: models.Author{Login: "user2"},
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dateRange := &config.ParsedDateRange{}
|
||||
|
||||
metrics, err := agg.Aggregate(data, dateRange)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that issue comments are counted
|
||||
require.Len(t, metrics.Repositories, 1)
|
||||
repo := metrics.Repositories[0]
|
||||
|
||||
// Find user1 and user2
|
||||
var user1, user2 *models.ContributorMetrics
|
||||
for i := range repo.Contributors {
|
||||
if repo.Contributors[i].Login == "user1" {
|
||||
user1 = &repo.Contributors[i]
|
||||
}
|
||||
if repo.Contributors[i].Login == "user2" {
|
||||
user2 = &repo.Contributors[i]
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, user1)
|
||||
assert.Equal(t, 2, user1.IssueComments) // user1 has 2 comments
|
||||
|
||||
require.NotNil(t, user2)
|
||||
assert.Equal(t, 1, user2.IssueComments) // user2 has 1 comment
|
||||
}
|
||||
|
||||
func TestAggregator_IssueReferencesInCommits(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
agg := New(cfg)
|
||||
|
||||
data := &models.RawData{
|
||||
Commits: []models.Commit{
|
||||
{
|
||||
SHA: "abc123",
|
||||
Message: "fixes #1 and closes #2",
|
||||
Author: models.Author{Login: "user1"},
|
||||
Repository: "owner/repo",
|
||||
},
|
||||
{
|
||||
SHA: "def456",
|
||||
Message: "Regular commit without issue refs",
|
||||
Author: models.Author{Login: "user1"},
|
||||
Repository: "owner/repo",
|
||||
},
|
||||
{
|
||||
SHA: "ghi789",
|
||||
Message: "resolves #3",
|
||||
Author: models.Author{Login: "user2"},
|
||||
Repository: "owner/repo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dateRange := &config.ParsedDateRange{}
|
||||
|
||||
metrics, err := agg.Aggregate(data, dateRange)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, metrics.Repositories, 1)
|
||||
repo := metrics.Repositories[0]
|
||||
|
||||
// Find user1 and user2
|
||||
var user1, user2 *models.ContributorMetrics
|
||||
for i := range repo.Contributors {
|
||||
if repo.Contributors[i].Login == "user1" {
|
||||
user1 = &repo.Contributors[i]
|
||||
}
|
||||
if repo.Contributors[i].Login == "user2" {
|
||||
user2 = &repo.Contributors[i]
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, user1)
|
||||
assert.Equal(t, 2, user1.IssueReferencesInCommits) // user1 has 2 issue references (fixes #1, closes #2)
|
||||
|
||||
require.NotNil(t, user2)
|
||||
assert.Equal(t, 1, user2.IssueReferencesInCommits) // user2 has 1 issue reference (resolves #3)
|
||||
}
|
||||
|
||||
@@ -267,6 +267,19 @@ func (a *App) collectRepoData(ctx context.Context, owner, name string, dateRange
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch issue comments
|
||||
issueComments, err := a.client.FetchIssueComments(ctx, owner, name, dateRange.Start, dateRange.End)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch issue comments: %w", err)
|
||||
}
|
||||
a.log(" Found %d issue comments", len(issueComments))
|
||||
|
||||
for _, comment := range issueComments {
|
||||
if !a.config.IsBot(comment.Author.Login) {
|
||||
data.IssueComments = append(data.IssueComments, comment)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ date_range:
|
||||
// Create temp config file
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
err := os.WriteFile(configPath, []byte(tt.configYAML), 0644)
|
||||
err := os.WriteFile(configPath, []byte(tt.configYAML), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load config
|
||||
@@ -940,7 +940,7 @@ func TestLoad_FileNotFound(t *testing.T) {
|
||||
func TestLoad_InvalidYAML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
err := os.WriteFile(configPath, []byte("invalid: yaml: content: ["), 0644)
|
||||
err := os.WriteFile(configPath, []byte("invalid: yaml: content: ["), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = Load(configPath)
|
||||
|
||||
@@ -84,6 +84,8 @@ type PointsConfig struct {
|
||||
ReviewComment int `yaml:"review_comment"` // PR review comments (not code comments)
|
||||
IssueOpened int `yaml:"issue_opened"`
|
||||
IssueClosed int `yaml:"issue_closed"`
|
||||
IssueComment int `yaml:"issue_comment"` // Commenting on an issue
|
||||
IssueReference int `yaml:"issue_reference_commit"` // Commit referencing an issue (fixes #123, etc.)
|
||||
FastReview1h int `yaml:"fast_review_1h"`
|
||||
FastReview4h int `yaml:"fast_review_4h"`
|
||||
FastReview24h int `yaml:"fast_review_24h"`
|
||||
@@ -197,8 +199,10 @@ func DefaultConfig() *Config {
|
||||
PRMerged: 50,
|
||||
PRReviewed: 30,
|
||||
ReviewComment: 5,
|
||||
IssueOpened: 15,
|
||||
IssueOpened: 10,
|
||||
IssueClosed: 20,
|
||||
IssueComment: 5,
|
||||
IssueReference: 5,
|
||||
FastReview1h: 50,
|
||||
FastReview4h: 25,
|
||||
FastReview24h: 10,
|
||||
@@ -371,5 +375,33 @@ func defaultAchievements() []AchievementConfig {
|
||||
{ID: "docs-del-500", Name: "Dead Code Hunter", Description: "Removed 500 lines of outdated comments", Icon: "fa-skull-crossbones", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 500}},
|
||||
{ID: "docs-del-1000", Name: "Comment Surgeon", Description: "Removed 1000 lines of outdated comments", Icon: "fa-scalpel", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 1000}},
|
||||
{ID: "docs-del-2500", Name: "Noise Eliminator", Description: "Removed 2500 lines of outdated comments", Icon: "fa-volume-xmark", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 2500}},
|
||||
|
||||
// ===== ISSUES OPENED (Tiers: 1, 5, 10, 25, 50) =====
|
||||
{ID: "issue-1", Name: "Bug Hunter", Description: "Opened your first issue", Icon: "fa-bug", Condition: AchievementCondition{Type: "issues_opened", Threshold: 1}},
|
||||
{ID: "issue-5", Name: "Issue Reporter", Description: "Opened 5 issues", Icon: "fa-flag", Condition: AchievementCondition{Type: "issues_opened", Threshold: 5}},
|
||||
{ID: "issue-10", Name: "Quality Advocate", Description: "Opened 10 issues", Icon: "fa-clipboard-list", Condition: AchievementCondition{Type: "issues_opened", Threshold: 10}},
|
||||
{ID: "issue-25", Name: "Issue Expert", Description: "Opened 25 issues", Icon: "fa-list-check", Condition: AchievementCondition{Type: "issues_opened", Threshold: 25}},
|
||||
{ID: "issue-50", Name: "Issue Champion", Description: "Opened 50 issues", Icon: "fa-bullhorn", Condition: AchievementCondition{Type: "issues_opened", Threshold: 50}},
|
||||
|
||||
// ===== ISSUES CLOSED (Tiers: 1, 5, 10, 25, 50) =====
|
||||
{ID: "issue-close-1", Name: "Problem Solver", Description: "Closed your first issue", Icon: "fa-circle-check", Condition: AchievementCondition{Type: "issues_closed", Threshold: 1}},
|
||||
{ID: "issue-close-5", Name: "Bug Squasher", Description: "Closed 5 issues", Icon: "fa-bug-slash", Condition: AchievementCondition{Type: "issues_closed", Threshold: 5}},
|
||||
{ID: "issue-close-10", Name: "Issue Resolver", Description: "Closed 10 issues", Icon: "fa-check-double", Condition: AchievementCondition{Type: "issues_closed", Threshold: 10}},
|
||||
{ID: "issue-close-25", Name: "Closure Expert", Description: "Closed 25 issues", Icon: "fa-square-check", Condition: AchievementCondition{Type: "issues_closed", Threshold: 25}},
|
||||
{ID: "issue-close-50", Name: "Issue Terminator", Description: "Closed 50 issues", Icon: "fa-crosshairs", Condition: AchievementCondition{Type: "issues_closed", Threshold: 50}},
|
||||
|
||||
// ===== ISSUE COMMENTS (Tiers: 5, 10, 25, 50, 100) =====
|
||||
{ID: "issue-comment-5", Name: "Issue Commenter", Description: "Left 5 issue comments", Icon: "fa-comment", Condition: AchievementCondition{Type: "issue_comments", Threshold: 5}},
|
||||
{ID: "issue-comment-10", Name: "Discussion Starter", Description: "Left 10 issue comments", Icon: "fa-comments", Condition: AchievementCondition{Type: "issue_comments", Threshold: 10}},
|
||||
{ID: "issue-comment-25", Name: "Issue Collaborator", Description: "Left 25 issue comments", Icon: "fa-people-arrows", Condition: AchievementCondition{Type: "issue_comments", Threshold: 25}},
|
||||
{ID: "issue-comment-50", Name: "Community Voice", Description: "Left 50 issue comments", Icon: "fa-bullhorn", Condition: AchievementCondition{Type: "issue_comments", Threshold: 50}},
|
||||
{ID: "issue-comment-100", Name: "Issue Guru", Description: "Left 100 issue comments", Icon: "fa-graduation-cap", Condition: AchievementCondition{Type: "issue_comments", Threshold: 100}},
|
||||
|
||||
// ===== ISSUE REFERENCES IN COMMITS (Tiers: 5, 10, 25, 50, 100) =====
|
||||
{ID: "issue-ref-5", Name: "Issue Linker", Description: "Referenced issues in 5 commits", Icon: "fa-link", Condition: AchievementCondition{Type: "issue_references", Threshold: 5}},
|
||||
{ID: "issue-ref-10", Name: "Commit Connector", Description: "Referenced issues in 10 commits", Icon: "fa-diagram-project", Condition: AchievementCondition{Type: "issue_references", Threshold: 10}},
|
||||
{ID: "issue-ref-25", Name: "Traceability Pro", Description: "Referenced issues in 25 commits", Icon: "fa-sitemap", Condition: AchievementCondition{Type: "issue_references", Threshold: 25}},
|
||||
{ID: "issue-ref-50", Name: "Issue Tracker", Description: "Referenced issues in 50 commits", Icon: "fa-chart-gantt", Condition: AchievementCondition{Type: "issue_references", Threshold: 50}},
|
||||
{ID: "issue-ref-100", Name: "Traceability Master", Description: "Referenced issues in 100 commits", Icon: "fa-network-wired", Condition: AchievementCondition{Type: "issue_references", Threshold: 100}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,10 @@ type ContributorMetrics struct {
|
||||
AvgReviewTime float64 `json:"avg_review_time_hours"`
|
||||
|
||||
// Issue metrics
|
||||
IssuesOpened int `json:"issues_opened"`
|
||||
IssuesClosed int `json:"issues_closed"`
|
||||
IssueComments int `json:"issue_comments"`
|
||||
IssuesOpened int `json:"issues_opened"`
|
||||
IssuesClosed int `json:"issues_closed"`
|
||||
IssueComments int `json:"issue_comments"`
|
||||
IssueReferencesInCommits int `json:"issue_references_in_commits"` // Commits referencing issues (fixes #123, etc.)
|
||||
|
||||
// Activity patterns
|
||||
ActiveDays int `json:"active_days"` // Unique days with activity
|
||||
@@ -87,6 +88,7 @@ type ScoreBreakdown struct {
|
||||
PRs int `json:"prs"`
|
||||
Reviews int `json:"reviews"`
|
||||
Comments int `json:"comments"` // PR review comments (not code comments)
|
||||
Issues int `json:"issues"` // Issue-related points (opened, closed, comments, references)
|
||||
ResponseBonus int `json:"response_bonus"`
|
||||
LineChanges int `json:"line_changes"`
|
||||
OutOfHours int `json:"out_of_hours"` // Bonus for out-of-hours commits
|
||||
|
||||
@@ -2,8 +2,9 @@ package models
|
||||
|
||||
// RawData holds the raw collected data from GitHub
|
||||
type RawData struct {
|
||||
Commits []Commit
|
||||
PullRequests []PullRequest
|
||||
Reviews []Review
|
||||
Issues []Issue
|
||||
Commits []Commit
|
||||
PullRequests []PullRequest
|
||||
Reviews []Review
|
||||
Issues []Issue
|
||||
IssueComments []IssueComment
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
|
||||
existing.PRsMerged += cm.PRsMerged
|
||||
existing.ReviewsGiven += cm.ReviewsGiven
|
||||
existing.ReviewComments += cm.ReviewComments
|
||||
// Issue metrics
|
||||
existing.IssuesOpened += cm.IssuesOpened
|
||||
existing.IssuesClosed += cm.IssuesClosed
|
||||
existing.IssueComments += cm.IssueComments
|
||||
existing.IssueReferencesInCommits += cm.IssueReferencesInCommits
|
||||
// Combine unique repositories
|
||||
for _, r := range cm.RepositoriesContributed {
|
||||
if !contains(existing.RepositoriesContributed, r) {
|
||||
@@ -181,6 +186,12 @@ func (c *Calculator) calculateScore(cm *models.ContributorMetrics) models.Score
|
||||
// Comment points (PR review comments)
|
||||
breakdown.Comments = cm.ReviewComments * points.ReviewComment
|
||||
|
||||
// Issue points
|
||||
breakdown.Issues = cm.IssuesOpened*points.IssueOpened +
|
||||
cm.IssuesClosed*points.IssueClosed +
|
||||
cm.IssueComments*points.IssueComment +
|
||||
cm.IssueReferencesInCommits*points.IssueReference
|
||||
|
||||
// Response time bonus
|
||||
if cm.ReviewsGiven > 0 && cm.AvgReviewTime > 0 {
|
||||
if cm.AvgReviewTime <= 1 {
|
||||
@@ -197,7 +208,8 @@ func (c *Calculator) calculateScore(cm *models.ContributorMetrics) models.Score
|
||||
|
||||
// Calculate total
|
||||
total := breakdown.Commits + breakdown.LineChanges + breakdown.PRs +
|
||||
breakdown.Reviews + breakdown.ResponseBonus + breakdown.Comments + breakdown.OutOfHours
|
||||
breakdown.Reviews + breakdown.ResponseBonus + breakdown.Comments +
|
||||
breakdown.Issues + breakdown.OutOfHours
|
||||
|
||||
return models.Score{
|
||||
Total: total,
|
||||
@@ -265,6 +277,15 @@ func (c *Calculator) checkAchievements(cm *models.ContributorMetrics) []string {
|
||||
earned = float64(cm.CommentLinesAdded) >= ach.Condition.Threshold
|
||||
case "comment_lines_deleted":
|
||||
earned = float64(cm.CommentLinesDeleted) >= ach.Condition.Threshold
|
||||
// Issue metrics
|
||||
case "issues_opened":
|
||||
earned = float64(cm.IssuesOpened) >= ach.Condition.Threshold
|
||||
case "issues_closed":
|
||||
earned = float64(cm.IssuesClosed) >= ach.Condition.Threshold
|
||||
case "issue_comments":
|
||||
earned = float64(cm.IssueComments) >= ach.Condition.Threshold
|
||||
case "issue_references":
|
||||
earned = float64(cm.IssueReferencesInCommits) >= ach.Condition.Threshold
|
||||
}
|
||||
|
||||
if earned {
|
||||
|
||||
@@ -1105,3 +1105,319 @@ func TestCalculator_CommentLinesAchievements(t *testing.T) {
|
||||
assert.NotContains(t, entry.Achievements, "docs-del-200", "60 < 200")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculator_IssueScoring(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("calculates issue points correctly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
cfg.Scoring.Points = config.PointsConfig{
|
||||
Commit: 10,
|
||||
IssueOpened: 10, // 10 points per issue opened
|
||||
IssueClosed: 20, // 20 points per issue closed
|
||||
IssueComment: 5, // 5 points per issue comment
|
||||
IssueReference: 5, // 5 points per issue reference in commit
|
||||
}
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "issue-worker",
|
||||
CommitCount: 10,
|
||||
IssuesOpened: 5, // 5 * 10 = 50
|
||||
IssuesClosed: 3, // 3 * 20 = 60
|
||||
IssueComments: 10, // 10 * 5 = 50
|
||||
IssueReferencesInCommits: 8, // 8 * 5 = 40
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
// Issue points: 50 + 60 + 50 + 40 = 200
|
||||
assert.Equal(t, 200, contributor.Score.Breakdown.Issues)
|
||||
// Commits: 10 * 10 = 100
|
||||
// Total: 100 + 200 = 300
|
||||
assert.Equal(t, 300, contributor.Score.Total)
|
||||
})
|
||||
|
||||
t.Run("aggregates issue metrics across repositories", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
cfg.Scoring.Points = config.PointsConfig{
|
||||
IssueOpened: 10,
|
||||
IssueClosed: 20,
|
||||
IssueComment: 5,
|
||||
IssueReference: 5,
|
||||
}
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo1",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "issue-worker",
|
||||
IssuesOpened: 3,
|
||||
IssuesClosed: 2,
|
||||
IssueComments: 5,
|
||||
IssueReferencesInCommits: 4,
|
||||
RepositoriesContributed: []string{"owner/repo1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "owner/repo2",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "issue-worker",
|
||||
IssuesOpened: 2,
|
||||
IssuesClosed: 1,
|
||||
IssueComments: 3,
|
||||
IssueReferencesInCommits: 2,
|
||||
RepositoriesContributed: []string{"owner/repo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
require.Len(t, result.Leaderboard, 1)
|
||||
// Aggregated: 5 opened, 3 closed, 8 comments, 6 references
|
||||
// Points: 5*10 + 3*20 + 8*5 + 6*5 = 50 + 60 + 40 + 30 = 180
|
||||
assert.Equal(t, 180, result.Leaderboard[0].Score)
|
||||
})
|
||||
|
||||
t.Run("zero issue metrics results in zero issue points", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
cfg.Scoring.Points = config.PointsConfig{
|
||||
Commit: 10,
|
||||
IssueOpened: 10,
|
||||
IssueClosed: 20,
|
||||
IssueComment: 5,
|
||||
IssueReference: 5,
|
||||
}
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "code-only",
|
||||
CommitCount: 20,
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
assert.Equal(t, 0, contributor.Score.Breakdown.Issues)
|
||||
// Only commits: 20 * 10 = 200
|
||||
assert.Equal(t, 200, contributor.Score.Total)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculator_IssueAchievements(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("earns issue opened achievements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "bug-hunter",
|
||||
IssuesOpened: 12, // Should earn issue-1, issue-5, issue-10
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
assert.Contains(t, contributor.Achievements, "issue-1", "Should earn issue-1 for 1+ issues opened")
|
||||
assert.Contains(t, contributor.Achievements, "issue-5", "Should earn issue-5 for 5+ issues opened")
|
||||
assert.Contains(t, contributor.Achievements, "issue-10", "Should earn issue-10 for 10+ issues opened")
|
||||
assert.NotContains(t, contributor.Achievements, "issue-25", "Should not earn issue-25 for <25 issues")
|
||||
})
|
||||
|
||||
t.Run("earns issue closed achievements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "problem-solver",
|
||||
IssuesClosed: 8, // Should earn issue-close-1, issue-close-5
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-1", "Should earn issue-close-1 for 1+ issues closed")
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-5", "Should earn issue-close-5 for 5+ issues closed")
|
||||
assert.NotContains(t, contributor.Achievements, "issue-close-10", "Should not earn issue-close-10 for <10 issues")
|
||||
})
|
||||
|
||||
t.Run("earns issue comment achievements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "discusser",
|
||||
IssueComments: 30, // Should earn issue-comment-5, issue-comment-10, issue-comment-25
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-5", "Should earn issue-comment-5 for 5+ comments")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-10", "Should earn issue-comment-10 for 10+ comments")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-25", "Should earn issue-comment-25 for 25+ comments")
|
||||
assert.NotContains(t, contributor.Achievements, "issue-comment-50", "Should not earn issue-comment-50 for <50 comments")
|
||||
})
|
||||
|
||||
t.Run("earns issue reference achievements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "linker",
|
||||
IssueReferencesInCommits: 15, // Should earn issue-ref-5, issue-ref-10
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-5", "Should earn issue-ref-5 for 5+ references")
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-10", "Should earn issue-ref-10 for 10+ references")
|
||||
assert.NotContains(t, contributor.Achievements, "issue-ref-25", "Should not earn issue-ref-25 for <25 references")
|
||||
})
|
||||
|
||||
t.Run("earns all issue achievement tiers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "super-issue-worker",
|
||||
IssuesOpened: 100,
|
||||
IssuesClosed: 100,
|
||||
IssueComments: 150,
|
||||
IssueReferencesInCommits: 150,
|
||||
RepositoriesContributed: []string{"owner/repo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
contributor := result.Repositories[0].Contributors[0]
|
||||
// Should have all issue opened achievements
|
||||
assert.Contains(t, contributor.Achievements, "issue-1")
|
||||
assert.Contains(t, contributor.Achievements, "issue-5")
|
||||
assert.Contains(t, contributor.Achievements, "issue-10")
|
||||
assert.Contains(t, contributor.Achievements, "issue-25")
|
||||
assert.Contains(t, contributor.Achievements, "issue-50")
|
||||
// Should have all issue closed achievements
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-1")
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-5")
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-10")
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-25")
|
||||
assert.Contains(t, contributor.Achievements, "issue-close-50")
|
||||
// Should have all issue comment achievements
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-5")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-10")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-25")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-50")
|
||||
assert.Contains(t, contributor.Achievements, "issue-comment-100")
|
||||
// Should have all issue reference achievements
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-5")
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-10")
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-25")
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-50")
|
||||
assert.Contains(t, contributor.Achievements, "issue-ref-100")
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -8,9 +8,9 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script type="module" crossorigin src="./assets/index-LBN7XWrH.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-IALpeAps.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/chart-Bcjh2pZL.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-8XjWwD9J.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DOVyCPqp.css">
|
||||
</head>
|
||||
<body class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 font-sans transition-colors duration-300">
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -611,6 +612,129 @@ func (c *Client) FetchIssues(ctx context.Context, owner, repo string, since, unt
|
||||
return allIssues, nil
|
||||
}
|
||||
|
||||
// FetchIssueComments fetches comments on issues from a repository
|
||||
// Uses early termination when sorted by date - stops when items are outside date range
|
||||
func (c *Client) FetchIssueComments(ctx context.Context, owner, repo string, since, until *time.Time) ([]models.IssueComment, error) {
|
||||
cacheKey := fmt.Sprintf("issue_comments:%s/%s:%v:%v", owner, repo, since, until)
|
||||
|
||||
// Check cache
|
||||
if cached, ok := c.cache.Get(cacheKey); ok {
|
||||
if comments, ok := cached.([]models.IssueComment); ok {
|
||||
c.progress(" Using cached issue comments data")
|
||||
return comments, nil
|
||||
}
|
||||
}
|
||||
|
||||
var allComments []models.IssueComment
|
||||
|
||||
// Sort by created date descending - newest first
|
||||
// This allows us to stop early when we hit items older than our date range
|
||||
opts := &github.IssueListCommentsOptions{
|
||||
Sort: github.Ptr("created"),
|
||||
Direction: github.Ptr("desc"),
|
||||
ListOptions: github.ListOptions{
|
||||
PerPage: 100,
|
||||
},
|
||||
}
|
||||
|
||||
// Set 'since' parameter if provided (GitHub filters by update time but we'll also filter manually)
|
||||
if since != nil {
|
||||
opts.Since = since
|
||||
}
|
||||
|
||||
page := 1
|
||||
reachedOldItems := false
|
||||
|
||||
for {
|
||||
var comments []*github.IssueComment
|
||||
var resp *github.Response
|
||||
|
||||
err := c.retryWithBackoff(ctx, "list issue comments", func() error {
|
||||
var err error
|
||||
// Passing empty issue number fetches all comments in the repo
|
||||
comments, resp, err = c.gh.Issues.ListComments(ctx, owner, repo, 0, opts)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list issue comments: %w", err)
|
||||
}
|
||||
|
||||
c.progress(fmt.Sprintf(" Fetching issue comments page %d (%d comments so far)...", page, len(allComments)))
|
||||
|
||||
oldItemsInPage := 0
|
||||
totalItems := len(comments)
|
||||
|
||||
for _, comment := range comments {
|
||||
createdAt := comment.GetCreatedAt().Time
|
||||
|
||||
// Skip items newer than our range (when until is specified)
|
||||
if until != nil && createdAt.After(*until) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we've gone past our date range (older than since), count it
|
||||
if since != nil && createdAt.Before(*since) {
|
||||
oldItemsInPage++
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract issue number from the issue URL
|
||||
issueNumber := 0
|
||||
if comment.IssueURL != nil {
|
||||
// Issue URL format: https://api.github.com/repos/{owner}/{repo}/issues/{number}
|
||||
parts := strings.Split(*comment.IssueURL, "/")
|
||||
if len(parts) > 0 {
|
||||
if num, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
|
||||
issueNumber = num
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var author models.Author
|
||||
if comment.User != nil {
|
||||
author = models.Author{
|
||||
Login: comment.User.GetLogin(),
|
||||
Name: comment.User.GetName(),
|
||||
AvatarURL: comment.User.GetAvatarURL(),
|
||||
}
|
||||
}
|
||||
|
||||
ic := models.IssueComment{
|
||||
ID: comment.GetID(),
|
||||
Issue: issueNumber,
|
||||
Repository: fmt.Sprintf("%s/%s", owner, repo),
|
||||
Author: author,
|
||||
Body: comment.GetBody(),
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
allComments = append(allComments, ic)
|
||||
}
|
||||
|
||||
// If all items in this page are older than our range, we can stop
|
||||
// (since results are sorted by created date descending)
|
||||
if oldItemsInPage == totalItems && totalItems > 0 {
|
||||
c.progress(fmt.Sprintf(" Reached issue comments older than date range, stopping early (page %d)", page))
|
||||
reachedOldItems = true
|
||||
break
|
||||
}
|
||||
|
||||
if resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
opts.Page = resp.NextPage
|
||||
page++
|
||||
}
|
||||
|
||||
if !reachedOldItems && page > 1 {
|
||||
c.progress(fmt.Sprintf(" Fetched all %d pages of issue comments", page))
|
||||
}
|
||||
|
||||
// Cache results
|
||||
c.cache.Set(cacheKey, allComments)
|
||||
|
||||
return allComments, nil
|
||||
}
|
||||
|
||||
// UserProfile contains GitHub user profile information useful for deduplication
|
||||
type UserProfile struct {
|
||||
ID int64 // GitHub user ID
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestServer_CacheMiddleware(t *testing.T) {
|
||||
// Create a test handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
// Wrap with cache middleware
|
||||
@@ -87,7 +87,7 @@ func TestServer_ServesStaticFiles(t *testing.T) {
|
||||
|
||||
// Create a test file with a simple name
|
||||
testFile := filepath.Join(tempDir, "hello.txt")
|
||||
err := os.WriteFile(testFile, []byte("Hello, World!"), 0644)
|
||||
err := os.WriteFile(testFile, []byte("Hello, World!"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "0")
|
||||
@@ -139,7 +139,7 @@ func TestServer_ServesNestedDirectories(t *testing.T) {
|
||||
|
||||
// Create a file in nested directory
|
||||
testFile := filepath.Join(nestedDir, "metrics.json")
|
||||
err = os.WriteFile(testFile, []byte(`{"count": 42}`), 0644)
|
||||
err = os.WriteFile(testFile, []byte(`{"count": 42}`), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
absPath, _ := filepath.Abs(tempDir)
|
||||
@@ -164,7 +164,7 @@ func TestServer_MiddlewareCombination(t *testing.T) {
|
||||
|
||||
innerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("response"))
|
||||
_, _ = w.Write([]byte("response"))
|
||||
})
|
||||
|
||||
// Combine middlewares like in the actual server
|
||||
@@ -189,7 +189,7 @@ func TestServer_ServesIndexHtml(t *testing.T) {
|
||||
|
||||
// Create an index.html
|
||||
indexFile := filepath.Join(tempDir, "index.html")
|
||||
err := os.WriteFile(indexFile, []byte("<html><body>Test Page</body></html>"), 0644)
|
||||
err := os.WriteFile(indexFile, []byte("<html><body>Test Page</body></html>"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
absPath, _ := filepath.Abs(tempDir)
|
||||
@@ -213,7 +213,7 @@ func TestServer_CreateHandler(t *testing.T) {
|
||||
|
||||
// Create an index.html
|
||||
indexFile := filepath.Join(tempDir, "index.html")
|
||||
err := os.WriteFile(indexFile, []byte("<html><body>Handler Test</body></html>"), 0644)
|
||||
err := os.WriteFile(indexFile, []byte("<html><body>Handler Test</body></html>"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "8080")
|
||||
@@ -281,7 +281,7 @@ func TestServer_ServesJSONWithCorrectContentType(t *testing.T) {
|
||||
|
||||
// Create a JSON file
|
||||
jsonFile := filepath.Join(tempDir, "data.json")
|
||||
err := os.WriteFile(jsonFile, []byte(`{"status": "ok"}`), 0644)
|
||||
err := os.WriteFile(jsonFile, []byte(`{"status": "ok"}`), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "0")
|
||||
@@ -306,7 +306,7 @@ func TestServer_ServesHTMLWithCorrectContentType(t *testing.T) {
|
||||
|
||||
// Create an HTML file
|
||||
htmlFile := filepath.Join(tempDir, "page.html")
|
||||
err := os.WriteFile(htmlFile, []byte("<html><body>HTML Page</body></html>"), 0644)
|
||||
err := os.WriteFile(htmlFile, []byte("<html><body>HTML Page</body></html>"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "0")
|
||||
@@ -331,7 +331,7 @@ func TestServer_CORSHeaders(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "0")
|
||||
@@ -354,7 +354,7 @@ func TestServer_CacheDisabledHeaders(t *testing.T) {
|
||||
|
||||
// Create a test file
|
||||
testFile := filepath.Join(tempDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
s := New(tempDir, "0")
|
||||
@@ -406,7 +406,7 @@ func TestServer_CacheMiddlewarePreservesResponseBody(t *testing.T) {
|
||||
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))
|
||||
_, _ = w.Write([]byte(expectedBody))
|
||||
})
|
||||
|
||||
wrapped := s.cacheMiddleware(handler)
|
||||
|
||||
Reference in New Issue
Block a user