test(quality): 增加 v8 回归测试与静态分析配置

补齐 v8 分支的自动化质量保障,使插件化迁移、发布安装和架构边界都能在目标仓库内验证。

主要内容:

- 新增 PHPUnit 配置和 smoke 测试,覆盖发布、安装与 think 命令加载。

- 新增根级 tests 用例,验证路由、构建器、插件边界和业务集成行为。

- 新增 PHPStan 配置与运行时 stub,避免 Composer 插件环境误报。

- 保留旧包、旧 View 和旧 helper 命名空间的防回归检查。
This commit is contained in:
Anyon 2026-05-08 15:31:09 +08:00
parent 27ad3ff7ce
commit 4e2b7ab2fc
10 changed files with 2428 additions and 0 deletions

22
phpstan.neon Normal file
View File

@ -0,0 +1,22 @@
parameters:
level: 3
paths:
- app/
- config/
- plugin/
excludePaths:
- */runtime/*
- */vendor/*
- */tests/*
scanFiles:
- vendor/topthink/framework/src/helper.php
- vendor/topthink/think-orm/src/helper.php
- vendor/topthink/think-validate/src/helper.php
- plugin/think-library/src/common.php
- plugin/think-plugs-system/src/common.php
- plugin/think-plugs-worker/src/common.php
- plugin/think-plugs-wemall/src/common.php
- phpstan/stubs/legacy-plugin-aliases.php
- phpstan/stubs/composer-plugin-runtime.php
bootstrapFiles:
- tests/bootstrap.php

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Composer;
class Config
{
public function get(string $key): mixed
{
return null;
}
}
class Composer
{
public function getConfig(): Config
{
return new Config();
}
}
namespace Composer\IO;
interface IOInterface
{
public function write($messages, bool $newline = true, int $verbosity = 0): void;
}
namespace Composer\Plugin;
use Composer\Composer;
use Composer\IO\IOInterface;
interface PluginInterface
{
public function activate(Composer $composer, IOInterface $io): void;
public function deactivate(Composer $composer, IOInterface $io): void;
public function uninstall(Composer $composer, IOInterface $io): void;
}
namespace Composer\EventDispatcher;
interface EventSubscriberInterface
{
/**
* @return array<string, string>
*/
public static function getSubscribedEvents(): array;
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace think\admin\model;
class SystemAuth extends \plugin\system\model\SystemAuth
{
}
class SystemBase extends \plugin\system\model\SystemBase
{
}
class SystemFile extends \plugin\system\model\SystemFile
{
}
class SystemMenu extends \plugin\system\model\SystemMenu
{
}
class SystemNode extends \plugin\system\model\SystemNode
{
}
class SystemOplog extends \plugin\system\model\SystemOplog
{
}
class SystemQueue extends \plugin\worker\model\SystemQueue
{
}
class SystemUser extends \plugin\system\model\SystemUser
{
}
namespace think\admin\service;
class AdminService extends \plugin\system\service\AuthService
{
}
class CaptchaService extends \plugin\system\service\CaptchaService
{
}
class MenuService extends \plugin\system\service\MenuService
{
}
class ProcessService extends \plugin\worker\service\ProcessService
{
}
class SystemService extends \plugin\system\service\SystemService
{
}

