291 lines
11 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|