Files
pwa/backend/tests/domain.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

228 lines
8.6 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 { exampleCompletionSignal } from '../src/modules/ai/sessionCompletion.js';
import { buildAiCoachSessionContext } from '../src/modules/ai/sessionContext.js';
import { getSessionByOrder, PROGRAM_SESSION_COUNT } from '../src/modules/sessions/catalog.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 = `domain-${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 };
}
describe('learning domain API', () => {
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('GET /ai/config is public and does not leak secrets', 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?.model, 'string');
assert.equal(typeof body.data?.provider, 'string');
assert.doesNotMatch(raw, /sk-/);
assert.doesNotMatch(raw, /OPENAI_API_KEY/);
assertNoSecrets(raw);
});
it('gates journey, assessments, sequential sessions, and chat persistence', async (t) => {
if (!dbAvailable) {
t.skip('MySQL is not running');
return;
}
const { token, user } = await registerUser('flow');
const beforePre = await requestJson(app, '/journey', { headers: authHeader(token) });
assert.equal(beforePre.status, 200);
const beforePermissions = beforePre.body.data?.permissions as {
canTakePreTest: boolean;
canOpenTraining: boolean;
canTakePostTest: boolean;
isCompleted: boolean;
};
assert.equal(beforePermissions.canTakePreTest, true);
assert.equal(beforePermissions.canOpenTraining, false);
assert.equal(beforePermissions.canTakePostTest, false);
const lockedChat = await requestJson(app, '/chat/sessions', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({}),
});
assert.equal(lockedChat.status, 403);
const postTooSoon = await requestJson(app, '/assessments/attempts', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ phase: 'post', answers: pcqAnswers(4) }),
});
assert.equal(postTooSoon.status, 403);
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);
assert.equal(pre.body.data?.scores && (pre.body.data.scores as { total: number }).total, 93);
const reverseAnswer = (
pre.body.data?.answers as { questionId: string; selectedValue: number; score: number }[]
).find((answer) => answer.questionId === 'pcq_13');
assert.equal(reverseAnswer?.selectedValue, 4);
assert.equal(reverseAnswer?.score, 3);
const retake = await requestJson(app, '/assessments/attempts', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ phase: 'pre', answers: pcqAnswers(5) }),
});
assert.equal(retake.status, 409);
assert.equal(retake.body.error?.code, 'CONFLICT');
const afterPre = await requestJson(app, '/journey', { headers: authHeader(token) });
assert.equal((afterPre.body.data as { currentStep: string }).currentStep, 'training_in_progress');
const afterPermissions = afterPre.body.data?.permissions as {
canTakePreTest: boolean;
canOpenTraining: boolean;
canTakePostTest: boolean;
};
assert.equal(afterPermissions.canTakePreTest, false);
assert.equal(afterPermissions.canOpenTraining, true);
assert.equal(afterPermissions.canTakePostTest, false);
const sessions = await requestJson(app, '/sessions', { headers: authHeader(token) });
assert.equal(sessions.status, 200);
const list = sessions.body.data as unknown as { order: number; unlocked: boolean; completed: boolean }[];
assert.equal(list.length, PROGRAM_SESSION_COUNT);
assert.equal(list[0]?.unlocked, true);
assert.equal(list[1]?.unlocked, false);
const sessionTwoComplete = await requestJson(app, '/sessions/2/complete', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({
...exampleCompletionSignal(buildAiCoachSessionContext(getSessionByOrder(2)!)),
}),
});
assert.equal(sessionTwoComplete.status, 403);
const chat = await requestJson(app, '/chat/sessions', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ sessionOrder: 1 }),
});
assert.equal(chat.status, 201);
const chatId = chat.body.data?.id as string;
assert.ok(chatId);
const firstMessage = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ content: 'سلام، آماده هستم.' }),
});
assert.ok(firstMessage.status === 503 || firstMessage.status === 201);
const secondMessage = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({ content: 'ادامه بده.' }),
});
assert.ok(secondMessage.status === 503 || secondMessage.status === 201);
const stored = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
headers: authHeader(token),
});
assert.equal(stored.status, 200);
const storedMessages = (stored.body.data?.messages as { role: string; content: string }[]) ?? [];
assert.ok(storedMessages.filter((message) => message.role === 'user').length >= 2);
const complete = await requestJson(app, '/sessions/1/complete', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({
...exampleCompletionSignal(buildAiCoachSessionContext(getSessionByOrder(1)!)),
chatSessionId: chatId,
}),
});
assert.equal(complete.status, 200);
const completedSessions = complete.body.data?.completedSessions as number[];
assert.deepEqual(completedSessions, [1]);
const endedChat = await requestJson(app, `/chat/sessions/${chatId}/messages`, {
headers: authHeader(token),
});
assert.equal(endedChat.status, 200);
assert.ok((endedChat.body.data?.session as { endedAt: string | null }).endedAt);
const listed = await requestJson(app, '/chat/sessions?sessionOrder=1', {
headers: authHeader(token),
});
assert.equal(listed.status, 200);
const listedSessions = listed.body.data as { id: string; sessionOrder: number; endedAt: string | null }[];
assert.ok(listedSessions.some((session) => session.id === chatId && session.endedAt));
const recomplete = await requestJson(app, '/sessions/1/complete', {
method: 'POST',
headers: authHeader(token),
body: JSON.stringify({
...exampleCompletionSignal(buildAiCoachSessionContext(getSessionByOrder(1)!)),
chatSessionId: chatId,
}),
});
assert.equal(recomplete.status, 200);
assert.deepEqual(recomplete.body.data?.completedSessions as number[], [1]);
const afterSession = await requestJson(app, '/sessions', { headers: authHeader(token) });
const afterList = afterSession.body.data as unknown as { order: number; unlocked: boolean }[];
assert.equal(afterList[1]?.unlocked, true);
const stillNoPost = await requestJson(app, '/journey', { headers: authHeader(token) });
assert.equal(
(stillNoPost.body.data?.permissions as { canTakePostTest: boolean }).canTakePostTest,
false,
);
});
});