/** * Card1 V.9: интеграция с реальной `clinic_tests` — старая попытка остаётся * на снимке версии и старых `question_id` после форка (новая версия). * * Запуск: `CLINIC_TESTS_INTEGRATION=1` и применённые миграции (`npm run migrate`), * `DATABASE_URL` (или DB_*) к той же базе. Без флага тесты помечаются skip. */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import pg from 'pg'; import bcrypt from 'bcryptjs'; import { getPoolConfig } from '../db/poolConfig.js'; import { saveTestDraft, createTestWithVersion } from '../services/testDraftService.js'; const { Pool } = pg; /** `CLINIC_TESTS_INTEGRATION=1` и успешный `SELECT 1` (без БД — skip, не fail). */ let runDb = false; if (process.env.CLINIC_TESTS_INTEGRATION === '1') { const probe = new Pool({ ...getPoolConfig(), connectionTimeoutMillis: 2000, }); try { await probe.query('SELECT 1'); runDb = true; } catch { runDb = false; } finally { await probe.end(); } } const qPayload = (label) => ({ title: 'V9 ' + label, questions: [ { text: `Q ${label}`, question_order: 1, hasMultipleAnswers: false, options: [ { text: 'yes', isCorrect: true, option_order: 1 }, { text: 'no', isCorrect: false, option_order: 2 }, ], }, ], }); /** * @param {import('pg').Pool} pool * @param {string} testId * @param {string} [exceptUserId] */ async function purgeTestChain(pool, testId, exceptUserId) { await pool.query( `DELETE FROM user_answers WHERE attempt_id IN ( SELECT id FROM test_attempts WHERE test_version_id IN ( SELECT id FROM test_versions WHERE test_id = $1 ) )`, [testId] ); await pool.query( `DELETE FROM test_attempts WHERE test_version_id IN ( SELECT id FROM test_versions WHERE test_id = $1 )`, [testId] ); await pool.query( `DELETE FROM answer_options WHERE question_id IN ( SELECT id FROM questions WHERE test_version_id IN ( SELECT id FROM test_versions WHERE test_id = $1 ) )`, [testId] ); await pool.query( `DELETE FROM questions WHERE test_version_id IN ( SELECT id FROM test_versions WHERE test_id = $1 )`, [testId] ); await pool.query(`DELETE FROM test_versions WHERE test_id = $1`, [testId]); await pool.query(`DELETE FROM tests WHERE id = $1`, [testId]); if (exceptUserId) { await pool.query(`DELETE FROM users WHERE id = $1`, [exceptUserId]); } } test( 'V.9: без попыток два saveTestDraft — одна строка test_versions (редактирование на месте)', { skip: !runDb }, async () => { const pool = new Pool(getPoolConfig()); const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`; let userId; let testId; try { const { rows: u } = await pool.query( `INSERT INTO users (login, password_hash, full_name, role, is_active) VALUES ($1, $2, 'V9 in-place', 'hr', true) RETURNING id`, [`v9p-${suffix}`, bcrypt.hashSync('x', 4)] ); userId = u[0].id; const c = await createTestWithVersion(pool, userId, { title: 'V9P' }); testId = c.testId; const { rows: v0 } = await pool.query( `SELECT id FROM test_versions WHERE test_id = $1 AND is_active = true`, [testId] ); const vid0 = v0[0].id; await saveTestDraft(pool, userId, testId, qPayload('A')); const { rows: c1 } = await pool.query( `SELECT count(*)::int AS n FROM test_versions WHERE test_id = $1`, [testId] ); assert.equal(c1[0].n, 1, 'должна остаться одна версия'); const { rows: v1 } = await pool.query( `SELECT id FROM test_versions WHERE test_id = $1 AND is_active = true`, [testId] ); assert.equal( v1[0].id, vid0, 'id активной версии не меняется при нуле попыток' ); await saveTestDraft(pool, userId, testId, qPayload('B')); const { rows: c2 } = await pool.query( `SELECT count(*)::int AS n FROM test_versions WHERE test_id = $1`, [testId] ); assert.equal(c2[0].n, 1); const { rows: v2 } = await pool.query( `SELECT id FROM test_versions WHERE test_id = $1 AND is_active = true`, [testId] ); assert.equal(v2[0].id, vid0); } finally { if (userId && testId) { await purgeTestChain(pool, testId, userId); } await pool.end(); } } ); test( 'V.9: после попытки форк — попытка и user_answers остаются на старых version_id / question_id', { skip: !runDb }, async () => { const pool = new Pool(getPoolConfig()); const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`; let userId; let testId; let v1Id; let q1Id; let opt1Id; let attemptId; try { const { rows: u } = await pool.query( `INSERT INTO users (login, password_hash, full_name, role, is_active) VALUES ($1, $2, 'V9 fork', 'hr', true) RETURNING id`, [`v9f-${suffix}`, bcrypt.hashSync('x', 4)] ); userId = u[0].id; const c = await createTestWithVersion(pool, userId, { title: 'V9F' }); testId = c.testId; await saveTestDraft(pool, userId, testId, qPayload('pre')); const { rows: tv0 } = await pool.query( `SELECT id FROM test_versions WHERE test_id = $1 AND is_active = true`, [testId] ); v1Id = tv0[0].id; const { rows: qu } = await pool.query( `SELECT id FROM questions WHERE test_version_id = $1 LIMIT 1`, [v1Id] ); q1Id = qu[0].id; const { rows: op } = await pool.query( `SELECT id FROM answer_options WHERE question_id = $1 AND is_correct = true LIMIT 1`, [q1Id] ); opt1Id = op[0].id; const { rows: at } = await pool.query( `INSERT INTO test_attempts (test_version_id, user_id, attempt_number, status, correct_count, total_questions, passed) VALUES ($1, $2, 1, 'completed', 1, 1, true) RETURNING id`, [v1Id, userId] ); attemptId = at[0].id; await pool.query( `INSERT INTO user_answers (attempt_id, question_id, selected_options) VALUES ($1, $2, $3::uuid[])`, [attemptId, q1Id, [opt1Id]] ); const out = await saveTestDraft(pool, userId, testId, qPayload('post-fork')); assert.equal(out.forked, true, 'должна создаться новая версия после попытки'); const { rows: att } = await pool.query( `SELECT test_version_id FROM test_attempts WHERE id = $1`, [attemptId] ); assert.equal( att[0].test_version_id, v1Id, 'попытка остаётся на версии, с которой проходили' ); const { rows: ua } = await pool.query( `SELECT question_id, selected_options FROM user_answers WHERE attempt_id = $1`, [attemptId] ); assert.equal(ua[0].question_id, q1Id); assert.equal(ua[0].selected_options[0], opt1Id); const { rows: qExists } = await pool.query( `SELECT 1 FROM questions WHERE id = $1 AND test_version_id = $2`, [q1Id, v1Id] ); assert.equal(qExists.length, 1, 'старый вопрос остаётся в старой версии'); const { rows: active } = await pool.query( `SELECT id FROM test_versions WHERE test_id = $1 AND is_active = true`, [testId] ); assert.notEqual(active[0].id, v1Id, 'новая версия — активна'); } finally { if (userId && testId) { await purgeTestChain(pool, testId, userId); } await pool.end(); } } );