Skip to content

Add integration tests for GitHub API interactions #8

@BekahHW

Description

@BekahHW

Description

The src/utils/github.ts file (317 lines) handles all GitHub API interactions but lacks integration tests. This is critical functionality that needs testing to ensure API calls work correctly, error handling is robust, and rate limiting is respected.

Current State

  • ❌ No integration tests for GitHubService class
  • ❌ No tests for API error scenarios
  • ❌ No tests for rate limiting behavior
  • ❌ No tests for organization-level community health file detection
  • ✅ GitHub API is properly abstracted in GitHubService
  • ✅ Error handling exists but needs testing

Acceptance Criteria

Test Coverage Goals

  • Integration tests for all public methods in GitHubService
  • Error handling tests for common API failure scenarios
  • Rate limiting tests to ensure proper handling
  • Mock API responses for consistent testing
  • Real API tests (optional, with test repository)

Methods to Test

Core Methods

  • getRepositoryInfo(owner, repo) - Main repository data fetching
  • createRepository(name, description, isPrivate) - Repository creation
  • Private helper methods (if exposed for testing)

Organization Community Health Files

  • Detection of org-level CONTRIBUTING.md
  • Detection of org-level CODE_OF_CONDUCT.md
  • Detection of org-level issue templates
  • Detection of org-level PR templates
  • Fallback behavior when org files don't exist

Error Scenarios

  • Repository not found (404)
  • Rate limit exceeded (403)
  • Network timeouts
  • Invalid tokens
  • Private repository access denied

Implementation Suggestions

Test File Structure

// src/__tests__/utils/github.test.ts
import { GitHubService } from '../../utils/github';
import { RepositoryInfo } from '../../types';

// Mock the @octokit/rest module
jest.mock('@octokit/rest');

describe('GitHubService', () => {
  let githubService: GitHubService;
  let mockOctokit: jest.Mocked<any>;

  beforeEach(() => {
    mockOctokit = {
      rest: {
        repos: {
          get: jest.fn(),
          create: jest.fn(),
          getContent: jest.fn()
        },
        issues: {
          list: jest.fn()
        },
        search: {
          issuesAndPullRequests: jest.fn()
        }
      }
    };
    
    githubService = new GitHubService('fake-token');
    (githubService as any).octokit = mockOctokit;
  });

  describe('getRepositoryInfo', () => {
    it('successfully fetches repository information', async () => {
      // Mock successful API responses
      mockOctokit.rest.repos.get.mockResolvedValue({
        data: {
          name: 'test-repo',
          description: 'Test repository',
          topics: ['test', 'typescript'],
          archived: false,
          default_branch: 'main'
        }
      });
      
      mockOctokit.rest.repos.getContent.mockImplementation((params) => {
        if (params.path === 'README.md') {
          return Promise.resolve({ data: { type: 'file' } });
        }
        return Promise.reject({ status: 404 });
      });
      
      const result = await githubService.getRepositoryInfo('test-owner', 'test-repo');
      
      expect(result).toMatchObject({
        owner: 'test-owner',
        repo: 'test-repo',
        name: 'test-repo',
        description: 'Test repository',
        topics: ['test', 'typescript'],
        hasReadme: true,
        isArchived: false
      });
    });
    
    it('handles repository not found error', async () => {
      mockOctokit.rest.repos.get.mockRejectedValue({
        status: 404,
        message: 'Not Found'
      });
      
      await expect(
        githubService.getRepositoryInfo('nonexistent', 'repo')
      ).rejects.toThrow('Repository not found');
    });
    
    it('handles rate limiting', async () => {
      mockOctokit.rest.repos.get.mockRejectedValue({
        status: 403,
        message: 'API rate limit exceeded'
      });
      
      await expect(
        githubService.getRepositoryInfo('test', 'repo')
      ).rejects.toThrow('rate limit');
    });
  });

  describe('Organization Community Health Files', () => {
    it('detects CONTRIBUTING.md in organization .github repo', async () => {
      // Mock repo without CONTRIBUTING.md
      mockOctokit.rest.repos.getContent
        .mockRejectedValueOnce({ status: 404 }) // repo level
        .mockResolvedValueOnce({ data: { type: 'file' } }); // org level
      
      const result = await githubService.getRepositoryInfo('test-org', 'test-repo');
      expect(result.hasContributing).toBe(true);
    });
  });
});

Mock Data Helpers

// src/__tests__/helpers/githubMocks.ts
export const mockRepoResponse = {
  data: {
    name: 'test-repo',
    description: 'A test repository',
    topics: ['typescript', 'cli'],
    archived: false,
    default_branch: 'main',
    license: { key: 'mit' }
  }
};

export const mockIssuesResponse = {
  data: [
    {
      labels: [{ name: 'good first issue' }],
      state: 'open'
    },
    {
      labels: [{ name: 'help wanted' }],
      state: 'open'
    }
  ]
};

export const mockFileNotFound = {
  status: 404,
  message: 'Not Found'
};

export const mockRateLimit = {
  status: 403,
  message: 'API rate limit exceeded'
};

Real API Tests (Optional)

// src/__tests__/integration/github-real.test.ts
// These tests run against real GitHub API (skip in CI)
describe('GitHub API Integration (Real)', () => {
  const githubService = new GitHubService(process.env.GITHUB_TEST_TOKEN);
  
  // Only run if token is available
  const skipIfNoToken = process.env.GITHUB_TEST_TOKEN ? describe : describe.skip;
  
  skipIfNoToken('Real API calls', () => {
    it('evaluates a known good repository', async () => {
      const result = await githubService.getRepositoryInfo('facebook', 'react');
      expect(result.hasReadme).toBe(true);
      expect(result.hasLicense).toBe(true);
    });
  });
});

Files to Create

  • src/__tests__/utils/github.test.ts (main test file)
  • src/__tests__/helpers/githubMocks.ts (mock data)
  • src/__tests__/integration/github-real.test.ts (optional real API tests)

Test Scenarios to Cover

Happy Path

  • Repository with all community health files
  • Repository using organization-level files
  • Public repository with good first issues
  • Repository creation with valid parameters

Error Handling

  • 404 Not Found (repository doesn't exist)
  • 403 Forbidden (private repo, rate limit)
  • Network timeouts and connection errors
  • Invalid token authentication
  • Malformed API responses

Edge Cases

  • Repository with no description
  • Repository with excessive topics
  • Archived repository
  • Repository with mixed file presence

Benefits

  • 🔍 Ensures API interactions work correctly
  • 🛡️ Validates error handling and edge cases
  • ⚡ Catches breaking changes in GitHub API
  • 🎯 Tests critical organization-level file detection
  • 🚀 Provides confidence for API-related changes

Testing Strategy

  1. Unit tests with mocks - Fast, reliable, run in CI
  2. Integration tests - Test actual API behavior (optional)
  3. Error simulation - Test failure scenarios
  4. Performance tests - Ensure rate limiting works

Resources

Dependencies

  • Should be implemented after basic test infrastructure is set up
  • Requires understanding of GitHub API responses
  • May need test GitHub token for real API tests

Estimated Effort

Medium to Hard - Complex API interactions and error scenarios to test.


Great for contributors who want to ensure robust GitHub API integration! 🔗

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions