clqms-be/tests/feature/TestsControllerTest.php
mahdahar d173098652 feat: implement audit logging and test management enhancements
Major Features:
- Add comprehensive audit logging system with AuditService
- Create AuditLogs database migration for tracking changes
- Implement TestValidationService for test data validation
- Add FRONTEND_TEST_MANAGEMENT_PROMPT.md documentation

Controllers:
- Update TestsController with improved test management

Models:
- Enhance PatientModel with additional functionality
- Update TestDefSiteModel for better site management

Database:
- Add CreateAuditLogs migration (2026-02-20-000011)
- Update TestSeeder with new test data

Services:
- Add AuditService for comprehensive audit trail logging

Documentation:
- Update AGENTS.md with improved guidelines
- Update audit-logging-plan.md with implementation details
- Add FRONTEND_TEST_MANAGEMENT_PROMPT.md for frontend guidance

API Documentation:
- Update api-docs.bundled.yaml
- Update tests.yaml schema definitions
- Update tests.yaml paths

Testing:
- Enhance TestsControllerTest with new test cases
- Update TestDefModelsTest for model coverage
2026-02-20 13:47:47 +07:00

418 lines
14 KiB
PHP

<?php
namespace Tests\Feature;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase;
use Firebase\JWT\JWT;
class TestsControllerTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected $token;
protected function setUp(): void
{
parent::setUp();
// Generate JWT Token
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
$payload = [
'iss' => 'localhost',
'aud' => 'localhost',
'iat' => time(),
'nbf' => time(),
'exp' => time() + 3600,
'uid' => 1,
'email' => 'admin@admin.com'
];
$this->token = JWT::encode($payload, $key, 'HS256');
}
protected function callProtected($method, $path, $params = [])
{
return $this->withHeaders(['Cookie' => 'token=' . $this->token])
->call($method, $path, $params);
}
public function testIndexReturnsSuccess()
{
$result = $this->callProtected('get', 'api/tests');
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
}
public function testShowReturnsDataIfFound()
{
// First get an ID
$indexResult = $this->callProtected('get', 'api/tests');
$indexData = json_decode($indexResult->getJSON(), true);
if (empty($indexData['data'])) {
$this->markTestSkipped('No test definitions found in database to test show.');
}
$id = $indexData['data'][0]['TestSiteID'];
$result = $this->callProtected('get', "api/tests/$id");
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('success', $data['status']);
$this->assertIsArray($data['data']);
$this->assertEquals($id, $data['data']['TestSiteID']);
}
public function testCreateTestWithThreshold()
{
$testData = [
'TestSiteCode' => 'TH' . substr(time(), -4),
'TestSiteName' => 'Threshold Test ' . time(),
'TestType' => 'TEST',
'SiteID' => 1,
'details' => [
'RefType' => 'THOLD',
'ResultType' => 'NMRIC'
],
'refnum' => [
[
'NumRefType' => 'THOLD',
'RangeType' => 'VALUE',
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 100,
'LowSign' => '>',
'Low' => 5.5,
'Interpretation' => 'High'
]
]
];
$result = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($testData))
->call('post', 'api/tests');
$result->assertStatus(201);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertEquals('created', $data['status']);
$id = $data['data']['TestSiteId'];
// Verify retrieval
$showResult = $this->callProtected('get', "api/tests/$id");
$showData = json_decode($showResult->getJSON(), true);
$this->assertArrayHasKey('refnum', $showData['data']);
$this->assertCount(1, $showData['data']['refnum']);
$this->assertEquals(5.5, $showData['data']['refnum'][0]['Low']);
$this->assertEquals('High', $showData['data']['refnum'][0]['Interpretation']);
}
/**
* Test valid TestType and ResultType combinations
* @dataProvider validTestTypeResultTypeProvider
*/
public function testValidTestTypeResultTypeCombinations($testType, $resultType, $refType, $shouldSucceed)
{
$testData = [
'TestSiteCode' => 'TT' . substr(time(), -4) . rand(10, 99),
'TestSiteName' => 'Type Test ' . time(),
'TestType' => $testType,
'SiteID' => 1,
'details' => [
'ResultType' => $resultType,
'RefType' => $refType
]
];
// Add reference data if needed
if ($refType === 'RANGE' || $refType === 'THOLD') {
$testData['refnum'] = [
[
'NumRefType' => $refType,
'RangeType' => 'VALUE',
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 100,
'LowSign' => '>',
'Low' => 5.5,
'Interpretation' => 'Normal'
]
];
} elseif ($refType === 'VSET' || $refType === 'TEXT') {
$testData['reftxt'] = [
[
'TxtRefType' => $refType,
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 100,
'RefTxt' => 'Normal range text',
'Flag' => 'N'
]
];
}
$result = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($testData))
->call('post', 'api/tests');
if ($shouldSucceed) {
$result->assertStatus(201);
$data = json_decode($result->getJSON(), true);
$this->assertEquals('created', $data['status']);
} else {
// Invalid combinations should fail validation or return error
$this->assertGreaterThanOrEqual(400, $result->getStatusCode());
}
}
public function validTestTypeResultTypeProvider()
{
return [
// TEST type - can have NMRIC, RANGE, TEXT, VSET
'TEST with NMRIC' => ['TEST', 'NMRIC', 'RANGE', true],
'TEST with RANGE' => ['TEST', 'RANGE', 'RANGE', true],
'TEST with TEXT' => ['TEST', 'TEXT', 'TEXT', true],
'TEST with VSET' => ['TEST', 'VSET', 'VSET', true],
'TEST with THOLD' => ['TEST', 'NMRIC', 'THOLD', true],
// PARAM type - can have NMRIC, RANGE, TEXT, VSET
'PARAM with NMRIC' => ['PARAM', 'NMRIC', 'RANGE', true],
'PARAM with RANGE' => ['PARAM', 'RANGE', 'RANGE', true],
'PARAM with TEXT' => ['PARAM', 'TEXT', 'TEXT', true],
'PARAM with VSET' => ['PARAM', 'VSET', 'VSET', true],
// CALC type - only NMRIC
'CALC with NMRIC' => ['CALC', 'NMRIC', 'RANGE', true],
// GROUP type - only NORES
'GROUP with NORES' => ['GROUP', 'NORES', 'NOREF', true],
// TITLE type - only NORES
'TITLE with NORES' => ['TITLE', 'NORES', 'NOREF', true],
];
}
/**
* Test ResultType to RefType mapping
* @dataProvider resultTypeToRefTypeProvider
*/
public function testResultTypeToRefTypeMapping($resultType, $refType, $expectedRefTable)
{
$testData = [
'TestSiteCode' => 'RT' . substr(time(), -4) . rand(10, 99),
'TestSiteName' => 'RefType Test ' . time(),
'TestType' => 'TEST',
'SiteID' => 1,
'details' => [
'ResultType' => $resultType,
'RefType' => $refType
]
];
// Add appropriate reference data
if ($expectedRefTable === 'refnum') {
$testData['refnum'] = [
[
'NumRefType' => $refType,
'RangeType' => 'VALUE',
'Sex' => '1',
'AgeStart' => 18,
'AgeEnd' => 99,
'LowSign' => 'GE',
'Low' => 10,
'HighSign' => 'LE',
'High' => 20,
'Interpretation' => 'Normal'
]
];
} elseif ($expectedRefTable === 'reftxt') {
$testData['reftxt'] = [
[
'TxtRefType' => $refType,
'Sex' => '1',
'AgeStart' => 18,
'AgeEnd' => 99,
'RefTxt' => 'Reference text',
'Flag' => 'N'
]
];
}
$result = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($testData))
->call('post', 'api/tests');
$result->assertStatus(201);
$data = json_decode($result->getJSON(), true);
$id = $data['data']['TestSiteId'];
// Verify the reference data was stored in correct table
$showResult = $this->callProtected('get', "api/tests/$id");
$showData = json_decode($showResult->getJSON(), true);
if ($expectedRefTable === 'refnum') {
$this->assertArrayHasKey('refnum', $showData['data']);
$this->assertNotEmpty($showData['data']['refnum']);
} elseif ($expectedRefTable === 'reftxt') {
$this->assertArrayHasKey('reftxt', $showData['data']);
$this->assertNotEmpty($showData['data']['reftxt']);
}
}
public function resultTypeToRefTypeProvider()
{
return [
// NMRIC with RANGE → refnum table
'NMRIC with RANGE uses refnum' => ['NMRIC', 'RANGE', 'refnum'],
// NMRIC with THOLD → refnum table
'NMRIC with THOLD uses refnum' => ['NMRIC', 'THOLD', 'refnum'],
// RANGE with RANGE → refnum table
'RANGE with RANGE uses refnum' => ['RANGE', 'RANGE', 'refnum'],
// RANGE with THOLD → refnum table
'RANGE with THOLD uses refnum' => ['RANGE', 'THOLD', 'refnum'],
// VSET with VSET → reftxt table
'VSET with VSET uses reftxt' => ['VSET', 'VSET', 'reftxt'],
// TEXT with TEXT → reftxt table
'TEXT with TEXT uses reftxt' => ['TEXT', 'TEXT', 'reftxt'],
];
}
/**
* Test CALC type always has NMRIC result type
*/
public function testCalcTypeAlwaysHasNmricResultType()
{
$testData = [
'TestSiteCode' => 'CALC' . substr(time(), -4),
'TestSiteName' => 'Calc Test ' . time(),
'TestType' => 'CALC',
'SiteID' => 1,
'details' => [
'DisciplineID' => 1,
'DepartmentID' => 1,
'FormulaInput' => 'WEIGHT,HEIGHT',
'FormulaCode' => 'WEIGHT/(HEIGHT/100)^2',
'Unit1' => 'kg/m2',
'Decimal' => 1
]
];
$result = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($testData))
->call('post', 'api/tests');
$result->assertStatus(201);
$data = json_decode($result->getJSON(), true);
$id = $data['data']['TestSiteId'];
// Verify CALC test was created
$showResult = $this->callProtected('get', "api/tests/$id");
$showData = json_decode($showResult->getJSON(), true);
$this->assertEquals('CALC', $showData['data']['TestType']);
$this->assertArrayHasKey('testdefcal', $showData['data']);
}
/**
* Test GROUP type has no result (NORES)
*/
public function testGroupTypeHasNoResult()
{
// First create member tests
$member1Data = [
'TestSiteCode' => 'M1' . substr(time(), -4),
'TestSiteName' => 'Member 1 ' . time(),
'TestType' => 'TEST',
'SiteID' => 1,
'details' => [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE'
],
'refnum' => [
[
'NumRefType' => 'RANGE',
'RangeType' => 'VALUE',
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 100,
'LowSign' => '>',
'Low' => 5.5,
'Interpretation' => 'Normal'
]
]
];
$result1 = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($member1Data))
->call('post', 'api/tests');
$data1 = json_decode($result1->getJSON(), true);
$member1Id = $data1['data']['TestSiteId'];
$member2Data = [
'TestSiteCode' => 'M2' . substr(time(), -4),
'TestSiteName' => 'Member 2 ' . time(),
'TestType' => 'TEST',
'SiteID' => 1,
'details' => [
'ResultType' => 'NMRIC',
'RefType' => 'RANGE'
],
'refnum' => [
[
'NumRefType' => 'RANGE',
'RangeType' => 'VALUE',
'Sex' => '1',
'AgeStart' => 0,
'AgeEnd' => 100,
'LowSign' => '>',
'Low' => 5.5,
'Interpretation' => 'Normal'
]
]
];
$result2 = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($member2Data))
->call('post', 'api/tests');
$data2 = json_decode($result2->getJSON(), true);
$member2Id = $data2['data']['TestSiteId'];
// Create group test
$groupData = [
'TestSiteCode' => 'GRP' . substr(time(), -4),
'TestSiteName' => 'Group Test ' . time(),
'TestType' => 'GROUP',
'SiteID' => 1,
'details' => [
'ResultType' => 'NORES',
'RefType' => 'NOREF'
],
'members' => [$member1Id, $member2Id]
];
$result = $this->withHeaders(['Cookie' => 'token=' . $this->token])
->withBody(json_encode($groupData))
->call('post', 'api/tests');
$result->assertStatus(201);
$data = json_decode($result->getJSON(), true);
$id = $data['data']['TestSiteId'];
// Verify GROUP test was created with members
$showResult = $this->callProtected('get', "api/tests/$id");
$showData = json_decode($showResult->getJSON(), true);
$this->assertEquals('GROUP', $showData['data']['TestType']);
$this->assertArrayHasKey('testdefgrp', $showData['data']);
}
}