enhanced questionnaire version handling and handling uploads from outdated questionnaires through automatic revisioning and checks
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-06-12 21:38:39 +02:00
parent 5dff24a232
commit b4d4de72f4
17 changed files with 1289 additions and 209 deletions

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Tests\Integration;
use Tests\Support\DatabaseSeeder;
use Tests\Support\QdbTestCase;
final class AppSubmitLegacyTest extends QdbTestCase
{
public function testLegacySubmitAcceptsRetiredQuestionAfterBump(): void
{
$login = $this->api()->loginMobileAndGetToken(
DatabaseSeeder::COACH_USERNAME,
DatabaseSeeder::PASSWORD
);
$f = $this->fixture();
$this->assertApiOk($this->submitQuestionnaireAs(
$login['token'],
$f->questionnaireId,
$f->clientCode,
[['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes']],
null,
null,
1
));
require_once dirname(__DIR__, 2) . '/lib/questionnaire_structure.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$rev = qdb_bump_structure_revision($pdo, $f->questionnaireId, 'legacy_test_retire');
qdb_retire_question($pdo, $f->questionId, $rev);
$currentRev = qdb_questionnaire_structure_revision($pdo, $f->questionnaireId);
qdb_save($tmpDb, $lockFp);
$this->assertGreaterThan(1, $currentRev);
$res = $this->submitQuestionnaireAs(
$login['token'],
$f->questionnaireId,
$f->clientCode,
[['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes']],
null,
null,
1
);
$this->assertApiOk($res);
$this->assertTrue($res['data']['submitted'] ?? false);
$this->assertTrue($res['data']['legacySubmit'] ?? false);
$this->assertSame(1, $res['data']['structureRevision'] ?? null);
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$sub = $pdo->prepare(
'SELECT structureRevision, structureSnapshotJson FROM questionnaire_submission
WHERE clientCode = :cc AND questionnaireID = :qn ORDER BY version DESC LIMIT 1'
);
$sub->execute([':cc' => $f->clientCode, ':qn' => $f->questionnaireId]);
$row = $sub->fetch(\PDO::FETCH_ASSOC);
$this->assertSame(1, (int)($row['structureRevision'] ?? 0));
$snap = json_decode($row['structureSnapshotJson'] ?? '{}', true) ?: [];
$this->assertNotEmpty($snap['questions'] ?? []);
$archived = $pdo->prepare(
'SELECT COUNT(*) FROM client_answer_submission cas
INNER JOIN questionnaire_submission qs ON qs.submissionID = cas.submissionID
WHERE qs.clientCode = :cc AND qs.questionnaireID = :qn AND cas.questionID = :qid'
);
$archived->execute([
':cc' => $f->clientCode,
':qn' => $f->questionnaireId,
':qid' => $f->questionId,
]);
$this->assertGreaterThan(0, (int)$archived->fetchColumn());
qdb_discard($tmpDb, $lockFp);
}
}

View File

@ -137,18 +137,24 @@ abstract class QdbTestCase extends PHPUnitTestCase
array $answers,
?int $startedAt = null,
?int $completedAt = null,
?int $structureRevision = null,
): array {
$now = time();
$startedAt ??= $now - 60;
$completedAt ??= $now;
return $this->api()->encryptedPost($token, 'app_questionnaires', [
$payload = [
'questionnaireID' => $questionnaireId,
'clientCode' => $clientCode,
'answers' => $answers,
'startedAt' => $startedAt,
'completedAt' => $completedAt,
]);
];
if ($structureRevision !== null) {
$payload['structureRevision'] = $structureRevision;
}
return $this->api()->encryptedPost($token, 'app_questionnaires', $payload);
}
protected function submissionVersionCount(string $clientCode, string $questionnaireId): int

View File

@ -37,6 +37,28 @@ final class AppSubmitValidateTest extends QdbTestCase
$this->assertSame('UNKNOWN_QUESTION', $errors[0]['code'] ?? '');
}
public function testUnknownQuestionAllowedInLegacyManifest(): void
{
require_once dirname(__DIR__, 2) . '/lib/app_submit_validate.php';
require_once dirname(__DIR__, 2) . '/lib/questionnaire_structure.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
$f = $this->fixture();
$manifest = qdb_build_structure_manifest($pdo, $f->questionnaireId, true);
$maps = qdb_submit_maps_from_manifest($pdo, $f->questionnaireId, $manifest);
$errors = qdb_validate_app_submit_payload(
$pdo,
$f->questionnaireId,
[['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes']],
$maps['shortIdMap'],
$maps['shortIdToType'],
$maps['symptomParentMap'],
$maps['optionMap'],
$manifest
);
qdb_discard($tmpDb, $lockFp);
$this->assertSame([], $errors);
}
public function testValidSingleChoiceWithOptionKeyPasses(): void
{
require_once dirname(__DIR__, 2) . '/lib/app_submit_validate.php';

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use Tests\Support\QdbTestCase;
final class QuestionnaireStructureTest extends QdbTestCase
{
public function testBumpIncrementsRevisionAndStoresSnapshot(): void
{
require_once dirname(__DIR__, 2) . '/lib/questionnaire_structure.php';
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$f = $this->fixture();
$before = qdb_questionnaire_structure_revision($pdo, $f->questionnaireId);
$newRev = qdb_bump_structure_revision($pdo, $f->questionnaireId, 'test_bump');
$after = qdb_questionnaire_structure_revision($pdo, $f->questionnaireId);
$this->assertSame($before + 1, $newRev);
$this->assertSame($newRev, $after);
$manifest = qdb_load_structure_manifest($pdo, $f->questionnaireId, $before);
$this->assertNotNull($manifest);
$this->assertNotEmpty($manifest['questions'] ?? []);
$this->assertSame($before, (int)($manifest['structureRevision'] ?? 0));
qdb_save($tmpDb, $lockFp);
}
public function testRetirePreservesClientAnswers(): void
{
require_once dirname(__DIR__, 2) . '/lib/questionnaire_structure.php';
$this->assertApiOk($this->submitFixtureQuestionnaire());
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
$f = $this->fixture();
$this->assertTrue(qdb_question_has_client_data($pdo, $f->questionId));
$rev = qdb_bump_structure_revision($pdo, $f->questionnaireId, 'retire_test');
qdb_retire_question($pdo, $f->questionId, $rev);
$row = $pdo->prepare('SELECT retiredAt FROM question WHERE questionID = :id');
$row->execute([':id' => $f->questionId]);
$this->assertGreaterThan(0, (int)$row->fetchColumn());
$ans = $pdo->prepare(
'SELECT COUNT(*) FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
);
$ans->execute([':cc' => $f->clientCode, ':qid' => $f->questionId]);
$this->assertSame(1, (int)$ans->fetchColumn());
qdb_save($tmpDb, $lockFp);
}
}