301 lines
11 KiB
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;
|
|
}
|
|
}
|