This commit is contained in:
78
tests/Integration/AppQuestionnairesTest.php
Normal file
78
tests/Integration/AppQuestionnairesTest.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
final class AppQuestionnairesTest extends ApiTestCase
|
||||
{
|
||||
public function testListActiveQuestionnaires(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
$res = $this->api('GET', 'app_questionnaires');
|
||||
$this->assertOk($res);
|
||||
$this->assertIsArray($res['body']['data']);
|
||||
$ids = array_column($res['body']['data'], 'id');
|
||||
$this->assertContains(DatabaseFixture::QUESTIONNAIRE_ID, $ids);
|
||||
$first = $res['body']['data'][0];
|
||||
$this->assertArrayHasKey('name', $first);
|
||||
$this->assertArrayHasKey('showPoints', $first);
|
||||
$this->assertArrayHasKey('condition', $first);
|
||||
}
|
||||
|
||||
public function testFetchQuestionnaireDetail(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
$res = $this->api('GET', 'app_questionnaires', null, null, [
|
||||
'id' => DatabaseFixture::QUESTIONNAIRE_ID,
|
||||
]);
|
||||
$this->assertOk($res);
|
||||
$this->assertSame(DatabaseFixture::QUESTIONNAIRE_ID, $res['body']['data']['meta']['id']);
|
||||
$this->assertNotEmpty($res['body']['data']['questions']);
|
||||
$q = $res['body']['data']['questions'][0];
|
||||
$this->assertSame('q1', $q['id']);
|
||||
$this->assertSame('radio_question', $q['layout']);
|
||||
}
|
||||
|
||||
public function testCoachCannotUploadForOtherCoachesClient(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
$res = $this->api('POST', 'app_questionnaires', [
|
||||
'questionnaireID' => DatabaseFixture::QUESTIONNAIRE_ID,
|
||||
'clientCode' => DatabaseFixture::CLIENT_COACH2,
|
||||
'answers' => [
|
||||
['questionID' => 'q1', 'answerOptionKey' => 'consent_signed'],
|
||||
],
|
||||
]);
|
||||
$this->assertError($res, 'NOT_FOUND');
|
||||
}
|
||||
|
||||
public function testUploadAnswersMapsShortQuestionIds(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
$res = $this->api('POST', 'app_questionnaires', [
|
||||
'questionnaireID' => DatabaseFixture::QUESTIONNAIRE_ID,
|
||||
'clientCode' => DatabaseFixture::CLIENT_COACH1,
|
||||
'completedAt' => time(),
|
||||
'answers' => [
|
||||
['questionID' => 'q1', 'answerOptionKey' => 'consent_signed', 'answeredAt' => time()],
|
||||
],
|
||||
]);
|
||||
$this->assertOk($res);
|
||||
$this->assertTrue($res['body']['data']['submitted']);
|
||||
$this->assertSame(5, $res['body']['data']['sumPoints']);
|
||||
}
|
||||
|
||||
public function testCoachClientsList(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
$res = $this->api('GET', 'app_questionnaires', null, null, ['clients' => '1']);
|
||||
$this->assertOk($res);
|
||||
$codes = array_column($res['body']['data']['clients'], 'clientCode');
|
||||
$this->assertContains(DatabaseFixture::CLIENT_COACH1, $codes);
|
||||
$this->assertNotContains(DatabaseFixture::CLIENT_COACH2, $codes);
|
||||
}
|
||||
}
|
||||
39
tests/Integration/AuthTest.php
Normal file
39
tests/Integration/AuthTest.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
final class AuthTest extends ApiTestCase
|
||||
{
|
||||
public function testLoginSuccess(): void
|
||||
{
|
||||
$res = $this->loginAs(DatabaseFixture::ADMIN_USERNAME);
|
||||
$this->assertOk($res);
|
||||
$this->assertSame('admin', $res['body']['data']['role']);
|
||||
$this->assertNotEmpty($this->bearerToken);
|
||||
}
|
||||
|
||||
public function testLoginInvalidPassword(): void
|
||||
{
|
||||
$res = $this->api('POST', 'auth/login', [
|
||||
'username' => DatabaseFixture::ADMIN_USERNAME,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
$this->assertError($res, 'INVALID_CREDENTIALS');
|
||||
$this->assertSame(401, $res['status']);
|
||||
}
|
||||
|
||||
public function testMustChangePasswordTempTokenBlocked(): void
|
||||
{
|
||||
$login = $this->loginAs(DatabaseFixture::MUST_CHANGE_USERNAME);
|
||||
$this->assertOk($login);
|
||||
$this->assertTrue($login['body']['data']['mustChangePassword'] ?? false);
|
||||
|
||||
$blocked = $this->api('GET', 'app_questionnaires');
|
||||
$this->assertLegacyAuthError($blocked, 403, 'Password change required before access');
|
||||
}
|
||||
}
|
||||
49
tests/Integration/ClientsTest.php
Normal file
49
tests/Integration/ClientsTest.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
|
||||
|
||||
final class ClientsTest extends ApiTestCase
|
||||
|
||||
{
|
||||
|
||||
public function testRequiresAuth(): void
|
||||
|
||||
{
|
||||
|
||||
$res = $this->api('GET', 'clients');
|
||||
|
||||
$this->assertLegacyAuthError($res, 401, 'Missing Bearer token');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testAdminSeesAllClients(): void
|
||||
|
||||
{
|
||||
|
||||
$this->loginAs(DatabaseFixture::ADMIN_USERNAME);
|
||||
|
||||
$res = $this->api('GET', 'clients');
|
||||
|
||||
$this->assertOk($res);
|
||||
|
||||
$codes = array_column($res['body']['data']['clients'], 'clientCode');
|
||||
|
||||
$this->assertContains(DatabaseFixture::CLIENT_COACH1, $codes);
|
||||
|
||||
$this->assertContains(DatabaseFixture::CLIENT_COACH2, $codes);
|
||||
25
tests/Integration/LogoutTest.php
Normal file
25
tests/Integration/LogoutTest.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
final class LogoutTest extends ApiTestCase
|
||||
{
|
||||
public function testLogoutRevokesToken(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::ADMIN_USERNAME);
|
||||
$token = $this->bearerToken;
|
||||
$this->assertNotNull($token);
|
||||
|
||||
$logout = $this->api('POST', 'logout');
|
||||
$this->assertOk($logout);
|
||||
|
||||
$this->bearerToken = $token;
|
||||
$after = $this->api('GET', 'questionnaires');
|
||||
$this->assertLegacyAuthError($after, 403, 'Invalid or expired token');
|
||||
}
|
||||
}
|
||||
25
tests/Integration/QuestionnairesTest.php
Normal file
25
tests/Integration/QuestionnairesTest.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
final class QuestionnairesTest extends ApiTestCase
|
||||
{
|
||||
public function testRequiresAuth(): void
|
||||
{
|
||||
$res = $this->api('GET', 'questionnaires');
|
||||
$this->assertLegacyAuthError($res, 401, 'Missing Bearer token');
|
||||
}
|
||||
|
||||
public function testAdminCanListQuestionnaires(): void
|
||||
{
|
||||
$this->loginAs(DatabaseFixture::ADMIN_USERNAME);
|
||||
$res = $this->api('GET', 'questionnaires');
|
||||
$this->assertOk($res);
|
||||
$this->assertNotEmpty($res['body']['data']['questionnaires'] ?? []);
|
||||
}
|
||||
}
|
||||
48
tests/Integration/UsersTest.php
Normal file
48
tests/Integration/UsersTest.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
|
||||
namespace Tests\Integration;
|
||||
|
||||
|
||||
|
||||
use Tests\Support\ApiTestCase;
|
||||
|
||||
use Tests\Support\DatabaseFixture;
|
||||
|
||||
|
||||
|
||||
final class UsersTest extends ApiTestCase
|
||||
|
||||
{
|
||||
|
||||
public function testRequiresAuth(): void
|
||||
|
||||
{
|
||||
|
||||
$res = $this->api('GET', 'users');
|
||||
|
||||
$this->assertLegacyAuthError($res, 401, 'Missing Bearer token');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testCoachForbidden(): void
|
||||
|
||||
{
|
||||
|
||||
$this->loginAs(DatabaseFixture::COACH1_USERNAME);
|
||||
|
||||
$res = $this->api('GET', 'users');
|
||||
|
||||
$this->assertLegacyAuthError($res, 403, 'Insufficient permissions');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
126
tests/Support/ApiTestCase.php
Normal file
126
tests/Support/ApiTestCase.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use QdbHttpResponse;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* In-process API tests via qdb_dispatch_api_request().
|
||||
|
||||
*
|
||||
|
||||
* Error envelopes:
|
||||
|
||||
* - Handlers: { ok: false, error: { code, message } } — use assertError()
|
||||
|
||||
* - Auth middleware (require_valid_token, require_role): { error: "message" } — use assertLegacyAuthError()
|
||||
|
||||
*/
|
||||
|
||||
abstract class ApiTestCase extends TestCase
|
||||
|
||||
{
|
||||
|
||||
protected ?string $bearerToken = null;
|
||||
|
||||
|
||||
|
||||
protected function setUp(): void
|
||||
|
||||
{
|
||||
|
||||
parent::setUp();
|
||||
|
||||
DatabaseFixture::forTests()->reset();
|
||||
|
||||
$this->bearerToken = null;
|
||||
|
||||
unset($GLOBALS['__TEST_JSON_BODY']);
|
||||
|
||||
$_GET = [];
|
||||
|
||||
$_POST = [];
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
* @return array{status: int, body: array<string, mixed>, raw: string}
|
||||
|
||||
*/
|
||||
|
||||
protected function api(
|
||||
|
||||
string $method,
|
||||
|
||||
string $route,
|
||||
|
||||
?array $jsonBody = null,
|
||||
|
||||
?string $token = null,
|
||||
|
||||
array $query = [],
|
||||
|
||||
): array {
|
||||
|
||||
$_GET = $query;
|
||||
|
||||
$_POST = [];
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = strtoupper($method);
|
||||
|
||||
$_SERVER['REQUEST_URI'] = '/api/' . ltrim($route, '/');
|
||||
|
||||
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
|
||||
|
||||
$_SERVER['HTTP_AUTHORIZATION'] = ($token ?? $this->bearerToken)
|
||||
|
||||
? 'Bearer ' . ($token ?? $this->bearerToken)
|
||||
|
||||
: '';
|
||||
|
||||
|
||||
|
||||
if ($jsonBody !== null) {
|
||||
|
||||
$GLOBALS['__TEST_JSON_BODY'] = json_encode($jsonBody, JSON_THROW_ON_ERROR);
|
||||
|
||||
} else {
|
||||
|
||||
unset($GLOBALS['__TEST_JSON_BODY']);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
ob_start();
|
||||
|
||||
$status = 200;
|
||||
|
||||
$body = [];
|
||||
|
||||
|
||||
|
||||
try {
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/api/dispatch.php';
|
||||
|
||||
qdb_dispatch_api_request();
|
||||
|
||||
} catch (QdbHttpResponse $e) {
|
||||
|
||||
186
tests/Support/DatabaseFixture.php
Normal file
186
tests/Support/DatabaseFixture.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class DatabaseFixture
|
||||
{
|
||||
public const PASSWORD = 'testpass123';
|
||||
|
||||
public const ADMIN_USERNAME = 'testadmin';
|
||||
public const SUPERVISOR1_USERNAME = 'testsuper1';
|
||||
public const SUPERVISOR2_USERNAME = 'testsuper2';
|
||||
public const COACH1_USERNAME = 'testcoach1';
|
||||
public const COACH2_USERNAME = 'testcoach2';
|
||||
public const MUST_CHANGE_USERNAME = 'testmustchange';
|
||||
|
||||
public const ADMIN_ID = 'admin001';
|
||||
public const SUPERVISOR1_ID = 'sup001';
|
||||
public const SUPERVISOR2_ID = 'sup002';
|
||||
public const COACH1_ID = 'coach001';
|
||||
public const COACH2_ID = 'coach002';
|
||||
|
||||
public const CLIENT_COACH1 = 'CLIENT-001';
|
||||
public const CLIENT_COACH1B = 'CLIENT-002';
|
||||
public const CLIENT_COACH2 = 'CLIENT-OTHER';
|
||||
|
||||
public const QUESTIONNAIRE_ID = 'questionnaire_test_demo';
|
||||
|
||||
public function __construct(
|
||||
private readonly string $uploadsDir,
|
||||
private readonly string $schemaPath,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function forTests(): self
|
||||
{
|
||||
return new self(
|
||||
__DIR__ . '/../var/uploads',
|
||||
__DIR__ . '/../fixtures/schema.sql',
|
||||
);
|
||||
}
|
||||
|
||||
public static function forE2E(string $projectRoot): self
|
||||
{
|
||||
return new self(
|
||||
$projectRoot . '/uploads',
|
||||
__DIR__ . '/../fixtures/schema.sql',
|
||||
);
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
if (!is_dir($this->uploadsDir)) {
|
||||
mkdir($this->uploadsDir, 0755, true);
|
||||
}
|
||||
|
||||
$dbFile = $this->uploadsDir . '/questionnaire_database';
|
||||
$lockFile = $this->uploadsDir . '/.qdb_lock';
|
||||
if (is_file($dbFile)) {
|
||||
unlink($dbFile);
|
||||
}
|
||||
if (is_file($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
|
||||
if (!defined('QDB_PATH')) {
|
||||
define('QDB_PATH', $dbFile);
|
||||
}
|
||||
if (!defined('QDB_LOCK')) {
|
||||
define('QDB_LOCK', $lockFile);
|
||||
}
|
||||
if (!defined('QDB_SCHEMA')) {
|
||||
define('QDB_SCHEMA', $this->schemaPath);
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/common.php';
|
||||
require_once dirname(__DIR__, 2) . '/db_init.php';
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(true);
|
||||
$this->seed($pdo);
|
||||
qdb_save($tmpDb, $lockFp);
|
||||
}
|
||||
|
||||
private function seed(PDO $pdo): void
|
||||
{
|
||||
$now = time();
|
||||
$hash = password_hash(self::PASSWORD, PASSWORD_DEFAULT);
|
||||
$mustChangeHash = password_hash(self::PASSWORD, PASSWORD_DEFAULT);
|
||||
|
||||
$pdo->exec('DELETE FROM session');
|
||||
$pdo->exec('DELETE FROM client_answer');
|
||||
$pdo->exec('DELETE FROM completed_questionnaire');
|
||||
$pdo->exec('DELETE FROM answer_option_translation');
|
||||
$pdo->exec('DELETE FROM question_translation');
|
||||
$pdo->exec('DELETE FROM string_translation');
|
||||
$pdo->exec('DELETE FROM answer_option');
|
||||
$pdo->exec('DELETE FROM question');
|
||||
$pdo->exec('DELETE FROM questionnaire');
|
||||
$pdo->exec('DELETE FROM client');
|
||||
$pdo->exec('DELETE FROM coach');
|
||||
$pdo->exec('DELETE FROM supervisor');
|
||||
$pdo->exec('DELETE FROM admin');
|
||||
$pdo->exec('DELETE FROM users');
|
||||
$pdo->exec('DELETE FROM language');
|
||||
|
||||
$insUser = $pdo->prepare(
|
||||
'INSERT INTO users (userID, username, passwordHash, role, entityID, mustChangePassword, createdAt)
|
||||
VALUES (:uid, :u, :h, :role, :eid, :mcp, :ca)'
|
||||
);
|
||||
|
||||
$insUser->execute([
|
||||
':uid' => 'user_admin', ':u' => self::ADMIN_USERNAME, ':h' => $hash,
|
||||
':role' => 'admin', ':eid' => self::ADMIN_ID, ':mcp' => 0, ':ca' => $now,
|
||||
]);
|
||||
$insUser->execute([
|
||||
':uid' => 'user_sup1', ':u' => self::SUPERVISOR1_USERNAME, ':h' => $hash,
|
||||
':role' => 'supervisor', ':eid' => self::SUPERVISOR1_ID, ':mcp' => 0, ':ca' => $now,
|
||||
]);
|
||||
$insUser->execute([
|
||||
':uid' => 'user_sup2', ':u' => self::SUPERVISOR2_USERNAME, ':h' => $hash,
|
||||
':role' => 'supervisor', ':eid' => self::SUPERVISOR2_ID, ':mcp' => 0, ':ca' => $now,
|
||||
]);
|
||||
$insUser->execute([
|
||||
':uid' => 'user_coach1', ':u' => self::COACH1_USERNAME, ':h' => $hash,
|
||||
':role' => 'coach', ':eid' => self::COACH1_ID, ':mcp' => 0, ':ca' => $now,
|
||||
]);
|
||||
$insUser->execute([
|
||||
':uid' => 'user_coach2', ':u' => self::COACH2_USERNAME, ':h' => $hash,
|
||||
':role' => 'coach', ':eid' => self::COACH2_ID, ':mcp' => 0, ':ca' => $now,
|
||||
]);
|
||||
$insUser->execute([
|
||||
':uid' => 'user_mustchange', ':u' => self::MUST_CHANGE_USERNAME, ':h' => $mustChangeHash,
|
||||
':role' => 'coach', ':eid' => self::COACH1_ID, ':mcp' => 1, ':ca' => $now,
|
||||
]);
|
||||
|
||||
$pdo->prepare('INSERT INTO admin (adminID, username, location) VALUES (?, ?, ?)')
|
||||
->execute([self::ADMIN_ID, self::ADMIN_USERNAME, 'Test']);
|
||||
$pdo->prepare('INSERT INTO supervisor (supervisorID, username, location) VALUES (?, ?, ?)')
|
||||
->execute([self::SUPERVISOR1_ID, self::SUPERVISOR1_USERNAME, 'Loc1']);
|
||||
$pdo->prepare('INSERT INTO supervisor (supervisorID, username, location) VALUES (?, ?, ?)')
|
||||
->execute([self::SUPERVISOR2_ID, self::SUPERVISOR2_USERNAME, 'Loc2']);
|
||||
$pdo->prepare('INSERT INTO coach (coachID, supervisorID, username) VALUES (?, ?, ?)')
|
||||
->execute([self::COACH1_ID, self::SUPERVISOR1_ID, self::COACH1_USERNAME]);
|
||||
$pdo->prepare('INSERT INTO coach (coachID, supervisorID, username) VALUES (?, ?, ?)')
|
||||
->execute([self::COACH2_ID, self::SUPERVISOR2_ID, self::COACH2_USERNAME]);
|
||||
|
||||
$pdo->prepare('INSERT INTO client (clientCode, coachID) VALUES (?, ?)')
|
||||
->execute([self::CLIENT_COACH1, self::COACH1_ID]);
|
||||
$pdo->prepare('INSERT INTO client (clientCode, coachID) VALUES (?, ?)')
|
||||
->execute([self::CLIENT_COACH1B, self::COACH1_ID]);
|
||||
$pdo->prepare('INSERT INTO client (clientCode, coachID) VALUES (?, ?)')
|
||||
->execute([self::CLIENT_COACH2, self::COACH2_ID]);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO questionnaire (questionnaireID, name, version, state, orderIndex, showPoints, conditionJson)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([
|
||||
self::QUESTIONNAIRE_ID,
|
||||
'Test Demo',
|
||||
'1.0',
|
||||
'active',
|
||||
0,
|
||||
1,
|
||||
'{}',
|
||||
]);
|
||||
|
||||
$qid = self::QUESTIONNAIRE_ID . '__q1';
|
||||
$pdo->prepare(
|
||||
'INSERT INTO question (questionID, questionnaireID, defaultText, type, orderIndex, isRequired, configJson)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$qid, self::QUESTIONNAIRE_ID, 'consent_instruction', 'radio_question', 0, 1, '{}']);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
)->execute(['ao1', $qid, 'consent_signed', 5, 0, '']);
|
||||
|
||||
$pdo->prepare(
|
||||
'INSERT INTO answer_option (answerOptionID, questionID, defaultText, points, orderIndex, nextQuestionId)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
)->execute(['ao2', $qid, 'consent_not_signed', 0, 1, '']);
|
||||
}
|
||||
}
|
||||
51
tests/Unit/CommonTest.php
Normal file
51
tests/Unit/CommonTest.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
final class CommonTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
require_once dirname(__DIR__, 2) . '/common.php';
|
||||
}
|
||||
|
||||
public function testAesRoundTrip(): void
|
||||
{
|
||||
$key = str_repeat('k', 32);
|
||||
$plain = 'encrypted questionnaire payload';
|
||||
$enc = aes256_cbc_encrypt_bytes($plain, $key);
|
||||
$dec = aes256_cbc_decrypt_bytes($enc, $key);
|
||||
$this->assertSame($plain, $dec);
|
||||
$this->assertGreaterThan(16, strlen($enc));
|
||||
}
|
||||
|
||||
public function testHkdfDeterministic(): void
|
||||
{
|
||||
$token = 'a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef';
|
||||
$k1 = hkdf_session_key_from_token($token);
|
||||
$k2 = hkdf_session_key_from_token($token);
|
||||
$this->assertSame($k1, $k2);
|
||||
$this->assertSame(32, strlen($k1));
|
||||
}
|
||||
|
||||
public function testRbacClientFilter(): void
|
||||
{
|
||||
[$adminClause, $adminParams] = rbac_client_filter(['role' => 'admin', 'entityID' => 'x'], 'cl');
|
||||
$this->assertSame('1=1', $adminClause);
|
||||
$this->assertSame([], $adminParams);
|
||||
|
||||
[$supClause, $supParams] = rbac_client_filter(['role' => 'supervisor', 'entityID' => 'sup001'], 'cl');
|
||||
$this->assertStringContainsString('supervisorID', $supClause);
|
||||
$this->assertSame([':rbac_eid' => 'sup001'], $supParams);
|
||||
|
||||
[$coachClause, $coachParams] = rbac_client_filter(['role' => 'coach', 'entityID' => 'coach001'], 'cl');
|
||||
$this->assertStringContainsString('coachID', $coachClause);
|
||||
$this->assertSame([':rbac_eid' => 'coach001'], $coachParams);
|
||||
|
||||
[$denyClause, $denyParams] = rbac_client_filter(['role' => 'guest', 'entityID' => ''], 'cl');
|
||||
$this->assertSame('0=1', $denyClause);
|
||||
}
|
||||
}
|
||||
41
tests/Unit/ResponseTest.php
Normal file
41
tests/Unit/ResponseTest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use QdbHttpResponse;
|
||||
|
||||
final class ResponseTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
require_once dirname(__DIR__, 2) . '/lib/response.php';
|
||||
}
|
||||
|
||||
public function testJsonSuccessEnvelope(): void
|
||||
{
|
||||
try {
|
||||
json_success(['token' => 'abc'], 201);
|
||||
$this->fail('Expected QdbHttpResponse');
|
||||
} catch (QdbHttpResponse $e) {
|
||||
$this->assertSame(201, $e->status);
|
||||
$this->assertTrue($e->body['ok']);
|
||||
$this->assertSame(['token' => 'abc'], $e->body['data']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testJsonErrorEnvelope(): void
|
||||
{
|
||||
try {
|
||||
json_error('INVALID_CREDENTIALS', 'Bad login', 401);
|
||||
$this->fail('Expected QdbHttpResponse');
|
||||
} catch (QdbHttpResponse $e) {
|
||||
$this->assertSame(401, $e->status);
|
||||
$this->assertFalse($e->body['ok']);
|
||||
$this->assertSame('INVALID_CREDENTIALS', $e->body['error']['code']);
|
||||
$this->assertSame('Bad login', $e->body['error']['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
tests/bin/seed-test-db.php
Normal file
23
tests/bin/seed-test-db.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Seed the project uploads/ database for local dev and Playwright E2E runs.
|
||||
*
|
||||
* Usage: php tests/bin/seed-test-db.php
|
||||
*/
|
||||
|
||||
$root = dirname(__DIR__, 2);
|
||||
|
||||
require_once $root . '/vendor/autoload.php';
|
||||
|
||||
putenv('QDB_MASTER_KEY=dGVzdC1tYXN0ZXIta2V5LXRoaXQtMzJieXRzIQ==');
|
||||
|
||||
if (!defined('QDB_TESTING')) {
|
||||
define('QDB_TESTING', false);
|
||||
}
|
||||
|
||||
\Tests\Support\DatabaseFixture::forE2E($root)->reset();
|
||||
|
||||
echo "Test database seeded in uploads/\n";
|
||||
38
tests/bootstrap.php
Normal file
38
tests/bootstrap.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
define('QDB_TESTING', true);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$varUploads = __DIR__ . '/var/uploads';
|
||||
|
||||
if (!is_dir($varUploads)) {
|
||||
mkdir($varUploads, 0755, true);
|
||||
}
|
||||
|
||||
define('QDB_PATH', $varUploads . '/questionnaire_database');
|
||||
define('QDB_LOCK', $varUploads . '/.qdb_lock');
|
||||
define('QDB_SCHEMA', __DIR__ . '/fixtures/schema.sql');
|
||||
|
||||
putenv('QDB_MASTER_KEY=dGVzdC1tYXN0ZXIta2V5LXRoaXQtMzJieXRzIQ==');
|
||||
|
||||
if (!extension_loaded('pdo_sqlite')) {
|
||||
$ini = php_ini_loaded_file() ?: '(unknown)';
|
||||
fwrite(STDERR, <<<MSG
|
||||
|
||||
PHPUnit requires the pdo_sqlite PHP extension (encrypted SQLite database).
|
||||
|
||||
Enable it in your php.ini (loaded from: {$ini}):
|
||||
extension=pdo_sqlite
|
||||
extension=sqlite3
|
||||
|
||||
Then verify: php -r "print_r(PDO::getAvailableDrivers());"
|
||||
|
||||
See TESTING.md for details.
|
||||
|
||||
MSG);
|
||||
exit(1);
|
||||
}
|
||||
18
tests/phpunit.xml
Normal file
18
tests/phpunit.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
|
||||
bootstrap="bootstrap.php"
|
||||
colors="true"
|
||||
cacheDirectory=".phpunit.cache">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Integration">
|
||||
<directory>Integration</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="QDB_MASTER_KEY" value="dGVzdC1tYXN0ZXIta2V5LXRoaXQtMzJieXRzIQ==" force="true"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
Reference in New Issue
Block a user