172 lines
5.9 KiB
TypeScript
172 lines
5.9 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'),
|
|
// 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/);
|
|
});
|
|
});
|