From 7fd3dfddd8fcb0a2d300b9cec3a22a7b180842da Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Thu, 16 Apr 2026 12:53:46 +0700 Subject: [PATCH] fix: add testmap search filters Allow the test map list endpoint to filter by host and client, and include container labels in detail responses. Update the API contract and feature coverage to match. --- app/Controllers/Test/TestMapController.php | 42 ++ app/Models/Test/TestMapDetailModel.php | 18 +- public/api-docs.bundled.yaml | 16 + public/components/schemas/tests.yaml | 15 +- public/paths/testmap.yaml | 23 +- tests/feature/Test/TestMapPatchTest.php | 549 ++++++++++++--------- 6 files changed, 409 insertions(+), 254 deletions(-) diff --git a/app/Controllers/Test/TestMapController.php b/app/Controllers/Test/TestMapController.php index c97e98d..f0ade22 100755 --- a/app/Controllers/Test/TestMapController.php +++ b/app/Controllers/Test/TestMapController.php @@ -48,6 +48,7 @@ class TestMapController extends BaseController { public function index() { $rows = $this->model->getUniqueGroupings(); + $rows = $this->applyIndexFilters($rows); if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } $rows = ValueSet::transformLabels($rows, [ @@ -223,6 +224,47 @@ class TestMapController extends BaseController { return $row; } + private function applyIndexFilters(array $rows): array + { + $hostFilter = trim((string) $this->request->getGet('host')); + $clientFilter = trim((string) $this->request->getGet('client')); + + if ($hostFilter === '' && $clientFilter === '') { + return $rows; + } + + return array_values(array_filter($rows, function (array $row) use ($hostFilter, $clientFilter): bool { + if ($hostFilter !== '' && !$this->matchesSearch($row, 'Host', $hostFilter)) { + return false; + } + + if ($clientFilter !== '' && !$this->matchesSearch($row, 'Client', $clientFilter)) { + return false; + } + + return true; + })); + } + + private function matchesSearch(array $row, string $prefix, string $filter): bool + { + $haystacks = [ + (string) ($row[$prefix . 'Name'] ?? ''), + (string) ($row[$prefix . 'ID'] ?? ''), + (string) ($row[$prefix . 'Type'] ?? ''), + ]; + + $needle = strtolower($filter); + + foreach ($haystacks as $value) { + if ($value !== '' && str_contains(strtolower($value), $needle)) { + return true; + } + } + + return false; + } + private function resolveDetailOperations(mixed $detailsPayload): ?array { if ($detailsPayload === null) { diff --git a/app/Models/Test/TestMapDetailModel.php b/app/Models/Test/TestMapDetailModel.php index 1e0177a..3a21efd 100755 --- a/app/Models/Test/TestMapDetailModel.php +++ b/app/Models/Test/TestMapDetailModel.php @@ -24,14 +24,16 @@ class TestMapDetailModel extends BaseModel { protected $useSoftDeletes = true; protected $deletedField = "EndDate"; - /** - * Get all details for a test map - */ - public function getDetailsByTestMap($testMapID) { - return $this->where('TestMapID', $testMapID) - ->where('EndDate IS NULL') - ->findAll(); - } + /** + * Get all details for a test map + */ + public function getDetailsByTestMap($testMapID) { + return $this->select('testmapdetail.*, containerdef.ConName AS ContainerLabel') + ->join('containerdef', 'containerdef.ConDefID = testmapdetail.ConDefID', 'left') + ->where('testmapdetail.TestMapID', $testMapID) + ->where('testmapdetail.EndDate IS NULL') + ->findAll(); + } /** * Get test map detail by host test code diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index 693f213..e24ed27 100755 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -4045,6 +4045,19 @@ paths: summary: List all test mappings security: - bearerAuth: [] + parameters: + - name: host + in: query + required: false + schema: + type: string + description: Filter by host name, type, or ID + - name: client + in: query + required: false + schema: + type: string + description: Filter by client name, type, or ID responses: '200': description: List of test mappings @@ -8713,6 +8726,9 @@ components: ConDefID: type: integer description: Container definition ID + ContainerLabel: + type: string + description: Container definition name ClientTestCode: type: string description: Test code in client system diff --git a/public/components/schemas/tests.yaml b/public/components/schemas/tests.yaml index dd7de5d..20ed8c2 100755 --- a/public/components/schemas/tests.yaml +++ b/public/components/schemas/tests.yaml @@ -595,12 +595,15 @@ TestMapDetail: HostTestName: type: string description: Test name in host system - ConDefID: - type: integer - description: Container definition ID - ClientTestCode: - type: string - description: Test code in client system + ConDefID: + type: integer + description: Container definition ID + ContainerLabel: + type: string + description: Container definition name + ClientTestCode: + type: string + description: Test code in client system ClientTestName: type: string description: Test name in client system diff --git a/public/paths/testmap.yaml b/public/paths/testmap.yaml index 17f7b49..88f2989 100755 --- a/public/paths/testmap.yaml +++ b/public/paths/testmap.yaml @@ -1,10 +1,23 @@ /api/test/testmap: - get: + get: tags: [Test] - summary: List all test mappings - security: - - bearerAuth: [] - responses: + summary: List all test mappings + security: + - bearerAuth: [] + parameters: + - name: host + in: query + required: false + schema: + type: string + description: Filter by host name, type, or ID + - name: client + in: query + required: false + schema: + type: string + description: Filter by client name, type, or ID + responses: '200': description: List of test mappings content: diff --git a/tests/feature/Test/TestMapPatchTest.php b/tests/feature/Test/TestMapPatchTest.php index af1967d..9d2c86c 100755 --- a/tests/feature/Test/TestMapPatchTest.php +++ b/tests/feature/Test/TestMapPatchTest.php @@ -1,56 +1,56 @@ - 'localhost', - 'aud' => 'localhost', - 'iat' => time(), - 'nbf' => time(), - 'exp' => time() + 3600, - 'uid' => 1, - 'email' => 'admin@admin.com', - ]; - $this->token = JWT::encode($payload, $key, 'HS256'); - } - - private function authHeaders(): array - { - return ['Cookie' => 'token=' . $this->token]; - } - + 'localhost', + 'aud' => 'localhost', + 'iat' => time(), + 'nbf' => time(), + 'exp' => time() + 3600, + 'uid' => 1, + 'email' => 'admin@admin.com', + ]; + $this->token = JWT::encode($payload, $key, 'HS256'); + } + + private function authHeaders(): array + { + return ['Cookie' => 'token=' . $this->token]; + } + private function createTestMap(array $data = []): array { - $payload = array_merge([ - 'HostType' => 'SITE', - 'HostID' => 1, - 'ClientType' => 'SITE', - 'ClientID' => 1, - ], $data); - - $response = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('post', $this->endpoint, $payload); - - $response->assertStatus(201); - $created = json_decode($response->getJSON(), true); - $id = $created['data']; - + $payload = array_merge([ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 1, + ], $data); + + $response = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('post', $this->endpoint, $payload); + + $response->assertStatus(201); + $created = json_decode($response->getJSON(), true); + $id = $created['data']; + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); $show->assertStatus(200); $showData = json_decode($show->getJSON(), true)['data']; @@ -60,201 +60,280 @@ class TestMapPatchTest extends CIUnitTestCase return $showData; } - public function testPartialUpdateTestMapSuccess() - { - $testMap = $this->createTestMap(); - $id = $testMap['TestMapID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['ClientType' => 'WST']); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('WST', $showData['ClientType']); - $this->assertEquals((string) $testMap['HostID'], (string) $showData['HostID']); - } - - public function testPartialUpdateTestMapNotFound() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/999999", ['ClientType' => 'WST']); - - $patch->assertStatus(404); - } - - public function testPartialUpdateTestMapInvalidId() - { - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/invalid", ['ClientType' => 'WST']); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestMapEmptyPayload() - { - $testMap = $this->createTestMap(); - $id = $testMap['TestMapID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", []); - - $patch->assertStatus(400); - } - - public function testPartialUpdateTestMapSingleField() - { - $testMap = $this->createTestMap(); - $id = $testMap['TestMapID']; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$id}", ['HostID' => 2]); - - $patch->assertStatus(200); - $showData = json_decode($this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/{$id}") - ->getJSON(), true)['data']; - - $this->assertEquals('2', (string) $showData['HostID']); - $this->assertEquals((string) $testMap['ClientID'], (string) $showData['ClientID']); - } - - public function testCreateTestMapWithDetails() - { - $details = [ - [ - 'HostTestCode' => 'HB', - 'HostTestName' => 'Hemoglobin', - 'ClientTestCode' => '2', - 'ClientTestName' => 'Hemoglobin', - ], - [ - 'HostTestCode' => 'HCT', - 'HostTestName' => 'Hematocrit', - 'ClientTestCode' => '3', - 'ClientTestName' => 'Hematocrit', - ], - ]; - - $testMap = $this->createTestMap([ - 'HostType' => 'SITE', - 'HostID' => 1, - 'ClientType' => 'SITE', - 'ClientID' => 2, - 'details' => $details, - ]); - - $this->assertCount(2, $testMap['details']); - $this->assertEquals('2', $testMap['details'][0]['ClientTestCode']); - } - - public function testPatchTestMapDetailOperations() - { - $initialDetails = [ - [ - 'HostTestCode' => 'HB', - 'HostTestName' => 'Hemoglobin', - 'ClientTestCode' => '2', - 'ClientTestName' => 'Hemoglobin', - ], - [ - 'HostTestCode' => 'HCT', - 'HostTestName' => 'Hematocrit', - 'ClientTestCode' => '3', - 'ClientTestName' => 'Hematocrit', - ], - ]; - - $testMap = $this->createTestMap([ - 'HostType' => 'SITE', - 'HostID' => 1, - 'ClientType' => 'SITE', - 'ClientID' => 1, - 'details' => $initialDetails, - ]); - - $existingDetails = $testMap['details']; - $editDetail = $existingDetails[0]; - $deleteDetail = $existingDetails[1]; - - $patch = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('patch', "{$this->endpoint}/{$testMap['TestMapID']}", [ - 'ClientType' => 'WST', - 'details' => [ - 'edited' => [ - [ - 'TestMapDetailID' => $editDetail['TestMapDetailID'], - 'ClientTestName' => 'Hemoglobin Updated', - ], - ], - 'created' => [ - [ - 'HostTestCode' => 'MCV', - 'HostTestName' => 'MCV', - 'ClientTestCode' => '4', - 'ClientTestName' => 'MCV', - ], - ], - 'deleted' => [$deleteDetail['TestMapDetailID']], - ], - ]); - - $patch->assertStatus(200); - $patchData = json_decode($patch->getJSON(), true); - $this->assertEquals('success', $patchData['status']); - - $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$testMap['TestMapID']}"); - $show->assertStatus(200); - $showData = json_decode($show->getJSON(), true)['data']; - - $this->assertEquals('WST', $showData['ClientType']); - $this->assertCount(2, $showData['details']); - $detailIds = array_column($showData['details'], 'TestMapDetailID'); - $this->assertContains($editDetail['TestMapDetailID'], $detailIds); - $this->assertNotContains($deleteDetail['TestMapDetailID'], $detailIds); - - $updatedDetails = array_values(array_filter($showData['details'], static fn ($row) => $row['TestMapDetailID'] === $editDetail['TestMapDetailID'])); - $this->assertNotEmpty($updatedDetails); - $this->assertEquals('Hemoglobin Updated', $updatedDetails[0]['ClientTestName']); - } - - public function testDeleteTestMapRemovesDetails() + public function testIndexFiltersByHost(): void { $testMap = $this->createTestMap([ 'HostType' => 'SITE', 'HostID' => 2, 'ClientType' => 'SITE', + 'ClientID' => 1, + ]); + + $response = $this->withHeaders($this->authHeaders()) + ->call('get', $this->endpoint . '?host=2'); + + $response->assertStatus(200); + $rows = json_decode($response->getJSON(), true)['data']; + + $this->assertNotEmpty(array_values(array_filter($rows, static fn (array $row): bool => (int) $row['TestMapID'] === (int) $testMap['TestMapID']))); + + foreach ($rows as $row) { + $this->assertTrue( + str_contains(strtolower((string) ($row['HostID'] ?? '')), '2') + || str_contains(strtolower((string) ($row['HostName'] ?? '')), '2') + || str_contains(strtolower((string) ($row['HostType'] ?? '')), '2') + ); + } + } + + public function testIndexFiltersByClient(): void + { + $testMap = $this->createTestMap([ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', 'ClientID' => 3, + ]); + + $response = $this->withHeaders($this->authHeaders()) + ->call('get', $this->endpoint . '?client=3'); + + $response->assertStatus(200); + $rows = json_decode($response->getJSON(), true)['data']; + + $this->assertNotEmpty(array_values(array_filter($rows, static fn (array $row): bool => (int) $row['TestMapID'] === (int) $testMap['TestMapID']))); + + foreach ($rows as $row) { + $this->assertTrue( + str_contains(strtolower((string) ($row['ClientID'] ?? '')), '3') + || str_contains(strtolower((string) ($row['ClientName'] ?? '')), '3') + || str_contains(strtolower((string) ($row['ClientType'] ?? '')), '3') + ); + } + } + + public function testPartialUpdateTestMapSuccess() + { + $testMap = $this->createTestMap(); + $id = $testMap['TestMapID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['ClientType' => 'WST']); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('WST', $showData['ClientType']); + $this->assertEquals((string) $testMap['HostID'], (string) $showData['HostID']); + } + + public function testPartialUpdateTestMapNotFound() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/999999", ['ClientType' => 'WST']); + + $patch->assertStatus(404); + } + + public function testPartialUpdateTestMapInvalidId() + { + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/invalid", ['ClientType' => 'WST']); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestMapEmptyPayload() + { + $testMap = $this->createTestMap(); + $id = $testMap['TestMapID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", []); + + $patch->assertStatus(400); + } + + public function testPartialUpdateTestMapSingleField() + { + $testMap = $this->createTestMap(); + $id = $testMap['TestMapID']; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$id}", ['HostID' => 2]); + + $patch->assertStatus(200); + $showData = json_decode($this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/{$id}") + ->getJSON(), true)['data']; + + $this->assertEquals('2', (string) $showData['HostID']); + $this->assertEquals((string) $testMap['ClientID'], (string) $showData['ClientID']); + } + + public function testCreateTestMapWithDetails() + { + $details = [ + [ + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ClientTestCode' => '2', + 'ClientTestName' => 'Hemoglobin', + ], + [ + 'HostTestCode' => 'HCT', + 'HostTestName' => 'Hematocrit', + 'ClientTestCode' => '3', + 'ClientTestName' => 'Hematocrit', + ], + ]; + + $testMap = $this->createTestMap([ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 2, + 'details' => $details, + ]); + + $this->assertCount(2, $testMap['details']); + $this->assertEquals('2', $testMap['details'][0]['ClientTestCode']); + } + + public function testPatchTestMapDetailOperations() + { + $initialDetails = [ + [ + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ClientTestCode' => '2', + 'ClientTestName' => 'Hemoglobin', + ], + [ + 'HostTestCode' => 'HCT', + 'HostTestName' => 'Hematocrit', + 'ClientTestCode' => '3', + 'ClientTestName' => 'Hematocrit', + ], + ]; + + $testMap = $this->createTestMap([ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 1, + 'details' => $initialDetails, + ]); + + $existingDetails = $testMap['details']; + $editDetail = $existingDetails[0]; + $deleteDetail = $existingDetails[1]; + + $patch = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('patch', "{$this->endpoint}/{$testMap['TestMapID']}", [ + 'ClientType' => 'WST', + 'details' => [ + 'edited' => [ + [ + 'TestMapDetailID' => $editDetail['TestMapDetailID'], + 'ClientTestName' => 'Hemoglobin Updated', + ], + ], + 'created' => [ + [ + 'HostTestCode' => 'MCV', + 'HostTestName' => 'MCV', + 'ClientTestCode' => '4', + 'ClientTestName' => 'MCV', + ], + ], + 'deleted' => [$deleteDetail['TestMapDetailID']], + ], + ]); + + $patch->assertStatus(200); + $patchData = json_decode($patch->getJSON(), true); + $this->assertEquals('success', $patchData['status']); + + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$testMap['TestMapID']}"); + $show->assertStatus(200); + $showData = json_decode($show->getJSON(), true)['data']; + + $this->assertEquals('WST', $showData['ClientType']); + $this->assertCount(2, $showData['details']); + $detailIds = array_column($showData['details'], 'TestMapDetailID'); + $this->assertContains($editDetail['TestMapDetailID'], $detailIds); + $this->assertNotContains($deleteDetail['TestMapDetailID'], $detailIds); + + $updatedDetails = array_values(array_filter($showData['details'], static fn ($row) => $row['TestMapDetailID'] === $editDetail['TestMapDetailID'])); + $this->assertNotEmpty($updatedDetails); + $this->assertEquals('Hemoglobin Updated', $updatedDetails[0]['ClientTestName']); + } + + public function testShowTestMapIncludesContainerLabel(): void + { + $testMap = $this->createTestMap([ + 'HostType' => 'SITE', + 'HostID' => 1, + 'ClientType' => 'SITE', + 'ClientID' => 2, 'details' => [ [ - 'HostTestCode' => 'PLT', - 'HostTestName' => 'Platelet', - 'ClientTestCode' => '5', - 'ClientTestName' => 'Platelet', + 'HostTestCode' => 'HB', + 'HostTestName' => 'Hemoglobin', + 'ConDefID' => 1, + 'ClientTestCode' => '2', + 'ClientTestName' => 'Hemoglobin', ], ], ]); - $delete = $this->withHeaders($this->authHeaders()) - ->withBodyFormat('json') - ->call('delete', $this->endpoint, ['TestMapID' => $testMap['TestMapID']]); + $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$testMap['TestMapID']}"); + $show->assertStatus(200); - $delete->assertStatus(200); - - $details = $this->withHeaders($this->authHeaders()) - ->call('get', "{$this->endpoint}/detail/by-testmap/{$testMap['TestMapID']}"); - $details->assertStatus(200); - $this->assertEquals([], json_decode($details->getJSON(), true)['data']); + $showData = json_decode($show->getJSON(), true)['data']; + $this->assertNotEmpty($showData['details']); + $this->assertArrayHasKey('ContainerLabel', $showData['details'][0]); + $this->assertNotEmpty($showData['details'][0]['ContainerLabel']); } -} + + public function testDeleteTestMapRemovesDetails() + { + $testMap = $this->createTestMap([ + 'HostType' => 'SITE', + 'HostID' => 2, + 'ClientType' => 'SITE', + 'ClientID' => 3, + 'details' => [ + [ + 'HostTestCode' => 'PLT', + 'HostTestName' => 'Platelet', + 'ClientTestCode' => '5', + 'ClientTestName' => 'Platelet', + ], + ], + ]); + + $delete = $this->withHeaders($this->authHeaders()) + ->withBodyFormat('json') + ->call('delete', $this->endpoint, ['TestMapID' => $testMap['TestMapID']]); + + $delete->assertStatus(200); + + $details = $this->withHeaders($this->authHeaders()) + ->call('get', "{$this->endpoint}/detail/by-testmap/{$testMap['TestMapID']}"); + $details->assertStatus(200); + $this->assertEquals([], json_decode($details->getJSON(), true)['data']); + } +}