import { describe, expect, it, vi } from 'vitest'; import { AccountAuthContext, authenticateCurrentAccount, } from '../src/services/account-auth-context.js'; import { createMockLogger } from './mockLogger.js'; describe('AccountAuthContext', () => { it('does not attempt Matrix capture until Reddit identity is authoritative', async () => { const refreshMatrixCredentials = vi.fn(); await expect(authenticateCurrentAccount({ getRedditIdentity: vi.fn().mockResolvedValue(null), refreshMatrixCredentials, })).resolves.toBeNull(); expect(refreshMatrixCredentials).not.toHaveBeenCalled(); }); it('fails closed until Reddit and Matrix resolve to one authenticated account', async () => { const auth = new AccountAuthContext(createMockLogger()); const authenticate = vi.fn().mockResolvedValue({ accountId: 't2_account', redditUsername: 'account', matrixUserId: '@t2_account:reddit.com', }); auth.setAuthenticator(authenticate); expect(auth.isFullyAuthenticated()).toBe(false); expect(auth.getSnapshot().status).toBe('login-required'); await expect(auth.ensureFullyAuthenticated()).resolves.toBe(true); expect(auth.isFullyAuthenticated()).toBe(true); expect(auth.getSnapshot()).toMatchObject({ status: 'authenticated', accountId: 't2_account', }); }); it('coalesces concurrent authentication checks globally', async () => { const auth = new AccountAuthContext(createMockLogger()); let resolveAuth: ((account: { accountId: string; redditUsername: string; matrixUserId: string; }) => void) | undefined; const authenticate = vi.fn(() => new Promise((resolve) => { resolveAuth = resolve; })); auth.setAuthenticator(authenticate); const first = auth.ensureFullyAuthenticated(); const second = auth.ensureFullyAuthenticated(); await vi.waitFor(() => expect(authenticate).toHaveBeenCalledOnce()); resolveAuth?.({ accountId: 't2_account', redditUsername: 'account', matrixUserId: '@t2_account:reddit.com', }); await expect(Promise.all([first, second])).resolves.toEqual([true, true]); }); it('invalidates the whole account context when either login layer expires', async () => { const auth = new AccountAuthContext(createMockLogger()); auth.markAuthenticated({ accountId: 't2_account', redditUsername: 'account', matrixUserId: '@t2_account:reddit.com', }); auth.invalidate('matrix token rejected'); expect(auth.isFullyAuthenticated()).toBe(false); expect(auth.getSnapshot()).toMatchObject({ status: 'login-required', accountId: null, reason: 'matrix token rejected', }); }); it('fails closed when the authentication lease expires', () => { let now = 1_000; const auth = new AccountAuthContext(createMockLogger(), { leaseMs: 100, retryDelayMs: 1_000, now: () => now, }); auth.markAuthenticated({ accountId: 't2_account', redditUsername: 'account', matrixUserId: '@t2_account:reddit.com', }); now = 1_101; expect(auth.isFullyAuthenticated()).toBe(false); expect(auth.getSnapshot().status).toBe('login-required'); }); it('throttles failed global authentication checks', async () => { let now = 1_000; const auth = new AccountAuthContext(createMockLogger(), { leaseMs: 100, retryDelayMs: 1_000, now: () => now, }); const authenticate = vi.fn().mockResolvedValue(null); auth.setAuthenticator(authenticate); await expect(auth.ensureFullyAuthenticated()).resolves.toBe(false); await expect(auth.ensureFullyAuthenticated()).resolves.toBe(false); expect(authenticate).toHaveBeenCalledOnce(); now = 2_001; await auth.ensureFullyAuthenticated(); expect(authenticate).toHaveBeenCalledTimes(2); }); it('fails closed when Reddit and Matrix belong to different accounts', async () => { const auth = new AccountAuthContext(createMockLogger()); auth.setAuthenticator(vi.fn().mockResolvedValue({ accountId: 't2_cookie', redditUsername: 'cookie-account', matrixUserId: '@t2_chat:reddit.com', })); await expect(auth.ensureFullyAuthenticated()).resolves.toBe(false); expect(auth.getSnapshot()).toMatchObject({ status: 'login-required', accountId: null, reason: expect.stringMatching(/does not match Matrix account/), }); }); });