206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
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 }),
|
|
);
|
|
});
|
|
});
|