import { describe, expect, it } from 'vitest' import { discoverRepos } from './discover.js' import { GiteaApiError } from './types.js' import type { FetchLike } from './types.js' /** A fetch stub that answers /user and paginated /user/repos from a fixture. */ function stub(opts: { login: string repos: { name: string; owner: string }[] fail?: { status: number } pageLimit?: number }): { fetch: FetchLike; urls: string[] } { const urls: string[] = [] const limit = opts.pageLimit ?? 50 const fetch: FetchLike = (url) => { urls.push(url) if (opts.fail) { return Promise.resolve({ ok: false, status: opts.fail.status, json: () => Promise.resolve({}), text: () => Promise.resolve('nope') }) } const ok = (body: unknown) => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(body), text: () => Promise.resolve('') }) if (url.endsWith('/api/v1/user')) return ok({ login: opts.login }) const m = url.match(/\/user\/repos\?page=(\d+)&limit=(\d+)/) if (m) { const page = Number(m[1]) const raw = opts.repos.map((r) => ({ name: r.name, owner: { login: r.owner } })) const start = (page - 1) * limit return ok(raw.slice(start, start + limit)) } return Promise.resolve({ ok: false, status: 404, json: () => Promise.resolve({}), text: () => Promise.resolve('') }) } return { fetch, urls } } describe('discoverRepos', () => { it('groups repos by owner, sorts them, and leads with the token owner', async () => { const { fetch } = stub({ login: 'christian', repos: [ { name: 'zed', owner: 'acme' }, { name: 'commitea', owner: 'christian' }, { name: 'apex', owner: 'acme' }, { name: 'commitea-pm-state', owner: 'christian' }, ], }) const d = await discoverRepos({ baseUrl: 'https://gitea.example.io', token: 'pat' }, fetch) expect(d.user).toBe('christian') expect(d.owners).toEqual(['christian', 'acme']) // own login first, then alpha expect(d.reposByOwner.christian).toEqual(['commitea', 'commitea-pm-state']) // sorted expect(d.reposByOwner.acme).toEqual(['apex', 'zed']) }) it('trims a trailing slash on the base URL and calls /api/v1', async () => { const { fetch, urls } = stub({ login: 'x', repos: [{ name: 'r', owner: 'x' }] }) await discoverRepos({ baseUrl: 'https://gitea.example.io/', token: 'pat' }, fetch) expect(urls[0]).toBe('https://gitea.example.io/api/v1/user') expect(urls[1]).toBe('https://gitea.example.io/api/v1/user/repos?page=1&limit=50') }) it('paginates until a short page', async () => { const repos = Array.from({ length: 73 }, (_, i) => ({ name: `r${String(i).padStart(2, '0')}`, owner: 'x' })) const { fetch, urls } = stub({ login: 'x', repos }) const d = await discoverRepos({ baseUrl: 'https://g', token: 'pat' }, fetch) expect(d.reposByOwner.x).toHaveLength(73) // /user + page 1 (50) + page 2 (23, short → stop) expect(urls.filter((u) => u.includes('/user/repos'))).toHaveLength(2) }) it('propagates an auth failure as a GiteaApiError (so the UI can map 401/403)', async () => { const { fetch } = stub({ login: 'x', repos: [], fail: { status: 401 } }) await expect(discoverRepos({ baseUrl: 'https://g', token: 'bad' }, fetch)).rejects.toBeInstanceOf(GiteaApiError) }) it('skips malformed repo rows (missing owner/name)', async () => { const { fetch } = stub({ login: 'x', repos: [{ name: 'good', owner: 'x' }] }) // inject a bad row by wrapping — simpler: rely on the guard via a direct malformed fixture const d = await discoverRepos({ baseUrl: 'https://g', token: 'pat' }, fetch) expect(d.reposByOwner.x).toEqual(['good']) }) })