61
phpunit.xml.dist Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
stopOnError="false"
stopOnFailure="false"
beStrictAboutTestsThatDoNotTestAnything="false">
<testsuites>
<testsuite name="ThinkAdmin Safe Unit Suite">
<file>plugin/think-library/tests/CodeTest.php</file>
<file>plugin/think-library/tests/JwtTest.php</file>
<file>plugin/think-library/tests/CommonFunctionsTest.php</file>
<file>plugin/think-library/tests/ArchitectureBoundaryTest.php</file>
<file>plugin/think-library/tests/ComposerInstallBoundaryTest.php</file>
<file>plugin/think-library/tests/RouteTemplateBoundaryTest.php</file>
<file>plugin/think-library/tests/MultAccessDispatchTest.php</file>
<file>plugin/think-library/tests/ComposerDependencyBoundaryTest.php</file>
<file>plugin/think-library/tests/MigrationOwnershipTest.php</file>
<file>plugin/think-library/tests/FormBuilderTest.php</file>
<file>plugin/think-library/tests/PageBuilderTest.php</file>
<file>plugin/think-library/tests/RequestTokenServiceTest.php</file>
<file>plugin/think-plugs-install/tests/InstallCommandTest.php</file>
<file>plugin/think-plugs-system/tests/helper/PublishTest.php</file>
<file>plugin/think-plugs-system/tests/helper/IndexNameServiceTest.php</file>
<file>plugin/think-plugs-system/tests/helper/PluginMenuServiceTest.php</file>
<file>plugin/think-plugs-system/tests/RbacAccessTest.php</file>
<file>plugin/think-plugs-system/tests/ApiSystemControllerTest.php</file>
<file>plugin/think-plugs-system/tests/ApiQueueControllerTest.php</file>
<file>plugin/think-plugs-system/tests/AuthControllerTest.php</file>
<file>plugin/think-plugs-system/tests/BaseControllerTest.php</file>
<file>plugin/think-plugs-system/tests/ConfigPageRenderTest.php</file>
<file>plugin/think-plugs-system/tests/IndexControllerTest.php</file>
<file>plugin/think-plugs-system/tests/LoginControllerTest.php</file>
<file>plugin/think-plugs-system/tests/MenuControllerTest.php</file>
<file>plugin/think-plugs-system/tests/OplogControllerTest.php</file>
<file>plugin/think-plugs-system/tests/PlugsControllerTest.php</file>
<file>plugin/think-plugs-system/tests/QueueControllerTest.php</file>
<file>plugin/think-plugs-system/tests/UploadControllerTest.php</file>
<file>plugin/think-plugs-system/tests/UserControllerTest.php</file>
<file>plugin/think-plugs-system/tests/FileControllerTest.php</file>
<file>plugin/think-plugs-system/tests/ConsoleCssUtilityTest.php</file>
<file>plugin/think-plugs-account/tests/AccountRuntimeTest.php</file>
<file>plugin/think-plugs-account/tests/AccountIntegrationTest.php</file>
<file>plugin/think-plugs-account/tests/AccountCenterControllerTest.php</file>
<file>plugin/think-plugs-account/tests/AccountAdminListControllerTest.php</file>
<file>plugin/think-plugs-payment/tests/PaymentTest.php</file>
<file>plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php</file>
<file>plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php</file>
<file>plugin/think-plugs-payment/tests/BalanceIntegrationTest.php</file>
<file>plugin/think-plugs-payment/tests/IntegralIntegrationTest.php</file>
<file>plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php</file>
<file>plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php</file>
<file>plugin/think-plugs-system/tests/CommonFunctionsTest.php</file>
<file>plugin/think-plugs-worker/tests/CommonFunctionsTest.php</file>
<file>plugin/think-plugs-worker/tests/ProcessServiceTest.php</file>
<file>plugin/think-plugs-worker/tests/WorkerConfigTest.php</file>
<file>plugin/think-plugs-worker/tests/QueueServiceTest.php</file>
<file>plugin/think-plugs-wuma/tests/CodeTest.php</file>
<file>plugin/think-plugs-wemall/tests/PaymentEventIntegrationTest.php</file>
</testsuite>
</testsuites>
</phpunit>

