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

/**
* 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();
}
}
);