268 lines
9.5 KiB
TypeScript
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);
|
|
});
|
|
});
|