import Database from 'better-sqlite3'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { RedditAccountsRepository } from '../src/database/account-repositories.js'; import { runMigrations } from '../src/database/client.js'; import { AccountAuthContext } from '../src/services/account-auth-context.js'; import { RedditAccountsService } from '../src/services/reddit-accounts.js'; const silentLogger = { info: () => undefined, warn: () => undefined, error: () => undefined, debug: () => undefined, } as never; describe('removing a Reddit account', () => { let db: Database.Database; let accounts: RedditAccountsRepository; let auth: AccountAuthContext; let stopped: string[]; let service: RedditAccountsService; beforeEach(() => { db = new Database(':memory:'); db.pragma('foreign_keys = ON'); runMigrations(db); accounts = new RedditAccountsRepository(db); auth = new AccountAuthContext(silentLogger); stopped = []; const runtimes = { get: () => null, stop: async (accountId: string) => { stopped.push(accountId); }, }; service = new RedditAccountsService( accounts, { import: async () => { throw new Error('not used'); } } as never, auth, runtimes as never, ); accounts.upsertVerified({ accountId: 't2_alice', redditUsername: 'alice-bot', matrixUserId: '@t2_alice:reddit.com', cookiesJson: JSON.stringify([{ name: 'reddit_session', value: 'secret' }]), matrixCredentialsJson: JSON.stringify({ accessToken: 'matrix-secret' }), now: 100, }); }); afterEach(() => db.close()); it('stops the runtime and destroys the session while keeping the account', async () => { const removed = await service.remove('t2_alice'); expect(stopped).toEqual(['t2_alice']); expect(removed).toMatchObject({ accountId: 't2_alice', redditUsername: 'alice-bot', isRunning: false, status: 'removed', }); // Retired, not deleted: the row survives so its content keeps an owner. expect(accounts.get('t2_alice')).toMatchObject({ status: 'removed', cookiesJson: '[]', matrixCredentialsJson: '{}', }); expect(service.list().map((account) => account.accountId)).toEqual(['t2_alice']); expect(service.listRemoved().map((account) => account.accountId)).toEqual(['t2_alice']); }); it('refuses to remove an account twice', async () => { await service.remove('t2_alice'); stopped = []; await expect(service.remove('t2_alice')).rejects.toThrow('Reddit account was already removed'); expect(stopped).toEqual([]); }); it('drops the authenticated session when the removed account held it', async () => { auth.markAuthenticated({ accountId: 't2_alice', redditUsername: 'alice-bot', matrixUserId: '@t2_alice:reddit.com', }); expect(auth.getSnapshot().accountId).toBe('t2_alice'); await service.remove('t2_alice'); expect(auth.getSnapshot()).toMatchObject({ status: 'login-required', accountId: null }); }); it('leaves another account\'s session alone', async () => { accounts.upsertVerified({ accountId: 't2_bob', redditUsername: 'bob-bot', matrixUserId: '@t2_bob:reddit.com', cookiesJson: JSON.stringify([{ name: 'reddit_session', value: 'bob-secret' }]), matrixCredentialsJson: JSON.stringify({ accessToken: 'bob-matrix-secret' }), now: 110, }); auth.markAuthenticated({ accountId: 't2_bob', redditUsername: 'bob-bot', matrixUserId: '@t2_bob:reddit.com', }); await service.remove('t2_alice'); expect(auth.getSnapshot()).toMatchObject({ status: 'authenticated', accountId: 't2_bob' }); expect(service.listRemoved().map((account) => account.accountId)).toEqual(['t2_alice']); // Bob is untouched by Alice's removal — still listed, still not retired. expect(service.list().find((account) => account.accountId === 't2_bob')?.status) .not.toBe('removed'); }); it('refuses to remove an account it does not know, without stopping anything', async () => { await expect(service.remove('t2_missing')).rejects.toThrow('Reddit account not found'); expect(stopped).toEqual([]); expect(accounts.get('t2_alice')).toMatchObject({ status: 'verified' }); }); });