Files
pwa/backend/tests/chat.test.ts
T
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

231 lines
8.1 KiB
TypeScript

import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { prisma } from '../src/lib/prisma.js';
import { PCQ_QUESTION_IDS } from '../src/modules/assessments/scoring/index.js';
import { COACH_SESSION_START_CUE } from '../src/modules/ai/coachPrompt.js';
import { seedCourseSessions } from '../src/modules/sessions/sessions.service.js';
import {
assertNoSecrets,
authHeader,
createTestApp,
isDatabaseAvailable,
requestJson,
} from './helpers.js';
const app = createTestApp();
const createdUserIds: bigint[] = [];
function pcqAnswers(value: number) {
return PCQ_QUESTION_IDS.map((questionId) => ({
questionId,
selectedValue: String(value),
}));
}
async function registerUser(suffix: string) {
const email = `chat-${suffix}-${Date.now()}@example.com`;
const { status, body } = await requestJson(app, '/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password: 'password12' }),
});
assert.equal(status, 201);
const token = body.data?.sessionToken as string;
const externalId = (body.data?.user as { id: string }).id;
const user = await prisma.user.findUnique({ where: { externalId } });
assert.ok(user);
createdUserIds.push(user.id);
return { token, user };
}
async function completePreTest(token: string) {
const pre = await requestJson(app, '/assessments/attempts', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ phase: 'pre', answers: pcqAnswers(4) }),
});
assert.equal(pre.status, 201);
}
describe('chat persistence and AI runtime', () => {
let dbAvailable = false;
before(async () => {
dbAvailable = await isDatabaseAvailable();
if (dbAvailable) {
await seedCourseSessions();
}
});
after(async () => {
if (dbAvailable && createdUserIds.length > 0) {
await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } });
}
});
it('rejects unauthenticated chat access', async () => {
const created = await requestJson(app, '/chat/sessions', {
method: 'POST',
body: JSON.stringify({}),
});
assert.equal(created.status, 401);
});
it('does not leak AI secrets through public config', async () => {
const { status, body, raw } = await requestJson(app, '/ai/config');
assert.equal(status, 200);
assert.equal(typeof body.data?.configured, 'boolean');
assert.equal(typeof body.data?.provider, 'string');
assert.ok((body.data?.provider as string).length > 0);
assert.doesNotMatch(raw, /sk-/);
assert.doesNotMatch(raw, /OPENAI_API_KEY/);
assert.doesNotMatch(raw, /Bearer /);
assertNoSecrets(raw);
});
it('lists AI runs for the authenticated user only', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const owner = await registerUser('runs-owner');
const other = await registerUser('runs-other');
await completePreTest(owner.token);
const created = await requestJson(app, '/chat/sessions', {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ sessionOrder: 1 }),
});
assert.equal(created.status, 201);
const chatId = created.body.data?.id as string;
await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ content: 'سلام' }),
});
const unauthorized = await requestJson(app, '/ai/runs');
assert.equal(unauthorized.status, 401);
const runs = await requestJson(app, '/ai/runs?limit=10', {
headers: authHeader(owner.token),
});
assert.equal(runs.status, 200);
const ownerRuns = runs.body.data as {
id: string;
provider: string;
chatSessionId: string | null;
status: string;
}[];
assert.ok(Array.isArray(ownerRuns));
assert.ok(ownerRuns.length >= 1);
assert.ok(ownerRuns.every((run) => typeof run.provider === 'string' && run.provider.length > 0));
assert.ok(ownerRuns.some((run) => run.chatSessionId === chatId));
assert.doesNotMatch(JSON.stringify(runs.body), /sk-/);
assert.doesNotMatch(JSON.stringify(runs.body), /OPENAI_API_KEY/);
const otherRuns = await requestJson(app, '/ai/runs', {
headers: authHeader(other.token),
});
assert.equal(otherRuns.status, 200);
const otherList = otherRuns.body.data as { chatSessionId: string | null }[];
assert.equal(otherList.some((run) => run.chatSessionId === chatId), false);
});
it('persists chat ownership, order, and history without exposing hidden prompts', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const owner = await registerUser('owner');
const other = await registerUser('other');
await completePreTest(owner.token);
const lockedFuture = await requestJson(app, '/chat/sessions', {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ sessionOrder: 7 }),
});
assert.equal(lockedFuture.status, 403);
const created = await requestJson(app, '/chat/sessions', {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ sessionOrder: 1 }),
});
assert.equal(created.status, 201);
const chatId = created.body.data?.id as string;
assert.ok(chatId);
assert.equal(created.body.data?.sessionOrder, 1);
assert.equal(created.body.data?.endedAt, null);
const listed = await requestJson(app, '/chat/sessions', {
headers: authHeader(owner.token),
});
assert.equal(listed.status, 200);
const listedSessions = listed.body.data as { id: string; sessionOrder: number }[];
assert.ok(listedSessions.some((session) => session.id === chatId));
const start = await requestJson(app, `/chat/sessions/${chatId}/start`, {
method: 'POST',
headers: authHeader(owner.token),
});
assert.ok(start.status === 200 || start.status === 503);
if (start.status === 200) {
const startMessages =
(start.body.data?.messages as { role: string; content: string }[]) ?? [];
assert.equal(startMessages.some((message) => message.role === 'system'), false);
assert.equal(
startMessages.some((message) => message.content.includes(COACH_SESSION_START_CUE)),
false,
);
}
const first = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ content: 'سلام، آماده هستم.' }),
});
assert.ok(first.status === 201 || first.status === 503);
const second = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(owner.token),
body: JSON.stringify({ content: 'لطفا ادامه بده.' }),
});
assert.ok(second.status === 201 || second.status === 503);
const history = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
headers: authHeader(owner.token),
});
assert.equal(history.status, 200);
const messages =
(history.body.data?.messages as { role: string; content: string; createdAt: string }[]) ?? [];
const userMessages = messages.filter((message) => message.role === 'user');
assert.ok(userMessages.length >= 2);
assert.equal(messages.some((message) => message.role === 'system'), false);
assert.equal(
messages.some((message) => message.content.includes(COACH_SESSION_START_CUE)),
false,
);
assert.equal(userMessages[0]?.content, 'سلام، آماده هستم.');
assert.equal(userMessages[1]?.content, 'لطفا ادامه بده.');
assert.ok(new Date(userMessages[0].createdAt).getTime() <= new Date(userMessages[1].createdAt).getTime());
const otherRead = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
headers: authHeader(other.token),
});
assert.equal(otherRead.status, 404);
const otherWrite = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(other.token),
body: JSON.stringify({ content: 'پیام غیرمجاز' }),
});
assert.equal(otherWrite.status, 404);
});
});