added unit tests
This commit is contained in:
181
tests/Support/ApiClient.php
Normal file
181
tests/Support/ApiClient.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
/**
|
||||
* Dispatches requests through api/index.php (same as production routing).
|
||||
*/
|
||||
final class ApiClient
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $apiIndexPath,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $headers
|
||||
* @return array{ok: bool, status: int, data?: mixed, error?: array<string, mixed>}
|
||||
*/
|
||||
public function request(
|
||||
string $method,
|
||||
string $route,
|
||||
?array $body = null,
|
||||
array $headers = [],
|
||||
array $query = [],
|
||||
): array {
|
||||
qdb_test_reset_http_state();
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($method);
|
||||
$queryString = $query !== [] ? '?' . http_build_query($query) : '';
|
||||
$_SERVER['REQUEST_URI'] = '/api/' . ltrim($route, '/') . $queryString;
|
||||
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
|
||||
$_GET = $query;
|
||||
if ($queryString !== '') {
|
||||
parse_str(ltrim($queryString, '?'), $_GET);
|
||||
}
|
||||
|
||||
unset($_SERVER['HTTP_AUTHORIZATION'], $_SERVER['Authorization']);
|
||||
unset($_SERVER['HTTP_X_QDB_CLIENT']);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
|
||||
if ($body !== null) {
|
||||
qdb_test_set_request_body(json_encode($body, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
ob_start();
|
||||
try {
|
||||
require $this->apiIndexPath;
|
||||
ob_end_clean();
|
||||
return ['ok' => false, 'status' => 500, 'error' => ['code' => 'NO_RESPONSE', 'message' => 'Handler did not return JSON']];
|
||||
} catch (JsonSuccessResponse $e) {
|
||||
$output = ob_get_clean();
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => $e->httpStatus,
|
||||
'data' => $e->data,
|
||||
'_raw' => $output,
|
||||
];
|
||||
} catch (JsonErrorResponse $e) {
|
||||
ob_get_clean();
|
||||
$err = ['code' => $e->errorCode, 'message' => $e->errorMessage];
|
||||
if ($e->details !== null) {
|
||||
$err['details'] = $e->details;
|
||||
}
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => $e->httpStatus,
|
||||
'error' => $err,
|
||||
];
|
||||
} catch (RawHttpResponse $e) {
|
||||
ob_get_clean();
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => $e->httpStatus,
|
||||
'raw' => true,
|
||||
'body' => $e->body,
|
||||
'headers' => $e->headers,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
ob_end_clean();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function loginWeb(string $username, string $password): array
|
||||
{
|
||||
return $this->request('POST', 'auth/login', [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
], ['X-QDB-Client' => 'web']);
|
||||
}
|
||||
|
||||
public function loginMobile(string $username, string $password): array
|
||||
{
|
||||
return $this->request('POST', 'auth/login', [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{token: string, data: array<string, mixed>}
|
||||
*/
|
||||
public function loginMobileAndGetToken(string $username, string $password): array
|
||||
{
|
||||
$res = $this->loginMobile($username, $password);
|
||||
if (!$res['ok']) {
|
||||
throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown'));
|
||||
}
|
||||
$token = $res['data']['token'] ?? '';
|
||||
if ($token === '') {
|
||||
throw new \RuntimeException('Login response missing token');
|
||||
}
|
||||
return ['token' => $token, 'data' => $res['data']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{token: string, data: array<string, mixed>}
|
||||
*/
|
||||
public function loginWebAndGetToken(string $username, string $password): array
|
||||
{
|
||||
$res = $this->loginWeb($username, $password);
|
||||
if (!$res['ok']) {
|
||||
throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown'));
|
||||
}
|
||||
$token = $res['data']['token'] ?? '';
|
||||
if ($token === '') {
|
||||
throw new \RuntimeException('Login response missing token');
|
||||
}
|
||||
return ['token' => $token, 'data' => $res['data']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $extraHeaders
|
||||
*/
|
||||
public function withToken(string $token, string $method, string $route, ?array $body = null, array $query = [], array $extraHeaders = []): array
|
||||
{
|
||||
$headers = array_merge([
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'X-QDB-Client' => 'web',
|
||||
], $extraHeaders);
|
||||
|
||||
return $this->request($method, $route, $body, $headers, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $extraHeaders
|
||||
*/
|
||||
public function withMobileToken(
|
||||
string $token,
|
||||
string $method,
|
||||
string $route,
|
||||
?array $body = null,
|
||||
array $query = [],
|
||||
array $extraHeaders = [],
|
||||
): array {
|
||||
$headers = array_merge(['Authorization' => 'Bearer ' . $token], $extraHeaders);
|
||||
|
||||
return $this->request($method, $route, $body, $headers, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST an encrypted payload (Android app API).
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function encryptedPost(string $token, string $route, array $payload): array
|
||||
{
|
||||
$envelope = qdb_sensitive_envelope(
|
||||
json_encode($payload, JSON_THROW_ON_ERROR),
|
||||
$token
|
||||
);
|
||||
|
||||
return $this->withMobileToken($token, 'POST', $route, $envelope);
|
||||
}
|
||||
}
|
||||
233
tests/Support/DatabaseSeeder.php
Normal file
233
tests/Support/DatabaseSeeder.php
Normal file
@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Seeds a minimal org graph for integration tests.
|
||||
*/
|
||||
final class DatabaseSeeder
|
||||
{
|
||||
public const PASSWORD = 'TestPass1!';
|
||||
|
||||
public const ADMIN_USERNAME = 'test_admin';
|
||||
public const SUPERVISOR_USERNAME = 'test_supervisor';
|
||||
public const COACH_USERNAME = 'test_coach';
|
||||
|
||||
public static function seed(PDO $pdo): FixtureIds
|
||||
{
|
||||
$now = time();
|
||||
$passwordHash = password_hash(self::PASSWORD, PASSWORD_DEFAULT);
|
||||
|
||||
// Stable IDs so fixture metadata always matches seeded rows.
|
||||
$adminId = 'admin-test';
|
||||
$adminUserId = 'u-admin-test';
|
||||
$supervisorId = 'sv-test';
|
||||
$supervisorUserId = 'u-sv-test';
|
||||
$coachId = 'coach-test';
|
||||
$coachUserId = 'u-coach-test';
|
||||
$qnId = 'qn-test';
|
||||
$questionId = $qnId . '__q1';
|
||||
$optionId = 'opt-test-q1-yes';
|
||||
$freeTextQuestionId = $qnId . '__q2';
|
||||
$glassQuestionId = $qnId . '__q3';
|
||||
$glassSymptomKey = 'pain';
|
||||
$multiQuestionId = $qnId . '__q4';
|
||||
$multiOptionAId = 'opt-test-q4-a';
|
||||
$multiOptionBId = 'opt-test-q4-b';
|
||||
$draftQuestionnaireId = 'qn-draft-test';
|
||||
|
||||
$pdo->prepare('INSERT INTO admin (adminID, username, location) VALUES (:id, :u, :loc)')
|
||||
->execute([':id' => $adminId, ':u' => self::ADMIN_USERNAME, ':loc' => 'HQ']);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt)
|
||||
VALUES (:uid, :u, :h, :role, :eid, 0, :now)'
|
||||
)->execute([
|
||||
':uid' => $adminUserId,
|
||||
':u' => self::ADMIN_USERNAME,
|
||||
':h' => $passwordHash,
|
||||
':role' => 'admin',
|
||||
':eid' => $adminId,
|
||||
':now' => $now,
|
||||
]);
|
||||
|
||||
$pdo->prepare('INSERT INTO supervisor (supervisorID, username, location) VALUES (:id, :u, :loc)')
|
||||
->execute([':id' => $supervisorId, ':u' => self::SUPERVISOR_USERNAME, ':loc' => 'Region A']);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt)
|
||||
VALUES (:uid, :u, :h, :role, :eid, 0, :now)'
|
||||
)->execute([
|
||||
':uid' => $supervisorUserId,
|
||||
':u' => self::SUPERVISOR_USERNAME,
|
||||
':h' => $passwordHash,
|
||||
':role' => 'supervisor',
|
||||
':eid' => $supervisorId,
|
||||
':now' => $now,
|
||||
]);
|
||||
|
||||
$pdo->prepare('INSERT INTO coach (coachID, supervisorID, username) VALUES (:id, :sid, :u)')
|
||||
->execute([':id' => $coachId, ':sid' => $supervisorId, ':u' => self::COACH_USERNAME]);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt)
|
||||
VALUES (:uid, :u, :h, :role, :eid, 0, :now)'
|
||||
)->execute([
|
||||
':uid' => $coachUserId,
|
||||
':u' => self::COACH_USERNAME,
|
||||
':h' => $passwordHash,
|
||||
':role' => 'coach',
|
||||
':eid' => $coachId,
|
||||
':now' => $now,
|
||||
]);
|
||||
|
||||
$pdo->prepare('INSERT INTO client (clientCode, coachID) VALUES (:cc, :cid)')
|
||||
->execute([':cc' => 'CLIENT-TEST-001', ':cid' => $coachId]);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson, categoryKey)
|
||||
VALUES (:id, :n, :v, :s, 1, 0, :cj, :ck)'
|
||||
)->execute([
|
||||
':id' => $qnId,
|
||||
':n' => 'Test questionnaire',
|
||||
':v' => '1',
|
||||
':s' => 'active',
|
||||
':cj' => '{}',
|
||||
':ck' => '',
|
||||
]);
|
||||
$pdo->prepare('UPDATE questionnaire SET showPoints = 1 WHERE questionnaireID = :id')
|
||||
->execute([':id' => $qnId]);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
|
||||
VALUES (:id, :qn, :txt, :type, 0, 1, :cfg)'
|
||||
)->execute([
|
||||
':id' => $questionId,
|
||||
':qn' => $qnId,
|
||||
':txt' => 'Consent?',
|
||||
':type' => 'single_choice_question',
|
||||
':cfg' => json_encode(['questionKey' => 'consent_q'], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
|
||||
VALUES (:id, :qid, :txt, 0, 0, :next)'
|
||||
)->execute([
|
||||
':next' => '',
|
||||
':id' => $optionId,
|
||||
':qid' => $questionId,
|
||||
':txt' => 'yes',
|
||||
]);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
|
||||
VALUES (:id, :qn, :txt, :type, 1, 0, :cfg)'
|
||||
)->execute([
|
||||
':id' => $freeTextQuestionId,
|
||||
':qn' => $qnId,
|
||||
':txt' => 'Notes',
|
||||
':type' => 'free_text_question',
|
||||
':cfg' => json_encode(['questionKey' => 'notes_q'], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
$glassConfig = json_encode(
|
||||
['questionKey' => 'symptoms_q', 'symptoms' => [$glassSymptomKey, 'fatigue']],
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
|
||||
VALUES (:id, :qn, :txt, :type, 2, 0, :cfg)'
|
||||
)->execute([
|
||||
':id' => $glassQuestionId,
|
||||
':qn' => $qnId,
|
||||
':txt' => 'Symptoms',
|
||||
':type' => 'glass_scale_question',
|
||||
':cfg' => $glassConfig,
|
||||
]);
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/lib/settings.php';
|
||||
qdb_settings_save_on_pdo($pdo, qdb_settings_defaults());
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
|
||||
VALUES (:id, :qn, :txt, :type, 3, 0, :cfg)'
|
||||
)->execute([
|
||||
':id' => $multiQuestionId,
|
||||
':qn' => $qnId,
|
||||
':txt' => 'Pick several',
|
||||
':type' => 'multi_check_box_question',
|
||||
':cfg' => json_encode(['questionKey' => 'multi_q'], JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
|
||||
VALUES (:id, :qid, :txt, 1, 0, :next)'
|
||||
)->execute([':next' => '', ':id' => $multiOptionAId, ':qid' => $multiQuestionId, ':txt' => 'opt_a']);
|
||||
$pdo->prepare(
|
||||
'INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
|
||||
VALUES (:id, :qid, :txt, 2, 1, :next)'
|
||||
)->execute([':next' => '', ':id' => $multiOptionBId, ':qid' => $multiQuestionId, ':txt' => 'opt_b']);
|
||||
qdb_upsert_source_translation($pdo, 'answer_option', $multiOptionAId, 'A');
|
||||
qdb_upsert_source_translation($pdo, 'answer_option', $multiOptionBId, 'B');
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson, categoryKey)
|
||||
VALUES (:id, :n, :v, :s, 2, 0, :cj, :ck)'
|
||||
)->execute([
|
||||
':id' => $draftQuestionnaireId,
|
||||
':n' => 'Draft only',
|
||||
':v' => '1',
|
||||
':s' => 'draft',
|
||||
':cj' => '{}',
|
||||
':ck' => '',
|
||||
]);
|
||||
|
||||
qdb_upsert_source_translation($pdo, 'answer_option', $optionId, 'Ja');
|
||||
|
||||
return new FixtureIds(
|
||||
adminUserId: $adminUserId,
|
||||
supervisorUserId: $supervisorUserId,
|
||||
coachUserId: $coachUserId,
|
||||
coachId: $coachId,
|
||||
supervisorId: $supervisorId,
|
||||
questionnaireId: $qnId,
|
||||
questionId: $questionId,
|
||||
questionShortId: 'q1',
|
||||
optionId: $optionId,
|
||||
freeTextQuestionId: $freeTextQuestionId,
|
||||
freeTextShortId: 'q2',
|
||||
glassQuestionId: $glassQuestionId,
|
||||
glassShortId: 'q3',
|
||||
glassSymptomKey: $glassSymptomKey,
|
||||
multiCheckQuestionId: $multiQuestionId,
|
||||
multiCheckShortId: 'q4',
|
||||
draftQuestionnaireId: $draftQuestionnaireId,
|
||||
clientCode: 'CLIENT-TEST-001',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class FixtureIds
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $adminUserId,
|
||||
public readonly string $supervisorUserId,
|
||||
public readonly string $coachUserId,
|
||||
public readonly string $coachId,
|
||||
public readonly string $supervisorId,
|
||||
public readonly string $questionnaireId,
|
||||
public readonly string $questionId,
|
||||
public readonly string $questionShortId,
|
||||
public readonly string $optionId,
|
||||
public readonly string $freeTextQuestionId,
|
||||
public readonly string $freeTextShortId,
|
||||
public readonly string $glassQuestionId,
|
||||
public readonly string $glassShortId,
|
||||
public readonly string $glassSymptomKey,
|
||||
public readonly string $multiCheckQuestionId,
|
||||
public readonly string $multiCheckShortId,
|
||||
public readonly string $draftQuestionnaireId,
|
||||
public readonly string $clientCode,
|
||||
) {
|
||||
}
|
||||
}
|
||||
20
tests/Support/JsonErrorResponse.php
Normal file
20
tests/Support/JsonErrorResponse.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** Thrown instead of exit() when QDB_TESTING is defined. */
|
||||
final class JsonErrorResponse extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $errorCode,
|
||||
public readonly string $errorMessage,
|
||||
public readonly int $httpStatus = 400,
|
||||
public readonly mixed $details = null,
|
||||
) {
|
||||
parent::__construct($errorMessage, $httpStatus);
|
||||
}
|
||||
}
|
||||
18
tests/Support/JsonSuccessResponse.php
Normal file
18
tests/Support/JsonSuccessResponse.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** Thrown instead of exit() when QDB_TESTING is defined. */
|
||||
final class JsonSuccessResponse extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly mixed $data,
|
||||
public readonly int $httpStatus = 200,
|
||||
) {
|
||||
parent::__construct('JSON success', $httpStatus);
|
||||
}
|
||||
}
|
||||
166
tests/Support/QdbTestCase.php
Normal file
166
tests/Support/QdbTestCase.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
|
||||
abstract class QdbTestCase extends PHPUnitTestCase
|
||||
{
|
||||
protected static ApiClient $api;
|
||||
|
||||
protected ?FixtureIds $fixtureIds = null;
|
||||
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
self::$api = new ApiClient(dirname(__DIR__, 2) . '/api/index.php');
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
TestFilesystem::resetUploads();
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$this->bootstrapDatabase();
|
||||
}
|
||||
|
||||
protected function bootstrapDatabase(): void
|
||||
{
|
||||
if (defined('QDB_PATH') && is_file(QDB_PATH)) {
|
||||
@unlink(QDB_PATH);
|
||||
}
|
||||
if (defined('QDB_LOCK') && is_file(QDB_LOCK)) {
|
||||
@unlink(QDB_LOCK);
|
||||
}
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
|
||||
$this->fixtureIds = DatabaseSeeder::seed($pdo);
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
}
|
||||
|
||||
protected function api(): ApiClient
|
||||
{
|
||||
return self::$api;
|
||||
}
|
||||
|
||||
protected function fixture(): FixtureIds
|
||||
{
|
||||
if ($this->fixtureIds === null) {
|
||||
$this->fail('Fixture not seeded');
|
||||
}
|
||||
return $this->fixtureIds;
|
||||
}
|
||||
|
||||
protected function adminToken(): string
|
||||
{
|
||||
return $this->api()->loginWebAndGetToken(
|
||||
DatabaseSeeder::ADMIN_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
)['token'];
|
||||
}
|
||||
|
||||
protected function supervisorToken(): string
|
||||
{
|
||||
return $this->api()->loginWebAndGetToken(
|
||||
DatabaseSeeder::SUPERVISOR_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
)['token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{token: string, data: array<string, mixed>}
|
||||
*/
|
||||
protected function coachMobileLogin(): array
|
||||
{
|
||||
return $this->api()->loginMobileAndGetToken(
|
||||
DatabaseSeeder::COACH_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
);
|
||||
}
|
||||
|
||||
protected function assertApiOk(array $response, string $message = ''): void
|
||||
{
|
||||
$this->assertTrue($response['ok'] ?? false, $message . ' ' . json_encode($response['error'] ?? []));
|
||||
}
|
||||
|
||||
protected function assertApiError(array $response, string $code, ?int $status = null): void
|
||||
{
|
||||
$this->assertFalse($response['ok'] ?? true);
|
||||
$this->assertSame($code, $response['error']['code'] ?? null);
|
||||
if ($status !== null) {
|
||||
$this->assertSame($status, $response['status']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertRawOk(array $response, string $contentTypePrefix): void
|
||||
{
|
||||
$this->assertTrue($response['ok'] ?? false);
|
||||
$this->assertTrue($response['raw'] ?? false);
|
||||
$this->assertStringStartsWith($contentTypePrefix, $response['headers']['Content-Type'] ?? '');
|
||||
$this->assertNotSame('', $response['body'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed> API response from app submit
|
||||
*/
|
||||
protected function submitFixtureQuestionnaire(): array
|
||||
{
|
||||
$login = $this->api()->loginMobileAndGetToken(
|
||||
DatabaseSeeder::COACH_USERNAME,
|
||||
DatabaseSeeder::PASSWORD
|
||||
);
|
||||
$f = $this->fixture();
|
||||
$now = time();
|
||||
|
||||
return $this->submitQuestionnaireAs(
|
||||
$login['token'],
|
||||
$f->questionnaireId,
|
||||
$f->clientCode,
|
||||
[
|
||||
['questionID' => $f->questionShortId, 'answerOptionKey' => 'yes'],
|
||||
],
|
||||
$now - 60,
|
||||
$now
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $answers
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function submitQuestionnaireAs(
|
||||
string $token,
|
||||
string $questionnaireId,
|
||||
string $clientCode,
|
||||
array $answers,
|
||||
?int $startedAt = null,
|
||||
?int $completedAt = null,
|
||||
): array {
|
||||
$now = time();
|
||||
$startedAt ??= $now - 60;
|
||||
$completedAt ??= $now;
|
||||
|
||||
return $this->api()->encryptedPost($token, 'app_questionnaires', [
|
||||
'questionnaireID' => $questionnaireId,
|
||||
'clientCode' => $clientCode,
|
||||
'answers' => $answers,
|
||||
'startedAt' => $startedAt,
|
||||
'completedAt' => $completedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function submissionVersionCount(string $clientCode, string $questionnaireId): int
|
||||
{
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM questionnaire_submission WHERE clientCode = :cc AND questionnaireID = :qn'
|
||||
);
|
||||
$stmt->execute([':cc' => $clientCode, ':qn' => $questionnaireId]);
|
||||
$count = (int)$stmt->fetchColumn();
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
22
tests/Support/RawHttpResponse.php
Normal file
22
tests/Support/RawHttpResponse.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** Thrown instead of exit() for file/download responses when QDB_TESTING is defined. */
|
||||
final class RawHttpResponse extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $headers
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $body,
|
||||
public readonly array $headers,
|
||||
public readonly int $httpStatus = 200,
|
||||
) {
|
||||
parent::__construct('Raw HTTP response', $httpStatus);
|
||||
}
|
||||
}
|
||||
48
tests/Support/TestFilesystem.php
Normal file
48
tests/Support/TestFilesystem.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
final class TestFilesystem
|
||||
{
|
||||
public static function resetUploads(): void
|
||||
{
|
||||
if (!defined('QDB_TEST_UPLOADS')) {
|
||||
return;
|
||||
}
|
||||
if (defined('QDB_PATH')) {
|
||||
if (is_file(QDB_PATH)) {
|
||||
@unlink(QDB_PATH);
|
||||
}
|
||||
if (defined('QDB_LOCK') && is_file(QDB_LOCK)) {
|
||||
@unlink(QDB_LOCK);
|
||||
}
|
||||
}
|
||||
self::rmTree(QDB_TEST_UPLOADS);
|
||||
mkdir(QDB_TEST_UPLOADS, 0755, true);
|
||||
}
|
||||
|
||||
private static function rmTree(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
$items = scandir($dir);
|
||||
if ($items === false) {
|
||||
return;
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $item;
|
||||
if (is_dir($path)) {
|
||||
self::rmTree($path);
|
||||
} else {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user