Files
zyt/server/tests/LocalAudioUploadContractTest.php
2026-08-20 17:47:14 +08:00

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";