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'), })); // Mock global fetch const mockFetch = vi.fn(); vi.stubGlobal('fetch', mockFetch); import { runRotateToken, mintAdminToken, persistToken } from './token-ops.js'; import { loadSession, validateSession } from '../../auth.js'; import { readMeta, writeMeta } from './daemon.js'; const mockLoadSession = vi.mocked(loadSession); const mockValidateSession = vi.mocked(validateSession); 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 rotated token (2026-04-04)', plaintext: 'abcdef1234567890', }; const fakeMeta = { version: '1.0.0', installedAt: '', entryPoint: '', host: 'localhost', port: 14242, }; describe('mintAdminToken', () => { beforeEach(() => { vi.clearAllMocks(); }); it('calls the admin tokens endpoint with the session cookie and returns the token', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, json: async () => fakeToken, }); const result = await mintAdminToken(baseUrl, fakeCookie, fakeToken.label); expect(mockFetch).toHaveBeenCalledWith( `${baseUrl}/api/admin/tokens`, expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Cookie: fakeCookie }), }), ); expect(result).toEqual(fakeToken); }); it('exits 2 on 401 from the server', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, text: async () => 'Unauthorized' }); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(2)'); expect(processExitSpy).toHaveBeenCalledWith(2); processExitSpy.mockRestore(); }); it('exits 2 on 403 from the server', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 403, text: async () => 'Forbidden' }); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(2)'); expect(processExitSpy).toHaveBeenCalledWith(2); processExitSpy.mockRestore(); }); it('exits 3 on other non-ok status', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'Internal Error' }); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(3)'); expect(processExitSpy).toHaveBeenCalledWith(3); processExitSpy.mockRestore(); }); it('exits 1 on network error', async () => { mockFetch.mockRejectedValueOnce(new Error('connection refused')); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(1)'); expect(processExitSpy).toHaveBeenCalledWith(1); processExitSpy.mockRestore(); }); }); describe('persistToken', () => { beforeEach(() => { vi.clearAllMocks(); }); it('writes the new token to meta.json', () => { mockReadMeta.mockReturnValueOnce(fakeMeta); const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); persistToken(baseUrl, fakeToken); expect(mockWriteMeta).toHaveBeenCalledWith( expect.objectContaining({ adminToken: fakeToken.plaintext }), ); consoleSpy.mockRestore(); }); it('prints a masked preview of the token', () => { mockReadMeta.mockReturnValueOnce(fakeMeta); const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); persistToken(baseUrl, fakeToken); const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n'); expect(allOutput).toContain('abcdef12...'); consoleSpy.mockRestore(); }); }); describe('runRotateToken', () => { beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(console, 'log').mockImplementation(() => {}); }); it('exits 2 when there is no stored session', async () => { mockLoadSession.mockReturnValueOnce(null); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(runRotateToken()).rejects.toThrow('process.exit(2)'); expect(processExitSpy).toHaveBeenCalledWith(2); processExitSpy.mockRestore(); }); it('exits 2 when session is invalid', async () => { mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' }); mockValidateSession.mockResolvedValueOnce(false); const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code?: number | string | null | undefined) => { throw new Error(`process.exit(${String(_code)})`); }); await expect(runRotateToken()).rejects.toThrow('process.exit(2)'); expect(processExitSpy).toHaveBeenCalledWith(2); processExitSpy.mockRestore(); }); it('mints and persists a new token when session is valid', 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 runRotateToken(); expect(mockWriteMeta).toHaveBeenCalledWith( expect.objectContaining({ adminToken: fakeToken.plaintext }), ); }); });