794 lines
30 KiB
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);
|