diff --git a/package-lock.json b/package-lock.json index f547cdc8..ee13047a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6378,7 +6378,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/src/app/api/metrics/repo-health/route.ts b/src/app/api/metrics/repo-health/route.ts index 5a8ee0f6..ba8bc8dc 100644 --- a/src/app/api/metrics/repo-health/route.ts +++ b/src/app/api/metrics/repo-health/route.ts @@ -76,7 +76,11 @@ export async function GET(req: NextRequest) { try { const signals = await fetchSignalsForRepo(session.accessToken!, repo.name, days); scores.push(computeHealthScore(repo.name, signals)); - } catch {} + } catch (err) { + console.error(`Failed to fetch signals for repo ${repo}:`, err); + // Continue with remaining repos + continue; + } } return { repos: scores }; }); diff --git a/test/resolve-user.test.ts b/test/resolve-user.test.ts new file mode 100644 index 00000000..d279f84f --- /dev/null +++ b/test/resolve-user.test.ts @@ -0,0 +1,98 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fromMock = vi.fn(); +const supabaseAdmin = { + from: fromMock, +}; + +vi.mock("../src/lib/supabase", () => ({ + supabaseAdmin, +})); + +const { resolveAppUser } = await import("../src/lib/resolve-user"); + +function createExistingChain(result: unknown) { + const single = vi.fn(async () => result); + const eq = vi.fn(() => ({ single })); + const select = vi.fn(() => ({ eq })); + return { select, eq, single }; +} + +function createUpsertChain(result: unknown) { + const single = vi.fn(async () => result); + const select = vi.fn(() => ({ single })); + const upsert = vi.fn(() => ({ select })); + return { upsert, select, single }; +} + +describe("resolveAppUser", () => { + beforeEach(() => { + fromMock.mockReset(); + }); + + it("returns null when user not found and githubLogin is null", async () => { + const existingChain = createExistingChain({ data: null }); + const upsertChain = createUpsertChain({ data: null }); + + fromMock + .mockImplementationOnce(() => existingChain) + .mockImplementationOnce(() => upsertChain); + + const result = await resolveAppUser("github-id", null as unknown as string); + + expect(result).toBeNull(); + expect(fromMock).toHaveBeenCalledTimes(2); + expect(existingChain.select).toHaveBeenCalledWith("id"); + expect(upsertChain.upsert).toHaveBeenCalledTimes(1); + }); + + it("upserts new user when githubLogin is provided", async () => { + const existingChain = createExistingChain({ data: null }); + const upsertChain = createUpsertChain({ data: { id: "new-user-id" } }); + + fromMock + .mockImplementationOnce(() => existingChain) + .mockImplementationOnce(() => upsertChain); + + const result = await resolveAppUser("github-id", "github-login"); + + expect(result).toEqual({ id: "new-user-id" }); + expect(fromMock).toHaveBeenCalledTimes(2); + expect(existingChain.select).toHaveBeenCalledWith("id"); + expect(upsertChain.upsert).toHaveBeenCalledTimes(1); + + const upsertPayload = upsertChain.upsert.mock.calls[0][0]; + expect(upsertPayload).toEqual( + expect.objectContaining({ + github_id: "github-id", + github_login: "github-login", + }) + ); + expect(typeof upsertPayload.updated_at).toBe("string"); + }); + + it("returns existing user data when found", async () => { + const existingChain = createExistingChain({ data: { id: "existing-id" } }); + + fromMock.mockImplementationOnce(() => existingChain); + + const result = await resolveAppUser("github-id", "ignored-login"); + + expect(result).toEqual({ id: "existing-id" }); + expect(fromMock).toHaveBeenCalledTimes(1); + }); + + it("handles database errors gracefully", async () => { + const existingChain = createExistingChain({ data: null, error: new Error("database failure") }); + const upsertChain = createUpsertChain({ data: null, error: new Error("insert failure") }); + + fromMock + .mockImplementationOnce(() => existingChain) + .mockImplementationOnce(() => upsertChain); + + const result = await resolveAppUser("github-id", "github-login"); + + expect(result).toBeNull(); + expect(fromMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..5c0ab798 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + resolve: { + alias: [ + { + find: "@", + replacement: resolve(__dirname, "src"), + }, + ], + }, +});