From 9f9c1c4b53b4c68ba51f89496aa8c90c361d3819 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 25 May 2026 07:37:16 +0530 Subject: [PATCH 1/3] test: added unit tests for auth.ts NextAuth callbacks --- test/auth.test.ts | 232 ++++++++++++++++++++++++++++++++++++++++++++++ test/setup.ts | 5 + 2 files changed, 237 insertions(+) create mode 100644 test/auth.test.ts create mode 100644 test/setup.ts diff --git a/test/auth.test.ts b/test/auth.test.ts new file mode 100644 index 00000000..8e74deca --- /dev/null +++ b/test/auth.test.ts @@ -0,0 +1,232 @@ +import "./setup"; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock supabaseAdmin +const mockUpsert = vi.fn(); +vi.mock('@/lib/supabase', () => ({ + supabaseAdmin: { + from: vi.fn().mockReturnValue({ + upsert: (...args: any[]) => mockUpsert(...args), + }), + }, +})); + +import { beforeAll } from 'vitest'; + +let authOptions: any; + +beforeAll(async () => { + process.env.NEXTAUTH_SECRET = 'test-secret'; + const mod = await import('../src/lib/auth'); + authOptions = mod.authOptions; +}); + +describe('auth.ts NextAuth callbacks', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUpsert.mockResolvedValue({ data: null, error: null }); + }); + + describe('signIn callback', () => { + it('upserts user to Supabase on GitHub sign-in', async () => { + const signInCallback = authOptions.callbacks?.signIn; + if (!signInCallback) return; + + const result = await signInCallback({ + account: { provider: 'github', access_token: 'tok', token_type: 'Bearer' } as any, + profile: { id: 12345, login: 'testuser' } as any, + user: {}, + } as any); + + expect(result).toBe(true); + expect(mockUpsert).toHaveBeenCalledWith( + expect.objectContaining({ + github_id: '12345', + github_login: 'testuser', + }), + expect.objectContaining({ onConflict: 'github_id' }) + ); + }); + + it('does not upsert for non-GitHub providers', async () => { + const signInCallback = authOptions.callbacks?.signIn; + if (!signInCallback) return; + + await signInCallback({ + account: { provider: 'google', access_token: 'tok' } as any, + profile: { id: 123 } as any, + user: {}, + } as any); + + expect(mockUpsert).not.toHaveBeenCalled(); + }); + + it('returns true even if profile is missing', async () => { + const signInCallback = authOptions.callbacks?.signIn; + if (!signInCallback) return; + + const result = await signInCallback({ + account: { provider: 'github' } as any, + profile: undefined, + user: {}, + } as any); + + expect(result).toBe(true); + expect(mockUpsert).not.toHaveBeenCalled(); + }); + + it('passes correct github_id as string from profile', async () => { + const signInCallback = authOptions.callbacks?.signIn; + if (!signInCallback) return; + + await signInCallback({ + account: { provider: 'github' } as any, + profile: { id: 999999, login: 'user99' } as any, + user: {}, + } as any); + + const upsertCall = mockUpsert.mock.calls[0][0]; + expect(upsertCall.github_id).toBe('999999'); + expect(upsertCall.github_login).toBe('user99'); + }); + }); + + describe('jwt callback', () => { + it('attaches access_token to token.jwt', async () => { + const jwtCallback = authOptions.callbacks?.jwt; + if (!jwtCallback) return; + + const token: Record = {}; + const result = await jwtCallback({ + token, + account: { provider: 'github', access_token: 'github-token-abc' } as any, + profile: undefined, + user: {}, + } as any); + + expect(result.accessToken).toBe('github-token-abc'); + }); + + it('does not attach accessToken if not present', async () => { + const jwtCallback = authOptions.callbacks?.jwt; + if (!jwtCallback) return; + + const token: Record = {}; + const result = await jwtCallback({ + token, + account: { provider: 'github' } as any, + profile: undefined, + user: {}, + } as any); + + expect(result.accessToken).toBeUndefined(); + }); + + it('attaches githubId and githubLogin from profile', async () => { + const jwtCallback = authOptions.callbacks?.jwt; + if (!jwtCallback) return; + + const token: Record = {}; + const result = await jwtCallback({ + token, + account: null, + profile: { id: 555, login: 'ghuser' } as any, + user: {}, + } as any); + + expect(result.githubId).toBe('555'); + expect(result.githubLogin).toBe('ghuser'); + }); + + it('returns token unchanged if no account or profile', async () => { + const jwtCallback = authOptions.callbacks?.jwt; + if (!jwtCallback) return; + + const token: Record = { existing: 'value' }; + const result = await jwtCallback({ token, account: null, profile: undefined, user: {} } as any); + + expect(result.existing).toBe('value'); + }); + }); + + describe('session callback', () => { + it('populates session.accessToken from token.jwt', async () => { + const sessionCallback = authOptions.callbacks?.session; + if (!sessionCallback) return; + + const session: Record = {}; + const token = { accessToken: 'jwt-token-xyz', githubId: '111', githubLogin: 'user1' } as any; + const result = await sessionCallback({ session, token, user: {} } as any); + + expect((result as any).accessToken).toBe('jwt-token-xyz'); + }); + + it('populates session.githubId from token.jwt', async () => { + const sessionCallback = authOptions.callbacks?.session; + if (!sessionCallback) return; + + const session: Record = {}; + const token = { accessToken: 'tok', githubId: '222', githubLogin: 'user2' } as any; + const result = await sessionCallback({ session, token, user: {} } as any); + + expect((result as any).githubId).toBe('222'); + }); + + it('populates session.githubLogin from token.jwt', async () => { + const sessionCallback = authOptions.callbacks?.session; + if (!sessionCallback) return; + + const session: Record = {}; + const token = { accessToken: 'tok', githubId: '333', githubLogin: 'user3' } as any; + const result = await sessionCallback({ session, token, user: {} } as any); + + expect((result as any).githubLogin).toBe('user3'); + }); + + it('does not set accessToken if not a string', async () => { + const sessionCallback = authOptions.callbacks?.session; + if (!sessionCallback) return; + + const session: Record = {}; + const token = { accessToken: 123, githubId: '333' } as any; + const result = await sessionCallback({ session, token, user: {} } as any); + + expect((result as any).accessToken).toBeUndefined(); + }); + + it('does not set githubId if not a string', async () => { + const sessionCallback = authOptions.callbacks?.session; + if (!sessionCallback) return; + + const session: Record = {}; + const token = { accessToken: 'tok', githubId: 999 } as any; + const result = await sessionCallback({ session, token, user: {} } as any); + + expect((result as any).githubId).toBeUndefined(); + }); + }); + + describe('authOptions configuration', () => { + it('has jwt strategy configured', () => { + expect(authOptions.session?.strategy).toBe('jwt'); + }); + + it('has correct session max age (30 days)', () => { + expect(authOptions.session?.maxAge).toBe(30 * 24 * 60 * 60); + }); + + it('has jwt max age configured', () => { + expect(authOptions.jwt?.maxAge).toBe(30 * 24 * 60 * 60); + }); + + it('has GitHub provider configured with correct scope', () => { + const githubProvider = authOptions.providers?.[0] as any; + expect(githubProvider?.id).toBe('github'); + expect(githubProvider?.options?.authorization?.params?.scope).toBe('read:user user:email repo read:discussion'); + }); + + it('has NEXTAUTH_SECRET set', () => { + expect(authOptions.secret).toBeDefined(); + }); + }); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 00000000..b3a25ef2 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,5 @@ +process.env.NEXT_PUBLIC_SUPABASE_URL = 'https://test.supabase.co'; +process.env.SUPABASE_SERVICE_ROLE_KEY = 'test-key'; +process.env.NEXTAUTH_SECRET = 'test-secret'; +process.env.GITHUB_ID = 'test-github-id'; +process.env.GITHUB_SECRET = 'test-github-secret'; From fed52f4a4ffde14a9ce4a0fa7b176365de5e8a9c Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 25 May 2026 08:15:57 +0530 Subject: [PATCH 2/3] fix: add missing E2E dashboard widget mocks --- e2e/dashboard-widgets.spec.js | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/e2e/dashboard-widgets.spec.js b/e2e/dashboard-widgets.spec.js index 40ab6562..7ec3ee59 100644 --- a/e2e/dashboard-widgets.spec.js +++ b/e2e/dashboard-widgets.spec.js @@ -15,6 +15,7 @@ test.beforeEach(async ({ page }) => { accessToken: "test-token", }, maxAge: 60 * 60, + cookieName: "next-auth.session-token", }); await page.context().addCookies([ @@ -93,6 +94,45 @@ test.beforeEach(async ({ page }) => { }); }); + await page.route("**/api/goals/sync", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ updated: 1, commitCount: 4 }), + }); + }); + + await page.route("**/api/ai-insights**", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + data: { + insights: [ + { + id: "insight-1", + type: "productivity", + title: "High Consistency", + description: "You have coded 5 days this week!", + severity: "positive", + }, + ], + trend: { direction: "up", percentage: 15 }, + aiSummary: "Great job shipping features this week. Keep up the high standard!", + generatedAt: "2026-05-18T12:00:00.000Z", + }, + }), + }); + }); + + await page.route("**/api/notifications**", async (route) => { + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + notifications: [], + unreadCount: 0, + }), + }); + }); + const metricRoutes = [ "**/api/metrics/prs**", "**/api/metrics/pr-breakdown**", From 6dd9d4e325d85cf9f18425712433089b93f8ea54 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 25 May 2026 14:03:57 +0530 Subject: [PATCH 3/3] chore: add test script to package.json to fix CI run --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8dbab4ec..2bb6df5d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit", + "test": "vitest run", "test:e2e": "playwright test" }, "dependencies": {