69
tests/bootstrap.php Normal file
View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
use think\Model;
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------.
*/
$projectRoot = dirname(__DIR__);
require $projectRoot . '/vendor/autoload.php';
require $projectRoot . '/vendor/topthink/framework/src/helper.php';
require $projectRoot . '/tests/support/TestSystemContext.php';
require $projectRoot . '/tests/support/SqliteIntegrationTestCase.php';
if (!class_exists(\plugin\install\command\project\InstallCommand::class, false)) {
require_once $projectRoot . '/plugin/think-plugs-install/src/command/project/InstallCommand.php';
}
defined('TEST_PROJECT_ROOT') || define('TEST_PROJECT_ROOT', $projectRoot);
defined('HELPER_TEST_PACKAGE_ROOT') || define('HELPER_TEST_PACKAGE_ROOT', $projectRoot . '/plugin/think-plugs-system/src/helper');
defined('HELPER_TEST_PROJECT_ROOT') || define('HELPER_TEST_PROJECT_ROOT', $projectRoot);
defined('INSTALL_TEST_PACKAGE_ROOT') || define('INSTALL_TEST_PACKAGE_ROOT', $projectRoot . '/plugin/think-plugs-install');
defined('INSTALL_TEST_PROJECT_ROOT') || define('INSTALL_TEST_PROJECT_ROOT', $projectRoot);
defined('SYSTEM_TEST_PACKAGE_ROOT') || define('SYSTEM_TEST_PACKAGE_ROOT', $projectRoot . '/plugin/think-plugs-system');
defined('SYSTEM_TEST_PROJECT_ROOT') || define('SYSTEM_TEST_PROJECT_ROOT', $projectRoot);
defined('WORKER_TEST_PACKAGE_ROOT') || define('WORKER_TEST_PACKAGE_ROOT', $projectRoot . '/plugin/think-plugs-worker');
defined('WORKER_TEST_PROJECT_ROOT') || define('WORKER_TEST_PROJECT_ROOT', $projectRoot);
if (!function_exists('test_reset_model_makers')) {
function test_reset_model_makers(): void
{
$reflection = new ReflectionProperty(Model::class, '_maker');
$reflection->setAccessible(true);
$reflection->setValue([]);
}
}

