From 1d6008753a4dac21f4dadbd003c08465c0bf8f28 Mon Sep 17 00:00:00 2001 From: Alimedhat000 Date: Fri, 28 Aug 2026 21:11:07 +0300 Subject: [PATCH] =?UTF-8?q?fix(server):=20session=20hardening=20=E2=80=94?= =?UTF-8?q?=20rotation,=20multi-device,=20isActive=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Session model (id, userId, jti, refreshToken, expiresAt) with migration 20260828010125_add_session; each login creates a Session instead of overwriting single User.refreshToken, enabling concurrent devices. - POST /auth/refresh now rotates jti/refreshToken, sets env-aware cookie (lax/non-secure in dev/test, none/secure in prod) and invalidates previous jti (401 on reuse). - POST /auth/logout deletes only the presented Session. - Enforce isActive in login, refresh and authenticate middleware. - Make E2E resilient to rotation by re-logging in when Dashboard not visible (shared storageState would otherwise present stale jti). --- client/e2e/dashboard.spec.ts | 14 +- client/e2e/logout.spec.ts | 7 +- client/e2e/utils.ts | 26 +++- client/playwright.config.ts | 16 +- .../20260828010125_add_session/migration.sql | 23 +++ server/prisma/schema.prisma | 14 ++ server/src/controllers/auth.controller.ts | 145 +++++++++++++++++- server/src/middlewares/auth.middleware.ts | 14 +- server/test/auth-isactive.test.ts | 82 ++++++++++ server/test/auth-multidevice.test.ts | 77 ++++++++++ server/test/auth-session-rotation.test.ts | 55 +++++++ server/test/setup.ts | 9 +- 12 files changed, 454 insertions(+), 28 deletions(-) create mode 100644 server/prisma/migrations/20260828010125_add_session/migration.sql create mode 100644 server/test/auth-isactive.test.ts create mode 100644 server/test/auth-multidevice.test.ts create mode 100644 server/test/auth-session-rotation.test.ts diff --git a/client/e2e/dashboard.spec.ts b/client/e2e/dashboard.spec.ts index bd4eaa6..a666a99 100644 --- a/client/e2e/dashboard.spec.ts +++ b/client/e2e/dashboard.spec.ts @@ -1,15 +1,15 @@ import { expect, test } from '@playwright/test'; -import { createTestDocument, getDocumentCard, openDocumentMenu } from './utils'; +import { + createTestDocument, + getDocumentCard, + gotoDashboard, + openDocumentMenu, +} from './utils'; test.describe('Dashboard', () => { test.beforeEach(async ({ page }) => { - await page.goto('/app'); - - // Authenticated via storageState (see auth.setup.ts) - await expect( - page.getByRole('heading', { name: 'Dashboard' }), - ).toBeVisible(); + await gotoDashboard(page); }); test('should display dashboard with document list', async ({ page }) => { diff --git a/client/e2e/logout.spec.ts b/client/e2e/logout.spec.ts index 2cb8719..8b3354f 100644 --- a/client/e2e/logout.spec.ts +++ b/client/e2e/logout.spec.ts @@ -1,14 +1,13 @@ import { expect, test } from '@playwright/test'; +import { gotoDashboard } from './utils'; + // Runs as the final project (after all other authenticated specs) because // logging out nulls the user's refresh token server-side, invalidating the // shared session for every other test context. test.describe('Logout', () => { test('should log out and block access to the app', async ({ page }) => { - await page.goto('/app'); - await expect( - page.getByRole('heading', { name: 'Dashboard' }), - ).toBeVisible(); + await gotoDashboard(page); await page.getByRole('button', { name: /user menu/i }).click(); await page.getByRole('menuitem', { name: /logout/i }).click(); diff --git a/client/e2e/utils.ts b/client/e2e/utils.ts index ad660da..ec868f5 100644 --- a/client/e2e/utils.ts +++ b/client/e2e/utils.ts @@ -17,12 +17,34 @@ export async function openDocumentMenu(page: Page, title: string) { await expect(page.getByRole('menu')).toBeVisible(); } +/** + * Navigate to /app and ensure the dashboard is visible. + * Resilient to refresh-token rotation: if the shared storageState's + * refresh token was already rotated by a previous test (multi-device + * sessions now rotate on refresh), the first goto will land on /login. + * In that case, re-login via the UI and continue. + */ +export async function gotoDashboard(page: Page) { + await page.goto('/app'); + const dashboard = page.getByRole('heading', { name: 'Dashboard' }); + try { + await expect(dashboard).toBeVisible({ timeout: 2_000 }); + } catch { + // Session from shared storageState was rotated — re-authenticate + await page.goto('/login'); + await page.getByLabel(/email/i).fill('test@example.com'); + await page.getByLabel(/password/i).fill('testpassword'); + await page.getByRole('button', { name: /login|sign in/i }).click(); + await expect(page).toHaveURL(/.*\/app/); + await expect(dashboard).toBeVisible(); + } +} + /** * Create a document end-to-end from the dashboard. */ export async function createTestDocument(page: Page, title: string) { - await page.goto('/app'); - await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); + await gotoDashboard(page); await page.getByLabel(/new document/i).click(); diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 787aa57..1199699 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -46,13 +46,15 @@ export default defineConfig({ /* Configure projects for major browsers. * - * Order matters: logging in overwrites the user's single stored refresh - * token (server-side), which invalidates every previously-issued session, - * and logging out nulls it entirely. The unauthenticated auth specs - * (which perform a real login) therefore run BEFORE the setup project saves - * the storageState, the authenticated specs run against that shared session, - * and the logout spec runs LAST. Token refresh itself does not rotate the - * stored token, so those tests can safely run in parallel. */ + * Order matters: with multi-device sessions (fix #51) each login creates + * its own Session row, but refresh now rotates the presented token and + * invalidates the previous jti. The unauthenticated auth specs (which + * perform real logins) still run BEFORE the setup project saves the + * storageState, the authenticated specs run against that shared session, + * and the logout spec runs LAST. Because refresh rotates, sharing the + * same storageState across sequential tests would make the second test's + * refresh present a stale jti — `createTestDocument` now re-logs in + * transparently when that happens. */ projects: [ { name: 'auth-specs', diff --git a/server/prisma/migrations/20260828010125_add_session/migration.sql b/server/prisma/migrations/20260828010125_add_session/migration.sql new file mode 100644 index 0000000..c97cee7 --- /dev/null +++ b/server/prisma/migrations/20260828010125_add_session/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "sessions" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "refreshToken" TEXT NOT NULL, + "jti" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_refreshToken_key" ON "sessions"("refreshToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_jti_key" ON "sessions"("jti"); + +-- CreateIndex +CREATE INDEX "sessions_userId_idx" ON "sessions"("userId"); + +-- AddForeignKey +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 13a3117..e7d87de 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -20,10 +20,24 @@ model User { Document Document[] Collaborator Collaborator[] CollaborationRequest CollaborationRequest[] + Session Session[] @@map("users") } +model Session { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + refreshToken String @unique + jti String @unique + expiresAt DateTime + createdAt DateTime @default(now()) + + @@index([userId]) + @@map("sessions") +} + model Document { id String @id @default(uuid()) title String diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index cc4aea9..2441318 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,4 +1,5 @@ import bcrypt from 'bcryptjs'; +import crypto from 'crypto'; import { Request, Response } from 'express'; import asyncErrorWrapper from 'express-async-handler'; import { StatusCodes } from 'http-status-codes'; @@ -117,6 +118,17 @@ export const loginUser = asyncErrorWrapper(async (req: Request, res: Response) = try { const user = await prisma.user.findUnique({ where: { email } }); + if (user && user.isActive === false) { + logger.warn('Login failed - user deactivated', { + action: 'LOGIN_USER_INACTIVE', + ...clientInfo, + userId: user.id, + email, + }); + res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid email or password' }); + return; + } + if (!user) { await bcrypt.compare(password, DUMMY_PASSWORD_HASH); @@ -157,11 +169,13 @@ export const loginUser = asyncErrorWrapper(async (req: Request, res: Response) = } ); - // Generate Access Token with a short expiration time + // Generate refresh token with jti for rotation / reuse detection + const jti = crypto.randomUUID(); const refreshToken = jwt.sign( { userId: user.id, username: user.username, + jti, }, process.env.JWT_REFRESH_SECRET, { @@ -169,7 +183,16 @@ export const loginUser = asyncErrorWrapper(async (req: Request, res: Response) = } ); - // update user with referesh token + // Create a new session for this device; do not overwrite other sessions + await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + jti, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }); + // Keep legacy column in sync for any external read (not used for auth) await prisma.user.update({ where: { email }, data: { refreshToken }, @@ -236,6 +259,22 @@ export const logoutUser = asyncErrorWrapper(async (req: AuthenticatedRequest, re } try { + const presented = req.cookies?.refreshToken as string | undefined; + if (presented) { + try { + const dec = jwt.verify(presented, process.env.JWT_REFRESH_SECRET!) as jwt.JwtPayload & { jti?: string }; + if (dec.jti) { + await prisma.session.deleteMany({ where: { jti: dec.jti, userId } }); + } else { + await prisma.session.deleteMany({ where: { refreshToken: presented, userId } }); + } + } catch { + await prisma.session.deleteMany({ where: { refreshToken: presented, userId } }); + } + } else { + // Fallback: clear all sessions for user if no token presented (e.g. legacy) + await prisma.session.deleteMany({ where: { userId } }); + } await prisma.user.update({ where: { id: userId }, data: { refreshToken: null }, @@ -307,21 +346,109 @@ export const refreshToken = asyncErrorWrapper(async (req: Request, res: Response return; } try { + const payloadWithJti = payload as jwt.JwtPayload & { jti?: string }; const user = await prisma.user.findUnique({ where: { id: payload.userId } }); - if (!user || user.refreshToken !== refreshToken) { + if (!user) { logger.warn('Token refresh failed - token mismatch or user not found', { action: 'REFRESH_TOKEN_MISMATCH', ...clientInfo, userId: payload.userId, - userExists: !!user, - tokenMatches: user?.refreshToken === refreshToken, + userExists: false, + tokenMatches: false, }); + res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid refresh token' }); + return; + } + // isActive enforcement — deactivated accounts cannot refresh + if (user.isActive === false) { + logger.warn('Token refresh failed - user deactivated', { + action: 'REFRESH_TOKEN_USER_INACTIVE', + ...clientInfo, + userId: user.id, + }); res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid refresh token' }); return; } + // Prefer Session lookup by jti (new tokens); fallback to legacy column for old tokens + let session: Awaited> | null = null; + if (payloadWithJti.jti) { + session = await prisma.session.findUnique({ where: { jti: payloadWithJti.jti } }); + + // Reuse detection: valid JWT for user but no matching session → token was already rotated/revoked + if (!session || session.refreshToken !== refreshToken || session.userId !== payload.userId) { + logger.warn('Token refresh failed - token reuse detected', { + action: 'REFRESH_TOKEN_REUSE', + ...clientInfo, + userId: payload.userId, + jti: payloadWithJti.jti, + }); + res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid refresh token' }); + return; + } + + if (session.expiresAt < new Date()) { + logger.warn('Token refresh failed - session expired', { + action: 'REFRESH_TOKEN_EXPIRED', + ...clientInfo, + userId: payload.userId, + }); + await prisma.session.delete({ where: { id: session.id } }); + res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid refresh token' }); + return; + } + } else { + // Legacy token without jti — fall back to single-column check + if (user.refreshToken !== refreshToken) { + logger.warn('Token refresh failed - token mismatch or user not found', { + action: 'REFRESH_TOKEN_MISMATCH', + ...clientInfo, + userId: payload.userId, + userExists: true, + tokenMatches: false, + }); + res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid refresh token' }); + return; + } + // Migrate legacy: create a session for this token so future rotates work + session = await prisma.session.create({ + data: { + userId: user.id, + refreshToken, + jti: crypto.randomUUID(), + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }); + } + + // Rotate: new jti + new refresh token, update same session row + const newJti = crypto.randomUUID(); + const newRefreshToken = jwt.sign( + { + userId: user.id, + username: user.username, + jti: newJti, + }, + process.env.JWT_REFRESH_SECRET!, + { expiresIn: '24h' } + ); + + await prisma.session.update({ + where: { id: session.id }, + data: { + refreshToken: newRefreshToken, + jti: newJti, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }); + // Keep legacy column in sync (not used for auth, but for observability) + await prisma.user.update({ + where: { id: user.id }, + data: { refreshToken: newRefreshToken }, + }); + const newAccessToken = jwt.sign( { userId: user.id, @@ -338,6 +465,14 @@ export const refreshToken = asyncErrorWrapper(async (req: Request, res: Response username: user.username, }); + const isProduction = process.env.NODE_ENV === 'production'; + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, + sameSite: isProduction ? 'none' : 'lax', + secure: isProduction, + }); + res.status(StatusCodes.OK).json({ accessToken: newAccessToken, user: { diff --git a/server/src/middlewares/auth.middleware.ts b/server/src/middlewares/auth.middleware.ts index a7e9409..6a57abb 100644 --- a/server/src/middlewares/auth.middleware.ts +++ b/server/src/middlewares/auth.middleware.ts @@ -3,7 +3,9 @@ import { StatusCodes } from 'http-status-codes'; import jwt from 'jsonwebtoken'; import { JwtPayload } from 'jsonwebtoken'; -export const authenticate = (req: AuthenticatedRequest, res: Response, next: NextFunction) => { +import { prisma } from '@/lib/prisma'; + +export const authenticate = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { @@ -13,7 +15,15 @@ export const authenticate = (req: AuthenticatedRequest, res: Response, next: Nex const token = authHeader.split(' ')[1]; try { - const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as JwtPayload; + const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as JwtPayload & { userId: string }; + // Enforce isActive so deactivated accounts lose access even with a valid JWT (15m window) + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { isActive: true }, + }); + if (user && user.isActive === false) { + return res.status(StatusCodes.UNAUTHORIZED).json({ error: 'Invalid or expired token' }); + } req.user = { userId: decoded.userId, username: decoded.username }; next(); } catch { diff --git a/server/test/auth-isactive.test.ts b/server/test/auth-isactive.test.ts new file mode 100644 index 0000000..a3a60b8 --- /dev/null +++ b/server/test/auth-isactive.test.ts @@ -0,0 +1,82 @@ +import { StatusCodes } from 'http-status-codes'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; + +import { prisma } from '@/lib/prisma'; +import { app } from '@/server'; + +function extractCookies(raw: string[] | string | undefined): string { + if (!raw) return ''; + const arr = Array.isArray(raw) ? raw : [raw]; + return arr.map(c => c.split(';')[0]).join('; '); +} + +describe('Session hardening — isActive enforcement (#51.2)', () => { + it('should reject login when user is deactivated', async () => { + await request(app).post('/api/auth/register').send({ + email: 'inactive-login@test.dev', + username: 'inactiveLogin', + password: 'secure123', + }); + + // deactivate + await prisma.user.update({ + where: { email: 'inactive-login@test.dev' }, + data: { isActive: false }, + }); + + const res = await request(app).post('/api/auth/login').send({ + email: 'inactive-login@test.dev', + password: 'secure123', + }); + + expect(res.status).toBe(StatusCodes.UNAUTHORIZED); + }); + + it('should reject refresh when user is deactivated', async () => { + await request(app).post('/api/auth/register').send({ + email: 'inactive-refresh@test.dev', + username: 'inactiveRefresh', + password: 'secure123', + }); + + const loginRes = await request(app).post('/api/auth/login').send({ + email: 'inactive-refresh@test.dev', + password: 'secure123', + }); + expect(loginRes.status).toBe(StatusCodes.OK); + const cookie = extractCookies(loginRes.headers['set-cookie']); + + await prisma.user.update({ + where: { email: 'inactive-refresh@test.dev' }, + data: { isActive: false }, + }); + + const refreshRes = await request(app).post('/api/auth/refresh').set('Cookie', cookie); + expect(refreshRes.status).toBe(StatusCodes.UNAUTHORIZED); + }); + + it('should reject protected route when user is deactivated (authenticate middleware)', async () => { + await request(app).post('/api/auth/register').send({ + email: 'inactive-auth@test.dev', + username: 'inactiveAuth', + password: 'secure123', + }); + + const loginRes = await request(app).post('/api/auth/login').send({ + email: 'inactive-auth@test.dev', + password: 'secure123', + }); + const token = loginRes.body.accessToken; + expect(token).toBeDefined(); + + await prisma.user.update({ + where: { email: 'inactive-auth@test.dev' }, + data: { isActive: false }, + }); + + const protectedRes = await request(app).get('/api/user').set('Authorization', `Bearer ${token}`); + + expect(protectedRes.status).toBe(StatusCodes.UNAUTHORIZED); + }); +}); diff --git a/server/test/auth-multidevice.test.ts b/server/test/auth-multidevice.test.ts new file mode 100644 index 0000000..0c07784 --- /dev/null +++ b/server/test/auth-multidevice.test.ts @@ -0,0 +1,77 @@ +import { StatusCodes } from 'http-status-codes'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; + +import { app } from '@/server'; + +function extractCookies(raw: string[] | string | undefined): string { + if (!raw) return ''; + const arr = Array.isArray(raw) ? raw : [raw]; + return arr.map(c => c.split(';')[0]).join('; '); +} + +describe('Session hardening — multi-device (#51.1)', () => { + it('should keep first device valid after second login (no single-column overwrite)', async () => { + await request(app).post('/api/auth/register').send({ + email: 'multi@test.dev', + username: 'multiUser', + password: 'secure123', + }); + + const login1 = await request(app).post('/api/auth/login').send({ + email: 'multi@test.dev', + password: 'secure123', + }); + expect(login1.status).toBe(StatusCodes.OK); + const cookie1 = extractCookies(login1.headers['set-cookie']); + + // Second login simulates another device + const login2 = await request(app).post('/api/auth/login').send({ + email: 'multi@test.dev', + password: 'secure123', + }); + expect(login2.status).toBe(StatusCodes.OK); + const cookie2 = extractCookies(login2.headers['set-cookie']); + + expect(cookie1).not.toBe(cookie2); + + // Both cookies must still refresh independently + const refresh1 = await request(app).post('/api/auth/refresh').set('Cookie', cookie1); + expect(refresh1.status).toBe(StatusCodes.OK); + + const refresh2 = await request(app).post('/api/auth/refresh').set('Cookie', cookie2); + expect(refresh2.status).toBe(StatusCodes.OK); + }); + + it('should only revoke the presented session on logout, leaving other device', async () => { + await request(app).post('/api/auth/register').send({ + email: 'multilogout@test.dev', + username: 'multiLogout', + password: 'secure123', + }); + + const login1 = await request(app).post('/api/auth/login').send({ + email: 'multilogout@test.dev', + password: 'secure123', + }); + const cookie1 = extractCookies(login1.headers['set-cookie']); + + const login2 = await request(app).post('/api/auth/login').send({ + email: 'multilogout@test.dev', + password: 'secure123', + }); + const cookie2 = extractCookies(login2.headers['set-cookie']); + + // Logout with first device + const logout1 = await request(app).post('/api/auth/logout').set('Cookie', cookie1); + expect(logout1.status).toBe(StatusCodes.OK); + + // First device must no longer refresh + const refresh1After = await request(app).post('/api/auth/refresh').set('Cookie', cookie1); + expect(refresh1After.status).toBe(StatusCodes.UNAUTHORIZED); + + // Second device must still refresh + const refresh2After = await request(app).post('/api/auth/refresh').set('Cookie', cookie2); + expect(refresh2After.status).toBe(StatusCodes.OK); + }); +}); diff --git a/server/test/auth-session-rotation.test.ts b/server/test/auth-session-rotation.test.ts new file mode 100644 index 0000000..d4f12f7 --- /dev/null +++ b/server/test/auth-session-rotation.test.ts @@ -0,0 +1,55 @@ +import { StatusCodes } from 'http-status-codes'; +import request from 'supertest'; +import { describe, expect, it } from 'vitest'; + +import { app } from '@/server'; + +function extractCookies(raw: string[] | string | undefined): string { + if (!raw) return ''; + const arr = Array.isArray(raw) ? raw : [raw]; + return arr.map(c => c.split(';')[0]).join('; '); +} + +function getRefreshCookie(setCookie: string[] | string | undefined): string | undefined { + if (!setCookie) return undefined; + const arr = Array.isArray(setCookie) ? setCookie : [setCookie]; + return arr.find(c => c.startsWith('refreshToken=')); +} + +describe('Session hardening — refresh rotation (#51.1)', () => { + it('should rotate refresh token on refresh and invalidate the old one', async () => { + await request(app).post('/api/auth/register').send({ + email: 'rotate@test.dev', + username: 'rotater', + password: 'secure123', + }); + + const loginRes = await request(app).post('/api/auth/login').send({ + email: 'rotate@test.dev', + password: 'secure123', + }); + expect(loginRes.status).toBe(StatusCodes.OK); + const firstCookie = getRefreshCookie(loginRes.headers['set-cookie']); + expect(firstCookie).toBeDefined(); + + const firstCookieHeader = extractCookies(loginRes.headers['set-cookie']); + + // First refresh — should issue a new refreshToken cookie + const refreshRes = await request(app).post('/api/auth/refresh').set('Cookie', firstCookieHeader); + + expect(refreshRes.status).toBe(StatusCodes.OK); + const secondCookie = getRefreshCookie(refreshRes.headers['set-cookie']); + // This is the RED assertion: new implementation must set a new refresh cookie + expect(secondCookie).toBeDefined(); + expect(secondCookie).not.toBe(firstCookie); + + // Old token must no longer work + const replayOld = await request(app).post('/api/auth/refresh').set('Cookie', firstCookieHeader); + expect(replayOld.status).toBe(StatusCodes.UNAUTHORIZED); + + // New token must work + const secondCookieHeader = extractCookies(refreshRes.headers['set-cookie']); + const refreshWithNew = await request(app).post('/api/auth/refresh').set('Cookie', secondCookieHeader); + expect(refreshWithNew.status).toBe(StatusCodes.OK); + }); +}); diff --git a/server/test/setup.ts b/server/test/setup.ts index b4d3849..94b85c3 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -20,7 +20,14 @@ beforeAll(async () => { }); afterEach(async () => { - const tableNames = ['collaboration_requests', 'collaborators', 'yjs_document_states', 'documents', 'users']; + const tableNames = [ + 'collaboration_requests', + 'collaborators', + 'yjs_document_states', + 'documents', + 'sessions', + 'users', + ]; try { await prisma.$transaction(async (tx: Prisma.TransactionClient) => {