import { describe, it, expect, vi, beforeEach } from 'vitest'; // ─── Mocks ────────────────────────────────────────────────────────────────── vi.mock('../../auth.js', () => ({ loadSession: vi.fn(), validateSession: vi.fn(), signIn: vi.fn(), saveSession: vi.fn(), })); vi.mock('./daemon.js', () => ({ readMeta: vi.fn(), writeMeta: vi.fn(), })); vi.mock('./login.js', () => ({ getGatewayUrl: vi.fn().mockReturnValue('http://localhost:14242'), // promptLine/promptSecret are used by ensureSession; return fixed values so tests don't block on stdin promptLine: vi.fn().mockResolvedValue('test@example.com'), promptSecret: vi.fn().mockResolvedValue('test-password'), })); const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); import { runRecoverToken, ensureSession } from './token-ops.js'; import { loadSession, validateSession, signIn, saveSession } from '../../auth.js'; import { readMeta, writeMeta } from './daemon.js'; const mockLoadSession = vi.mocked(loadSession); const mockValidateSession = vi.mocked(validateSession); const mockSignIn = vi.mocked(signIn); const mockSaveSession = vi.mocked(saveSession); const mockReadMeta = vi.mocked(readMeta); const mockWriteMeta = vi.mocked(writeMeta); const baseUrl = 'http://localhost:14242'; const fakeCookie = 'better-auth.session_token=sess123'; const fakeToken = { id: 'tok-1', label: 'CLI recovery token (2026-04-04 12:00)', plaintext: 'abcdef1234567890', }; const fakeMeta = { version: '1.0.0', installedAt: '', entryPoint: '', host: 'localhost', port: 14242, }; describe('ensureSession', () => { beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, 'log').mockImplementation(() => {}); }); it('returns cookie from stored session when valid', async () => { mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' }); mockValidateSession.mockResolvedValueOnce(true); const cookie = await ensureSession(baseUrl); expect(cookie).toBe(fakeCookie); expect(mockSignIn).not.toHaveBeenCalled(); }); it('prompts for credentials and signs in when stored session is invalid', async () => { mockLoadSession.mockReturnValueOnce({ cookie: 'old-cookie', userId: 'u1', email: 'a@b.com' }); mockValidateSession.mockResolvedValueOnce(false); const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'a@b.com' }; mockSignIn.mockResolvedValueOnce(newAuth); const cookie = await ensureSession(baseUrl); expect(cookie).toBe(fakeCookie); expect(mockSaveSession).toHaveBeenCalledWith(baseUrl, newAuth); }); it('prompts for credentials when no session exists', async () => { mockLoadSession.mockReturnValueOnce(null); const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'a@b.com' }; mockSignIn.mockResolvedValueOnce(newAuth); const cookie = await ensureSession(baseUrl); expect(cookie).toBe(fakeCookie); expect(mockSignIn).toHaveBeenCalled(); }); it('exits non-zero when signIn fails', async () => { mockLoadSession.mockReturnValueOnce(null); mockSignIn.mockRejectedValueOnce(new Error('Sign-in failed (401): bad creds')); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); await expect(ensureSession(baseUrl)).rejects.toThrow('process.exit(2)'); expect(processExitSpy).toHaveBeenCalledWith(2); processExitSpy.mockRestore(); consoleErrorSpy.mockRestore(); }); }); describe('runRecoverToken', () => { beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); }); it('prompts for login, mints a token, and persists it when no session exists', async () => { mockLoadSession.mockReturnValueOnce(null); const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'admin@test.com' }; mockSignIn.mockResolvedValueOnce(newAuth); mockReadMeta.mockReturnValue(fakeMeta); mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => fakeToken, }); await runRecoverToken(); expect(mockSignIn).toHaveBeenCalled(); expect(mockFetch).toHaveBeenCalledWith( `${baseUrl}/api/admin/tokens`, expect.objectContaining({ method: 'POST' }), ); expect(mockWriteMeta).toHaveBeenCalledWith( expect.objectContaining({ adminToken: fakeToken.plaintext }), ); }); it('skips login when a valid session exists and mints a recovery token', async () => { mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' }); mockValidateSession.mockResolvedValueOnce(true); mockReadMeta.mockReturnValue(fakeMeta); mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => fakeToken, }); await runRecoverToken(); expect(mockSignIn).not.toHaveBeenCalled(); expect(mockWriteMeta).toHaveBeenCalledWith( expect.objectContaining({ adminToken: fakeToken.plaintext }), ); }); it('uses label containing "recovery token"', async () => { mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' }); mockValidateSession.mockResolvedValueOnce(true); mockReadMeta.mockReturnValue(fakeMeta); mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => fakeToken, }); await runRecoverToken(); const call = mockFetch.mock.calls[0] as [string, RequestInit]; const body = JSON.parse(call[1].body as string) as { label: string }; expect(body.label).toMatch(/CLI recovery token/); }); });