fix: expand patient check matching and stabilize tests\n\n- allow hyphens and dots in patient identifiers\n- support email and phone lookups in patient existence checks\n- update OpenAPI docs and feature tests for the new request contract\n- load .env during PHPUnit bootstrap so the test database config is available

This commit is contained in:
mahdahar 2026-04-13 11:25:41 +07:00
parent c743049ed1
commit c49743bbf3
6 changed files with 233 additions and 276 deletions

View File

@ -17,8 +17,8 @@ class PatientController extends Controller {
$this->db = \Config\Database::connect(); $this->db = \Config\Database::connect();
$this->model = new PatientModel(); $this->model = new PatientModel();
$this->rules = [ $this->rules = [
'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'PatientID' => 'required|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]', 'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
'Sex' => 'required', 'Sex' => 'required',
@ -157,8 +157,8 @@ class PatientController extends Controller {
$rules = []; $rules = [];
$fieldRules = [ $fieldRules = [
'PatientID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'PatientID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', 'AlternatePID' => 'permit_empty|regex_match[/^[A-Za-z0-9.-]+$/]|max_length[30]',
'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]', 'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]',
'Sex' => 'permit_empty', 'Sex' => 'permit_empty',
'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]', 'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]',
@ -252,46 +252,66 @@ class PatientController extends Controller {
} }
} }
public function patientCheck() { public function patientCheck() {
try { try {
$PatientID = $this->request->getVar('PatientID'); $PatientID = $this->request->getVar('PatientID');
$EmailAddress1 = $this->request->getVar('EmailAddress1'); $EmailAddress = $this->request->getVar('EmailAddress');
$EmailAddress1 = $this->request->getVar('EmailAddress1');
$tableName = ''; $EmailAddress2 = $this->request->getVar('EmailAddress2');
$searchName = ''; $Phone = $this->request->getVar('Phone');
if (!empty($PatientID)){ if (!empty($PatientID)){
$tableName = 'PatientID'; if (!preg_match('/^[A-Za-z0-9.-]+$/', (string) $PatientID)) {
$searchName = $PatientID; return $this->respond([
} elseif (!empty($EmailAddress1)){ 'status' => 'error',
$tableName = 'EmailAddress1'; 'message' => 'PatientID format is invalid.',
$searchName = $EmailAddress1; 'data' => null
} else { ], 400);
return $this->respond([ }
'status' => 'error',
'message' => 'PatientID or EmailAddress1 parameter is required.', $patient = $this->db->table('patient')
'data' => null ->where('PatientID', $PatientID)
], 400); ->get()
} ->getRowArray();
} elseif (!empty($EmailAddress) || !empty($EmailAddress1) || !empty($EmailAddress2)){
$patient = $this->db->table('patient') $searchEmail = $EmailAddress ?: $EmailAddress1 ?: $EmailAddress2;
->where($tableName, $searchName)
->get() $patient = $this->db->table('patient')
->getRowArray(); ->groupStart()
->where('EmailAddress1', $searchEmail)
if (!$patient) { ->orWhere('EmailAddress2', $searchEmail)
return $this->respond([ ->groupEnd()
'status' => 'success', ->get()
'message' => "$tableName not found.", ->getRowArray();
'data' => true, } elseif (!empty($Phone)){
], 200); $patient = $this->db->table('patient')
} ->groupStart()
->where('Phone', $Phone)
return $this->respond([ ->orWhere('MobilePhone', $Phone)
'status' => 'success', ->groupEnd()
'message' => "$tableName already exists.", ->get()
'data' => false, ->getRowArray();
], 200); } else {
return $this->respond([
'status' => 'error',
'message' => 'PatientID, EmailAddress, or Phone parameter is required.',
'data' => null
], 400);
}
if (!$patient) {
return $this->respond([
'status' => 'success',
'message' => !empty($PatientID) ? 'PatientID not found.' : (!empty($Phone) ? 'Phone not found.' : 'EmailAddress not found.'),
'data' => true,
], 200);
}
return $this->respond([
'status' => 'success',
'message' => !empty($PatientID) ? 'PatientID already exists.' : (!empty($Phone) ? 'Phone already exists.' : 'EmailAddress already exists.'),
'data' => false,
], 200);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -54,12 +54,5 @@
<const name="CONFIGPATH" value="./app/Config/"/> <const name="CONFIGPATH" value="./app/Config/"/>
<!-- Directory containing the front controller (index.php) --> <!-- Directory containing the front controller (index.php) -->
<const name="PUBLICPATH" value="./public/"/> <const name="PUBLICPATH" value="./public/"/>
<!-- Database configuration --> </php>
<env name="database.tests.hostname" value="localhost"/> <!-- WAJIB DISESUAIKAN --> </phpunit>
<env name="database.tests.database" value="clqms01_test"/> <!-- WAJIB DISESUAIKAN -->
<env name="database.tests.username" value="root"/> <!-- WAJIB DISESUAIKAN -->
<env name="database.tests.password" value="databaseadminsakti"/> <!-- WAJIB DISESUAIKAN -->
<env name="database.tests.DBDriver" value="MySQLi"/> <!-- WAJIB DISESUAIKAN -->
<!-- <env name="database.tests.DBPrefix" value="tests_"/> -->
</php>
</phpunit>

View File

@ -2931,12 +2931,29 @@ paths:
schema: schema:
type: string type: string
description: Patient ID to check description: Patient ID to check
- name: EmailAddress1 - name: EmailAddress
in: query in: query
schema: schema:
type: string type: string
format: email format: email
description: Email address to check description: Email address to check
- name: EmailAddress1
in: query
schema:
type: string
format: email
description: Email address alias to check
- name: EmailAddress2
in: query
schema:
type: string
format: email
description: Email address alias to check
- name: Phone
in: query
schema:
type: string
description: Phone number to check
responses: responses:
'200': '200':
description: Patient check result description: Patient check result

View File

@ -94,24 +94,41 @@
/api/patient/check: /api/patient/check:
get: get:
tags: [Patient] tags: [Patient]
summary: Check if patient exists summary: Check if patient exists
security: security:
- bearerAuth: [] - bearerAuth: []
parameters: parameters:
- name: PatientID - name: PatientID
in: query in: query
schema: schema:
type: string type: string
description: Patient ID to check description: Patient ID to check
- name: EmailAddress1 - name: EmailAddress
in: query in: query
schema: schema:
type: string type: string
format: email format: email
description: Email address to check description: Email address to check
responses: - name: EmailAddress1
'200': in: query
description: Patient check result schema:
type: string
format: email
description: Email address alias to check
- name: EmailAddress2
in: query
schema:
type: string
format: email
description: Email address alias to check
- name: Phone
in: query
schema:
type: string
description: Phone number to check
responses:
'200':
description: Patient check result
content: content:
application/json: application/json:
schema: schema:

View File

@ -1,213 +1,119 @@
<?php <?php
namespace Tests\Feature\Patients; namespace Tests\Feature\Patients;
use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\FeatureTestTrait;
use Faker\Factory; use Faker\Factory;
/**
* PatientCheckTest
*
* Test cases for Patient Check endpoint
* Endpoint: GET api/patient/check
*
* This endpoint checks if a PatientID or EmailAddress1 already exists in the database.
* Returns data: true if NOT found (available), false if found (already exists)
*
* Test Cases:
* 1. testCheckPatientIDExists - Check existing PatientID
* 2. testCheckPatientIDNotExists - Check non-existing PatientID
* 3. testCheckEmailExists - Check existing Email
* 4. testCheckEmailNotExists - Check non-existing Email
* 5. testCheckWithoutParams - Check without any parameters
* 6. testCheckWithBothParams - Check with both parameters (PatientID takes priority)
*/
class PatientCheckTest extends CIUnitTestCase class PatientCheckTest extends CIUnitTestCase
{ {
use FeatureTestTrait; use FeatureTestTrait;
protected $endpoint = 'api/patient/check'; protected $endpoint = 'api/patient/check';
protected function setUp(): void public function testCheckPatientIDExists()
{ {
parent::setUp(); $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'PatientID' => 'SMAJ1',
]);
$result->assertStatus(200);
$data = json_decode($result->getJSON(), true);
$this->assertSame('success', $data['status']);
$this->assertSame(false, $data['data']);
} }
/** public function testCheckPatientIDWithHyphenExists()
* Test Case 1: Check existing PatientID {
* Expected: 200 OK with data: false (already exists) $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
*/ 'PatientID' => 'SMAJ-1',
public function testCheckPatientIDExists() ]);
{
// Assuming SMAJ1 exists in the test database $result->assertStatus(200);
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ $data = json_decode($result->getJSON(), true);
'PatientID' => 'SMAJ1'
]); $this->assertSame('success', $data['status']);
$this->assertIsBool($data['data']);
$result->assertStatus(200); }
$json = $result->getJSON(); public function testCheckPatientIDNotExists()
$data = json_decode($json, true); {
$faker = Factory::create();
$this->assertEquals('success', $data['status']);
// If patient exists, data should be false (not available) $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
if ($data['data'] === false) { 'PatientID' => 'NONEXISTENT-' . $faker->numberBetween(100000, 999999),
$this->assertStringContainsString('already exists', $data['message']); ]);
}
} $result->assertStatus(200);
$data = json_decode($result->getJSON(), true);
/**
* Test Case 2: Check non-existing PatientID $this->assertSame('success', $data['status']);
* Expected: 200 OK with data: true (available) $this->assertTrue($data['data']);
*/ }
public function testCheckPatientIDNotExists()
{ public function testCheckEmailAddressExists()
$faker = Factory::create(); {
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ 'EmailAddress' => 'dummy1@test.com',
'PatientID' => 'NONEXISTENT' . $faker->numberBetween(100000, 999999) ]);
]);
$result->assertStatus(200);
$result->assertStatus(200); $data = json_decode($result->getJSON(), true);
$json = $result->getJSON(); $this->assertSame('success', $data['status']);
$data = json_decode($json, true); $this->assertIsBool($data['data']);
}
$this->assertEquals('success', $data['status']);
$this->assertTrue($data['data']); // Available means true public function testCheckEmailAddressAliasExists()
$this->assertStringContainsString('not found', $data['message']); {
} $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'EmailAddress1' => 'dummy1@test.com',
/** ]);
* Test Case 3: Check existing Email
* Expected: 200 OK with data: false (already exists) $result->assertStatus(200);
* $data = json_decode($result->getJSON(), true);
* NOTE: Requires a known email in the database
*/ $this->assertSame('success', $data['status']);
public function testCheckEmailFormat() $this->assertIsBool($data['data']);
{ }
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'EmailAddress1' => 'test@example.com' public function testCheckPhoneExists()
]); {
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
$result->assertStatus(200); 'Phone' => '092029',
]);
$json = $result->getJSON();
$data = json_decode($json, true); $result->assertStatus(200);
$data = json_decode($result->getJSON(), true);
$this->assertEquals('success', $data['status']);
$this->assertIsBool($data['data']); $this->assertSame('success', $data['status']);
} $this->assertIsBool($data['data']);
}
/**
* Test Case 4: Check non-existing Email public function testCheckWithoutParams()
* Expected: 200 OK with data: true (available) {
*/ $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint);
public function testCheckEmailNotExists()
{ $result->assertStatus(400);
$faker = Factory::create(); $data = json_decode($result->getJSON(), true);
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ $this->assertSame('error', $data['status']);
'EmailAddress1' => 'nonexistent_' . $faker->numberBetween(100000, 999999) . '@test.com' $this->assertSame('PatientID, EmailAddress, or Phone parameter is required.', $data['message']);
]); }
$result->assertStatus(200); public function testCheckResponseStructure()
{
$json = $result->getJSON(); $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
$data = json_decode($json, true); 'PatientID' => 'TEST123',
]);
$this->assertEquals('success', $data['status']);
$this->assertTrue($data['data']); // Available means true $result->assertStatus(200);
$this->assertStringContainsString('not found', $data['message']); $data = json_decode($result->getJSON(), true);
}
$this->assertArrayHasKey('status', $data);
/** $this->assertArrayHasKey('message', $data);
* Test Case 5: Check with empty PatientID $this->assertArrayHasKey('data', $data);
* Expected: May return 500 or handle gracefully }
*/ }
public function testCheckWithEmptyPatientID()
{
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'PatientID' => ''
]);
// Should either return 200 (graceful handling) or 500 (error)
// Workaround for environment wrapping JSON in HTML causing null status code
if ($result->getStatusCode() === null) {
$body = strip_tags($result->getBody());
$data = json_decode($body, true);
$this->assertEquals('error', $data['status'] ?? '');
$this->assertEquals('PatientID or EmailAddress1 parameter is required.', $data['message'] ?? '');
return;
}
$this->assertContains($result->getStatusCode(), [200, 400, 500]);
}
/**
* Test Case 6: Check for response structure
* Expected: Response should have status, message, and data keys
*/
public function testCheckResponseStructure()
{
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'PatientID' => 'TEST123'
]);
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
$this->assertArrayHasKey('status', $data);
$this->assertArrayHasKey('message', $data);
$this->assertArrayHasKey('data', $data);
}
/**
* Test Case 7: Check with special characters in PatientID
* Expected: Should handle gracefully
*/
public function testCheckWithSpecialCharacters()
{
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'PatientID' => "TEST'123"
]);
// Should not crash - either 200 or appropriate error
// Workaround for environment wrapping
if ($result->getStatusCode() === null) {
$body = strip_tags($result->getBody());
$data = json_decode($body, true);
$this->assertEquals('success', $data['status'] ?? '');
// message could be 'PatientID not found.' or '... already exists.'
$this->assertArrayHasKey('message', $data);
return;
}
$this->assertContains($result->getStatusCode(), [200, 400, 500]);
}
/**
* Test Case 8: Check with very long PatientID
* Expected: Should handle gracefully
*/
public function testCheckWithVeryLongPatientID()
{
$longId = str_repeat('A', 100);
$result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [
'PatientID' => $longId
]);
$result->assertStatus(200);
$json = $result->getJSON();
$data = json_decode($json, true);
// Long ID that doesn't exist should return true (available)
$this->assertEquals('success', $data['status']);
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php'; require_once __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php';
use CodeIgniter\Config\DotEnv;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\MigrationRunner; use CodeIgniter\Database\MigrationRunner;
use Config\Database; use Config\Database;
use Config\Migrations as MigrationsConfig; use Config\Migrations as MigrationsConfig;
@ -14,6 +16,8 @@ if (defined('CLQMS_PHPUNIT_BOOTSTRAPPED') || ENVIRONMENT !== 'testing') {
define('CLQMS_PHPUNIT_BOOTSTRAPPED', true); define('CLQMS_PHPUNIT_BOOTSTRAPPED', true);
(new DotEnv(ROOTPATH))->load();
$db = Database::connect('tests'); $db = Database::connect('tests');
$forge = Database::forge('tests'); $forge = Database::forge('tests');