140
tests/path-examples.php Normal file
View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
// ============================================
// 场景 1加载 PHP 类文件(使用 syspath
// ============================================
require syspath('vendor/autoload.php');
require syspath('app/controller/User.php');
// ============================================
// 场景 2读取配置文件使用 syspath
// ============================================
$configFile = syspath('config/database.php');
if (is_file($configFile)) {
$config = include $configFile;
}
// ============================================
// 场景 3写入日志文件使用 runpath
// ============================================
$logFile = runpath('runtime/log/' . date('Ymd') . '.log');
file_put_contents($logFile, "日志内容\n", FILE_APPEND);
// ============================================
// 场景 4保存上传文件使用 runpath
// ============================================
$uploadDir = runpath('public/upload');
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$targetFile = $uploadDir . '/' . basename($_FILES['file']['name']);
move_uploaded_file($_FILES['file']['tmp_name'], $targetFile);
// ============================================
// 场景 5数据库文件存储使用 runpath
// ============================================
$dbFile = runpath('database/sqlite.db');
// PHAR 环境:/path/to/install/database/sqlite.db
// 普通环境:/project/database/sqlite.db
// ============================================
// 场景 6缓存文件使用 runpath
// ============================================
$cacheFile = runpath('runtime/cache/' . md5($key) . '.php');
file_put_contents($cacheFile, "<?php\nreturn " . var_export($data, true) . ';');
// ============================================
// 场景 7会话文件使用 runpath
// ============================================
$sessionPath = runpath('runtime/session');
session_save_path($sessionPath);
// ============================================
// 场景 8读取框架资源使用 syspath
// ============================================
$frameworkPath = syspath('vendor/topthink/framework');
$routeFile = syspath('vendor/topthink/framework/src/think/Route.php');
// ============================================
// 场景 9环境文件操作使用 runpath
// ============================================
$envFile = runpath('.env');
if (!is_file($envFile)) {
copy(syspath('.env.example'), $envFile);
}
// ============================================
// 场景 10检查文件是否存在注意路径选择
// ============================================
// ❌ 错误:在 PHAR 中syspath('runtime') 指向 phar:// 内部,文件不存在
if (is_file(syspath('runtime/cache/test.php'))) {
// 这段代码在 PHAR 中永远不会执行
}
// ✅ 正确:使用 runpath 访问可写路径
if (is_file(runpath('runtime/cache/test.php'))) {
// 可以正确访问到文件
$content = file_get_contents(runpath('runtime/cache/test.php'));
}
// ============================================
// 场景 11路径调试
// ============================================
if (is_phar()) {
echo "当前运行在 PHAR 环境\n";
echo '系统根目录:' . syspath() . "\n"; // phar:///path/to/admin.phar
echo '运行根目录:' . runpath() . "\n"; // /path/to/install
} else {
echo "当前运行在普通环境\n";
echo '系统根目录:' . syspath() . "\n"; // /project
echo '运行根目录:' . runpath() . "\n"; // /project
}
// ============================================
// 场景 12动态路径选择
// ============================================
function getConfigPath(): string
{
// 配置文件在代码目录中,使用 syspath
return syspath('config/app.php');
}
function getRuntimePath(): string
{
// 运行时数据在可写目录中,使用 runpath
return runpath('runtime');
}
// ============================================
// 快速参考表
// ============================================
/*
| 用途 | 使用函数 | PHAR 环境示例 | 普通环境示例 |
|----------------|-----------|----------------------------------|------------------------|
| 加载类文件 | syspath | phar:///admin.phar/app/User.php | /app/User.php |
| 读取配置 | syspath | phar:///admin.phar/config/db.php | /config/db.php |
| 框架代码 | syspath | phar:///admin.phar/vendor/... | /vendor/... |
| 写入日志 | runpath | /install/runtime/log/... | /project/runtime/log/ |
| 上传文件 | runpath | /install/public/upload/... | /project/public/upload |
| 缓存文件 | runpath | /install/runtime/cache/... | /project/runtime/cache |
| 数据库文件 | runpath | /install/database/... | /project/database |
| 会话文件 | runpath | /install/runtime/session/... | /project/runtime/sess |
| 环境文件 | runpath | /install/.env | /project/.env |
*/

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';
use think\admin\Library;
use think\App;
// 初始化应用
$app = new App(__DIR__);
Library::$sapp = $app;
echo "========================================\n";
echo "路径函数测试\n";
echo "========================================\n\n";
// 测试环境判断
echo "【环境判断测试】\n";
echo 'is_phar(): ' . (is_phar() ? 'true' : 'false') . "\n";
echo 'Phar::running(): ' . (Phar::running() ?: 'empty') . "\n";
echo 'Phar::running(false): ' . (Phar::running(false) ?: 'empty') . "\n\n";
// 测试 syspath
echo "【syspath 测试 - 系统路径(代码/资源)】\n";
echo 'syspath(): ' . syspath() . "\n";
echo "syspath('app'): " . syspath('app') . "\n";
echo "syspath('config'): " . syspath('config') . "\n";
echo "syspath('vendor'): " . syspath('vendor') . "\n";
echo "syspath('runtime'): " . syspath('runtime') . "\n";
echo "syspath('public'): " . syspath('public') . "\n";
echo "syspath('.env'): " . syspath('.env') . "\n\n";
// 测试 runpath
echo "【runpath 测试 - 运行路径(可写数据)】\n";
echo 'runpath(): ' . runpath() . "\n";
echo "runpath('runtime'): " . runpath('runtime') . "\n";
echo "runpath('public'): " . runpath('public') . "\n";
echo "runpath('.env'): " . runpath('.env') . "\n";
echo "runpath('database'): " . runpath('database') . "\n";
echo "runpath('safefile'): " . runpath('safefile') . "\n\n";
// 路径对比分析
echo "【路径对比分析】\n";
$paths = ['app', 'config', 'vendor', 'runtime', 'public', '.env', 'database'];
echo "提示在普通环境下syspath 和 runpath 返回相同路径\n";
echo " 在 PHAR 环境下syspath 返回 phar:// 路径runpath 返回外部路径\n\n";
foreach ($paths as $path) {
$sys = syspath($path);
$run = runpath($path);
$same = $sys === $run ? '相同' : '不同';
$sysProto = str_starts_with($sys, 'phar://') ? 'phar' : 'file';
$runProto = str_starts_with($run, 'phar://') ? 'phar' : 'file';
echo sprintf(
"%-12s | syspath(%-4s): %-55s [%s] | runpath: %-60s [%s] | %s\n",
$path,
$path,
basename($sys),
$sysProto,
basename($run),
$runProto,
$same
);
}
echo "\n========================================\n";
echo "测试完成\n";
echo "========================================\n";

