maestro/src/db/repository-auth.test.ts
2026-06-03 05:08:00 +00:00

268 lines
9.5 KiB
TypeScript

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);
});
});