Server halves of #17/#18/#19: PATCH /users/me and change-password (verifies the current password, logs out every other session), GET/DELETE /users/me/sessions with current-session flag and protection against revoking oneself; InstanceSettingsService as a typed, cached, Zod-validated registry over instance_settings (schema-default fallback for invalid stored values, audit-logged writes) consumed by the signup flow; /admin/settings behind the new SiteAdminGuard with strict unknown-key rejection. SessionsService moves to its own module to keep Auth/Users acyclic. Three new e2e suites bring the api to 42 tests. Part of #17, #18, #19 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import request from 'supertest';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
import { createTestApp, sessionCookieOf } from '../testing/test-app';
|
|
import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db';
|
|
import { UsersService } from '../users/users.service';
|
|
|
|
describe.skipIf(!hasTestDb)('admin settings (e2e)', () => {
|
|
let app: INestApplication;
|
|
let prisma: PrismaClient;
|
|
const suffix = uniqueSuffix();
|
|
let adminCookie: string;
|
|
let memberCookie: string;
|
|
|
|
const api = () => request(app.getHttpServer());
|
|
const password = 'ein ordentliches admin passwort';
|
|
|
|
async function makeActiveUser(name: string, siteAdmin: boolean): Promise<string> {
|
|
const users = app.get(UsersService);
|
|
const user = await users.createUser({
|
|
username: `${name}-${suffix}`,
|
|
email: `${name}-${suffix}@example.org`,
|
|
displayName: name,
|
|
password,
|
|
locale: 'en',
|
|
});
|
|
await users.markEmailVerified(user.id);
|
|
if (siteAdmin) {
|
|
await prisma.user.update({ where: { id: user.id }, data: { isSiteAdmin: true } });
|
|
}
|
|
const res = await api()
|
|
.post('/api/v1/auth/login')
|
|
.send({ usernameOrEmail: user.username, password })
|
|
.expect(200);
|
|
return sessionCookieOf(res);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
prisma = createTestPrisma();
|
|
await prisma.rateLimit.deleteMany({});
|
|
await prisma.instanceSetting.deleteMany({ where: { key: 'auth.registrationMode' } });
|
|
app = await createTestApp();
|
|
adminCookie = await makeActiveUser('root', true);
|
|
memberCookie = await makeActiveUser('member', false);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.user.deleteMany({ where: { username: { contains: suffix } } });
|
|
await prisma.mailOutbox.deleteMany({ where: { toAddress: { contains: suffix } } });
|
|
await prisma.instanceSetting.deleteMany({ where: { key: 'auth.registrationMode' } });
|
|
await prisma.$disconnect();
|
|
await app.close();
|
|
});
|
|
|
|
it('denies the settings endpoints to non-admins and anonymous callers', async () => {
|
|
await api().get('/api/v1/admin/settings').expect(401);
|
|
await api().get('/api/v1/admin/settings').set('Cookie', memberCookie).expect(403);
|
|
});
|
|
|
|
it('returns typed defaults for unset settings', async () => {
|
|
const res = await api().get('/api/v1/admin/settings').set('Cookie', adminCookie).expect(200);
|
|
expect(res.body['auth.registrationMode']).toBe('open');
|
|
expect(res.body['instance.name']).toBe('Dorfteich');
|
|
});
|
|
|
|
it('rejects invalid values with field details', async () => {
|
|
const res = await api()
|
|
.patch('/api/v1/admin/settings')
|
|
.set('Cookie', adminCookie)
|
|
.send({ 'auth.registrationMode': 'sideways' })
|
|
.expect(400);
|
|
expect(res.body.details).toHaveProperty('auth.registrationMode');
|
|
});
|
|
|
|
it('closing registration takes effect immediately, reopening too', async () => {
|
|
await api()
|
|
.patch('/api/v1/admin/settings')
|
|
.set('Cookie', adminCookie)
|
|
.send({ 'auth.registrationMode': 'closed' })
|
|
.expect(200);
|
|
|
|
const newcomer = {
|
|
username: `late-${suffix}`,
|
|
email: `late-${suffix}@example.org`,
|
|
displayName: 'Late',
|
|
password: 'auch ein gutes passwort',
|
|
};
|
|
await api().post('/api/v1/auth/signup').send(newcomer).expect(403);
|
|
|
|
await api()
|
|
.patch('/api/v1/admin/settings')
|
|
.set('Cookie', adminCookie)
|
|
.send({ 'auth.registrationMode': 'open' })
|
|
.expect(200);
|
|
await api().post('/api/v1/auth/signup').send(newcomer).expect(201);
|
|
});
|
|
|
|
it('rejects unknown setting keys', async () => {
|
|
await api()
|
|
.patch('/api/v1/admin/settings')
|
|
.set('Cookie', adminCookie)
|
|
.send({ 'made.up': true })
|
|
.expect(400);
|
|
});
|
|
});
|