import Database from 'better-sqlite3'; import { Customer } from '../src/auth'; import { SqliteCustomerRepo } from '../src/db/customer-repo'; const customer = (apiKey: string): Customer => ({ id: 'cust_1', name: 'Ada', tierId: 'pro', apiKey, stripeCustomerId: 'cus_123', }); describe('SqliteCustomerRepo', () => { it('finds a customer by API key', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); expect(repo.findByApiKey('key-ada')?.tierId).toBe('pro'); }); it('returns undefined for an unknown key', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); expect(repo.findByApiKey('wrong')).toBeUndefined(); }); it('lists all customers with their Stripe ids', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); const all = repo.list(); expect(all).toHaveLength(1); expect(all[0].stripeCustomerId).toBe('cus_123'); }); it('seeds only when the table is empty', () => { const db = new Database(':memory:'); new SqliteCustomerRepo(db, [customer('key-ada')]); const again = new SqliteCustomerRepo(db, [customer('key-other')]); expect(again.list().map((c) => c.apiKey)).toEqual(['key-ada']); }); it('save() upserts including the multiplier override', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); repo.save({ ...customer('key-ada'), tierId: 'business', multiplierOverride: 0.4 }); const updated = repo.findByApiKey('key-ada'); expect(updated?.tierId).toBe('business'); expect(updated?.multiplierOverride).toBe(0.4); expect(repo.list()).toHaveLength(1); }); it('round-trips billingType and email', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); repo.save({ ...customer('key-ada'), billingType: 'purchase_order', email: 'ada@example.com' }); const updated = repo.findByApiKey('key-ada'); expect(updated?.billingType).toBe('purchase_order'); expect(updated?.email).toBe('ada@example.com'); }); it('migrates a legacy table, defaulting billingType to stripe', () => { const db = new Database(':memory:'); db.exec(`CREATE TABLE customers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, tier_id TEXT NOT NULL, api_key TEXT NOT NULL UNIQUE, stripe_customer_id TEXT, multiplier_override REAL )`); db.prepare( `INSERT INTO customers (id, name, tier_id, api_key) VALUES ('cust_1', 'Ada', 'pro', 'key-ada')`, ).run(); const repo = new SqliteCustomerRepo(db); const migrated = repo.findByApiKey('key-ada'); expect(migrated?.billingType).toBe('stripe'); expect(migrated?.email).toBeUndefined(); // and new columns are writable after migration repo.save({ ...migrated!, billingType: 'purchase_order' }); expect(repo.findByApiKey('key-ada')?.billingType).toBe('purchase_order'); }); it('finds a customer by email (case-insensitive)', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [ { ...customer('key-ada'), email: 'Ada@Example.com' }, ]); expect(repo.findByEmail('ada@example.com')?.id).toBe('cust_1'); expect(repo.findByEmail('nobody@example.com')).toBeUndefined(); }); it('round-trips portal identity fields', () => { const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]); repo.save({ ...customer('key-ada'), passwordHash: 'scrypt:16384:8:1:c2FsdA==:aGFzaA==', totpSecret: 'ABC234', totpEnabled: true, balanceCents: 2500, emailInvoicing: true, }); const updated = repo.findByApiKey('key-ada'); expect(updated?.passwordHash).toBe('scrypt:16384:8:1:c2FsdA==:aGFzaA=='); expect(updated?.totpSecret).toBe('ABC234'); expect(updated?.totpEnabled).toBe(true); expect(updated?.balanceCents).toBe(2500); expect(updated?.emailInvoicing).toBe(true); }); it('defaults portal identity fields for legacy rows', () => { const db = new Database(':memory:'); db.exec(`CREATE TABLE customers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, tier_id TEXT NOT NULL, api_key TEXT NOT NULL UNIQUE, stripe_customer_id TEXT, multiplier_override REAL )`); db.prepare( `INSERT INTO customers (id, name, tier_id, api_key) VALUES ('cust_1', 'Ada', 'pro', 'key-ada')`, ).run(); const repo = new SqliteCustomerRepo(db); const migrated = repo.findByApiKey('key-ada'); expect(migrated?.passwordHash).toBeUndefined(); expect(migrated?.totpSecret).toBeUndefined(); expect(migrated?.totpEnabled).toBe(false); expect(migrated?.balanceCents).toBe(0); expect(migrated?.emailInvoicing).toBe(false); }); });