valaxy-addon-git-log
Version:
Integrates git logs into your page of Valaxy site.
247 lines (196 loc) • 7.85 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getGitHubToken, guessGitHubUsername, parseGithubUrl, resolveGitHubUsers } from '../../utils/github'
describe('parseGithubUrl', () => {
it('should parse HTTPS GitHub URL', () => {
const result = parseGithubUrl('https://github.com/valaxyjs/valaxy-addon-git-log')
expect(result).toEqual({ owner: 'valaxyjs', repo: 'valaxy-addon-git-log' })
})
it('should parse HTTPS GitHub URL with .git suffix', () => {
const result = parseGithubUrl('https://github.com/valaxyjs/valaxy-addon-git-log.git')
expect(result).toEqual({ owner: 'valaxyjs', repo: 'valaxy-addon-git-log' })
})
it('should parse SSH GitHub URL', () => {
const result = parseGithubUrl('git@github.com:valaxyjs/valaxy-addon-git-log.git')
expect(result).toEqual({ owner: 'valaxyjs', repo: 'valaxy-addon-git-log' })
})
it('should parse SSH GitHub URL without .git suffix', () => {
const result = parseGithubUrl('git@github.com:user/repo')
expect(result).toEqual({ owner: 'user', repo: 'repo' })
})
it('should throw for invalid URL', () => {
expect(() => parseGithubUrl('https://gitlab.com/user/repo'))
.toThrow('valaxy-addon-git-log: Invalid GitHub URL')
})
it('should throw for empty string', () => {
expect(() => parseGithubUrl(''))
.toThrow('valaxy-addon-git-log: Invalid GitHub URL')
})
})
describe('guessGitHubUsername', () => {
it('should extract username from noreply email', () => {
expect(guessGitHubUsername('yunyou@users.noreply.github.com')).toBe('yunyou')
})
it('should extract username from noreply email with numeric prefix', () => {
expect(guessGitHubUsername('12345+yunyou@users.noreply.github.com')).toBe('yunyou')
})
it('should return null for regular email', () => {
expect(guessGitHubUsername('user@example.com')).toBeNull()
})
it('should return null for empty string', () => {
expect(guessGitHubUsername('')).toBeNull()
})
})
describe('getGitHubToken', () => {
const original = { GITHUB_TOKEN: process.env.GITHUB_TOKEN, GH_TOKEN: process.env.GH_TOKEN }
afterEach(() => {
process.env.GITHUB_TOKEN = original.GITHUB_TOKEN
process.env.GH_TOKEN = original.GH_TOKEN
})
it('should prefer GITHUB_TOKEN', () => {
process.env.GITHUB_TOKEN = 'gh_token'
process.env.GH_TOKEN = 'cli_token'
expect(getGitHubToken()).toBe('gh_token')
})
it('should fall back to GH_TOKEN', () => {
delete process.env.GITHUB_TOKEN
process.env.GH_TOKEN = 'cli_token'
expect(getGitHubToken()).toBe('cli_token')
})
it('should return undefined when no token is set', () => {
delete process.env.GITHUB_TOKEN
delete process.env.GH_TOKEN
expect(getGitHubToken()).toBeUndefined()
})
})
describe('resolveGitHubUsers', () => {
const original = { GITHUB_TOKEN: process.env.GITHUB_TOKEN, GH_TOKEN: process.env.GH_TOKEN }
beforeEach(() => {
delete process.env.GITHUB_TOKEN
delete process.env.GH_TOKEN
})
afterEach(() => {
process.env.GITHUB_TOKEN = original.GITHUB_TOKEN
process.env.GH_TOKEN = original.GH_TOKEN
})
it('should return empty map for empty emails', async () => {
const result = await resolveGitHubUsers('owner', 'repo', [])
expect(result).toBeInstanceOf(Map)
expect(result.size).toBe(0)
})
it('should authenticate Octokit when a token is present', async () => {
process.env.GITHUB_TOKEN = 'secret_token'
const octokitSpy = vi.fn()
vi.doMock('@octokit/rest', () => ({
Octokit: class {
constructor(opts?: unknown) {
octokitSpy(opts)
}
repos = {
listCommits: vi.fn().mockResolvedValue({ data: [] }),
}
},
}))
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
await resolve('owner', 'repo', ['user@example.com'])
expect(octokitSpy).toHaveBeenCalledWith({ auth: 'secret_token' })
vi.doUnmock('@octokit/rest')
})
it('should construct Octokit without auth when no token is present', async () => {
const octokitSpy = vi.fn()
vi.doMock('@octokit/rest', () => ({
Octokit: class {
constructor(opts?: unknown) {
octokitSpy(opts)
}
repos = {
listCommits: vi.fn().mockResolvedValue({ data: [] }),
}
},
}))
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
await resolve('owner', 'repo', ['user@example.com'])
expect(octokitSpy).toHaveBeenCalledWith({})
vi.doUnmock('@octokit/rest')
})
it('should resolve emails from recent commits', async () => {
vi.doMock('@octokit/rest', () => ({
Octokit: class {
repos = {
listCommits: vi.fn().mockResolvedValue({
data: [
{ commit: { author: { email: 'alice@example.com' } }, author: { login: 'alice' } },
{ commit: { author: { email: 'bob@example.com' } }, author: { login: 'bob' } },
],
}),
}
},
}))
// Re-import to pick up the mock
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
const result = await resolve('owner', 'repo', ['alice@example.com', 'bob@example.com'])
expect(result.get('alice@example.com')).toBe('alice')
expect(result.get('bob@example.com')).toBe('bob')
vi.doUnmock('@octokit/rest')
})
it('should fall back to per-email queries for unresolved emails', async () => {
const listCommitsFn = vi.fn()
// Phase 1: recent commits - returns only alice
.mockResolvedValueOnce({
data: [
{ commit: { author: { email: 'alice@example.com' } }, author: { login: 'alice' } },
],
})
// Phase 3: per-email query for bob
.mockResolvedValueOnce({
data: [
{ commit: { author: { email: 'bob@example.com' } }, author: { login: 'bob' } },
],
})
vi.doMock('@octokit/rest', () => ({
Octokit: class {
repos = { listCommits: listCommitsFn }
},
}))
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
const result = await resolve('owner', 'repo', ['alice@example.com', 'bob@example.com'])
expect(result.get('alice@example.com')).toBe('alice')
expect(result.get('bob@example.com')).toBe('bob')
// Phase 1 (1) + Phase 3 per-email (1) = 2 calls
expect(listCommitsFn).toHaveBeenCalledTimes(2)
vi.doUnmock('@octokit/rest')
})
it('should gracefully handle API errors', async () => {
vi.doMock('@octokit/rest', () => ({
Octokit: class {
repos = {
listCommits: vi.fn().mockRejectedValue(new Error('Network error')),
}
},
}))
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
const result = await resolve('owner', 'repo', ['test@example.com'])
expect(result.size).toBe(0)
expect(warnSpy).toHaveBeenCalledOnce()
warnSpy.mockRestore()
vi.doUnmock('@octokit/rest')
})
it('should deduplicate emails', async () => {
const listCommitsFn = vi.fn().mockResolvedValue({
data: [
{ commit: { author: { email: 'dup@example.com' } }, author: { login: 'dupuser' } },
],
})
vi.doMock('@octokit/rest', () => ({
Octokit: class {
repos = { listCommits: listCommitsFn }
},
}))
const { resolveGitHubUsers: resolve } = await import('../../utils/github')
const result = await resolve('owner', 'repo', ['dup@example.com', 'dup@example.com', 'dup@example.com'])
expect(result.get('dup@example.com')).toBe('dupuser')
// Only 1 API call despite 3 duplicate emails
expect(listCommitsFn).toHaveBeenCalledTimes(1)
vi.doUnmock('@octokit/rest')
})
})