1, 'latest_version' => '0.2.0', 'min_version' => '0.1.5', 'force_update' => 0, 'title' => '医生工作站 0.2.0', 'notes' => '稳定性更新', 'packages' => [ 'windows_x64' => [ 'url' => 'https://cdn.example.com/DoctorWorkstation-Setup-Windows-x64-0.2.0.exe', 'sha256' => $sha, 'size' => 123, 'filename' => 'DoctorWorkstation-Setup-Windows-x64-0.2.0.exe', 'type' => 'inno_setup', ], ], ]; $optional = DesktopWorkstationLogic::evaluate($config, '0.1.8', 'windows', 'x64'); desktopUpdateExpect($optional['has_update'] === true, 'newer published version is an update'); desktopUpdateExpect($optional['force'] === false, 'force stays off when above min version'); desktopUpdateExpect($optional['can_install'] === true, 'hashed package can be installed'); desktopUpdateExpect( is_array($optional['package']) && $optional['package']['sha256'] === $sha && $optional['package']['type'] === 'inno_setup', 'matching Windows installer package is returned with its explicit type' ); $forcedByMin = DesktopWorkstationLogic::evaluate($config, '0.1.0', 'windows', 'x64'); desktopUpdateExpect($forcedByMin['force'] === true, 'below min version forces upgrade'); $config['force_update'] = 1; $forcedAll = DesktopWorkstationLogic::evaluate($config, '0.1.9', 'windows', 'x64'); desktopUpdateExpect($forcedAll['force'] === true, 'force_update blocks every older client'); $current = DesktopWorkstationLogic::evaluate($config, '0.2.0', 'windows', 'x64'); desktopUpdateExpect($current['has_update'] === false, 'current latest version is not an update'); $macosMissing = DesktopWorkstationLogic::evaluate($config, '0.1.0', 'macos', 'arm64'); desktopUpdateExpect($macosMissing['has_update'] === true, 'other platforms still see the new version'); desktopUpdateExpect($macosMissing['can_install'] === false, 'missing platform package cannot auto-install'); desktopUpdateExpect($macosMissing['force'] === false, 'force requires an installable package'); $disabled = $config; $disabled['enabled'] = 0; $quiet = DesktopWorkstationLogic::evaluate($disabled, '0.1.0', 'windows', 'x64'); desktopUpdateExpect($quiet['has_update'] === false, 'disabled detection never prompts'); $controller = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/setting/DesktopWorkstationController.php'); desktopUpdateExpect(is_string($controller), 'controller source is readable'); desktopUpdateExpect( str_contains($controller, "public array \$notNeedLogin = ['check']") && str_contains($controller, 'public function check()') && str_contains($controller, 'DesktopWorkstationLogic::check('), 'check action is public and delegates to logic' ); $adminView = file_get_contents(dirname(__DIR__, 2) . '/admin/src/views/setting/desktop_workstation/index.vue'); desktopUpdateExpect(is_string($adminView), 'admin view source is readable'); desktopUpdateExpect( str_contains($adminView, 'setting.desktop_workstation/setConfig') && str_contains($adminView, 'force_update') && str_contains($adminView, 'inno_setup') && str_contains($adminView, 'type="desktop_package"') && str_contains($adminView, 'direct'), 'admin page saves update settings and sends installers through direct upload' ); $directUploadReflection = new ReflectionClass(DirectUploadService::class); $validateExtension = $directUploadReflection->getMethod('validateFileExtension'); $validateExtension->invoke( null, DirectUploadService::TYPE_DESKTOP_PACKAGE, 'uploads/desktop_package/7/' . date('Ymd') . '/package.exe', 'DoctorWorkstation.EXE' ); $invalidExtensionRejected = false; try { $validateExtension->invoke( null, DirectUploadService::TYPE_DESKTOP_PACKAGE, 'uploads/desktop_package/7/' . date('Ymd') . '/package.php', 'package.php' ); } catch (Throwable $e) { $invalidExtensionRejected = true; } desktopUpdateExpect($invalidExtensionRejected, 'desktop direct upload rejects non-EXE/ZIP files'); $validateObjectKey = $directUploadReflection->getMethod('isAllowedObjectKey'); $ownedKey = 'uploads/desktop_package/7/' . date('Ymd') . '/package.exe'; desktopUpdateExpect( $validateObjectKey->invoke(null, DirectUploadService::TYPE_DESKTOP_PACKAGE, $ownedKey, 7) === true && $validateObjectKey->invoke(null, DirectUploadService::TYPE_DESKTOP_PACKAGE, $ownedKey, 8) === false, 'desktop package keys are bound to the issuing admin' ); $uploadController = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/UploadController.php'); $qcloudEngine = file_get_contents(dirname(__DIR__) . '/app/common/service/storage/engine/Qcloud.php'); $directUploadService = file_get_contents( dirname(__DIR__) . '/app/common/service/DirectUploadService.php' ); desktopUpdateExpect( is_string($uploadController) && str_contains($uploadController, 'assertDirectUploadPermission') && str_contains($uploadController, 'setting.desktop_workstation/setconfig'), 'desktop package credentials and confirmation require publish permission' ); desktopUpdateExpect( is_string($qcloudEngine) && str_contains($qcloudEngine, 'bool $exactObject = false') && str_contains($qcloudEngine, '$exactObject ? $scope : $scope .'), 'COS credentials can be restricted to one server-issued object key' ); desktopUpdateExpect( is_string($directUploadService) && str_contains($directUploadService, "trim(\$name) !== ''") && str_contains($directUploadService, '$objectKey !== \'\''), 'desktop credentials remain compatible with uploaders that do not send a filename' ); $migration = file_get_contents( dirname(__DIR__) . '/sql/1.9.20260821/add_desktop_workstation_update_menu.sql' ); desktopUpdateExpect(is_string($migration), 'menu migration is readable'); desktopUpdateExpect( substr_count($migration, "WHERE `perms` = 'setting.desktop_workstation/getConfig'") >= 1 && str_contains($migration, 'setting.desktop_workstation/setConfig') && str_contains($migration, 'setting/desktop_workstation/index'), 'menu registration is idempotent and points at the admin view' ); echo "Desktop workstation update contract: OK\n";