'One folder name, or all (default: all wired folders).', '--list' => 'List every scenario ID without executing.', ]; private int $pass = 0; private int $fail = 0; private int $skip = 0; /** @var list */ private array $catalog = []; /** @var list */ private array $wiredModules = [ 'claim_files', 'client_kyc_documents', 'excel', 'import_excel', 'lead_files', 'attachments', 'claim_dump_excel', 'claims_mis', 'claims_dump', 'statements', 'bds_dump_excel', 'hr_files', 'non_eb_asset_files', 'non_eb_rack_rate', ]; public function run(array $params) { helper('utility'); $this->buildCatalog(); if (CLI::getOption('list') !== null) { $this->printCatalog(); return EXIT_SUCCESS; } $storage = \Config\Services::getFileStorageService(false); $moduleOpt = strtolower((string) (CLI::getOption('module') ?? 'all')); $modules = $moduleOpt === 'all' || $moduleOpt === '' ? $this->wiredModules : [basename($moduleOpt)]; CLI::write('========== STORAGE FULL SCENARIO RUN ==========', 'cyan'); CLI::write('usesS3: ' . ($storage->usesS3() ? 'YES' : 'NO')); CLI::write('Bucket: ' . ($storage->getBucket() ?? 'N/A')); CLI::write('RETAIN_LOCAL: ' . (storage_env_retain_local() ? 'true' : 'false')); CLI::write('Modules: ' . implode(', ', $modules)); CLI::write('Total catalog scenarios: ' . count($this->catalog)); CLI::newLine(); // ENV / helper checks $this->runEnvScenarios($storage); // Per-module full lifecycle for every wired folder foreach ($modules as $mod) { $this->runModuleLifecycle($storage, $mod); } // Cross-cutting claim / mail / mirror / soft-delete $this->runClaimScenarios($storage); $this->runMailScenarios($storage); $this->runMirrorScenario($storage); $this->runRetainLocalSimulation($storage); $this->runClaimsDumpResolve($storage); CLI::newLine(); CLI::write( sprintf('RESULT: %d passed | %d failed | %d skipped', $this->pass, $this->fail, $this->skip), $this->fail === 0 ? 'green' : 'red' ); CLI::newLine(); $this->printManualOnlyRemaining(); return $this->fail > 0 ? EXIT_ERROR : EXIT_SUCCESS; } private function buildCatalog(): void { $this->catalog = [ ['id' => 'ENV-01', 'group' => 'ENV', 'title' => 'FILE_STORAGE / bucket / RETAIN_LOCAL readable'], ['id' => 'ENV-02', 'group' => 'ENV', 'title' => 'Runtime temp dirs writable'], ['id' => 'ENV-03', 'group' => 'ENV', 'title' => 'usesS3 matches FILE_STORAGE_DRIVER'], ]; foreach ($this->wiredModules as $mod) { $this->catalog[] = ['id' => "MOD-{$mod}-01", 'group' => "MODULE:{$mod}", 'title' => 'Upload to S3/local']; $this->catalog[] = ['id' => "MOD-{$mod}-02", 'group' => "MODULE:{$mod}", 'title' => 'Exists after upload']; $this->catalog[] = ['id' => "MOD-{$mod}-03", 'group' => "MODULE:{$mod}", 'title' => 'Download content']; $this->catalog[] = ['id' => "MOD-{$mod}-04", 'group' => "MODULE:{$mod}", 'title' => 'ensure_local from storage']; $this->catalog[] = ['id' => "MOD-{$mod}-05", 'group' => "MODULE:{$mod}", 'title' => 'No permanent local when RETAIN_LOCAL=false (S3)']; $this->catalog[] = ['id' => "MOD-{$mod}-06", 'group' => "MODULE:{$mod}", 'title' => 'Delete removes object']; $this->catalog[] = ['id' => "MOD-{$mod}-07", 'group' => "MODULE:{$mod}", 'title' => 'Presigned URL (S3 only)']; } $extra = [ ['id' => 'CLAIM-01', 'group' => 'CLAIM', 'title' => 'resolve_claim_file_path'], ['id' => 'CLAIM-02', 'group' => 'CLAIM', 'title' => 'claim_file_download_url (TPA)'], ['id' => 'CLAIM-03', 'group' => 'CLAIM', 'title' => 'soft_delete cleans S3 when unused (DB)'], ['id' => 'MERGE-PDF', 'group' => 'CLAIM', 'title' => 'Dedicated PDF merge suite — php spark storage:smoke-test-merge-pdf'], ['id' => 'MAIL-01', 'group' => 'MAIL', 'title' => 'resolve_mail_attachment_path'], ['id' => 'GEN-01', 'group' => 'GENERATED', 'title' => 'mirror_generated_file then local gone'], ['id' => 'RETAIN-01', 'group' => 'RETAIN_LOCAL', 'title' => 'Simulate retain=false (S3-only)'], ['id' => 'RETAIN-02', 'group' => 'RETAIN_LOCAL', 'title' => 'Simulate retain=true (dual-write keep local)'], ['id' => 'DUMP-01', 'group' => 'CLAIMS_DUMP', 'title' => 'resolveDumpLocalPath / ensure_local'], ]; foreach ($extra as $row) { $this->catalog[] = $row; } } private function printCatalog(): void { CLI::write('========== ALL SCENARIO TEST CASES ==========', 'cyan'); $current = ''; foreach ($this->catalog as $row) { if ($row['group'] !== $current) { $current = $row['group']; CLI::newLine(); CLI::write("--- {$current} ---", 'yellow'); } CLI::write(" {$row['id']} {$row['title']}"); } CLI::newLine(); CLI::write('Manual-only (UI/TPA jobs — run by human after automated):', 'yellow'); foreach ($this->manualScenarioLines() as $line) { CLI::write(' ' . $line); } CLI::newLine(); CLI::write('Run all automated: php spark storage:smoke-test'); } private function step(string $id, bool $ok, string $detail = ''): void { $title = $this->titleFor($id); if ($ok) { $this->pass++; CLI::write("[PASS] {$id} {$title}" . ($detail !== '' ? " — {$detail}" : ''), 'green'); } else { $this->fail++; CLI::write("[FAIL] {$id} {$title}" . ($detail !== '' ? " — {$detail}" : ''), 'red'); } } private function skip(string $id, string $reason): void { $this->skip++; CLI::write("[SKIP] {$id} {$this->titleFor($id)} — {$reason}", 'yellow'); } private function titleFor(string $id): string { foreach ($this->catalog as $row) { if ($row['id'] === $id) { return $row['title']; } } return $id; } private function uploadPath(string $mod): string { return rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $mod; } private function makeTmp(string $name, string $body): string { $dir = storage_runtime_temp_dir(); if (! is_dir($dir)) { mkdir($dir, 0755, true); } $path = $dir . DIRECTORY_SEPARATOR . $name; file_put_contents($path, $body); return $path; } private function runEnvScenarios($storage): void { CLI::write('--- ENV ---', 'white'); $driver = strtolower((string) (getenv('FILE_STORAGE_DRIVER') ?: env('FILE_STORAGE_DRIVER') ?: '')); $bucket = (string) (getenv('AWS_FILE_UPLOAD_BUCKET') ?: getenv('AWS_BUCKET') ?: ''); $this->step('ENV-01', true, "driver=" . ($driver !== '' ? $driver : 'default') . ", retain=" . (storage_env_retain_local() ? '1' : '0') . ", bucket=" . ($bucket !== '' ? 'set' : 'empty')); $runtime = storage_runtime_temp_dir(); $claimRt = rtrim(WRITEPATH, '/\\') . '/cache/claim_files_runtime'; foreach ([$runtime, $claimRt] as $d) { if (! is_dir($d)) { @mkdir($d, 0755, true); } } $probe = $runtime . DIRECTORY_SEPARATOR . 'probe_' . uniqid('', true) . '.txt'; $wrote = @file_put_contents($probe, 'ok') !== false; if ($wrote) { @unlink($probe); } $this->step('ENV-02', $wrote && is_dir($runtime)); if ($driver === 'local') { $this->step('ENV-03', ! $storage->usesS3(), 'local driver => usesS3 false'); } elseif ($driver === 's3' || $driver === '') { $this->step('ENV-03', $storage->usesS3(), 's3 driver => usesS3 true (bucket required)'); } else { $this->skip('ENV-03', 'unknown FILE_STORAGE_DRIVER=' . $driver); } } private function runModuleLifecycle($storage, string $mod): void { CLI::write("--- MODULE: {$mod} ---", 'white'); $path = $this->uploadPath($mod); $fileName = 'smoke_' . $mod . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt'; $tmp = $this->makeTmp($fileName, "smoke-{$mod}\n" . date('c') . "\n"); $up = $storage->upload($tmp, $path, $fileName); @unlink($tmp); $this->step("MOD-{$mod}-01", (bool) ($up['success'] ?? false), (string) ($up['message'] ?? '')); if (! ($up['success'] ?? false)) { // Skip rest of module if upload failed foreach (['02', '03', '04', '05', '06', '07'] as $n) { $this->skip("MOD-{$mod}-{$n}", 'upload failed'); } return; } $this->step("MOD-{$mod}-02", $storage->exists($path, $fileName)); $dl = $storage->download($path, $fileName); $contentOk = ($dl['success'] ?? false) && ( ((! empty($dl['content'])) && str_contains((string) $dl['content'], "smoke-{$mod}")) || ((! empty($dl['path'])) && is_file((string) $dl['path']) && str_contains((string) file_get_contents((string) $dl['path']), "smoke-{$mod}")) ); $this->step("MOD-{$mod}-03", $contentOk, (string) ($dl['message'] ?? '')); // Force missing permanent local then ensure_local $permanent = $storage->resolveLocalPath($path, $fileName); if (is_file($permanent)) { @unlink($permanent); } $ensured = storage_ensure_local_file($path, $fileName); $this->step( "MOD-{$mod}-04", is_string($ensured) && is_file($ensured) && str_contains((string) file_get_contents($ensured), "smoke-{$mod}"), $ensured ? basename($ensured) : 'null' ); if ($ensured) { storage_cleanup_temp_file($ensured); } if ($storage->usesS3() && ! storage_env_retain_local()) { $this->step("MOD-{$mod}-05", ! is_file($permanent), 'permanent path absent'); } else { $this->skip("MOD-{$mod}-05", $storage->usesS3() ? 'RETAIN_LOCAL=true' : 'local driver'); } $del = $storage->delete($path, $fileName); $this->step("MOD-{$mod}-06", (bool) ($del['success'] ?? false) && ! $storage->exists($path, $fileName)); if ($storage->usesS3()) { $tmp = $this->makeTmp($fileName, "presign-{$mod}\n"); $storage->upload($tmp, $path, $fileName); @unlink($tmp); $pre = $storage->getPresignedUrl($path, $fileName, 5); $this->step("MOD-{$mod}-07", (bool) ($pre['success'] ?? false) && ! empty($pre['url'])); $storage->delete($path, $fileName); } else { $this->skip("MOD-{$mod}-07", 'local driver'); } } private function runClaimScenarios($storage): void { CLI::write('--- CLAIM ---', 'white'); $claimDir = $this->uploadPath('claim_files'); $fileName = 'smoke_claim_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.pdf'; $tmp = $this->makeTmp($fileName, "%PDF-1.4 smoke-claim\n"); $up = $storage->upload($tmp, $claimDir, $fileName); @unlink($tmp); if (! ($up['success'] ?? false)) { $this->skip('CLAIM-01', 'upload claim sample failed'); $this->skip('CLAIM-02', 'upload claim sample failed'); $this->skip('CLAIM-03', 'upload claim sample failed'); return; } // Ensure no permanent leftover interferes $permanent = $storage->resolveLocalPath($claimDir, $fileName); if (is_file($permanent)) { @unlink($permanent); } $resolved = storage_resolve_claim_file_path($claimDir, $fileName); $this->step( 'CLAIM-01', (bool) ($resolved['success'] ?? false) && ! empty($resolved['path']) && is_file((string) $resolved['path']), (string) ($resolved['message'] ?? '') ); storage_cleanup_temp_claim_file($resolved); if ($storage->usesS3()) { $url = storage_claim_file_download_url($fileName, null, 10); $this->step('CLAIM-02', is_string($url) && str_starts_with($url, 'http')); } else { $this->skip('CLAIM-02', 'local driver'); } // Soft-delete: insert temp claim_files row, soft delete, expect S3 gone try { $db = \Config\Database::connect(); $insert = [ 'ticket_master_id' => 0, 'file_name' => $fileName, 'url' => $fileName, 'doc_name' => 'smoke-test', 'file_type' => 2, 'is_active' => 1, 'created_at' => date('Y-m-d H:i:s'), ]; // Only use columns that exist $fields = $db->getFieldNames('claim_files'); $row = []; foreach ($insert as $k => $v) { if (in_array($k, $fields, true)) { $row[$k] = $v; } } $db->table('claim_files')->insert($row); $id = (int) $db->insertID(); $ok = $id > 0 && storage_soft_delete_claim_file($id); $gone = ! $storage->exists($claimDir, $fileName); $this->step('CLAIM-03', $ok && $gone, 'id=' . $id); // Hard cleanup leftover row if ($id > 0) { $db->table('claim_files')->where('id', $id)->delete(); } } catch (\Throwable $e) { $this->skip('CLAIM-03', 'DB soft-delete unavailable: ' . $e->getMessage()); $storage->delete($claimDir, $fileName); } } private function runMailScenarios($storage): void { CLI::write('--- MAIL ---', 'white'); $dir = $this->uploadPath('attachments'); $fileName = 'smoke_mail_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt'; $tmp = $this->makeTmp($fileName, "mail\n"); $up = $storage->upload($tmp, $dir, $fileName); @unlink($tmp); if (! ($up['success'] ?? false)) { $this->step('MAIL-01', false, (string) ($up['message'] ?? '')); return; } $permanent = $storage->resolveLocalPath($dir, $fileName); if (is_file($permanent)) { @unlink($permanent); } $resolved = storage_resolve_mail_attachment_path('uploads/attachments/' . $fileName, $fileName); $this->step( 'MAIL-01', (bool) ($resolved['success'] ?? false) && ! empty($resolved['path']) && is_file((string) $resolved['path']) ); if (! empty($resolved['path'])) { storage_cleanup_temp_file((string) $resolved['path']); } $storage->delete($dir, $fileName); } private function runMirrorScenario($storage): void { CLI::write('--- GENERATED ---', 'white'); $dir = $this->uploadPath('excel'); $fileName = 'smoke_gen_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.txt'; $local = $this->makeTmp($fileName, "generated\n"); $ok = storage_mirror_generated_file($local, $dir, $fileName); if ($storage->usesS3()) { $this->step('GEN-01', $ok && ! is_file($local) && $storage->exists($dir, $fileName)); $storage->delete($dir, $fileName); } else { $this->step('GEN-01', $ok && is_file($local)); @unlink($local); } } private function runRetainLocalSimulation($storage): void { CLI::write('--- RETAIN_LOCAL simulation ---', 'white'); if (! $storage->usesS3()) { $this->skip('RETAIN-01', 'local driver'); $this->skip('RETAIN-02', 'local driver'); return; } $dir = $this->uploadPath('excel'); // retain=false behaviour: upload via temp then unlink (current default path) $f1 = 'smoke_retain_off_' . bin2hex(random_bytes(2)) . '.txt'; $t1 = $this->makeTmp($f1, "off\n"); $storage->upload($t1, $dir, $f1); @unlink($t1); $p1 = $storage->resolveLocalPath($dir, $f1); $this->step('RETAIN-01', ! is_file($p1) && $storage->exists($dir, $f1)); $storage->delete($dir, $f1); // retain=true behaviour: keep local file after S3 upload $f2 = 'smoke_retain_on_' . bin2hex(random_bytes(2)) . '.txt'; // Use runtime path as the "kept local" copy (CLI may lack write perms under uploads/) $p2 = $this->makeTmp($f2, "on\n"); $up2 = $storage->upload($p2, $dir, $f2); $this->step( 'RETAIN-02', (bool) ($up2['success'] ?? false) && is_file($p2) && $storage->exists($dir, $f2), 'local kept + S3' ); @unlink($p2); $storage->delete($dir, $f2); } private function runClaimsDumpResolve($storage): void { CLI::write('--- CLAIMS_DUMP ---', 'white'); $dir = $this->uploadPath('claims_dump'); $fileName = 'smoke_cdump_' . date('Ymd_His') . '_' . bin2hex(random_bytes(2)) . '.xlsx'; $tmp = $this->makeTmp($fileName, "claims-dump\n"); $up = $storage->upload($tmp, $dir, $fileName); @unlink($tmp); if (! ($up['success'] ?? false)) { $this->step('DUMP-01', false, (string) ($up['message'] ?? '')); return; } $permanent = $storage->resolveLocalPath($dir, $fileName); if (is_file($permanent)) { @unlink($permanent); } $resolved = storage_ensure_local_file($dir, $fileName); $this->step('DUMP-01', is_string($resolved) && is_file($resolved)); if ($resolved) { storage_cleanup_temp_file($resolved); } $storage->delete($dir, $fileName); } /** @return list */ private function manualScenarioLines(): array { return [ 'UI-KYC-01 Upload KYC in browser', 'UI-KYC-02 Download download-kyc-docs/{id}', 'UI-KYC-03 Re-upload / delete KYC', 'UI-PT-01 Upload PT doc + download-pt-docs/{id}', 'UI-PT-02 REST downloadPolicyFiles', 'UI-CLAIM-01 Upload claim file in ticket UI', 'UI-CLAIM-02 downloadClaimFile / viewClaimFile', 'UI-CLAIM-03 Remove claim file in UI', 'UI-CLAIM-04 Merge PDFs in UI (core automated: php spark storage:smoke-test-merge-pdf)', 'UI-TPA-01 FHPL claim submit (base64)', 'UI-TPA-02 Vidal claim submit (base64)', 'UI-TPA-03 HealthIndia claim submit', 'UI-TPA-04 MediAssist uses presigned URL', 'UI-TPA-05 Volo uses presigned URL', 'UI-EMP-01 Employee excel upload + background job', 'UI-EMP-02 download-file-list / download_import_file', 'UI-EMP-03 HR upload + hrFileDownload', 'UI-LEAD-01 Lead member upload + demography job', 'UI-LEAD-02 downloadMemberFile / error excel', 'UI-DUMP-01 Claim dump / MIS / statements / BDS UI jobs', 'UI-MAIL-01 Compose mail with attachment send', 'UI-NEB-01 Non-EB asset + rack rate UI', 'UI-NEG-01 Invalid extension rejected in UI', 'UI-NEG-02 Toggle FILE_STORAGE_DRIVER=local and retest one upload', 'UI-NEG-03 Toggle RETAIN_LOCAL=true/false and retest one upload', ]; } private function printManualOnlyRemaining(): void { CLI::write('========== MANUAL UI / TPA / JOB (not auto-runnable) ==========', 'yellow'); CLI::write('These need browser login / real TPA — tick after you test:'); foreach ($this->manualScenarioLines() as $i => $line) { CLI::write(sprintf(' [ ] %s', $line)); } CLI::newLine(); CLI::write('Commands:'); CLI::write(' php spark storage:smoke-test # run ALL automated scenarios'); CLI::write(' php spark storage:smoke-test --list # list every scenario ID'); CLI::write(' php spark storage:smoke-test --module claim_files'); } }