Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/services/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export async function fetchRepos(org, repoCount, pat) {
for (let page = 1; page <= maxPages; page++) {
const url = `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}&sort=updated`
const data = await fetchWithCache(url, pat)
// The API returns a message/error object instead of an array for some
// valid states (disabled issues, empty repo, 204 No Content); treat that
// as an empty, final page rather than crashing on the spread below.
if (!Array.isArray(data)) break
all.push(...data)
if (data.length < 100) break
}
Expand All @@ -104,6 +108,7 @@ export async function fetchContributors(org, repo, pat) {
for(let page = 1; page<=maxPages ; page++) {
const url = `https://api.github.com/repos/${org}/${repo}/contributors?per_page=100&page=${page}`
const data = await fetchWithCache(url, pat)
if (!Array.isArray(data)) break
all.push(...data)
if(data.length < 100) break
}
Expand All @@ -116,6 +121,7 @@ export async function fetchIssues(org, repo, pat) {
for(let page = 1; page<=maxPages ; page++) {
const url = `https://api.github.com/repos/${org}/${repo}/issues?state=all&per_page=100&page=${page}`
const data = await fetchWithCache(url, pat)
if (!Array.isArray(data)) break
all.push(...data)
if(data.length < 100) break
}
Expand All @@ -128,6 +134,7 @@ export async function fetchPulls(org, repo, pat) {
for(let page = 1; page<=maxPages ; page++) {
const url = `https://api.github.com/repos/${org}/${repo}/pulls?state=all&per_page=100&page=${page}`
const data = await fetchWithCache(url, pat)
if (!Array.isArray(data)) break
all.push(...data)
if(data.length < 100) break
}
Expand Down
57 changes: 57 additions & 0 deletions src/services/github.pagination.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { fetchRepos, fetchContributors, fetchIssues, fetchPulls } from './github'

// GitHub returns a message/error object instead of an array for several
// valid states: issues disabled on a repo, an empty repository, or a 204 No
// Content response. `all.push(...data)` on one of those throws
// `TypeError: data is not iterable` and aborts the whole fetch.
function mockFetchOnce(body, status = 200) {
global.fetch = vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
headers: { get: () => null },
json: () => Promise.resolve(body)
})
}

describe('paginated fetchers guard against non-array API responses', () => {
beforeEach(() => {
// Bypass the IndexedDB-backed cache layer so each call reliably hits fetch.
global.indexedDB = undefined
})

afterEach(() => {
vi.restoreAllMocks()
})

it('fetchRepos returns [] instead of throwing when the API returns an error object', async () => {
mockFetchOnce({ message: 'Git Repository is empty.' })

await expect(fetchRepos('org', 0, null)).resolves.toEqual([])
})

it('fetchContributors returns [] instead of throwing when issues/contributors are disabled', async () => {
mockFetchOnce({ message: 'Issues are disabled in this repository' })

await expect(fetchContributors('org', 'repo', null)).resolves.toEqual([])
})

it('fetchIssues returns [] instead of throwing on a non-array payload', async () => {
mockFetchOnce({ message: 'Issues are disabled in this repository' })

await expect(fetchIssues('org', 'repo', null)).resolves.toEqual([])
})

it('fetchPulls returns [] instead of throwing on a non-array payload', async () => {
mockFetchOnce({ message: 'Git Repository is empty.' })

await expect(fetchPulls('org', 'repo', null)).resolves.toEqual([])
})

it('fetchRepos still collects normal array pages', async () => {
const repos = Array.from({ length: 3 }, (_, i) => ({ id: i }))
mockFetchOnce(repos)

await expect(fetchRepos('org', 3, null)).resolves.toEqual(repos)
})
})
Loading