nhance/tests/smoke_claims_search.php

794 lines
30 KiB
PHP

<?php
/**
* Deep smoke test: EmployeeRestController::claimsSearch
*
* Covers GET (filter masters) and POST (search) behaviour against the live DB.
*
* Run:
* php tests/smoke_claims_search.php
* php tests/smoke_claims_search.php [client_id]
*/
declare(strict_types=1);
ob_start();
define('FCPATH', __DIR__ . '/../public/');
chdir(FCPATH);
require FCPATH . '../app/Config/Paths.php';
$paths = new Config\Paths();
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
defined('ENVIRONMENT') || define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development'));
$boot = APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
if (is_file($boot)) {
require_once $boot;
}
// Smoke encode/decode needs a secret; use a local fallback if JWT_SECRET is unset.
if (empty(env('JWT_SECRET'))) {
$smokeJwtSecret = 'smoke-claims-search-jwt-secret';
putenv('JWT_SECRET=' . $smokeJwtSecret);
$_ENV['JWT_SECRET'] = $smokeJwtSecret;
$_SERVER['JWT_SECRET'] = $smokeJwtSecret;
}
use App\Controllers\EmployeeRestController;
use App\Helpers\JWTToken;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use Config\Services;
use Firebase\JWT\JWT;
$pass = 0;
$fail = 0;
$skip = 0;
$results = [];
$notes = [];
function ok(string $label, bool $cond, string $detail = ''): void
{
global $pass, $fail, $results;
if ($cond) {
$pass++;
$results[] = '[PASS] ' . $label . ($detail !== '' ? "{$detail}" : '');
} else {
$fail++;
$results[] = '[FAIL] ' . $label . ($detail !== '' ? "{$detail}" : '');
}
}
function skip(string $label, string $detail = ''): void
{
global $skip, $results;
$skip++;
$results[] = '[SKIP] ' . $label . ($detail !== '' ? "{$detail}" : '');
}
function note(string $msg): void
{
global $notes;
$notes[] = $msg;
}
/**
* Build a HS512 JWT with allowed_active_policies (does not touch DB).
*
* @param list<int>|string|null $activePolicyIds
*/
function makeHrJwt($activePolicyIds): string
{
$claim = is_array($activePolicyIds)
? json_encode(array_values($activePolicyIds))
: $activePolicyIds;
$payload = [
'id' => 1,
'post_hr_id' => 1,
'allowed_active_policies' => $claim,
];
return JWT::encode($payload, (string) env('JWT_SECRET'), 'HS512');
}
/**
* Invoke claimsSearch with GET query or POST JSON body.
*
* @param list<int>|string|null|false $tokenPolicyIds false = no Authorization header
* @return array{http:int,body:array,raw:string,error:?string}
*/
function invokeClaimsSearch(string $method, array $payload = [], $tokenPolicyIds = false): array
{
try {
$uri = new URI('http://localhost/nhance_v2/claimsSearch');
$bodyString = null;
if (strtoupper($method) === 'GET') {
if ($payload !== []) {
$uri->setQuery(http_build_query($payload));
}
} else {
$bodyString = json_encode($payload, JSON_THROW_ON_ERROR);
}
$request = new IncomingRequest(
config('App'),
$uri,
$bodyString,
new UserAgent()
);
$request->setMethod(strtolower($method));
if (strtoupper($method) === 'GET') {
$request->setGlobal('get', $payload);
} else {
$request->setHeader('Content-Type', 'application/json');
$request->setGlobal('post', []);
}
if ($tokenPolicyIds !== false) {
$request->setHeader('Authorization', 'Bearer ' . makeHrJwt($tokenPolicyIds));
}
// Fresh controller per call — TicketController caches claimType in ctor
$controller = new EmployeeRestController();
$response = Services::response();
$logger = Services::logger();
$controller->initController($request, $response, $logger);
$resp = $controller->claimsSearch();
$raw = $resp->getBody();
$body = json_decode($raw, true);
return [
'http' => $resp->getStatusCode(),
'body' => is_array($body) ? $body : [],
'raw' => (string) $raw,
'error' => null,
];
} catch (Throwable $e) {
return [
'http' => 500,
'body' => [],
'raw' => '',
'error' => $e->getMessage(),
];
}
}
function ticketTypeIds(array $ticketTypes): array
{
return array_map('strval', array_column($ticketTypes, 'ticket_type'));
}
// ─── Fixtures from live DB ───────────────────────────────────────────────────
$db = db_connect();
$cliClientId = isset($argv[1]) && ctype_digit((string) $argv[1]) ? (int) $argv[1] : null;
$sampleTicket = $db->query(
"SELECT tm.id, tm.client_id, tm.client_policy_id, tm.claim_number, tm.claim_status_id,
tm.ticket_type_id, tm.emp_code, tm.emp_name, tm.created_at, tm.is_active,
cp.policy_no
FROM ticket_master tm
LEFT JOIN client_policy cp ON cp.id = tm.client_policy_id
WHERE tm.is_active = 1 AND tm.client_id IS NOT NULL
ORDER BY tm.id DESC
LIMIT 1"
)->getRowArray();
if ($cliClientId !== null) {
$override = $db->query(
"SELECT tm.id, tm.client_id, tm.client_policy_id, tm.claim_number, tm.claim_status_id,
tm.ticket_type_id, tm.emp_code, tm.emp_name, tm.created_at, tm.is_active,
cp.policy_no
FROM ticket_master tm
LEFT JOIN client_policy cp ON cp.id = tm.client_policy_id
WHERE tm.is_active = 1 AND tm.client_id = ?
ORDER BY tm.id DESC
LIMIT 1",
[$cliClientId]
)->getRowArray();
if ($override) {
$sampleTicket = $override;
}
}
ok('Fixture: active ticket available', !empty($sampleTicket['id']), json_encode($sampleTicket ?: []));
$clientId = (int) ($sampleTicket['client_id'] ?? 0);
$clientMd5 = $clientId > 0 ? md5((string) $clientId) : '';
$policyNo = $sampleTicket['policy_no'] ?? null;
$policyId = $sampleTicket['client_policy_id'] ?? null;
$claimStatusId = $sampleTicket['claim_status_id'] ?? null;
$ticketTypeId = $sampleTicket['ticket_type_id'] ?? null;
$empCode = $sampleTicket['emp_code'] ?? null;
$createdAt = $sampleTicket['created_at'] ?? null;
$policyTypes = $clientId > 0
? $db->query(
'SELECT DISTINCT policy_type_id FROM client_policy WHERE client_id = ? AND is_active = 1',
[$clientId]
)->getResultArray()
: [];
$policyTypeIds = array_map('intval', array_column($policyTypes, 'policy_type_id'));
$opdClient = $db->query(
'SELECT client_id FROM client_policy WHERE policy_type_id = 72 AND is_active = 1 LIMIT 1'
)->getRowArray();
$statusRow = $db->query(
"SELECT id, display_name FROM ticket_claim_status
WHERE is_active = 1 AND display_name IS NOT NULL AND display_name <> ''
LIMIT 1"
)->getRowArray();
$sameDisplayIds = [];
if (!empty($statusRow['display_name'])) {
$sameDisplayIds = array_map(
'intval',
array_column(
$db->query(
'SELECT id FROM ticket_claim_status WHERE is_active = 1 AND display_name = ?',
[$statusRow['display_name']]
)->getResultArray(),
'id'
)
);
}
$inactiveTicket = $db->query(
'SELECT id, client_id FROM ticket_master WHERE is_active = 0 AND client_id IS NOT NULL LIMIT 1'
)->getRowArray();
$expectedRowKeys = [
'id', 'ticket_type_id', 'original_status', 'status', 'cl_type', 'claim_no',
'claim_status_id', 'emp_name', 'emp_code', 'client_name', 'ticket_created_date',
'policy_no', 'policy_type', 'client_policy_no',
];
echo "========== claimsSearch DEEP SMOKE ==========\n";
echo 'client_id=' . $clientId . ' md5=' . $clientMd5 . "\n";
echo 'sample_ticket_id=' . ($sampleTicket['id'] ?? 'n/a') . "\n";
echo 'policy_types=' . json_encode($policyTypeIds) . "\n\n";
// ═══════════════════════════════════════════════════════════════════════════
// GET — filter masters
// ═══════════════════════════════════════════════════════════════════════════
$getNoClient = invokeClaimsSearch('GET');
$d = $getNoClient['body']['data'] ?? [];
ok(
'GET no client_id: envelope success/200',
($getNoClient['body']['status'] ?? '') === 'success' && (int) ($getNoClient['body']['code'] ?? 0) === 200,
json_encode(['status' => $getNoClient['body']['status'] ?? null, 'code' => $getNoClient['body']['code'] ?? null])
);
ok('GET no client_id: has claim_status', isset($d['claim_status']) && is_array($d['claim_status']) && count($d['claim_status']) > 0, 'count=' . count($d['claim_status'] ?? []));
ok('GET no client_id: has claim_type', isset($d['claim_type']) && is_array($d['claim_type']), 'keys=' . json_encode(array_keys($d['claim_type'] ?? [])));
ok(
'GET no client_id: default ticket_type = GMC/GPA/EDLI/GTLI',
ticketTypeIds($d['ticket_type'] ?? []) === ['1', '2', '3', '4'],
json_encode($d['ticket_type'] ?? [])
);
ok(
'GET no client_id: default ticket_type excludes OPD (72)',
!in_array('72', ticketTypeIds($d['ticket_type'] ?? []), true),
json_encode(ticketTypeIds($d['ticket_type'] ?? []))
);
$statusSample = $d['claim_status'][0] ?? [];
ok(
'GET claim_status rows expose id/ticket_type/claim_status',
isset($statusSample['id'], $statusSample['ticket_type'], $statusSample['claim_status']),
json_encode($statusSample)
);
$claimType = $d['claim_type'] ?? [];
ok(
'GET claim_type structure (type 1 + 2)',
isset($claimType[1][1], $claimType[2][1])
&& $claimType[1][1] === 'Main Hospitalization'
&& $claimType[2][1] === 'TTD',
json_encode($claimType)
);
// Numeric client_id — expected ticket types from policy_type_id mapping
if ($clientId > 0) {
$getClient = invokeClaimsSearch('GET', ['client_id' => $clientId]);
$types = ticketTypeIds($getClient['body']['data']['ticket_type'] ?? []);
$expect = [];
if (array_intersect($policyTypeIds, [2, 3, 4, 5])) {
$expect[] = '1'; // Claim-GMC
}
if (in_array(1, $policyTypeIds, true)) {
$expect[] = '2'; // Claim-GPA
}
if (in_array(6, $policyTypeIds, true)) {
$expect[] = '3'; // EDLI
}
if (in_array(7, $policyTypeIds, true)) {
$expect[] = '4'; // GTLI
}
if (in_array(72, $policyTypeIds, true)) {
$expect[] = '72'; // OPD
}
sort($expect, SORT_NUMERIC);
$got = $types;
sort($got, SORT_NUMERIC);
ok(
'GET numeric client_id: ticket_type mapped from active policies',
$got === $expect,
'expected=' . json_encode($expect) . ' got=' . json_encode($types) . ' policy_types=' . json_encode($policyTypeIds)
);
ok(
'GET numeric client_id: ticket_type sorted ascending',
$types === $got,
json_encode($types)
);
$getMd5 = invokeClaimsSearch('GET', ['client_id' => $clientMd5]);
ok(
'GET MD5 client_id resolves same ticket_type as numeric',
ticketTypeIds($getMd5['body']['data']['ticket_type'] ?? []) === $types,
'md5_types=' . json_encode(ticketTypeIds($getMd5['body']['data']['ticket_type'] ?? []))
);
} else {
skip('GET numeric/MD5 client_id mapping', 'no sample client');
}
if (!empty($opdClient['client_id'])) {
$getOpd = invokeClaimsSearch('GET', ['client_id' => (int) $opdClient['client_id']]);
ok(
'GET OPD client includes ticket_type 72',
in_array('72', ticketTypeIds($getOpd['body']['data']['ticket_type'] ?? []), true),
json_encode($getOpd['body']['data']['ticket_type'] ?? [])
);
} else {
skip('GET OPD ticket_type', 'no active OPD policy_type_id=72 client');
}
$getBogusMd5 = invokeClaimsSearch('GET', ['client_id' => str_repeat('a', 32)]);
ok(
'GET unknown MD5 client_id: still success envelope with empty/partial ticket_type',
($getBogusMd5['body']['status'] ?? '') === 'success'
&& isset($getBogusMd5['body']['data']['ticket_type']),
json_encode([
'status' => $getBogusMd5['body']['status'] ?? null,
'ticket_type' => $getBogusMd5['body']['data']['ticket_type'] ?? null,
])
);
// ═══════════════════════════════════════════════════════════════════════════
// POST — search
// ═══════════════════════════════════════════════════════════════════════════
$postNoToken = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], false);
ok(
'POST without JWT token: failed/404 empty',
$postNoToken['http'] === 200
&& ($postNoToken['body']['status'] ?? '') === 'failed'
&& (int) ($postNoToken['body']['code'] ?? 0) === 404
&& ($postNoToken['body']['data'] ?? null) === [],
json_encode($postNoToken['body'])
);
$postEmptyClaim = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], []);
ok(
'POST JWT allowed_active_policies=[] → failed/404 empty',
($postEmptyClaim['body']['status'] ?? '') === 'failed'
&& (int) ($postEmptyClaim['body']['code'] ?? 0) === 404
&& ($postEmptyClaim['body']['data'] ?? null) === [],
json_encode($postEmptyClaim['body'])
);
$postNullClaim = invokeClaimsSearch('POST', ['client_id' => $clientId ?: 1], null);
ok(
'POST JWT allowed_active_policies=null → failed/404 empty',
($postNullClaim['body']['status'] ?? '') === 'failed'
&& (int) ($postNullClaim['body']['code'] ?? 0) === 404
&& ($postNullClaim['body']['data'] ?? null) === [],
json_encode($postNullClaim['body'])
);
// Helper can parse JSON-string claim shape used in HR tokens
$helperJwt = makeHrJwt([(int) ($policyId ?: 1)]);
$helperReq = new IncomingRequest(
config('App'),
new URI('http://localhost/nhance_v2/claimsSearch'),
null,
new UserAgent()
);
$helperReq->setHeader('Authorization', 'Bearer ' . $helperJwt);
ok(
'JWTToken::getValueFromToken(request, key) parses JSON claim',
JWTToken::getValueFromToken($helperReq, 'allowed_active_policies', []) === [(int) ($policyId ?: 1)],
json_encode(JWTToken::getValueFromToken($helperReq, 'allowed_active_policies', []))
);
ok(
'JWTToken::getValueFromToken returns default for missing key',
JWTToken::getValueFromToken($helperReq, 'not_a_real_claim', 'missing') === 'missing',
(string) JWTToken::getValueFromToken($helperReq, 'not_a_real_claim', 'missing')
);
if ($clientId > 0 && !empty($policyId)) {
$tokenPolicies = [(int) $policyId];
$basePost = ['client_id' => $clientId];
$byClient = invokeClaimsSearch('POST', $basePost, $tokenPolicies);
$rows = $byClient['body']['data'] ?? [];
$allMatch = is_array($rows) && count($rows) > 0;
if ($allMatch) {
$ids = array_column($rows, 'id');
$mismatch = $db->query(
'SELECT COUNT(*) AS c FROM ticket_master WHERE id IN (' . implode(',', array_map('intval', $ids)) . ') AND client_id <> ?',
[$clientId]
)->getRowArray();
ok(
'POST client_id: all returned tickets belong to client',
(int) ($mismatch['c'] ?? 1) === 0,
'rows=' . count($rows) . ' mismatches=' . ($mismatch['c'] ?? '?')
);
ok(
'POST client_id: includes fixture ticket',
in_array((string) $sampleTicket['id'], array_map('strval', $ids), true)
|| in_array((int) $sampleTicket['id'], array_map('intval', $ids), true),
'fixture_id=' . $sampleTicket['id']
);
$missingKeys = [];
foreach ($expectedRowKeys as $k) {
if (!array_key_exists($k, $rows[0])) {
$missingKeys[] = $k;
}
}
ok('POST result row has expected keys', $missingKeys === [], 'missing=' . json_encode($missingKeys));
ok(
'POST status is UPPER display/fallback',
is_string($rows[0]['status'] ?? null)
&& ($rows[0]['status'] === '' || $rows[0]['status'] === strtoupper($rows[0]['status'])),
'status=' . ($rows[0]['status'] ?? 'null')
);
ok(
'POST cl_type present (Reimbursement fallback or tpa_claim_type)',
isset($rows[0]['cl_type']) && $rows[0]['cl_type'] !== '',
'cl_type=' . ($rows[0]['cl_type'] ?? 'null')
);
ok(
'POST with active_policy_ids: ordered by id DESC',
count($rows) < 2 || (int) $rows[0]['id'] >= (int) $rows[1]['id'],
'first=' . ($rows[0]['id'] ?? '?') . ' second=' . ($rows[1]['id'] ?? 'n/a')
);
} else {
ok('POST client_id: returns data for known client', false, json_encode($byClient['body']));
}
$byMd5 = invokeClaimsSearch('POST', [
'client_id' => $clientMd5,
], $tokenPolicies);
ok(
'POST MD5 client_id: same row count as numeric',
count($byMd5['body']['data'] ?? []) === count($rows),
'numeric=' . count($rows) . ' md5=' . count($byMd5['body']['data'] ?? [])
);
// Body active_policy_ids must NOT widen access beyond token
$bodyWiden = invokeClaimsSearch('POST', [
'client_id' => $clientId,
'active_policy_ids' => [(int) $policyId, 999999999],
], $tokenPolicies);
ok(
'POST ignores body active_policy_ids (token is source of truth)',
count($bodyWiden['body']['data'] ?? []) === count($rows),
'token_only=' . count($rows) . ' with_body_extra=' . count($bodyWiden['body']['data'] ?? [])
);
// Date range — day of fixture created_at
if (!empty($createdAt)) {
$day = date('d-m-Y', strtotime($createdAt));
$byDate = invokeClaimsSearch('POST', $basePost + [
'from_date' => $day,
'to_date' => $day,
], $tokenPolicies);
$dateRows = $byDate['body']['data'] ?? [];
$idsOnDay = array_map('strval', array_column($dateRows, 'id'));
ok(
'POST from_date+to_date includes fixture on that day',
in_array((string) $sampleTicket['id'], $idsOnDay, true),
'day=' . $day . ' count=' . count($dateRows)
);
$onlyFrom = invokeClaimsSearch('POST', $basePost + [
'from_date' => $day,
], $tokenPolicies);
// Code requires BOTH from_date AND to_date — partial should not apply date filter
ok(
'POST from_date alone does NOT restrict by date (needs both)',
count($onlyFrom['body']['data'] ?? []) >= count($dateRows),
'with_both=' . count($dateRows) . ' from_only=' . count($onlyFrom['body']['data'] ?? [])
);
$farPast = invokeClaimsSearch('POST', $basePost + [
'from_date' => '01-01-2000',
'to_date' => '02-01-2000',
], $tokenPolicies);
ok(
'POST date range with no tickets → failed/404 empty',
($farPast['body']['status'] ?? '') === 'failed'
&& (int) ($farPast['body']['code'] ?? 0) === 404
&& ($farPast['body']['data'] ?? null) === [],
json_encode([
'status' => $farPast['body']['status'] ?? null,
'code' => $farPast['body']['code'] ?? null,
'data' => $farPast['body']['data'] ?? null,
])
);
}
if (!empty($ticketTypeId)) {
$byType = invokeClaimsSearch('POST', $basePost + [
'ticket_type_id' => $ticketTypeId,
], $tokenPolicies);
$typeRows = $byType['body']['data'] ?? [];
$badType = 0;
foreach ($typeRows as $r) {
if ((string) ($r['ticket_type_id'] ?? '') !== (string) $ticketTypeId) {
$badType++;
}
}
ok(
'POST dynamic where ticket_type_id filters correctly',
count($typeRows) > 0 && $badType === 0,
'ticket_type_id=' . $ticketTypeId . ' rows=' . count($typeRows) . ' bad=' . $badType
);
}
if (!empty($empCode)) {
$byEmp = invokeClaimsSearch('POST', $basePost + [
'emp_code' => $empCode,
], $tokenPolicies);
$empRows = $byEmp['body']['data'] ?? [];
$badEmp = 0;
foreach ($empRows as $r) {
if ((string) ($r['emp_code'] ?? '') !== (string) $empCode) {
$badEmp++;
}
}
ok(
'POST dynamic where emp_code filters correctly',
count($empRows) > 0 && $badEmp === 0,
'emp_code=' . $empCode . ' rows=' . count($empRows) . ' bad=' . $badEmp
);
}
// Empty / zero values must be ignored in dynamic where
$ignoreZero = invokeClaimsSearch('POST', $basePost + [
'ticket_type_id' => 0,
'claim_type' => '',
'priority' => '0',
], $tokenPolicies);
ok(
'POST empty/0 dynamic fields ignored (count ≈ client-only)',
count($ignoreZero['body']['data'] ?? []) === count($rows),
'client_only=' . count($rows) . ' with_empties=' . count($ignoreZero['body']['data'] ?? [])
);
// client_branch_id is explicitly unset — must not break search
$withBranch = invokeClaimsSearch('POST', $basePost + [
'client_branch_id' => 999999,
], $tokenPolicies);
ok(
'POST client_branch_id is ignored (unset before where)',
count($withBranch['body']['data'] ?? []) === count($rows),
'without=' . count($rows) . ' with_branch=' . count($withBranch['body']['data'] ?? [])
);
if (!empty($policyNo)) {
$byPolicy = invokeClaimsSearch('POST', $basePost + [
'policy_no' => $policyNo,
], $tokenPolicies);
$policyRows = $byPolicy['body']['data'] ?? [];
$badPol = 0;
foreach ($policyRows as $r) {
if (($r['client_policy_no'] ?? null) !== null
&& (string) $r['client_policy_no'] !== (string) $policyNo
&& (string) ($r['policy_no'] ?? '') !== (string) $policyNo
) {
if ((string) ($r['client_policy_no'] ?? '') !== (string) $policyNo) {
$badPol++;
}
}
}
ok(
'POST policy_no filters via client_policy.policy_no',
count($policyRows) > 0 && $badPol === 0,
'policy_no=' . $policyNo . ' rows=' . count($policyRows) . ' bad=' . $badPol
);
} else {
skip('POST policy_no filter', 'fixture has no policy_no');
}
$byActive = invokeClaimsSearch('POST', $basePost, $tokenPolicies);
ok(
'POST token active_policy_ids does not fatal',
$byActive['error'] === null,
$byActive['error'] ?? 'ok'
);
$activeRows = $byActive['body']['data'] ?? [];
$badActive = 0;
foreach ($activeRows as $r) {
if ((string) ($r['client_policy_id'] ?? '') !== (string) $policyId) {
$badActive++;
}
}
ok(
'POST token allowed_active_policies restricts to those client_policy ids',
$byActive['error'] === null && count($activeRows) > 0 && $badActive === 0,
'policy_id=' . $policyId . ' rows=' . count($activeRows) . ' bad=' . $badActive
);
$byMulti = invokeClaimsSearch('POST', $basePost, [(int) $policyId, 0]);
ok(
'POST token with zero id still returns fixture policy rows',
$byMulti['error'] === null && count($byMulti['body']['data'] ?? []) >= count($activeRows),
'single=' . count($activeRows) . ' multi=' . count($byMulti['body']['data'] ?? [])
);
if (!empty($claimStatusId) || !empty($statusRow['id'])) {
$statusFilterId = (int) ($statusRow['id'] ?? $claimStatusId);
$byStatus = invokeClaimsSearch('POST', $basePost + [
'claim_status_id' => $statusFilterId,
], $tokenPolicies);
$statusRows = $byStatus['body']['data'] ?? [];
$allowed = $sameDisplayIds ?: [(int) $statusFilterId];
$badSt = 0;
foreach ($statusRows as $r) {
if (!in_array((int) ($r['claim_status_id'] ?? 0), $allowed, true)) {
$badSt++;
}
}
ok(
'POST claim_status_id expands via display_name helper',
$badSt === 0,
'filter_id=' . $statusFilterId
. ' allowed=' . json_encode($allowed)
. ' rows=' . count($statusRows)
. ' bad=' . $badSt
. ' display=' . ($statusRow['display_name'] ?? '')
);
$ctrl = new EmployeeRestController();
$expanded = $ctrl->getTicketClaimStatusIdBasedOnTheDisplayName($statusFilterId);
sort($expanded);
$expectedExpanded = $allowed;
sort($expectedExpanded);
ok(
'Helper getTicketClaimStatusIdBasedOnTheDisplayName matches DB',
array_map('intval', $expanded) === array_map('intval', $expectedExpanded),
'helper=' . json_encode($expanded) . ' db=' . json_encode($expectedExpanded)
);
}
// Combined filters should be AND
if (!empty($ticketTypeId) && !empty($createdAt)) {
$day = date('d-m-Y', strtotime($createdAt));
$combined = invokeClaimsSearch('POST', $basePost + [
'ticket_type_id' => $ticketTypeId,
'from_date' => $day,
'to_date' => $day,
'emp_code' => $empCode,
], $tokenPolicies);
$cRows = $combined['body']['data'] ?? [];
$okCombined = true;
foreach ($cRows as $r) {
if ((string) ($r['ticket_type_id'] ?? '') !== (string) $ticketTypeId) {
$okCombined = false;
}
if ($empCode && (string) ($r['emp_code'] ?? '') !== (string) $empCode) {
$okCombined = false;
}
}
ok(
'POST combined filters use AND semantics',
$okCombined && (
in_array((string) $sampleTicket['id'], array_map('strval', array_column($cRows, 'id')), true)
|| count($cRows) === 0
),
'rows=' . count($cRows) . ' fixture_in=' . (in_array((string) $sampleTicket['id'], array_map('strval', array_column($cRows, 'id')), true) ? 'yes' : 'no')
);
}
// Unknown client → empty failed
$unknown = invokeClaimsSearch('POST', [
'client_id' => 999999999,
], $tokenPolicies);
ok(
'POST unknown client_id → failed/404 empty data',
($unknown['body']['status'] ?? '') === 'failed'
&& (int) ($unknown['body']['code'] ?? 0) === 404
&& ($unknown['body']['data'] ?? null) === [],
json_encode($unknown['body'])
);
// Inactive tickets must never appear
if (!empty($inactiveTicket['id'])) {
$huntInactive = invokeClaimsSearch('POST', [
'client_id' => (int) $inactiveTicket['client_id'],
], $tokenPolicies);
$inactiveFound = false;
foreach ($huntInactive['body']['data'] ?? [] as $r) {
if ((int) ($r['id'] ?? 0) === (int) $inactiveTicket['id']) {
$inactiveFound = true;
break;
}
}
ok(
'POST excludes is_active=0 tickets',
!$inactiveFound,
'inactive_id=' . $inactiveTicket['id']
);
} else {
skip('POST inactive exclusion', 'no inactive ticket fixture');
}
// SQL-ish junk in dates — should not fatal
$inj = invokeClaimsSearch('POST', $basePost + [
'from_date' => "01-01-2020' OR '1'='1",
'to_date' => '31-12-2020',
], $tokenPolicies);
ok(
'POST crafted from_date does not fatal',
isset($inj['body']['status']) || $inj['error'] !== null,
'status=' . ($inj['body']['status'] ?? 'null') . ' http=' . $inj['http'] . ' error=' . ($inj['error'] ?? 'null')
);
if (($inj['body']['status'] ?? '') === 'success' && count($inj['body']['data'] ?? []) > 0) {
note('SECURITY: date filter is string-interpolated; crafted from_date still returned rows — review SQL injection risk');
}
// Dynamic where column injection attempt — unknown keys become tm.<key>
$bogusCol = invokeClaimsSearch('POST', $basePost + [
'definitely_not_a_col' => 'x',
], $tokenPolicies);
ok(
'POST unknown dynamic field handled without uncaught fatal',
true,
'error=' . ($bogusCol['error'] ?? 'null')
. ' status=' . ($bogusCol['body']['status'] ?? 'null')
. ' code=' . ($bogusCol['body']['code'] ?? 'null')
);
if ($bogusCol['error'] !== null
|| str_contains(strtolower($bogusCol['raw']), 'unknown column')
|| str_contains(strtolower($bogusCol['raw']), 'sqlstate')
) {
note('BUG: unknown JSON keys are passed into WHERE as tm.<key> — ' . ($bogusCol['error'] ?: 'SQL error in response'));
}
} else {
skip('POST client-scoped filters', 'no sample client/policy');
}
// ─── Report ─────────────────────────────────────────────────────────────────
ob_end_clean();
foreach ($results as $line) {
echo $line . PHP_EOL;
}
if ($notes !== []) {
echo PHP_EOL . '──── Notes / findings ────' . PHP_EOL;
foreach ($notes as $n) {
echo '- ' . $n . PHP_EOL;
}
}
echo PHP_EOL . "Summary: PASS={$pass} FAIL={$fail} SKIP={$skip}" . PHP_EOL;
exit($fail > 0 ? 1 : 0);