nhance_partner_be/app/Commands/StorageUploadTest.php

301 lines
11 KiB
PHP

<?php
namespace App\Commands;
use App\Services\Storage\FileStorageException;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Storage;
class StorageUploadTest extends BaseCommand
{
protected $group = 'Testing';
protected $name = 'storage:upload-test';
protected $description = 'Run a live file upload/download/delete check against configured storage (local or S3).';
protected $usage = 'storage:upload-test [options]';
protected $options = [
'--keep' => 'Keep uploaded test files in storage (default: delete after success).',
'--pdf' => 'Use this PDF file path for policy_pdf upload instead of a generated sample.',
];
/** @var list<array{module: string, subFolder: string, fileName: string}> */
private array $uploadedObjects = [];
private string $tempDir = '';
public function run(array $params)
{
helper('storage_helper');
$config = config('Storage');
$this->printEnvironment($config);
$this->tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'nhance_storage_upload_test_' . uniqid('', true);
if (! mkdir($this->tempDir, 0777, true) && ! is_dir($this->tempDir)) {
CLI::error('Unable to create temp directory: ' . $this->tempDir);
return EXIT_ERROR;
}
$keepFiles = (bool) CLI::getOption('keep');
$failed = 0;
$passed = 0;
CLI::newLine();
CLI::write('Starting live storage upload scenarios...', 'yellow');
CLI::newLine();
$scenarios = [
'policy_pdf' => fn () => $this->scenarioPolicyPdf(),
'policy_payment_receipt' => fn () => $this->scenarioPolicyReceipt(),
'policy_md' => fn () => $this->scenarioPolicyMarkdown(),
'endorsement_original' => fn () => $this->scenarioEndorsementOriginal(),
'endorsement_completion' => fn () => $this->scenarioEndorsementCompletion(),
];
foreach ($scenarios as $label => $callback) {
try {
$callback();
CLI::write('[PASS] ' . $label, 'green');
$passed++;
} catch (\Throwable $e) {
CLI::error('[FAIL] ' . $label . ' -> ' . $e->getMessage());
$failed++;
}
CLI::newLine();
}
if (! $keepFiles && $failed === 0) {
$this->cleanupUploadedObjects();
CLI::write('Cleaned up uploaded test files from storage.', 'dark_gray');
} elseif ($keepFiles) {
CLI::write('Uploaded test files were kept (--keep).', 'yellow');
foreach ($this->uploadedObjects as $object) {
CLI::write(
' - ' . storage()->resolveKey($object['module'], $object['subFolder'], $object['fileName']),
'white'
);
}
}
$this->cleanupTempDir();
CLI::newLine();
CLI::write("Summary: {$passed} passed, {$failed} failed", $failed === 0 ? 'green' : 'red');
return $failed === 0 ? EXIT_SUCCESS : EXIT_ERROR;
}
private function printEnvironment(Storage $config): void
{
CLI::write('Storage upload test', 'green');
CLI::write('Driver: ' . ($config->driver === 's3' ? 's3' : 'local'), 'cyan');
if ($config->driver === 's3') {
CLI::write('Bucket: ' . ($config->s3Bucket ?: '(empty)'), 'cyan');
CLI::write('Region: ' . $config->s3Region, 'cyan');
CLI::write('Prefix: ' . ($config->s3Prefix !== '' ? $config->s3Prefix : '(none)'), 'cyan');
} else {
CLI::write('Local root: ' . $config->localRoot, 'cyan');
}
}
private function scenarioPolicyPdf(): void
{
$customPdf = CLI::getOption('pdf');
$source = is_string($customPdf) && $customPdf !== '' && is_file($customPdf)
? $customPdf
: $this->createSamplePdf();
$fileName = time() . '_spark_test_policy.pdf';
$content = file_get_contents($source);
if ($content === false || $content === '') {
throw new FileStorageException('Sample PDF is empty or unreadable');
}
$this->uploadFile('policy', 'policy_pdf', $fileName, $source, $content);
$this->assertReadable('policy', 'policy_pdf', $fileName, $content);
$this->assertLocalProcessingPath('policy', 'policy_pdf', $fileName);
$this->assertTemporaryUrl('policy', 'policy_pdf', $fileName);
}
private function scenarioPolicyReceipt(): void
{
$source = $this->createTempFile('sample_receipt.jpg', 'JPEG receipt test content for spark upload-test');
$fileName = time() . '_spark_test_receipt.jpg';
$content = file_get_contents($source);
$this->uploadFile('policy', 'policy_payment_receipt', $fileName, $source, $content);
$this->assertReadable('policy', 'policy_payment_receipt', $fileName, $content);
$this->assertTemporaryUrl('policy', 'policy_payment_receipt', $fileName);
}
private function scenarioPolicyMarkdown(): void
{
$fileName = time() . '_spark_test_policy.md';
$content = "# Spark Storage Test\n\n" . str_repeat('Policy markdown body. ', 40);
storage_put('policy', 'policy_md', $fileName, $content, ['content_type' => 'text/markdown']);
$this->trackUploaded('policy', 'policy_md', $fileName);
if (! policy_md_exists($fileName)) {
throw new FileStorageException('policy_md file not found after upload');
}
if (policy_read_md($fileName) !== $content) {
throw new FileStorageException('policy_md content mismatch after read');
}
}
private function scenarioEndorsementOriginal(): void
{
$source = $this->createTempFile('endorsement_original.pdf', '%PDF-1.4 endorsement original test');
$fileName = time() . '_spark_test_endorsement.pdf';
$content = file_get_contents($source);
$this->uploadFile('endorsement', '', $fileName, $source, $content);
$this->assertReadable('endorsement', '', $fileName, $content);
$this->assertTemporaryUrl('endorsement', '', $fileName);
}
private function scenarioEndorsementCompletion(): void
{
$source = $this->createTempFile('endorsement_completion.pdf', '%PDF-1.4 endorsement completion test');
$fileName = time() . '_spark_test_completion.pdf';
$content = file_get_contents($source);
$this->uploadFile('endorsement', 'endorsement_pdf', $fileName, $source, $content);
$this->assertReadable('endorsement', 'endorsement_pdf', $fileName, $content);
$this->assertTemporaryUrl('endorsement', 'endorsement_pdf', $fileName);
}
/**
* @param string|false $content
*/
private function uploadFile(
string $module,
string $subFolder,
string $fileName,
string $sourcePath,
$content
): void {
storage()->upload($sourcePath, storage()->resolveKey($module, $subFolder, $fileName));
$this->trackUploaded($module, $subFolder, $fileName);
if (! storage_exists($module, $subFolder, $fileName)) {
throw new FileStorageException('File not found immediately after upload');
}
if ($content !== false && storage_read($module, $subFolder, $fileName) !== $content) {
throw new FileStorageException('Uploaded file content mismatch on read-back');
}
CLI::write(' uploaded: ' . storage()->resolveKey($module, $subFolder, $fileName), 'white');
}
/**
* @param string|false $expectedContent
*/
private function assertReadable(string $module, string $subFolder, string $fileName, $expectedContent): void
{
if (! storage_exists($module, $subFolder, $fileName)) {
throw new FileStorageException('exists() returned false');
}
$readBack = storage_read($module, $subFolder, $fileName);
if ($expectedContent !== false && $readBack !== $expectedContent) {
throw new FileStorageException('read() content mismatch');
}
CLI::write(' read-back: OK (' . strlen($readBack) . ' bytes)', 'white');
}
private function assertLocalProcessingPath(string $module, string $subFolder, string $fileName): void
{
$path = storage_local_path($module, $subFolder, $fileName);
if (! is_file($path)) {
throw new FileStorageException('Local processing path not readable: ' . $path);
}
CLI::write(' local path: ' . $path, 'white');
}
private function assertTemporaryUrl(string $module, string $subFolder, string $fileName): void
{
$url = storage_temporary_url($module, $subFolder, $fileName);
if ($url === '') {
throw new FileStorageException('temporary URL is empty');
}
CLI::write(' url: ' . $url, 'white');
}
private function trackUploaded(string $module, string $subFolder, string $fileName): void
{
$this->uploadedObjects[] = [
'module' => $module,
'subFolder' => $subFolder,
'fileName' => $fileName,
];
}
private function cleanupUploadedObjects(): void
{
foreach ($this->uploadedObjects as $object) {
try {
storage_delete($object['module'], $object['subFolder'], $object['fileName']);
} catch (\Throwable $e) {
CLI::write(
' cleanup warning: ' . $object['fileName'] . ' -> ' . $e->getMessage(),
'yellow'
);
}
}
}
private function cleanupTempDir(): void
{
if ($this->tempDir === '' || ! is_dir($this->tempDir)) {
return;
}
foreach (scandir($this->tempDir) ?: [] as $item) {
if ($item === '.' || $item === '..') {
continue;
}
@unlink($this->tempDir . DIRECTORY_SEPARATOR . $item);
}
@rmdir($this->tempDir);
}
private function createSamplePdf(): string
{
return $this->createTempFile(
'sample_policy.pdf',
"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n110\n%%EOF\n"
);
}
private function createTempFile(string $name, string $contents): string
{
$path = $this->tempDir . DIRECTORY_SEPARATOR . $name;
if (file_put_contents($path, $contents) === false) {
throw new FileStorageException('Failed to create temp file: ' . $path);
}
return $path;
}
}