feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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: '[email protected]', 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user