169 lines
5.6 KiB
PHP
169 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
final class LocalAudioUploadContractExpect
|
|
{
|
|
public static function contains(string $haystack, string $needle, string $message): void
|
|
{
|
|
if (!str_contains($haystack, $needle)) {
|
|
throw new RuntimeException($message . ': missing ' . $needle);
|
|
}
|
|
}
|
|
|
|
public static function isTrue(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
throw new RuntimeException($message);
|
|
}
|
|
}
|
|
}
|
|
|
|
$root = dirname(__DIR__);
|
|
$controllerPath = $root . '/app/adminapi/controller/tcm/DiagnosisController.php';
|
|
$logicPath = $root . '/app/adminapi/logic/tcm/DiagnosisLogic.php';
|
|
$migrationPath = $root . '/database/migrations/2026_08_19_add_local_call_audio_fields.sql';
|
|
|
|
$controller = (string)file_get_contents($controllerPath);
|
|
$logic = (string)file_get_contents($logicPath);
|
|
$migration = (string)file_get_contents($migrationPath);
|
|
|
|
LocalAudioUploadContractExpect::contains(
|
|
$controller,
|
|
'public function uploadCallRecording()',
|
|
'The ThinkPHP controller must expose the desktop upload action'
|
|
);
|
|
LocalAudioUploadContractExpect::contains(
|
|
$controller,
|
|
'DiagnosisLogic::uploadCallRecording($params)',
|
|
'The controller must delegate multipart uploads to the diagnosis logic'
|
|
);
|
|
|
|
foreach (
|
|
[
|
|
"'webm'",
|
|
"'ogg'",
|
|
"'media_kind' => \$isLocalAudio ? 'local_audio' : 'video'",
|
|
'attachLocalCallAudio($attachmentParams)',
|
|
"'local_audio_urls'",
|
|
"'local_audio_status' => 2",
|
|
"'local_audio_urls_list'",
|
|
"'local_audio_status_text'",
|
|
"'uploads/audio/'",
|
|
'FileEnum::FILE_TYPE',
|
|
] as $needle
|
|
) {
|
|
LocalAudioUploadContractExpect::contains(
|
|
$logic,
|
|
$needle,
|
|
'Local audio upload and DTO contract must remain complete'
|
|
);
|
|
}
|
|
|
|
foreach (
|
|
[
|
|
"'uploads/video/'",
|
|
'FileEnum::VIDEO_TYPE',
|
|
'attachLocalCallRecording($attachmentParams)',
|
|
"'recording_status' => \$isLocalAudio ? 0 : 2",
|
|
] as $needle
|
|
) {
|
|
LocalAudioUploadContractExpect::contains(
|
|
$logic,
|
|
$needle,
|
|
'Manual video uploads must remain separate from local audio'
|
|
);
|
|
}
|
|
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
str_contains($logic, '!$isAmbiguousWebm && $hasAudioMime'),
|
|
'Known non-WebM video extensions must not be misclassified when an old client sends audio/webm'
|
|
);
|
|
|
|
foreach (['zyt_tcm_call_record', 'local_audio_urls', 'local_audio_status'] as $needle) {
|
|
LocalAudioUploadContractExpect::contains(
|
|
$migration,
|
|
$needle,
|
|
'The database migration must include every local audio field'
|
|
);
|
|
}
|
|
|
|
// Exercise the real internal-file path used after all recording chunks are
|
|
// merged. A source-code assertion alone did not catch the missing realPath key
|
|
// (or the missing File object required by the local storage engine).
|
|
$sourceBase = tempnam(sys_get_temp_dir(), 'zyt-audio-source-');
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
is_string($sourceBase),
|
|
'A temporary local-audio source file must be creatable'
|
|
);
|
|
$sourcePath = $sourceBase . '.webm';
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
rename($sourceBase, $sourcePath),
|
|
'The temporary local-audio source must keep its WebM extension'
|
|
);
|
|
$payload = "\x1A\x45\xDF\xA3local-audio-regression";
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
file_put_contents($sourcePath, $payload) === strlen($payload),
|
|
'The local-audio fixture must be written completely'
|
|
);
|
|
$targetDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR
|
|
. 'zyt-local-audio-target-' . bin2hex(random_bytes(6));
|
|
|
|
try {
|
|
$localStorage = new \app\common\service\storage\engine\Local();
|
|
$localStorage->setUploadFileByReal($sourcePath);
|
|
$fileInfo = $localStorage->getFileInfo();
|
|
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
isset($fileInfo['realPath']) && $fileInfo['realPath'] === realpath($sourcePath),
|
|
'Internal storage metadata must include the merged recording realPath'
|
|
);
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
($fileInfo['tmp_name'] ?? '') === realpath($sourcePath),
|
|
'Internal storage metadata must preserve a compatible temporary path'
|
|
);
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
str_ends_with($localStorage->getFileName(), '.webm'),
|
|
'The generated storage name must preserve the recording extension'
|
|
);
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
$localStorage->upload($targetDir),
|
|
'The local storage driver must accept a merged internal recording'
|
|
);
|
|
|
|
$storedPath = $targetDir . DIRECTORY_SEPARATOR . $localStorage->getFileName();
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
is_file($storedPath) && file_get_contents($storedPath) === $payload,
|
|
'The internally uploaded local recording must remain byte-identical'
|
|
);
|
|
} finally {
|
|
if (isset($storedPath) && is_file($storedPath)) {
|
|
unlink($storedPath);
|
|
}
|
|
if (is_file($sourcePath)) {
|
|
unlink($sourcePath);
|
|
}
|
|
if (is_dir($targetDir)) {
|
|
rmdir($targetDir);
|
|
}
|
|
}
|
|
|
|
$missingPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR
|
|
. 'zyt-missing-local-audio-' . bin2hex(random_bytes(8)) . '.webm';
|
|
$missingRejected = false;
|
|
try {
|
|
$localStorage = new \app\common\service\storage\engine\Local();
|
|
$localStorage->setUploadFileByReal($missingPath);
|
|
} catch (Throwable $exception) {
|
|
$missingRejected = !str_contains($exception->getMessage(), 'realPath')
|
|
&& str_contains($exception->getMessage(), 'does not exist');
|
|
}
|
|
LocalAudioUploadContractExpect::isTrue(
|
|
$missingRejected,
|
|
'A missing merged recording must fail clearly before storage-name generation'
|
|
);
|
|
|
|
echo "Local audio upload contract: OK\n";
|