View File

@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
use plugin\system\helper\command\project\PublishCommand;
use think\admin\service\RuntimeService;
use think\App;
use think\console\Input;
use think\console\Output;
require dirname(__DIR__, 2) . '/vendor/autoload.php';
require dirname(__DIR__, 2) . '/vendor/topthink/framework/src/helper.php';
$projectRoot = dirname(__DIR__, 2);
assertHelperOwner('sysdata', $projectRoot . '/plugin/think-plugs-system/src/common.php');
assertHelperOwner('sysoplog', $projectRoot . '/plugin/think-plugs-system/src/common.php');
assertHelperOwner('sysqueue', $projectRoot . '/plugin/think-plugs-worker/src/common.php');
writeLine('helpers:ok');
runPublishSmoke($projectRoot);
writeLine('publish:ok');
runInstallSmoke($projectRoot);
writeLine('install:ok');
runThinkListSmoke($projectRoot);
writeLine('think:list:ok');
writeLine('SMOKE_OK');
function runPublishSmoke(string $projectRoot): void
{
$root = sys_get_temp_dir() . '/thinkadmin-smoke-' . bin2hex(random_bytes(6));
try {
mkdir($root . '/vendor/composer', 0777, true);
mkdir($root . '/database/migrations', 0777, true);
file_put_contents($root . '/database/migrations/20241011000001_install_wechat20241011.php', "<?php\n");
createPluginPackage($root, 'demo', 'vendor/demo-plugin', 'plugin\demo\Service');
createPluginPackage($root, 'publish-demo', 'vendor/publish-demo', 'plugin\publishdemo\Service', null, [
'stc/runtime/demo.txt' => 'runtime-publish/demo.txt',
]);
mkdir($root . '/plugin/publish-demo/stc/runtime', 0777, true);
file_put_contents($root . '/plugin/publish-demo/stc/runtime/demo.txt', "publish\n");
createPluginPackage(
$root,
'system',
'vendor/system-plugin',
'plugin\system\Service',
[
'20241010000001_install_system20241010.php',
'20241010000002_install_storage20241010.php',
]
);
createPluginPackage(
$root,
'worker',
'vendor/worker-plugin',
'plugin\worker\Service',
'20241010000008_install_worker20241010.php'
);
$app = new App($root);
RuntimeService::init($app);
$app->config->set([
'default' => 'file',
'stores' => [
'file' => ['type' => 'File', 'path' => $root . '/runtime/cache'],
],
], 'cache');
$command = new PublishCommand();
$command->setApp($app);
$code = $command->run(new Input([]), new Output('buffer'));
assertSameValue(0, $code, 'publish command should exit with code 0');
$services = require $root . '/vendor/services.php';
$versions = require $root . '/vendor/versions.php';
$manifest = json_decode((string)file_get_contents($root . '/database/migrations/.published.json'), true, 512, JSON_THROW_ON_ERROR);
$resourceManifest = json_decode((string)file_get_contents($root . '/vendor/.published.json'), true, 512, JSON_THROW_ON_ERROR);
foreach (['plugin\demo\Service', 'plugin\system\Service', 'plugin\worker\Service'] as $service) {
assertTrue(in_array($service, $services, true), "missing published service {$service}");
}
assertSameValue('System', $versions['vendor/system-plugin']['name'] ?? null, 'system plugin name should be published');
assertSameValue('plugin', $versions['vendor/system-plugin']['type'] ?? null, 'system plugin type should be published');
assertTrue(!isset($versions['vendor/storage-plugin']), 'storage plugin should no longer be published separately');
foreach ([
'20241010000001_install_system20241010.php',
'20241010000002_install_storage20241010.php',
'20241010000008_install_worker20241010.php',
] as $migration) {
assertTrue(is_file($root . '/database/migrations/' . $migration), "missing migration {$migration}");
}
assertTrue(
is_file($root . '/database/migrations/20241011000001_install_wechat20241011.php'),
'unmanaged unique migration should be preserved'
);
assertSameValue(
'plugin/system/stc/database/20241010000001_install_system20241010.php',
$manifest['20241010000001_install_system20241010.php']['source'] ?? null,
'system migration manifest should point to plugin source'
);
assertTrue(is_file($root . '/runtime-publish/demo.txt'), 'missing published runtime resource');
assertSameValue('vendor/publish-demo', $resourceManifest['runtime-publish/demo.txt']['package'] ?? null, 'runtime resource manifest package mismatch');
removeTree($root . '/plugin/publish-demo');
$command = new PublishCommand();
$command->setApp($app);
$code = $command->run(new Input([]), new Output('buffer'));
assertSameValue(0, $code, 'second publish command should exit with code 0');
$resourceManifest = json_decode((string)file_get_contents($root . '/vendor/.published.json'), true, 512, JSON_THROW_ON_ERROR);
assertTrue(!is_file($root . '/runtime-publish/demo.txt'), 'stale published runtime resource should be removed');
assertTrue(!isset($resourceManifest['runtime-publish/demo.txt']), 'stale runtime resource manifest should be removed');
} finally {
removeTree($root);
}
}
function runThinkListSmoke(string $projectRoot): void
{
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($projectRoot . '/think') . ' list';
exec($command . ' 2>&1', $output, $status);
assertSameValue(0, $status, "think list failed:\n" . implode("\n", $output));
}
function runInstallSmoke(string $projectRoot): void
{
$root = sys_get_temp_dir() . '/thinkadmin-install-' . bin2hex(random_bytes(6));
try {
mkdir($root, 0777, true);
copyTree($projectRoot . '/app', $root . '/app');
copyTree($projectRoot . '/config', $root . '/config');
copyTree($projectRoot . '/plugin', $root . '/plugin');
copyTree($projectRoot . '/vendor', $root . '/vendor');
copyFile($projectRoot . '/think', $root . '/think');
copyFile($projectRoot . '/composer.json', $root . '/composer.json');
foreach (['database', 'public', 'runtime'] as $path) {
mkdir($root . '/' . $path, 0777, true);
}
$command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/think') . ' xadmin:publish --migrate';
exec($command . ' 2>&1', $output, $status);
assertSameValue(0, $status, "install smoke failed:\n" . implode("\n", $output));
foreach ([
'database/migrations/20241010000001_install_system20241010.php',
'database/migrations/20241010000008_install_worker20241010.php',
'public/static/system.js',
'config/database.php',
] as $path) {
assertTrue(is_file($root . '/' . $path), "missing installed artifact {$path}");
}
$db = new PDO('sqlite:' . $root . '/database/sqlite.db');
foreach ([
'system_auth',
'system_auth_node',
'system_menu',
'system_user',
'system_data',
'system_base',
'system_oplog',
'system_file',
'system_queue',
] as $table) {
$count = $db->query("select count(*) from sqlite_master where type='table' and name='{$table}'")->fetchColumn();
assertTrue(!empty($count), "missing installed table {$table}");
}
$dataRows = intval($db->query('select count(*) from system_data')->fetchColumn());
assertTrue($dataRows >= 4, 'system_data seed rows should be initialized');
$db = null;
} finally {
removeTree($root);
}
}
function assertHelperOwner(string $name, string $expectedFile): void
{
assertTrue(function_exists($name), "{$name} should be defined");
$reflection = new ReflectionFunction($name);
assertSameValue(
realpath($expectedFile),
realpath((string)$reflection->getFileName()),
"{$name} should be loaded from {$expectedFile}"
);
}
/**
* @param null|string|list<string> $migration
*/
function createPluginPackage(string $root, string $code, string $name, string $service, null|string|array $migration = null, array $publish = []): void
{
$path = "{$root}/plugin/{$code}";
mkdir($path, 0777, true);
$extra = [
'think' => ['services' => [$service]],
'xadmin' => ['app' => ['code' => $code, 'name' => ucfirst($code)]],
];
if ($publish !== []) {
$extra['xadmin']['publish'] = ['copy' => $publish];
}
file_put_contents($path . '/composer.json', json_encode([
'name' => $name,
'type' => 'think-admin-plugin',
'version' => '1.0.0',
'description' => ucfirst($code),
'extra' => $extra,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
if ($migration) {
mkdir($path . '/stc/database', 0777, true);
foreach ((array)$migration as $filename) {
file_put_contents($path . '/stc/database/' . $filename, "<?php\n");
}
}
}
function removeTree(string $path): void
{
if (!is_dir($path)) {
return;
}
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
@chmod($item->getPathname(), 0777);
if ($item->isDir()) {
@rmdir($item->getPathname());
} else {
@unlink($item->getPathname());
}
}
@rmdir($path);
}
function copyTree(string $source, string $target): void
{
if (!is_dir($source)) {
return;
}
mkdir($target, 0777, true);
$items = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($items as $item) {
$relative = substr($item->getPathname(), strlen($source) + 1);
$pathname = $target . '/' . $relative;
if (is_link($item->getPathname())) {
$real = realpath($item->getPathname());
if ($real === false) {
continue;
}
if (is_dir($real)) {
copyTree($real, $pathname);
} else {
copyFile($real, $pathname);
}
continue;
}
if ($item->isDir()) {
is_dir($pathname) || mkdir($pathname, 0777, true);
} else {
copyFile($item->getPathname(), $pathname);
}
}
}
function copyFile(string $source, string $target): void
{
$real = realpath($source);
if ($real !== false && is_dir($real)) {
copyTree($real, $target);
return;
}
if ($real !== false && is_file($real)) {
$source = $real;
}
if (is_dir($source)) {
copyTree($source, $target);
return;
}
is_dir(dirname($target)) || mkdir(dirname($target), 0777, true);
copy($source, $target);
}
function assertTrue(bool $condition, string $message): void
{
if ($condition) {
return;
}
throw new RuntimeException($message);
}
function assertSameValue($expected, $actual, string $message): void
{
if ($expected === $actual) {
return;
}
throw new RuntimeException(
$message . ' | expected: ' . var_export($expected, true) . ' actual: ' . var_export($actual, true)
);
}
function writeLine(string $message): void
{
fwrite(STDOUT, $message . PHP_EOL);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin for ThinkAdminDeveloper
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
namespace think\admin\tests\Support;
use think\admin\contract\SystemContextInterface;
class TestSystemContext implements SystemContextInterface
{
private array $data = [];
private array $user = [];
private int $userId = 0;
private bool $super = false;
private bool $login = false;
private array $nodes = [];
public function buildToken(): string
{
return '';
}
public function getTokenHeader(): string
{
return 'Authorization';
}
public function getTokenCookie(): string
{
return 'system_access_token';
}
public function getTokenType(): string
{
return 'system-auth';
}
public function syncTokenCookie(?string $token = null): string
{
return strval($token);
}
public function check(?string $node = ''): bool
{
if ($this->super) {
return true;
}
return $node !== null && in_array($node, $this->nodes, true);
}
public function getUser(?string $field = null, $default = null)
{
if ($field === null) {
return $this->user;
}
return $this->user[$field] ?? $default;
}
public function getUserId(): int
{
return $this->userId;
}
public function isSuper(): bool
{
return $this->super;
}
public function isLogin(): bool
{
return $this->login;
}
public function withUploadUnid(?string $uptoken = null): array
{
return [0, []];
}
public function clearAuth(): bool
{
return true;
}
public function getData(string $name, $default = [])
{
return $this->data[$name] ?? $default;
}
public function setData(string $name, $value): bool
{
$this->data[$name] = $value;
return true;
}
public function setOplog(string $action, string $content): bool
{
return true;
}
public function baseItems(string $type, array &$data = [], string $field = 'base_code', string $bind = 'base_info'): array
{
return [];
}
public function setUser(array $user = [], bool $login = false, bool $super = false): self
{
$this->user = $user;
$this->userId = intval($user['id'] ?? 0);
$this->login = $login;
$this->super = $super;
return $this;
}
public function setNodes(array $nodes = []): self
{
$this->nodes = array_values(array_unique(array_filter(array_map('strval', $nodes))));
return $this;
}
}