135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
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/),
|
|
});
|
|
});
|
|
});
|