maestro/src/bridge/server.test.ts
2026-06-03 05:08:00 +00:00

291 lines
11 KiB
TypeScript

import { describe, expect, it, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import express, { Request, Response, NextFunction } from 'express';
import request from 'supertest';
import { Repository } from '../db/repository.js';
import { mountUsersApi } from './users-api.js';
describe('GET /api/jobs/:id visibility', () => {
let tempDir = '';
afterEach(() => {
if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
});
it('non-viewer gets null from repo.getJob (drives 404 in handler)', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-vis-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
const job = await repo.createJob({
repo: 'local/task-1',
issueNumber: 1,
instruction: 'x',
pieceName: 'chat',
ownerId: alice.id,
visibility: 'private',
visibilityScopeOrgId: null,
});
const bobUser: Express.User = {
id: 'bob-id', email: 'b@x.com', name: 'b', avatarUrl: null,
role: 'user', status: 'active',
orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
};
const aliceUser: Express.User = {
...alice,
orgIds: [],
defaultVisibility: 'private' as const,
defaultVisibilityOrgId: null,
};
// Verify at the data layer: bob (non-owner, no orgs) cannot see alice's private job.
expect(await repo.getJob(job.id, { viewer: bobUser })).toBeNull();
// Alice (owner) can.
expect(await repo.getJob(job.id, { viewer: aliceUser })).not.toBeNull();
// Internal callers (no viewer) still get the row (worker/scheduler pass-through).
expect(await repo.getJob(job.id)).not.toBeNull();
} finally {
repo.close();
}
});
it('admin sees any job regardless of visibility', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-vis-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'a', role: 'user', status: 'active' });
const job = await repo.createJob({
repo: 'local/task-1',
issueNumber: 1,
instruction: 'x',
pieceName: 'chat',
ownerId: alice.id,
visibility: 'private',
visibilityScopeOrgId: null,
});
const adminUser: Express.User = {
id: 'admin-id', email: 'admin@x.com', name: 'admin', avatarUrl: null,
role: 'admin', status: 'active',
orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
};
expect(await repo.getJob(job.id, { viewer: adminUser })).not.toBeNull();
} finally {
repo.close();
}
});
});
describe('GET /api/users/me/orgs', () => {
let tempDir = '';
afterEach(() => {
if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
});
/**
* Build a test app that mounts the REAL /api/users/me/orgs route via
* mountUsersApi (the same entry point createCoreServer uses) and injects a
* mocked req.user ahead of it. Pass `injectUser = null` to skip injection
* and exercise requireAuth.
*/
function buildApp(
repo: Repository,
injectUser: (Partial<Express.User> & { id: string }) | null,
): express.Application {
const app = express();
if (injectUser) {
app.use((req: Request, _res: Response, next: NextFunction) => {
(req as Request & { user: Express.User }).user = {
email: 'u@x.com', name: 'u', avatarUrl: null, role: 'user', status: 'active',
orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
...injectUser,
} as Express.User;
(req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => true;
next();
});
// authActive=false: skip requireAuth (we pre-populate req.user above).
mountUsersApi(app, repo, false);
} else {
// authActive=true: exercise the real requireAuth guard. isAuthenticated()
// is missing so requireAuth should return 401.
app.use((req: Request, _res: Response, next: NextFunction) => {
(req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => false;
next();
});
mountUsersApi(app, repo, true);
}
return app;
}
it('returns 401 when the request is unauthenticated (requireAuth gate)', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const app = buildApp(repo, null);
const res = await request(app).get('/api/users/me/orgs');
expect(res.status).toBe(401);
expect(res.body.error).toBe('Unauthorized');
} finally {
repo.close();
}
});
it('returns the cached gitea orgs for the authenticated user', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
repo.replaceUserGiteaOrgs(alice.id, [
{ orgId: 'org-1', orgName: 'alpha' },
{ orgId: 'org-2', orgName: 'beta' },
]);
const app = buildApp(repo, { id: alice.id });
const res = await request(app).get('/api/users/me/orgs');
expect(res.status).toBe(200);
expect(res.body.orgs).toHaveLength(2);
// listUserGiteaOrgs ORDERs by org_name ASC
expect(res.body.orgs[0].orgName).toBe('alpha');
expect(res.body.orgs[1].orgName).toBe('beta');
expect(res.body.orgs[0].orgId).toBe('org-1');
} finally {
repo.close();
}
});
it('returns empty array when user has no cached orgs', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-orgs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const bob = repo.createUser({ email: 'b@x.com', name: 'Bob', role: 'user', status: 'active' });
const app = buildApp(repo, { id: bob.id });
const res = await request(app).get('/api/users/me/orgs');
expect(res.status).toBe(200);
expect(res.body.orgs).toEqual([]);
} finally {
repo.close();
}
});
});
describe('PATCH /api/users/me/preferences', () => {
let tempDir = '';
afterEach(() => {
if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
});
function buildApp(
repo: Repository,
injectUser: (Partial<Express.User> & { id: string }) | null,
): express.Application {
const app = express();
if (injectUser) {
app.use((req: Request, _res: Response, next: NextFunction) => {
(req as Request & { user: Express.User }).user = {
email: 'u@x.com', name: 'u', avatarUrl: null, role: 'user', status: 'active',
orgIds: [], defaultVisibility: 'private', defaultVisibilityOrgId: null,
...injectUser,
} as Express.User;
(req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => true;
next();
});
mountUsersApi(app, repo, false);
} else {
app.use((req: Request, _res: Response, next: NextFunction) => {
(req as Request & { isAuthenticated: () => boolean }).isAuthenticated = () => false;
next();
});
mountUsersApi(app, repo, true);
}
return app;
}
it('returns 400 when defaultVisibility is invalid', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
const app = buildApp(repo, { id: alice.id });
const res = await request(app)
.patch('/api/users/me/preferences')
.send({ defaultVisibility: 'bogus' });
expect(res.status).toBe(400);
expect(res.body.error).toBe('invalid defaultVisibility');
} finally {
repo.close();
}
});
it('returns 400 when defaultVisibilityOrgId is not one of the user orgs', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
const res = await request(app)
.patch('/api/users/me/preferences')
.send({ defaultVisibility: 'org', defaultVisibilityOrgId: '99' });
expect(res.status).toBe(400);
} finally {
repo.close();
}
});
it('returns 400 when defaultVisibility=org is sent without defaultVisibilityOrgId', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
for (const payload of [
{ defaultVisibility: 'org' },
{ defaultVisibility: 'org', defaultVisibilityOrgId: null },
{ defaultVisibility: 'org', defaultVisibilityOrgId: '' },
]) {
const res = await request(app).patch('/api/users/me/preferences').send(payload);
expect(res.status).toBe(400);
}
expect(repo.getUserById(alice.id)!.defaultVisibility).toBe('private');
} finally {
repo.close();
}
});
it('writes preferences on valid input and persists them', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const alice = repo.createUser({ email: 'a@x.com', name: 'Alice', role: 'user', status: 'active' });
const app = buildApp(repo, { id: alice.id, orgIds: ['10'] });
const res = await request(app)
.patch('/api/users/me/preferences')
.send({ defaultVisibility: 'org', defaultVisibilityOrgId: '10' });
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
const after = repo.getUserById(alice.id);
expect(after!.defaultVisibility).toBe('org');
expect(after!.defaultVisibilityOrgId).toBe('10');
} finally {
repo.close();
}
});
it('returns 401 when unauthenticated', async () => {
tempDir = mkdtempSync(join(tmpdir(), 'server-prefs-'));
const repo = new Repository(join(tempDir, 'db.sqlite'));
try {
const app = buildApp(repo, null);
const res = await request(app)
.patch('/api/users/me/preferences')
.send({ defaultVisibility: 'public' });
expect(res.status).toBe(401);
} finally {
repo.close();
}
});
});