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.
This commit is contained in:
mahdahar 2026-04-16 12:53:46 +07:00
parent 577ceb3d54
commit 7fd3dfddd8
6 changed files with 409 additions and 254 deletions

View File

@ -48,6 +48,7 @@ class TestMapController extends BaseController {
public function index() { public function index() {
$rows = $this->model->getUniqueGroupings(); $rows = $this->model->getUniqueGroupings();
$rows = $this->applyIndexFilters($rows);
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
$rows = ValueSet::transformLabels($rows, [ $rows = ValueSet::transformLabels($rows, [
@ -223,6 +224,47 @@ class TestMapController extends BaseController {
return $row; 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 private function resolveDetailOperations(mixed $detailsPayload): ?array
{ {
if ($detailsPayload === null) { if ($detailsPayload === null) {

View File

@ -24,14 +24,16 @@ class TestMapDetailModel extends BaseModel {
protected $useSoftDeletes = true; protected $useSoftDeletes = true;
protected $deletedField = "EndDate"; protected $deletedField = "EndDate";
/** /**
* Get all details for a test map * Get all details for a test map
*/ */
public function getDetailsByTestMap($testMapID) { public function getDetailsByTestMap($testMapID) {
return $this->where('TestMapID', $testMapID) return $this->select('testmapdetail.*, containerdef.ConName AS ContainerLabel')
->where('EndDate IS NULL') ->join('containerdef', 'containerdef.ConDefID = testmapdetail.ConDefID', 'left')
->findAll(); ->where('testmapdetail.TestMapID', $testMapID)
} ->where('testmapdetail.EndDate IS NULL')
->findAll();
}
/** /**
* Get test map detail by host test code * Get test map detail by host test code

View File

@ -4045,6 +4045,19 @@ paths:
summary: List all test mappings summary: List all test mappings
security: security:
- bearerAuth: [] - 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: responses:
'200': '200':
description: List of test mappings description: List of test mappings
@ -8713,6 +8726,9 @@ components:
ConDefID: ConDefID:
type: integer type: integer
description: Container definition ID description: Container definition ID
ContainerLabel:
type: string
description: Container definition name
ClientTestCode: ClientTestCode:
type: string type: string
description: Test code in client system description: Test code in client system

View File

@ -595,12 +595,15 @@ TestMapDetail:
HostTestName: HostTestName:
type: string type: string
description: Test name in host system description: Test name in host system
ConDefID: ConDefID:
type: integer type: integer
description: Container definition ID description: Container definition ID
ClientTestCode: ContainerLabel:
type: string type: string
description: Test code in client system description: Container definition name
ClientTestCode:
type: string
description: Test code in client system
ClientTestName: ClientTestName:
type: string type: string
description: Test name in client system description: Test name in client system

View File

@ -1,10 +1,23 @@
/api/test/testmap: /api/test/testmap:
get: get:
tags: [Test] tags: [Test]
summary: List all test mappings summary: List all test mappings
security: security:
- bearerAuth: [] - bearerAuth: []
responses: 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': '200':
description: List of test mappings description: List of test mappings
content: content:

View File

@ -1,56 +1,56 @@
<?php <?php
namespace Tests\Feature\Test; namespace Tests\Feature\Test;
use CodeIgniter\Test\FeatureTestTrait; use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\CIUnitTestCase;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
class TestMapPatchTest extends CIUnitTestCase class TestMapPatchTest extends CIUnitTestCase
{ {
use FeatureTestTrait; use FeatureTestTrait;
protected string $token; protected string $token;
protected string $endpoint = 'api/test/testmap'; protected string $endpoint = 'api/test/testmap';
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$key = getenv('JWT_SECRET') ?: 'my-secret-key'; $key = getenv('JWT_SECRET') ?: 'my-secret-key';
$payload = [ $payload = [
'iss' => 'localhost', 'iss' => 'localhost',
'aud' => 'localhost', 'aud' => 'localhost',
'iat' => time(), 'iat' => time(),
'nbf' => time(), 'nbf' => time(),
'exp' => time() + 3600, 'exp' => time() + 3600,
'uid' => 1, 'uid' => 1,
'email' => 'admin@admin.com', 'email' => 'admin@admin.com',
]; ];
$this->token = JWT::encode($payload, $key, 'HS256'); $this->token = JWT::encode($payload, $key, 'HS256');
} }
private function authHeaders(): array private function authHeaders(): array
{ {
return ['Cookie' => 'token=' . $this->token]; return ['Cookie' => 'token=' . $this->token];
} }
private function createTestMap(array $data = []): array private function createTestMap(array $data = []): array
{ {
$payload = array_merge([ $payload = array_merge([
'HostType' => 'SITE', 'HostType' => 'SITE',
'HostID' => 1, 'HostID' => 1,
'ClientType' => 'SITE', 'ClientType' => 'SITE',
'ClientID' => 1, 'ClientID' => 1,
], $data); ], $data);
$response = $this->withHeaders($this->authHeaders()) $response = $this->withHeaders($this->authHeaders())
->withBodyFormat('json') ->withBodyFormat('json')
->call('post', $this->endpoint, $payload); ->call('post', $this->endpoint, $payload);
$response->assertStatus(201); $response->assertStatus(201);
$created = json_decode($response->getJSON(), true); $created = json_decode($response->getJSON(), true);
$id = $created['data']; $id = $created['data'];
$show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}"); $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$id}");
$show->assertStatus(200); $show->assertStatus(200);
$showData = json_decode($show->getJSON(), true)['data']; $showData = json_decode($show->getJSON(), true)['data'];
@ -60,201 +60,280 @@ class TestMapPatchTest extends CIUnitTestCase
return $showData; return $showData;
} }
public function testPartialUpdateTestMapSuccess() public function testIndexFiltersByHost(): void
{
$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()
{ {
$testMap = $this->createTestMap([ $testMap = $this->createTestMap([
'HostType' => 'SITE', 'HostType' => 'SITE',
'HostID' => 2, 'HostID' => 2,
'ClientType' => 'SITE', '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, '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' => [ 'details' => [
[ [
'HostTestCode' => 'PLT', 'HostTestCode' => 'HB',
'HostTestName' => 'Platelet', 'HostTestName' => 'Hemoglobin',
'ClientTestCode' => '5', 'ConDefID' => 1,
'ClientTestName' => 'Platelet', 'ClientTestCode' => '2',
'ClientTestName' => 'Hemoglobin',
], ],
], ],
]); ]);
$delete = $this->withHeaders($this->authHeaders()) $show = $this->withHeaders($this->authHeaders())->call('get', "{$this->endpoint}/{$testMap['TestMapID']}");
->withBodyFormat('json') $show->assertStatus(200);
->call('delete', $this->endpoint, ['TestMapID' => $testMap['TestMapID']]);
$delete->assertStatus(200); $showData = json_decode($show->getJSON(), true)['data'];
$this->assertNotEmpty($showData['details']);
$details = $this->withHeaders($this->authHeaders()) $this->assertArrayHasKey('ContainerLabel', $showData['details'][0]);
->call('get', "{$this->endpoint}/detail/by-testmap/{$testMap['TestMapID']}"); $this->assertNotEmpty($showData['details'][0]['ContainerLabel']);
$details->assertStatus(200);
$this->assertEquals([], json_decode($details->getJSON(), true)['data']);
} }
}
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']);
}
}