/** * Tests for server/utils/tokenStore.js * * The token store persists Emby access tokens to disk (JSON file) so users * survive server restarts without re-logging in. Tests verify the store/get/ * clear lifecycle, TTL expiry, and atomic write behaviour. * * Each test imports a FRESH module instance (vi.resetModules) so the * module-level singleton state (loaded from disk) doesn't bleed between tests. */ import { vi } from 'vitest'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Each test gets its own isolated temp dir let tmpDir; let tokenStore; async function freshStore(dir) { vi.resetModules(); process.env.DATA_DIR = dir; const mod = await import('../../server/utils/tokenStore.js'); return mod; } beforeEach(async () => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sofarr-ts-')); tokenStore = await freshStore(tmpDir); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); describe('tokenStore', () => { it('stores and retrieves a token', () => { tokenStore.storeToken('user1', 'access-token-abc'); const result = tokenStore.getToken('user1'); expect(result).not.toBeNull(); expect(result.accessToken).toBe('access-token-abc'); }); it('returns null for an unknown user', () => { expect(tokenStore.getToken('nobody')).toBeNull(); }); it('clears a stored token', () => { tokenStore.storeToken('user1', 'token-xyz'); tokenStore.clearToken('user1'); expect(tokenStore.getToken('user1')).toBeNull(); }); it('clearToken is a no-op for unknown user', () => { expect(() => tokenStore.clearToken('ghost')).not.toThrow(); }); it('overwrites existing token on re-store', () => { tokenStore.storeToken('user1', 'old-token'); tokenStore.storeToken('user1', 'new-token'); expect(tokenStore.getToken('user1').accessToken).toBe('new-token'); }); it('persists to disk (tokens.json exists after store)', () => { tokenStore.storeToken('u1', 'tok'); const storePath = path.join(tmpDir, 'tokens.json'); expect(fs.existsSync(storePath)).toBe(true); const data = JSON.parse(fs.readFileSync(storePath, 'utf8')); expect(data.u1.accessToken).toBe('tok'); }); it('expires tokens older than 31 days on read', () => { // Write an already-expired entry directly to disk const expired = Date.now() - (32 * 24 * 60 * 60 * 1000); const storePath = path.join(tmpDir, 'tokens.json'); fs.writeFileSync(storePath, JSON.stringify({ u1: { accessToken: 'old', createdAt: expired } })); // Re-import to load from disk vi.resetModules(); return import('../../server/utils/tokenStore.js').then(mod => { expect(mod.getToken('u1')).toBeNull(); }); }); });