From 03fe419d3d4b12f325ac13348f0924d697d912ae Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:14:13 +0530 Subject: [PATCH 1/6] fix(connect): hard-fail OAuth callback when Redis unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CSRF nonce check in /connect/github/callback was guarded by if (app.redis && ...), meaning a Redis outage silently bypassed all CSRF verification. An attacker could craft a base64 state for any userId, submit a stolen OAuth code to the callback, and have a github_follow token stored under an arbitrary user's account. Changes: - Replace the conditional CSRF guard with an explicit hard-fail: if !app.redis || app.redis.status !== 'ready', return 503 immediately. The callback never reaches token exchange or DB writes without a verified nonce. - Remove all pp.redis ? ... : null ternaries from the callback path — every Redis access is now unconditional, matching the initiator's strictness. - Nonce deletion ( edis.del) is now unconditional after a successful verification, preventing replay attacks. Tests added (connect.test.ts): - Redis unavailable (status !== 'ready') → 503, no upsert - Redis is null/falsy → 503, no upsert - Crafted state with unknown nonce → invalid_state redirect, no upsert - Nonce present but userId mismatch → invalid_state redirect, no upsert - Valid round-trip → connected=github redirect, nonce consumed - Nonce replay → second request rejected after first consumes the nonce - Missing code/state params → missing_params redirect - Malformed base64 state → connect_failed redirect --- apps/backend/src/__tests__/connect.test.ts | 207 ++++++++++++++++++--- apps/backend/src/routes/connect.ts | 21 ++- 2 files changed, 192 insertions(+), 36 deletions(-) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index 371cec7f..e3ba4d97 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,39 +1,188 @@ -import { describe, it, expect } from 'vitest'; +import Fastify, { type FastifyInstance } from 'fastify'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import type { PrismaClient } from '@prisma/client'; +import { connectRoutes } from '../routes/connect.js'; -// Mock test for GitHub OAuth callback state validation -// Note: This test verifies the expected behavior of the -// /api/connect/github/callback endpoint when invalid or -// malformed OAuth state values are received. -// -// The implementation in connect.ts now: -// - safely parses OAuth state via parseGoogleState() -// - validates required fields (userId + nonce) -// - redirects invalid callbacks safely -// -// Security note: -// OAuth state validation helps prevent tampered callback -// requests and malformed state payload attacks. +const USER_ID = 'user-abc'; +const VALID_NONCE = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; +const VALID_STATE = Buffer.from(JSON.stringify({ userId: USER_ID, nonce: VALID_NONCE })).toString('base64'); +const ATTACKER_USER_ID = 'user-victim'; +const CRAFTED_STATE = Buffer.from(JSON.stringify({ userId: ATTACKER_USER_ID, nonce: 'nonce-never-issued' })).toString('base64'); -describe('GET /api/connect/github/callback - Invalid OAuth State', () => { +const mockPrisma = { + oAuthToken: { + findMany: vi.fn(), + upsert: vi.fn(), + delete: vi.fn(), + }, +}; - it('should redirect with connect_failed when state is invalid', async () => { - // Expected behavior: - // parseGoogleState('invalid_state') -> null - // reply.redirect(`${PUBLIC_APP_URL}/settings?error=connect_failed`) +// Redis mock that reports as connected and ready +const mockRedis = { + status: 'ready', + set: vi.fn(), + get: vi.fn(), + del: vi.fn(), +}; - expect(true).toBe(true); +// Redis mock that simulates connection failure +const mockRedisDown = { + status: 'end', + set: vi.fn(), + get: vi.fn(), + del: vi.fn(), +}; + +async function buildApp(redisOverride?: object | null): Promise { + const app = Fastify({ logger: false }); + app.decorate('prisma', mockPrisma as unknown as PrismaClient); + app.decorate('redis', (redisOverride === undefined ? mockRedis : redisOverride) as any); + app.decorate('authenticate', async (request: any) => { + request.user = { id: USER_ID }; + }); + // stub fetch globally for token exchange + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ + json: async () => ({ access_token: 'gh_token_abc', scope: 'user:follow' }), + })); + app.register(connectRoutes, { prefix: '/api/connect' }); + await app.ready(); + return app; +} + +// ───────────────────────────────────────────────────────────────────────────── +// GET /api/connect/github/callback — CSRF nonce enforcement +// ───────────────────────────────────────────────────────────────────────────── + +describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.PUBLIC_APP_URL = 'https://app.devcard.test'; + process.env.BACKEND_URL = 'https://api.devcard.test'; + process.env.GITHUB_CLIENT_ID = 'gh_client_id'; + process.env.GITHUB_CLIENT_SECRET = 'gh_client_secret'; + }); + + it('returns 503 when Redis is unavailable (status !== ready)', async () => { + const app = await buildApp(mockRedisDown); + const res = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, + }); + + expect(res.statusCode).toBe(503); + // Must not attempt token exchange or write any token + expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); + // Must not have queried Redis (it's down — no point) + expect(mockRedisDown.get).not.toHaveBeenCalled(); + }); + + it('returns 503 when app.redis is null/falsy', async () => { + const app = await buildApp(null); + const res = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, }); - it('should reject malformed oauth state payloads', async () => { - // Example malformed payload: - // { invalid: true } - // - // Expected: - // - missing userId - // - missing nonce - // - redirect to connect_failed + expect(res.statusCode).toBe(503); + expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); + }); + + it('redirects to invalid_state when nonce was never issued (crafted state)', async () => { + // Simulates an attacker constructing a state for an arbitrary userId + // with a nonce that was never stored by this server + mockRedis.get.mockResolvedValue(null); // nonce not in Redis + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${CRAFTED_STATE}`, + }); + + expect(res.statusCode).toBe(302); + expect(res.headers.location).toContain('error=invalid_state'); + expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); + }); + + it('redirects to invalid_state when nonce is present but userId does not match', async () => { + // Nonce exists but was issued for a different user — state tampering + mockRedis.get.mockResolvedValue('user-different'); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, + }); + + expect(res.statusCode).toBe(302); + expect(res.headers.location).toContain('error=invalid_state'); + expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); + }); + + it('completes the OAuth flow and stores the token when nonce is valid', async () => { + mockRedis.get.mockResolvedValue(USER_ID); // nonce matches userId + mockRedis.del.mockResolvedValue(1); + mockPrisma.oAuthToken.upsert.mockResolvedValue({}); + + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, + }); + + expect(res.statusCode).toBe(302); + expect(res.headers.location).toContain('connected=github'); + // Nonce must be consumed after a successful verification + expect(mockRedis.del).toHaveBeenCalledWith(`oauth:nonce:${VALID_NONCE}`); + expect(mockPrisma.oAuthToken.upsert).toHaveBeenCalledOnce(); + }); + + it('consumes the nonce exactly once — replay of the same state is rejected', async () => { + // First call: nonce present → succeeds and deletes nonce + mockRedis.get.mockResolvedValueOnce(USER_ID); + mockRedis.del.mockResolvedValue(1); + mockPrisma.oAuthToken.upsert.mockResolvedValue({}); + + const app = await buildApp(); + const first = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, + }); + expect(first.statusCode).toBe(302); + expect(first.headers.location).toContain('connected=github'); + + // Second call: nonce already deleted → Redis returns null → rejected + mockRedis.get.mockResolvedValueOnce(null); + const second = await app.inject({ + method: 'GET', + url: `/api/connect/github/callback?code=gh_code&state=${VALID_STATE}`, + }); + expect(second.statusCode).toBe(302); + expect(second.headers.location).toContain('error=invalid_state'); + // upsert must only have been called once across both requests + expect(mockPrisma.oAuthToken.upsert).toHaveBeenCalledOnce(); + }); + + it('redirects to connect_failed when code or state is missing', async () => { + const app = await buildApp(); + + const noCode = await app.inject({ method: 'GET', url: '/api/connect/github/callback?state=abc' }); + expect(noCode.statusCode).toBe(302); + expect(noCode.headers.location).toContain('error=missing_params'); + + const noState = await app.inject({ method: 'GET', url: '/api/connect/github/callback?code=abc' }); + expect(noState.statusCode).toBe(302); + expect(noState.headers.location).toContain('error=missing_params'); + }); - expect(true).toBe(true); + it('redirects to connect_failed when state is not valid base64 JSON', async () => { + const app = await buildApp(); + const res = await app.inject({ + method: 'GET', + url: '/api/connect/github/callback?code=gh_code&state=not_valid_base64!!!', }); + expect(res.statusCode).toBe(302); + expect(res.headers.location).toContain('error=connect_failed'); + expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index bb04194d..7178cbdc 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -93,16 +93,23 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=connect_failed`); } - // Verify nonce was issued by this server -- prevents CSRF - const storedUserId = app.redis ? await app.redis.get(`oauth:nonce:${decodedState.nonce}`) : null; + // Hard-fail when Redis is unavailable: proceeding without nonce + // verification would allow CSRF — an attacker could craft a valid-looking + // state for any userId and have a token stored under their target's account. + if (!app.redis || app.redis.status !== 'ready') { + app.log.error('OAuth CSRF check skipped: Redis unavailable — aborting callback'); + return reply.status(503).send({ error: 'Service temporarily unavailable. Please try again.' }); + } + + const storedUserId = await app.redis.get(`oauth:nonce:${decodedState.nonce}`); - if (app.redis && (!storedUserId || storedUserId !== decodedState.userId)) { - app.log.warn({ nonce: decodedState.nonce }, 'OAuth CSRF check failed: nonce mismatch'); + if (!storedUserId || storedUserId !== decodedState.userId) { + app.log.warn({ nonce: decodedState.nonce }, 'OAuth CSRF check failed: nonce mismatch or nonce not found'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=invalid_state`); } - // Consume the nonce -- one-time use only (if redis configured) - if (app.redis) await app.redis.del(`oauth:nonce:${decodedState.nonce}`); + // Consume the nonce — one-time use, prevents replay attacks + await app.redis.del(`oauth:nonce:${decodedState.nonce}`); const userId = decodedState.userId; @@ -218,4 +225,4 @@ function parseOAuthState(state: string): ParsedOAuthState | null { function generateState(): string { return randomBytes(32).toString('hex'); -} +} \ No newline at end of file From 658664e489aace44ef1d4a83acd27dc39c6073b9 Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:25:12 +0530 Subject: [PATCH 2/6] fix(lint): fix import group ordering in connect.ts and connect.test.ts --- apps/backend/src/__tests__/connect.test.ts | 22 +++++++++------------- apps/backend/src/routes/connect.ts | 16 +++++++++------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index e3ba4d97..95441569 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,6 +1,7 @@ -import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, vi } from 'vitest'; +import Fastify, { type FastifyInstance } from 'fastify'; import type { PrismaClient } from '@prisma/client'; + import { connectRoutes } from '../routes/connect.js'; const USER_ID = 'user-abc'; @@ -9,6 +10,11 @@ const VALID_STATE = Buffer.from(JSON.stringify({ userId: USER_ID, nonce: VALID_N const ATTACKER_USER_ID = 'user-victim'; const CRAFTED_STATE = Buffer.from(JSON.stringify({ userId: ATTACKER_USER_ID, nonce: 'nonce-never-issued' })).toString('base64'); +// Mock encrypt so the token-storage path does not throw in tests +vi.mock('../utils/encryption.js', () => ({ + encrypt: vi.fn().mockReturnValue('encrypted_token'), +})); + const mockPrisma = { oAuthToken: { findMany: vi.fn(), @@ -40,7 +46,6 @@ async function buildApp(redisOverride?: object | null): Promise app.decorate('authenticate', async (request: any) => { request.user = { id: USER_ID }; }); - // stub fetch globally for token exchange vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: async () => ({ access_token: 'gh_token_abc', scope: 'user:follow' }), })); @@ -70,9 +75,7 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { }); expect(res.statusCode).toBe(503); - // Must not attempt token exchange or write any token expect(mockPrisma.oAuthToken.upsert).not.toHaveBeenCalled(); - // Must not have queried Redis (it's down — no point) expect(mockRedisDown.get).not.toHaveBeenCalled(); }); @@ -88,9 +91,7 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { }); it('redirects to invalid_state when nonce was never issued (crafted state)', async () => { - // Simulates an attacker constructing a state for an arbitrary userId - // with a nonce that was never stored by this server - mockRedis.get.mockResolvedValue(null); // nonce not in Redis + mockRedis.get.mockResolvedValue(null); const app = await buildApp(); const res = await app.inject({ @@ -104,7 +105,6 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { }); it('redirects to invalid_state when nonce is present but userId does not match', async () => { - // Nonce exists but was issued for a different user — state tampering mockRedis.get.mockResolvedValue('user-different'); const app = await buildApp(); @@ -119,7 +119,7 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { }); it('completes the OAuth flow and stores the token when nonce is valid', async () => { - mockRedis.get.mockResolvedValue(USER_ID); // nonce matches userId + mockRedis.get.mockResolvedValue(USER_ID); mockRedis.del.mockResolvedValue(1); mockPrisma.oAuthToken.upsert.mockResolvedValue({}); @@ -131,13 +131,11 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { expect(res.statusCode).toBe(302); expect(res.headers.location).toContain('connected=github'); - // Nonce must be consumed after a successful verification expect(mockRedis.del).toHaveBeenCalledWith(`oauth:nonce:${VALID_NONCE}`); expect(mockPrisma.oAuthToken.upsert).toHaveBeenCalledOnce(); }); it('consumes the nonce exactly once — replay of the same state is rejected', async () => { - // First call: nonce present → succeeds and deletes nonce mockRedis.get.mockResolvedValueOnce(USER_ID); mockRedis.del.mockResolvedValue(1); mockPrisma.oAuthToken.upsert.mockResolvedValue({}); @@ -150,7 +148,6 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { expect(first.statusCode).toBe(302); expect(first.headers.location).toContain('connected=github'); - // Second call: nonce already deleted → Redis returns null → rejected mockRedis.get.mockResolvedValueOnce(null); const second = await app.inject({ method: 'GET', @@ -158,7 +155,6 @@ describe('GET /api/connect/github/callback — CSRF nonce enforcement', () => { }); expect(second.statusCode).toBe(302); expect(second.headers.location).toContain('error=invalid_state'); - // upsert must only have been called once across both requests expect(mockPrisma.oAuthToken.upsert).toHaveBeenCalledOnce(); }); diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 7178cbdc..e7af79f2 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,7 +1,9 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; + import { encrypt } from '../utils/encryption.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -30,9 +32,9 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch (_e) { reply.status(401).send({ error: 'Unauthorized' }) } }], - }, async (request: FastifyRequest, reply: FastifyReply) => { + }, async (request: FastifyRequest, _reply: FastifyReply) => { const userId = (request.user as any).id; const tokens = await app.prisma.oAuthToken.findMany({ @@ -50,7 +52,7 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch (_e) { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; @@ -182,7 +184,7 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch (_e) { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async (request: FastifyRequest<{ Params: { platform: string } }>, reply: FastifyReply) => { const userId = (request.user as any).id; @@ -203,7 +205,7 @@ export async function connectRoutes(app: FastifyInstance) { }, }); return { success: true }; - } catch (error) { + } catch (_error) { return reply.status(404).send({ error: 'Connection not found' }); } }); From ed0ef6c97bc41b50ec7732e0f8c8cbee6b7c8bc6 Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:35:11 +0530 Subject: [PATCH 3/6] fix(lint): fix import group ordering in connect.test.ts --- apps/backend/src/__tests__/connect.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index 95441569..fa53a915 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,9 +1,11 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; import Fastify, { type FastifyInstance } from 'fastify'; -import type { PrismaClient } from '@prisma/client'; + +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { connectRoutes } from '../routes/connect.js'; +import type { PrismaClient } from '@prisma/client'; + const USER_ID = 'user-abc'; const VALID_NONCE = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2'; const VALID_STATE = Buffer.from(JSON.stringify({ userId: USER_ID, nonce: VALID_NONCE })).toString('base64'); From 693cd046f9efbba06e8da4af3c6fb5a3fdb83442 Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:37:31 +0530 Subject: [PATCH 4/6] fix(lint): fix import group ordering in connect.test.ts --- apps/backend/src/__tests__/connect.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index fa53a915..9d9f6dfc 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,9 +1,6 @@ import Fastify, { type FastifyInstance } from 'fastify'; - import { describe, it, expect, beforeEach, vi } from 'vitest'; - import { connectRoutes } from '../routes/connect.js'; - import type { PrismaClient } from '@prisma/client'; const USER_ID = 'user-abc'; From a742d937a2d94e64390e67a933d0c0ff5fbbb917 Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:41:35 +0530 Subject: [PATCH 5/6] fix(lint): fix import group ordering in connect.test.ts --- apps/backend/src/__tests__/connect.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index 9d9f6dfc..fa53a915 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,6 +1,9 @@ import Fastify, { type FastifyInstance } from 'fastify'; + import { describe, it, expect, beforeEach, vi } from 'vitest'; + import { connectRoutes } from '../routes/connect.js'; + import type { PrismaClient } from '@prisma/client'; const USER_ID = 'user-abc'; From d739e023b29471023900f61d0a9d5c1e0fc26497 Mon Sep 17 00:00:00 2001 From: Hari Om Date: Fri, 5 Jun 2026 16:45:32 +0530 Subject: [PATCH 6/6] fix(lint): consolidate external imports into single group in connect.test.ts --- apps/backend/src/__tests__/connect.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/backend/src/__tests__/connect.test.ts b/apps/backend/src/__tests__/connect.test.ts index fa53a915..2221801e 100644 --- a/apps/backend/src/__tests__/connect.test.ts +++ b/apps/backend/src/__tests__/connect.test.ts @@ -1,5 +1,4 @@ import Fastify, { type FastifyInstance } from 'fastify'; - import { describe, it, expect, beforeEach, vi } from 'vitest'; import { connectRoutes } from '../routes/connect.js';