import { afterEach, describe, it, expect, beforeEach } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { Repository } from './repository.js'; import { runMigrations } from './migrate.js'; describe('Repository user CRUD', () => { let tempDir = ''; let repo: Repository; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'maestro-auth-')); repo = new Repository(join(tempDir, 'orchestrator.db')); runMigrations(repo.getDb()); }); afterEach(() => { repo.close(); if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; } }); // ── createUser ──────────────────────────────────────────────── it('createUser creates a user with correct fields', () => { const user = repo.createUser({ email: 'alice@example.com', name: 'Alice', role: 'user', status: 'pending', }); expect(user.id).toBeTruthy(); expect(user.email).toBe('alice@example.com'); expect(user.name).toBe('Alice'); expect(user.role).toBe('user'); expect(user.status).toBe('pending'); expect(user.avatarUrl).toBeNull(); expect(user.createdAt).toBeTruthy(); expect(user.updatedAt).toBeTruthy(); }); it('createUser stores avatarUrl when provided', () => { const user = repo.createUser({ email: 'bob@example.com', name: 'Bob', role: 'admin', status: 'active', avatarUrl: 'https://example.com/avatar.png', }); expect(user.avatarUrl).toBe('https://example.com/avatar.png'); expect(user.role).toBe('admin'); expect(user.status).toBe('active'); }); // ── getUserByEmail ──────────────────────────────────────────── it('getUserByEmail returns null for non-existent email', () => { const user = repo.getUserByEmail('nonexistent@example.com'); expect(user).toBeNull(); }); it('getUserByEmail returns the user for known email', () => { repo.createUser({ email: 'carol@example.com', name: 'Carol', role: 'user', status: 'active' }); const user = repo.getUserByEmail('carol@example.com'); expect(user).not.toBeNull(); expect(user?.email).toBe('carol@example.com'); expect(user?.name).toBe('Carol'); }); // ── getUserById ─────────────────────────────────────────────── it('getUserById returns the user for known id', () => { const created = repo.createUser({ email: 'dave@example.com', name: 'Dave', role: 'user', status: 'active' }); const found = repo.getUserById(created.id); expect(found).not.toBeNull(); expect(found?.id).toBe(created.id); }); it('getUserById returns null for unknown id', () => { const found = repo.getUserById('00000000-0000-0000-0000-000000000000'); expect(found).toBeNull(); }); // ── findOrCreateUserByOAuth ─────────────────────────────────── it('findOrCreateUserByOAuth creates new user on first login with status=pending', () => { const user = repo.findOrCreateUserByOAuth({ provider: 'discord', providerId: 'discord-uid-1', email: 'eve@example.com', name: 'Eve', }); expect(user.email).toBe('eve@example.com'); expect(user.name).toBe('Eve'); expect(user.status).toBe('pending'); }); it('findOrCreateUserByOAuth returns same user on subsequent login (same provider_id)', () => { const params = { provider: 'discord', providerId: 'discord-uid-2', email: 'frank@example.com', name: 'Frank', }; const first = repo.findOrCreateUserByOAuth(params); const second = repo.findOrCreateUserByOAuth(params); expect(second.id).toBe(first.id); expect(second.email).toBe('frank@example.com'); }); it('findOrCreateUserByOAuth links second provider to same user by matching email', () => { // First login via discord const discordUser = repo.findOrCreateUserByOAuth({ provider: 'discord', providerId: 'discord-uid-3', email: 'grace@example.com', name: 'Grace', }); // Second login via github with same email const githubUser = repo.findOrCreateUserByOAuth({ provider: 'github', providerId: 'github-uid-3', email: 'grace@example.com', name: 'Grace GitHub', }); // Should return same user expect(githubUser.id).toBe(discordUser.id); // Both providers should now be linked to this user const userAgain = repo.findOrCreateUserByOAuth({ provider: 'discord', providerId: 'discord-uid-3', email: 'grace@example.com', name: 'Grace', }); expect(userAgain.id).toBe(discordUser.id); }); // ── listUsers ───────────────────────────────────────────────── it('listUsers returns all users', () => { repo.createUser({ email: 'u1@example.com', name: 'U1', role: 'user', status: 'active' }); repo.createUser({ email: 'u2@example.com', name: 'U2', role: 'user', status: 'pending' }); repo.createUser({ email: 'u3@example.com', name: 'U3', role: 'admin', status: 'active' }); const users = repo.listUsers(); expect(users.length).toBe(3); const emails = users.map(u => u.email); expect(emails).toContain('u1@example.com'); expect(emails).toContain('u2@example.com'); expect(emails).toContain('u3@example.com'); }); // ── updateUser ──────────────────────────────────────────────── it('updateUser changes status and role', () => { const user = repo.createUser({ email: 'henry@example.com', name: 'Henry', role: 'user', status: 'pending' }); repo.updateUser(user.id, { status: 'active', role: 'admin' }); const updated = repo.getUserById(user.id); expect(updated?.status).toBe('active'); expect(updated?.role).toBe('admin'); }); // ── deleteUser ──────────────────────────────────────────────── it('deleteUser removes user from DB', () => { const user = repo.createUser({ email: 'iris@example.com', name: 'Iris', role: 'user', status: 'active' }); repo.deleteUser(user.id); const found = repo.getUserById(user.id); expect(found).toBeNull(); }); it('deleteUser cascades to oauth_accounts', () => { const user = repo.findOrCreateUserByOAuth({ provider: 'discord', providerId: 'discord-uid-cascade', email: 'jack@example.com', name: 'Jack', }); repo.deleteUser(user.id); // OAuth account should be gone too (CASCADE) // Verify by trying to re-create the same OAuth -> should create fresh user const newUser = repo.findOrCreateUserByOAuth({ provider: 'discord', providerId: 'discord-uid-cascade', email: 'jack@example.com', name: 'Jack', }); expect(newUser.id).not.toBe(user.id); }); }); // ── owner_id filtering ──────────────────────────────────────────── describe('Repository owner_id filtering', () => { let tempDir = ''; let repo: Repository; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'maestro-auth-')); repo = new Repository(join(tempDir, 'orchestrator.db')); runMigrations(repo.getDb()); }); afterEach(() => { repo.close(); if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; } }); it('createLocalTask accepts ownerId parameter', async () => { const user = repo.createUser({ email: 'owner@example.com', name: 'Owner', role: 'user', status: 'active' }); const task = await repo.createLocalTask({ title: 'Test Task', body: 'body', ownerId: user.id, }); expect(task.id).toBeTruthy(); // Verify owner_id was stored in DB const db = repo.getDb(); const row = db.prepare('SELECT owner_id FROM local_tasks WHERE id = ?').get(task.id) as { owner_id: string | null } | undefined; expect(row?.owner_id).toBe(user.id); }); it('listLocalTasks filters by ownerId', async () => { const user1 = repo.createUser({ email: 'owner1@example.com', name: 'Owner1', role: 'user', status: 'active' }); const user2 = repo.createUser({ email: 'owner2@example.com', name: 'Owner2', role: 'user', status: 'active' }); await repo.createLocalTask({ title: 'Task A', body: '', ownerId: user1.id }); await repo.createLocalTask({ title: 'Task B', body: '', ownerId: user1.id }); await repo.createLocalTask({ title: 'Task C', body: '', ownerId: user2.id }); await repo.createLocalTask({ title: 'Task D', body: '' }); // no owner const user1Tasks = await repo.listLocalTasks({ ownerId: user1.id }); expect(user1Tasks.length).toBe(2); const titles1 = user1Tasks.map(t => t.title); expect(titles1).toContain('Task A'); expect(titles1).toContain('Task B'); const user2Tasks = await repo.listLocalTasks({ ownerId: user2.id }); expect(user2Tasks.length).toBe(1); expect(user2Tasks[0].title).toBe('Task C'); // Without filter returns all tasks const allTasks = await repo.listLocalTasks(); expect(allTasks.length).toBe(4); }); });