import { INestApplication } from '@nestjs/common'; import { collabTokenClaimsSchema } from '@dorfteich/shared'; import { verifyCollabToken } from '@dorfteich/shared/token-crypto'; import { PrismaClient } from '@prisma/client'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { AuthTokensService } from '../auth/auth-tokens.service'; import { AppConfig } from '../config/app-config.service'; import { createTestApp, sessionCookieOf } from '../testing/test-app'; import { createTestPrisma, hasTestDb, uniqueSuffix } from '../testing/test-db'; import { UsersService } from '../users/users.service'; describe.skipIf(!hasTestDb)('collab token (e2e, issue #34)', () => { let app: INestApplication; let prisma: PrismaClient; let secret: string; const suffix = uniqueSuffix(); const password = 'tokens fuer teiche 1'; const owner = { username: `tom-token-${suffix}`, displayName: `Tom Token ${suffix}` }; const outsider = { username: `oona-out-${suffix}`, displayName: `Oona Out ${suffix}` }; let ownerCookie: string; let outsiderCookie: string; let pageId: string; const api = () => request(app.getHttpServer()); async function loginOf(username: string): Promise { const res = await api() .post('/api/v1/auth/login') .send({ usernameOrEmail: username, password }) .expect(200); return sessionCookieOf(res); } beforeAll(async () => { prisma = createTestPrisma(); await prisma.rateLimit.deleteMany({}); app = await createTestApp(); secret = app.get(AppConfig).env.COLLAB_TOKEN_SECRET; const users = app.get(UsersService); const tokens = app.get(AuthTokensService); const ownerUser = await users.createUser({ username: owner.username, email: `${owner.username}@example.org`, displayName: owner.displayName, password, locale: 'en', }); const verifyToken = await tokens.issue(ownerUser.id, 'EMAIL_VERIFICATION', 600); await api().post('/api/v1/auth/verify-email').send({ token: verifyToken }).expect(204); ownerCookie = await loginOf(owner.username); const outsiderUser = await users.createUser({ username: outsider.username, email: `${outsider.username}@example.org`, displayName: outsider.displayName, password, locale: 'en', }); await users.markEmailVerified(outsiderUser.id); outsiderCookie = await loginOf(outsider.username); const ponds = await api().get('/api/v1/ponds').set('Cookie', ownerCookie).expect(200); const pondId = ponds.body.find((p: { type: string }) => p.type === 'personal').id; const page = await api() .post(`/api/v1/ponds/${pondId}/pages`) .set('Cookie', ownerCookie) .send({ title: 'Collaborative page' }) .expect(201); pageId = page.body.id; }); afterAll(async () => { const users = await prisma.user.findMany({ where: { username: { contains: suffix } }, select: { id: true }, }); await prisma.page.deleteMany({ where: { pond: { owner: { username: { contains: suffix } } } }, }); await prisma.quotaOverride.deleteMany({ where: { subjectId: { in: users.map((u) => u.id) } } }); await prisma.pond.deleteMany({ where: { owner: { username: { contains: suffix } } } }); await prisma.user.deleteMany({ where: { username: { contains: suffix } } }); await prisma.$disconnect(); await app.close(); }); it('hides a private page from an anonymous visitor (404, not 401 — issue #53)', async () => { // The endpoint is public so anonymous visitors can reach public pages, but // without a `public` grant this page is unreadable and its existence stays // hidden — the same 404 an authenticated non-member gets. await api().get(`/api/v1/pages/${pageId}/collab-token`).expect(404); }); it('issues a valid rw token to a member and encodes the right claims', async () => { const res = await api() .get(`/api/v1/pages/${pageId}/collab-token`) .set('Cookie', ownerCookie) .expect(200); expect(res.body.mode).toBe('rw'); expect(res.body.expiresInSeconds).toBe(60); const verified = verifyCollabToken(res.body.token, secret); expect(verified.valid).toBe(true); if (verified.valid) { expect(collabTokenClaimsSchema.parse(verified.claims)).toEqual({ userId: expect.any(String), pageId, mode: 'rw', }); } }); it('hides the page from a non-member (404, not 403)', async () => { await api() .get(`/api/v1/pages/${pageId}/collab-token`) .set('Cookie', outsiderCookie) .expect(404); }); it('404s for an unknown page id', async () => { await api() .get('/api/v1/pages/00000000-0000-4000-8000-0000000000ff/collab-token') .set('Cookie', ownerCookie) .expect(404); }); });