diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6c71182..8883a33 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,6 @@ "github.copilot-workspace", "GitHub.vscode-pull-request-github", "GitHub.remotehub", - "GitHub.vscode-codeql", "golang.Go" ] } diff --git a/.github/workflows/baseline.yml b/.github/workflows/baseline.yml deleted file mode 100644 index 8fa8481..0000000 --- a/.github/workflows/baseline.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Validate Repository Configuration - -permissions: - contents: read - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -jobs: - validate: - name: Validate Baseline Configuration - uses: chrisreddington/reusable-workflows/.github/workflows/baseline-validator.yml@d62f6e0cbe864707a620bba0be92695711514442 - with: - required-features: "ghcr.io/devcontainers/features/github-cli:1" diff --git a/github/client.go b/github/client.go index ed11802..54e641b 100644 --- a/github/client.go +++ b/github/client.go @@ -3,10 +3,7 @@ package github import ( - "bytes" - "encoding/json" "fmt" - "io" "time" "github.com/github/gh-skyline/errors" @@ -15,8 +12,7 @@ import ( // APIClient interface defines the methods we need from the client type APIClient interface { - Get(path string, response interface{}) error - Post(path string, body io.Reader, response interface{}) error + Do(query string, variables map[string]interface{}, response interface{}) error } // Client holds the API client @@ -31,17 +27,31 @@ func NewClient(apiClient APIClient) *Client { // GetAuthenticatedUser fetches the authenticated user's login name from GitHub. func (c *Client) GetAuthenticatedUser() (string, error) { - response := struct{ Login string }{} - err := c.api.Get("user", &response) + // GraphQL query to fetch the authenticated user's login. + query := ` + query { + viewer { + login + } + }` + + var response struct { + Viewer struct { + Login string `json:"login"` + } `json:"viewer"` + } + + // Execute the GraphQL query. + err := c.api.Do(query, nil, &response) if err != nil { return "", errors.New(errors.NetworkError, "failed to fetch authenticated user", err) } - if response.Login == "" { + if response.Viewer.Login == "" { return "", errors.New(errors.ValidationError, "received empty username from GitHub API", nil) } - return response.Login, nil + return response.Viewer.Login, nil } // FetchContributions retrieves the contribution data for a given username and year from GitHub. @@ -54,9 +64,10 @@ func (c *Client) FetchContributions(username string, year int) (*types.Contribut return nil, errors.New(errors.ValidationError, "year cannot be before GitHub's launch (2008)", nil) } - startDate := fmt.Sprintf("%d-01-01", year) - endDate := fmt.Sprintf("%d-12-31", year) + startDate := fmt.Sprintf("%d-01-01T00:00:00Z", year) + endDate := fmt.Sprintf("%d-12-31T23:59:59Z", year) + // GraphQL query to fetch the user's contributions within the specified date range. query := ` query ContributionGraph($username: String!, $from: DateTime!, $to: DateTime!) { user(login: $username) { @@ -77,34 +88,23 @@ func (c *Client) FetchContributions(username string, year int) (*types.Contribut variables := map[string]interface{}{ "username": username, - "from": startDate + "T00:00:00Z", - "to": endDate + "T23:59:59Z", + "from": startDate, + "to": endDate, } - payload := struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - }{ - Query: query, - Variables: variables, - } + var response types.ContributionsResponse - body, err := json.Marshal(payload) + // Execute the GraphQL query. + err := c.api.Do(query, variables, &response) if err != nil { - return nil, err - } - - var resp types.ContributionsResponse - if err := c.api.Post("graphql", bytes.NewBuffer(body), &resp); err != nil { - return nil, errors.New(errors.GraphQLError, "failed to fetch contributions", err) + return nil, errors.New(errors.NetworkError, "failed to fetch contributions", err) } - // Validate response - if resp.Data.User.Login == "" { - return nil, errors.New(errors.GraphQLError, "user not found", nil) + if response.User.Login == "" { + return nil, errors.New(errors.ValidationError, "received empty username from GitHub API", nil) } - return &resp, nil + return &response, nil } // GetUserJoinYear fetches the year a user joined GitHub using the GitHub API. @@ -113,6 +113,7 @@ func (c *Client) GetUserJoinYear(username string) (int, error) { return 0, errors.New(errors.ValidationError, "username cannot be empty", nil) } + // GraphQL query to fetch the user's account creation date. query := ` query UserJoinDate($username: String!) { user(login: $username) { @@ -124,35 +125,23 @@ func (c *Client) GetUserJoinYear(username string) (int, error) { "username": username, } - payload := struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - }{ - Query: query, - Variables: variables, + var response struct { + User struct { + CreatedAt time.Time `json:"createdAt"` + } `json:"user"` } - body, err := json.Marshal(payload) + // Execute the GraphQL query. + err := c.api.Do(query, variables, &response) if err != nil { - return 0, err - } - - var resp struct { - Data struct { - User struct { - CreatedAt string `json:"createdAt"` - } `json:"user"` - } `json:"data"` - } - if err := c.api.Post("graphql", bytes.NewBuffer(body), &resp); err != nil { - return 0, errors.New(errors.GraphQLError, "failed to fetch user join date", err) + return 0, errors.New(errors.NetworkError, "failed to fetch user's join date", err) } // Parse the join date - joinDate, err := time.Parse(time.RFC3339, resp.Data.User.CreatedAt) - if err != nil { - return 0, errors.New(errors.ValidationError, "failed to parse join date", err) + joinYear := response.User.CreatedAt.Year() + if joinYear == 0 { + return 0, errors.New(errors.ValidationError, "invalid join date received from GitHub API", nil) } - return joinDate.Year(), nil + return joinYear, nil } diff --git a/github/client_test.go b/github/client_test.go index 5587783..1d822ad 100644 --- a/github/client_test.go +++ b/github/client_test.go @@ -1,98 +1,50 @@ package github import ( - "encoding/json" - "io" "testing" + "time" "github.com/github/gh-skyline/errors" + "github.com/github/gh-skyline/testutil/mocks" + "github.com/github/gh-skyline/types" ) -type MockAPIClient struct { - GetFunc func(path string, response interface{}) error - PostFunc func(path string, body io.Reader, response interface{}) error -} - -func (m *MockAPIClient) Get(path string, response interface{}) error { - return m.GetFunc(path, response) -} - -func (m *MockAPIClient) Post(path string, body io.Reader, response interface{}) error { - return m.PostFunc(path, body, response) -} - -// mockAPIClient implements APIClient for testing -type mockAPIClient struct { - getResponse string - postResponse string - shouldError bool -} - -func (m *mockAPIClient) Get(_ string, response interface{}) error { - if m.shouldError { - return errors.New(errors.NetworkError, "mock error", nil) - } - return json.Unmarshal([]byte(m.getResponse), response) -} - -func (m *mockAPIClient) Post(_ string, _ io.Reader, response interface{}) error { - if m.shouldError { - return errors.New(errors.NetworkError, "mock error", nil) - } - return json.Unmarshal([]byte(m.postResponse), response) -} - -func TestNewClient(t *testing.T) { - mock := &mockAPIClient{} - client := NewClient(mock) - if client == nil { - t.Fatal("NewClient returned nil") - } - if client.api != mock { - t.Error("NewClient did not set api client correctly") - } -} - func TestGetAuthenticatedUser(t *testing.T) { tests := []struct { name string - response string - shouldError bool + mockResponse string + mockError error expectedUser string expectedError bool }{ { name: "successful response", - response: `{"login": "testuser"}`, + mockResponse: "testuser", expectedUser: "testuser", expectedError: false, }, { name: "empty username", - response: `{"login": ""}`, + mockResponse: "", expectedError: true, }, { name: "network error", - shouldError: true, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - getResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) + client := NewClient(&mocks.MockGitHubClient{ + Username: tt.mockResponse, + Err: tt.mockError, + }) user, err := client.GetAuthenticatedUser() - if tt.expectedError && err == nil { - t.Error("expected error but got none") - } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } if user != tt.expectedUser { t.Errorf("expected user %q, got %q", tt.expectedUser, user) @@ -101,123 +53,153 @@ func TestGetAuthenticatedUser(t *testing.T) { } } -func TestFetchContributions(t *testing.T) { +func TestGetUserJoinYear(t *testing.T) { tests := []struct { name string username string - year int - response string - shouldError bool + mockResponse time.Time + mockError error + expectedYear int expectedError bool }{ { - name: "successful response", - username: "testuser", - year: 2023, - response: `{"data":{"user":{"login":"testuser","contributionsCollection":{"contributionCalendar":{"totalContributions":100,"weeks":[]}}}}}`, + name: "successful response", + username: "testuser", + mockResponse: time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC), + expectedYear: 2015, + expectedError: false, }, { name: "empty username", username: "", - year: 2023, - expectedError: true, - }, - { - name: "invalid year", - username: "testuser", - year: 2007, expectedError: true, }, { name: "network error", username: "testuser", - year: 2023, - shouldError: true, - expectedError: true, - }, - { - name: "user not found", - username: "testuser", - year: 2023, - response: `{"data":{"user":{"login":""}}}`, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - postResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) - - resp, err := client.FetchContributions(tt.username, tt.year) - if tt.expectedError && err == nil { - t.Error("expected error but got none") + client := NewClient(&mocks.MockGitHubClient{ + JoinYear: tt.expectedYear, + Err: tt.mockError, + }) + + year, err := client.GetUserJoinYear(tt.username) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) - } - if !tt.expectedError && resp == nil { - t.Error("expected response but got nil") + if !tt.expectedError && year != tt.expectedYear { + t.Errorf("expected year %d, got %d", tt.expectedYear, year) } }) } } -func TestGetUserJoinYear(t *testing.T) { +func TestFetchContributions(t *testing.T) { + mockContributions := &types.ContributionsResponse{ + User: struct { + Login string `json:"login"` + ContributionsCollection struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + } `json:"contributionsCollection"` + }{ + Login: "chrisreddington", + ContributionsCollection: struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + }{ + ContributionCalendar: struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + }{ + TotalContributions: 100, + Weeks: []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + }{ + { + ContributionDays: []types.ContributionDay{ + { + ContributionCount: 5, + Date: "2023-01-01", + }, + }, + }, + }, + }, + }, + }, + } + tests := []struct { name string username string - response string - shouldError bool - expectedYear int + year int + mockResponse *types.ContributionsResponse + mockError error expectedError bool }{ { name: "successful response", username: "testuser", - response: `{"data":{"user":{"createdAt":"2015-01-01T00:00:00Z"}}}`, - expectedYear: 2015, + year: 2023, + mockResponse: mockContributions, expectedError: false, }, { name: "empty username", username: "", + year: 2023, expectedError: true, }, { - name: "network error", + name: "invalid year", username: "testuser", - shouldError: true, + year: 2007, expectedError: true, }, { - name: "invalid date format", + name: "network error", username: "testuser", - response: `{"data":{"user":{"createdAt":"invalid-date"}}}`, + year: 2023, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - postResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) + client := NewClient(&mocks.MockGitHubClient{ + Username: tt.username, + MockData: tt.mockResponse, + Err: tt.mockError, + }) - joinYear, err := client.GetUserJoinYear(tt.username) - if tt.expectedError && err == nil { - t.Error("expected error but got none") - } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) + resp, err := client.FetchContributions(tt.username, tt.year) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } - if joinYear != tt.expectedYear { - t.Errorf("expected year %d, got %d", tt.expectedYear, joinYear) + if !tt.expectedError { + if resp == nil { + t.Error("expected response but got nil") + } else if resp.User.Login != "testuser" { + t.Errorf("expected user testuser, got %s", resp.User.Login) + } } }) } diff --git a/main.go b/main.go index e815810..253bfdf 100644 --- a/main.go +++ b/main.go @@ -217,22 +217,22 @@ var initializeGitHubClient = defaultGitHubClient // defaultGitHubClient is the default implementation of client initialization func defaultGitHubClient() (*github.Client, error) { - apiClient, err := api.DefaultRESTClient() + apiClient, err := api.DefaultGraphQLClient() if err != nil { - return nil, fmt.Errorf("failed to create REST client: %w", err) + return nil, fmt.Errorf("failed to create GraphQL client: %w", err) } return github.NewClient(apiClient), nil } // fetchContributionData retrieves and formats the contribution data for the specified year. func fetchContributionData(client *github.Client, username string, year int) ([][]types.ContributionDay, error) { - resp, err := client.FetchContributions(username, year) + response, err := client.FetchContributions(username, year) if err != nil { return nil, fmt.Errorf("failed to fetch contributions: %w", err) } // Convert weeks data to 2D array for STL generation - weeks := resp.Data.User.ContributionsCollection.ContributionCalendar.Weeks + weeks := response.User.ContributionsCollection.ContributionCalendar.Weeks contributionGrid := make([][]types.ContributionDay, len(weeks)) for i, week := range weeks { contributionGrid[i] = week.ContributionDays diff --git a/main_test.go b/main_test.go index ec9d22d..be9ae03 100644 --- a/main_test.go +++ b/main_test.go @@ -1,142 +1,25 @@ package main import ( - "io" "testing" - "time" - "encoding/json" "fmt" - "strings" "github.com/github/gh-skyline/github" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/testutil/fixtures" + "github.com/github/gh-skyline/testutil/mocks" ) -// MockGitHubClient implements the github.APIClient interface -type MockGitHubClient struct { - username string - joinYear int - shouldError bool // Add error flag -} - -// Get implements the APIClient interface -func (m *MockGitHubClient) Get(_ string, _ interface{}) error { - return nil -} - -// Post implements the APIClient interface -func (m *MockGitHubClient) Post(path string, body io.Reader, response interface{}) error { - if path == "graphql" { - // Read the request body to determine which GraphQL query is being made - bodyBytes, _ := io.ReadAll(body) - bodyStr := string(bodyBytes) - - if strings.Contains(bodyStr, "UserJoinDate") { - // Handle user join date query - resp := response.(*struct { - Data struct { - User struct { - CreatedAt string `json:"createdAt"` - } `json:"user"` - } `json:"data"` - }) - resp.Data.User.CreatedAt = time.Date(m.joinYear, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339) - return nil - } - - if strings.Contains(bodyStr, "ContributionGraph") { - // Handle contribution graph query (existing logic) - return json.Unmarshal(contributionResponse(m.username), response) - } - } - return nil -} - -// Helper function to generate mock contribution response -func contributionResponse(username string) []byte { - response := fmt.Sprintf(`{ - "data": { - "user": { - "login": "%s", - "contributionsCollection": { - "contributionCalendar": { - "totalContributions": 1, - "weeks": [ - { - "contributionDays": [ - { - "contributionCount": 1, - "date": "2024-01-01" - } - ] - } - ] - } - } - } - } - }`, username) - return []byte(response) -} - -// GetAuthenticatedUser returns the authenticated user's username or an error -// if the mock client is set to error or the username is not set. -func (m *MockGitHubClient) GetAuthenticatedUser() (string, error) { - // Return error if shouldError is true - if m.shouldError { - return "", fmt.Errorf("mock client error") - } - // Validate username is not empty - if m.username == "" { - return "", fmt.Errorf("mock username not set") - } - return m.username, nil -} - -// GetUserJoinYear implements the GitHubClientInterface. -// It returns the year the user joined GitHub. -func (m *MockGitHubClient) GetUserJoinYear(_ string) (int, error) { - return m.joinYear, nil -} - -// FetchContributions mocks fetching GitHub contributions for a user -// in a given year, returning minimal valid data. -func (m *MockGitHubClient) FetchContributions(username string, year int) (*types.ContributionsResponse, error) { - // Return minimal valid response - resp := &types.ContributionsResponse{} - resp.Data.User.Login = username - // Add a single week with a single day for minimal valid data - week := struct { - ContributionDays []types.ContributionDay `json:"contributionDays"` - }{ - ContributionDays: []types.ContributionDay{ - { - ContributionCount: 1, - Date: time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), - }, - }, - } - resp.Data.User.ContributionsCollection.ContributionCalendar.Weeks = []struct { - ContributionDays []types.ContributionDay `json:"contributionDays"` - }{week} - return resp, nil -} - // MockBrowser implements the Browser interface type MockBrowser struct { - LastURL string - ShouldError bool + LastURL string + Err error } // Browse implements the Browser interface -// Changed from pointer receiver to value receiver func (m *MockBrowser) Browse(url string) error { m.LastURL = url - if m.ShouldError { - return fmt.Errorf("mock browser error") - } - return nil + return m.Err } func TestFormatYearRange(t *testing.T) { @@ -313,7 +196,7 @@ func TestGenerateSkyline(t *testing.T) { endYear int targetUser string full bool - mockClient *MockGitHubClient + mockClient *mocks.MockGitHubClient wantErr bool }{ { @@ -322,9 +205,10 @@ func TestGenerateSkyline(t *testing.T) { endYear: 2024, targetUser: "testuser", full: false, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, @@ -334,21 +218,23 @@ func TestGenerateSkyline(t *testing.T) { endYear: 2024, targetUser: "testuser", full: false, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, { name: "full range", - startYear: 2020, + startYear: 2008, endYear: 2024, targetUser: "testuser", full: true, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2008, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, @@ -374,23 +260,22 @@ func TestOpenGitHubProfile(t *testing.T) { tests := []struct { name string targetUser string - mockClient *MockGitHubClient + mockClient *mocks.MockGitHubClient wantURL string wantErr bool }{ { name: "specific user", targetUser: "testuser", - mockClient: &MockGitHubClient{}, + mockClient: &mocks.MockGitHubClient{}, wantURL: "https://github.com/testuser", wantErr: false, }, { name: "authenticated user", targetUser: "", - mockClient: &MockGitHubClient{ - username: "authuser", - shouldError: false, + mockClient: &mocks.MockGitHubClient{ + Username: "authuser", }, wantURL: "https://github.com/authuser", wantErr: false, @@ -398,9 +283,8 @@ func TestOpenGitHubProfile(t *testing.T) { { name: "client error", targetUser: "", - mockClient: &MockGitHubClient{ - username: "", - shouldError: true, + mockClient: &mocks.MockGitHubClient{ + Err: fmt.Errorf("mock error"), }, wantErr: true, }, @@ -408,8 +292,10 @@ func TestOpenGitHubProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create MockBrowser and call openGitHubProfile - mockBrowser := &MockBrowser{ShouldError: tt.wantErr} + mockBrowser := &MockBrowser{} + if tt.wantErr { + mockBrowser.Err = fmt.Errorf("mock error") + } err := openGitHubProfile(tt.targetUser, tt.mockClient, mockBrowser) if (err != nil) != tt.wantErr { diff --git a/testutil/fixtures/github.go b/testutil/fixtures/github.go new file mode 100644 index 0000000..491baba --- /dev/null +++ b/testutil/fixtures/github.go @@ -0,0 +1,43 @@ +// Package fixtures provides test utilities and mock data generators +// for testing the gh-skyline application. +package fixtures + +import ( + "time" + + "github.com/github/gh-skyline/types" +) + +// GenerateContributionsResponse creates a mock contributions response +func GenerateContributionsResponse(username string, year int) *types.ContributionsResponse { + response := &types.ContributionsResponse{} + response.User.Login = username + response.User.ContributionsCollection.ContributionCalendar.TotalContributions = 100 + + // Create sample weeks with contribution days + weeks := make([]struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + }, 52) + + for i := range weeks { + days := make([]types.ContributionDay, 7) + for j := range days { + days[j] = types.ContributionDay{ + ContributionCount: (i + j) % 10, + Date: time.Date(year, 1, 1+i*7+j, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + } + } + weeks[i].ContributionDays = days + } + + response.User.ContributionsCollection.ContributionCalendar.Weeks = weeks + return response +} + +// CreateMockContributionDay creates a mock contribution day +func CreateMockContributionDay(date time.Time, count int) types.ContributionDay { + return types.ContributionDay{ + ContributionCount: count, + Date: date.Format("2006-01-02"), + } +} diff --git a/testutil/mocks/github.go b/testutil/mocks/github.go new file mode 100644 index 0000000..b03409e --- /dev/null +++ b/testutil/mocks/github.go @@ -0,0 +1,79 @@ +// Package mocks provides mock implementations of interfaces used in testing +package mocks + +import ( + "fmt" + "time" + + "github.com/github/gh-skyline/testutil/fixtures" + "github.com/github/gh-skyline/types" +) + +// MockGitHubClient implements both GitHubClientInterface and APIClient interfaces +type MockGitHubClient struct { + Username string + JoinYear int + MockData *types.ContributionsResponse + Response interface{} // Generic response field for testing + Err error // Error to return if needed +} + +// GetAuthenticatedUser implements GitHubClientInterface +func (m *MockGitHubClient) GetAuthenticatedUser() (string, error) { + if m.Err != nil { + return "", m.Err + } + if m.Username == "" { + return "", fmt.Errorf("mock username not set") + } + return m.Username, nil +} + +// GetUserJoinYear implements GitHubClientInterface +func (m *MockGitHubClient) GetUserJoinYear(_ string) (int, error) { + if m.Err != nil { + return 0, m.Err + } + if m.JoinYear == 0 { + return 0, fmt.Errorf("mock join year not set") + } + return m.JoinYear, nil +} + +// FetchContributions implements GitHubClientInterface +func (m *MockGitHubClient) FetchContributions(username string, year int) (*types.ContributionsResponse, error) { + if m.Err != nil { + return nil, m.Err + } + // Always return generated mock data with valid contributions + return fixtures.GenerateContributionsResponse(username, year), nil +} + +// Do implements APIClient +func (m *MockGitHubClient) Do(_ string, _ map[string]interface{}, response interface{}) error { + if m.Err != nil { + return m.Err + } + + switch v := response.(type) { + case *struct { + Viewer struct { + Login string `json:"login"` + } `json:"viewer"` + }: + v.Viewer.Login = m.Username + case *struct { + User struct { + CreatedAt time.Time `json:"createdAt"` + } `json:"user"` + }: + if m.JoinYear > 0 { + v.User.CreatedAt = time.Date(m.JoinYear, 1, 1, 0, 0, 0, 0, time.UTC) + } + case *types.ContributionsResponse: + // Always use generated mock data instead of empty response + mockResp := fixtures.GenerateContributionsResponse(m.Username, time.Now().Year()) + *v = *mockResp + } + return nil +} diff --git a/types/types.go b/types/types.go index f70f84d..13f6dd6 100644 --- a/types/types.go +++ b/types/types.go @@ -9,9 +9,8 @@ import ( ) // ContributionDay represents a single day of GitHub contributions. -// It contains the number of contributions made on a specific date. type ContributionDay struct { - ContributionCount int + ContributionCount int `json:"contributionCount"` Date string `json:"date"` } @@ -37,22 +36,19 @@ func (c ContributionDay) Validate() error { return nil } -// ContributionsResponse represents the GitHub GraphQL API response structure -// for fetching user contributions data. +// ContributionsResponse represents the contribution data returned by the GitHub API. type ContributionsResponse struct { - Data struct { - User struct { - Login string - ContributionsCollection struct { - ContributionCalendar struct { - TotalContributions int `json:"totalContributions"` - Weeks []struct { - ContributionDays []ContributionDay `json:"contributionDays"` - } `json:"weeks"` - } `json:"contributionCalendar"` - } `json:"contributionsCollection"` - } `json:"user"` - } `json:"data"` + User struct { + Login string `json:"login"` + ContributionsCollection struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + } `json:"contributionsCollection"` + } `json:"user"` } // Point3D represents a point in 3D space using float64 for accuracy in calculations. diff --git a/types/types_test.go b/types/types_test.go index 29a9be2..ccf1798 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -44,23 +44,21 @@ func TestContributionDaySerialization(t *testing.T) { // is properly parsed with nested fields. func TestContributionsResponseParsing(t *testing.T) { sampleResponse := `{ - "data": { - "user": { - "login": "testuser", - "contributionsCollection": { - "contributionCalendar": { - "totalContributions": 100, - "weeks": [ - { - "contributionDays": [ - { - "contributionCount": 5, - "date": "2024-03-21" - } - ] - } - ] - } + "user": { + "login": "testuser", + "contributionsCollection": { + "contributionCalendar": { + "totalContributions": 100, + "weeks": [ + { + "contributionDays": [ + { + "contributionCount": 5, + "date": "2024-03-21" + } + ] + } + ] } } } @@ -75,12 +73,12 @@ func TestContributionsResponseParsing(t *testing.T) { expectedUsername := "testuser" expectedTotalContributions := 100 - if parsedResponse.Data.User.Login != expectedUsername { - t.Errorf("username mismatch: got %q, want %q", parsedResponse.Data.User.Login, expectedUsername) + if parsedResponse.User.Login != expectedUsername { + t.Errorf("username mismatch: got %q, want %q", parsedResponse.User.Login, expectedUsername) } - if parsedResponse.Data.User.ContributionsCollection.ContributionCalendar.TotalContributions != expectedTotalContributions { + if parsedResponse.User.ContributionsCollection.ContributionCalendar.TotalContributions != expectedTotalContributions { t.Errorf("total contributions mismatch: got %d, want %d", - parsedResponse.Data.User.ContributionsCollection.ContributionCalendar.TotalContributions, + parsedResponse.User.ContributionsCollection.ContributionCalendar.TotalContributions, expectedTotalContributions) } }