You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
7.8 KiB
234 lines
7.8 KiB
/** |
|
* 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(); |
|
} |
|
} |
|
);
|
|
|