From c49743bbf394df45ae0c3d9cf9086cc48706ac05 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Mon, 13 Apr 2026 11:25:41 +0700 Subject: [PATCH] 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 --- app/Controllers/Patient/PatientController.php | 108 +++--- phpunit.xml.dist | 11 +- public/api-docs.bundled.yaml | 19 +- public/paths/patients.yaml | 47 ++- tests/feature/Patients/PatientCheckTest.php | 320 +++++++----------- tests/phpunit-bootstrap.php | 4 + 6 files changed, 233 insertions(+), 276 deletions(-) diff --git a/app/Controllers/Patient/PatientController.php b/app/Controllers/Patient/PatientController.php index 27666c5..e10f931 100755 --- a/app/Controllers/Patient/PatientController.php +++ b/app/Controllers/Patient/PatientController.php @@ -17,8 +17,8 @@ class PatientController extends Controller { $this->db = \Config\Database::connect(); $this->model = new PatientModel(); $this->rules = [ - 'PatientID' => 'required|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', - 'AlternatePID' => 'permit_empty|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]', 'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]', 'Sex' => 'required', @@ -157,8 +157,8 @@ class PatientController extends Controller { $rules = []; $fieldRules = [ - 'PatientID' => 'permit_empty|regex_match[/^[A-Za-z0-9]+$/]|max_length[30]', - 'AlternatePID' => '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]', 'Prefix' => 'permit_empty|regex_match[/^[A-Za-z\'\. ]+$/]|max_length[10]', 'Sex' => 'permit_empty', 'NameFirst' => 'required|regex_match[/^[A-Za-z\'\. ]+$/]|min_length[1]|max_length[60]', @@ -252,46 +252,66 @@ class PatientController extends Controller { } } - public function patientCheck() { - try { - $PatientID = $this->request->getVar('PatientID'); - $EmailAddress1 = $this->request->getVar('EmailAddress1'); - - $tableName = ''; - $searchName = ''; - - if (!empty($PatientID)){ - $tableName = 'PatientID'; - $searchName = $PatientID; - } elseif (!empty($EmailAddress1)){ - $tableName = 'EmailAddress1'; - $searchName = $EmailAddress1; - } else { - return $this->respond([ - 'status' => 'error', - 'message' => 'PatientID or EmailAddress1 parameter is required.', - 'data' => null - ], 400); - } - - $patient = $this->db->table('patient') - ->where($tableName, $searchName) - ->get() - ->getRowArray(); - - if (!$patient) { - return $this->respond([ - 'status' => 'success', - 'message' => "$tableName not found.", - 'data' => true, - ], 200); - } - - return $this->respond([ - 'status' => 'success', - 'message' => "$tableName already exists.", - 'data' => false, - ], 200); + public function patientCheck() { + try { + $PatientID = $this->request->getVar('PatientID'); + $EmailAddress = $this->request->getVar('EmailAddress'); + $EmailAddress1 = $this->request->getVar('EmailAddress1'); + $EmailAddress2 = $this->request->getVar('EmailAddress2'); + $Phone = $this->request->getVar('Phone'); + + if (!empty($PatientID)){ + if (!preg_match('/^[A-Za-z0-9.-]+$/', (string) $PatientID)) { + return $this->respond([ + 'status' => 'error', + 'message' => 'PatientID format is invalid.', + 'data' => null + ], 400); + } + + $patient = $this->db->table('patient') + ->where('PatientID', $PatientID) + ->get() + ->getRowArray(); + } elseif (!empty($EmailAddress) || !empty($EmailAddress1) || !empty($EmailAddress2)){ + $searchEmail = $EmailAddress ?: $EmailAddress1 ?: $EmailAddress2; + + $patient = $this->db->table('patient') + ->groupStart() + ->where('EmailAddress1', $searchEmail) + ->orWhere('EmailAddress2', $searchEmail) + ->groupEnd() + ->get() + ->getRowArray(); + } elseif (!empty($Phone)){ + $patient = $this->db->table('patient') + ->groupStart() + ->where('Phone', $Phone) + ->orWhere('MobilePhone', $Phone) + ->groupEnd() + ->get() + ->getRowArray(); + } 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) { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c8d761d..d669e73 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -54,12 +54,5 @@ - - - - - - - - - + + diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index db3d6bc..8a827b5 100755 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -2931,12 +2931,29 @@ paths: schema: type: string description: Patient ID to check - - name: EmailAddress1 + - name: EmailAddress in: query schema: type: string format: email 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: '200': description: Patient check result diff --git a/public/paths/patients.yaml b/public/paths/patients.yaml index 67892c1..537ef2e 100755 --- a/public/paths/patients.yaml +++ b/public/paths/patients.yaml @@ -94,24 +94,41 @@ /api/patient/check: get: tags: [Patient] - summary: Check if patient exists + summary: Check if patient exists security: - bearerAuth: [] parameters: - - name: PatientID - in: query - schema: - type: string - description: Patient ID to check - - name: EmailAddress1 - in: query - schema: - type: string - format: email - description: Email address to check - responses: - '200': - description: Patient check result + - name: PatientID + in: query + schema: + type: string + description: Patient ID to check + - name: EmailAddress + in: query + schema: + type: string + format: email + 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: + '200': + description: Patient check result content: application/json: schema: diff --git a/tests/feature/Patients/PatientCheckTest.php b/tests/feature/Patients/PatientCheckTest.php index e88a430..691b15c 100755 --- a/tests/feature/Patients/PatientCheckTest.php +++ b/tests/feature/Patients/PatientCheckTest.php @@ -1,213 +1,119 @@ -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']); } - - /** - * Test Case 1: Check existing PatientID - * Expected: 200 OK with data: false (already exists) - */ - public function testCheckPatientIDExists() - { - // Assuming SMAJ1 exists in the test database - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'SMAJ1' - ]); - - $result->assertStatus(200); - - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - // If patient exists, data should be false (not available) - if ($data['data'] === false) { - $this->assertStringContainsString('already exists', $data['message']); - } - } - - /** - * Test Case 2: Check non-existing PatientID - * Expected: 200 OK with data: true (available) - */ - public function testCheckPatientIDNotExists() - { - $faker = Factory::create(); - - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'PatientID' => 'NONEXISTENT' . $faker->numberBetween(100000, 999999) - ]); - - $result->assertStatus(200); - - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - $this->assertTrue($data['data']); // Available means true - $this->assertStringContainsString('not found', $data['message']); - } - - /** - * Test Case 3: Check existing Email - * Expected: 200 OK with data: false (already exists) - * - * NOTE: Requires a known email in the database - */ - public function testCheckEmailFormat() - { - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'EmailAddress1' => 'test@example.com' - ]); - - $result->assertStatus(200); - - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - $this->assertIsBool($data['data']); - } - - /** - * Test Case 4: Check non-existing Email - * Expected: 200 OK with data: true (available) - */ - public function testCheckEmailNotExists() - { - $faker = Factory::create(); - - $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ - 'EmailAddress1' => 'nonexistent_' . $faker->numberBetween(100000, 999999) . '@test.com' - ]); - - $result->assertStatus(200); - - $json = $result->getJSON(); - $data = json_decode($json, true); - - $this->assertEquals('success', $data['status']); - $this->assertTrue($data['data']); // Available means true - $this->assertStringContainsString('not found', $data['message']); - } - - /** - * Test Case 5: Check with empty PatientID - * 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']); - } -} + + public function testCheckPatientIDWithHyphenExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'SMAJ-1', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckPatientIDNotExists() + { + $faker = Factory::create(); + + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'NONEXISTENT-' . $faker->numberBetween(100000, 999999), + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertTrue($data['data']); + } + + public function testCheckEmailAddressExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'EmailAddress' => 'dummy1@test.com', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckEmailAddressAliasExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'EmailAddress1' => 'dummy1@test.com', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckPhoneExists() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'Phone' => '092029', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('success', $data['status']); + $this->assertIsBool($data['data']); + } + + public function testCheckWithoutParams() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint); + + $result->assertStatus(400); + $data = json_decode($result->getJSON(), true); + + $this->assertSame('error', $data['status']); + $this->assertSame('PatientID, EmailAddress, or Phone parameter is required.', $data['message']); + } + + public function testCheckResponseStructure() + { + $result = $this->withHeaders(['Accept' => 'application/json'])->call('get', $this->endpoint, [ + 'PatientID' => 'TEST123', + ]); + + $result->assertStatus(200); + $data = json_decode($result->getJSON(), true); + + $this->assertArrayHasKey('status', $data); + $this->assertArrayHasKey('message', $data); + $this->assertArrayHasKey('data', $data); + } +} diff --git a/tests/phpunit-bootstrap.php b/tests/phpunit-bootstrap.php index ef88ffd..471792f 100755 --- a/tests/phpunit-bootstrap.php +++ b/tests/phpunit-bootstrap.php @@ -4,6 +4,8 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/codeigniter4/framework/system/Test/bootstrap.php'; +use CodeIgniter\Config\DotEnv; +use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\MigrationRunner; use Config\Database; use Config\Migrations as MigrationsConfig; @@ -14,6 +16,8 @@ if (defined('CLQMS_PHPUNIT_BOOTSTRAPPED') || ENVIRONMENT !== 'testing') { define('CLQMS_PHPUNIT_BOOTSTRAPPED', true); +(new DotEnv(ROOTPATH))->load(); + $db = Database::connect('tests'); $forge = Database::forge('tests');