import { describe, it, expect, beforeEach } from 'vitest'; import type Database from 'better-sqlite3'; import { createPulse, castVote, getUserVote, countPulseParticipants } from '../src/lib/db.js'; const db = (globalThis as typeof globalThis & { __bifrost_db?: Database.Database }).__bifrost_db!; function seedUser(name: string, role: 'pilot' | 'cab' | 'fenja' = 'cab'): number { const r = db.prepare( 'INSERT INTO users (email, password_hash, name, organisation, role, slug) VALUES (?,?,?,?,?,?)' ).run(`${name}@example.test`, 'x', name, 'TestOrg', role, name.toLowerCase()); return Number(r.lastInsertRowid); } function seedPulse(createdBy: number): number { return createPulse({ question: 'Should we ship X?', context: null, options: ['Yes', 'No'], opens_at: new Date(Date.now() - 60_000).toISOString(), closes_at: new Date(Date.now() + 60 * 60_000).toISOString(), status: 'open', created_by: createdBy, }); } beforeEach(() => { db.exec(` DELETE FROM votes; DELETE FROM pulses; DELETE FROM activity; DELETE FROM users; `); }); describe('vote uniqueness (UNIQUE(pulse_id, user_id))', () => { it('rejects a second vote by the same user on the same pulse (first wins)', () => { const admin = seedUser('admin', 'fenja'); const voter = seedUser('voter', 'cab'); const pulseId = seedPulse(admin); castVote(pulseId, voter, 0); // INSERT OR IGNORE → silent no-op for the duplicate. castVote(pulseId, voter, 1); expect(getUserVote(pulseId, voter)).toBe(0); expect(countPulseParticipants(pulseId)).toBe(1); }); it('raises a SqliteError when bypassing OR IGNORE with a raw INSERT', () => { const admin = seedUser('admin', 'fenja'); const voter = seedUser('voter', 'cab'); const pulseId = seedPulse(admin); castVote(pulseId, voter, 0); expect(() => db.prepare('INSERT INTO votes (pulse_id, user_id, option_index) VALUES (?,?,?)') .run(pulseId, voter, 1) ).toThrow(/UNIQUE constraint failed/); }); it('allows two different users to vote on the same pulse', () => { const admin = seedUser('admin', 'fenja'); const a = seedUser('alice', 'cab'); const b = seedUser('bob', 'cab'); const pulseId = seedPulse(admin); castVote(pulseId, a, 0); castVote(pulseId, b, 1); expect(countPulseParticipants(pulseId)).toBe(2); expect(getUserVote(pulseId, a)).toBe(0); expect(getUserVote(pulseId, b)).toBe(1); }); it('allows the same user to vote on two different pulses', () => { const admin = seedUser('admin', 'fenja'); const voter = seedUser('voter', 'cab'); const p1 = seedPulse(admin); const p2 = seedPulse(admin); castVote(p1, voter, 0); castVote(p2, voter, 1); expect(getUserVote(p1, voter)).toBe(0); expect(getUserVote(p2, voter)).toBe(1); }); });