test(storage): S3与WebDAV驱动解析及URL生成测试

This commit is contained in:
augushong
2026-08-20 22:25:02 +08:00
parent aba0d20615
commit 2b71f71d4c

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace tests\Feature;
use app\admin\model\SystemConfig;
use app\common\service\UploadService;
use app\common\test\TestCase;
use think\exception\ValidateException;
use think\facade\Cache;
use think\facade\Filesystem;
use think\filesystem\driver\S3;
use think\filesystem\driver\Webdav;
use think\File;
/**
* 存储驱动测试S3 与 WebDAV 磁盘解析、URL 拼接、上传白名单放行.
*
* 覆盖 storage-webdav-s3 任务的运行时行为:
* 1. Filesystem::disk('s3') / disk('webdav') 能按 sysconfig 正确解析驱动实例
* 2. 两个驱动的 url() 拼接domain + path
* 3. UploadService 对新类型 s3 / webdav 真正通过 in:{upload_allow_type} 白名单闸门
* UploadServiceBase.php:40 的校验规则,非仅构造)
*
* 隔离策略:
* - 测试自播种 sysconfig不依赖库中既有种子值写入发生在 setUp 事务内,
* tearDown 的 Db::rollback 自动撤销
* - sysconfig 有 3600s 文件缓存extend/base/helper.php sysconfig()
* 播种后必须 Cache::tag('sysconfig')->clear() 才能读到新值;
* tearDown 里回滚后同样要清一次,防止测试期缓存值泄漏给同进程后续套件
*/
class StorageDriverTest extends TestCase
{
/**
* S3 七项配置(与后台表单 / T5 种子键名一致).
*/
private const S3_KV = [
's3_access_key' => 'testkey',
's3_secret_key' => 'testsecret',
's3_region' => 'us-east-1',
's3_bucket' => 'test-bucket',
's3_domain' => 'https://s3-test.example.com',
's3_endpoint' => '',
's3_path_style' => '0',
];
/**
* WebDAV 五项配置.
*/
private const WEBDAV_KV = [
'webdav_url' => 'https://webdav-test.example.com/dav/',
'webdav_username' => 'u',
'webdav_password' => 'p',
'webdav_prefix' => '',
'webdav_domain' => 'https://webdav-test.example.com',
];
private const ALLOW_TYPE_ALL = 'local_public,alioss,qnoss,txcos,s3,webdav';
protected function tearDown(): void
{
// 先回滚事务parent 内含 Db::rollback不要手动重复
parent::tearDown();
// 回滚后文件缓存仍持有测试期值,不清会泄漏给同进程后续套件与下次运行
Cache::tag('sysconfig')->clear();
}
/**
* 播种 upload 组 sysconfig 并清缓存.
*
* 注think-orm Model 没有 updateOrCreate已验证 method_exists 为 false
* 用等价的 find → save / create 实现同样的 upsert 语义。
*/
private function seedUploadConfigs(array $kv): void
{
foreach ($kv as $name => $value) {
$row = SystemConfig::where('group', 'upload')->where('name', $name)->find();
if ($row) {
$row->save(['value' => (string) $value, 'remark' => 'test', 'sort' => 0]);
} else {
SystemConfig::create([
'group' => 'upload',
'name' => $name,
'value' => (string) $value,
'remark' => 'test',
'sort' => 0,
]);
}
}
// sysconfig 有 3600s 缓存,不清会读到旧值
Cache::tag('sysconfig')->clear();
}
/**
* 用例 1播种 s3_* 配置后 disk('s3') 解析为 S3 驱动实例.
*/
public function test_s3_disk_resolves(): void
{
$this->seedUploadConfigs(self::S3_KV + ['upload_allow_type' => self::ALLOW_TYPE_ALL]);
$disk = Filesystem::disk('s3');
$this->assertInstanceOf(S3::class, $disk);
}
/**
* 用例 2播种 webdav_* 配置后 disk('webdav') 解析为 Webdav 驱动实例.
*/
public function test_webdav_disk_resolves(): void
{
$this->seedUploadConfigs(self::WEBDAV_KV);
$disk = Filesystem::disk('webdav');
$this->assertInstanceOf(Webdav::class, $disk);
}
/**
* 用例 3S3 url() 拼接 = s3_domain + '/' + path.
*
* 本用例内重新播种全部七项url() 实时读 sysconfig且前一用例的种子
* 已随其 tearDown 回滚;--filter 单独跑本用例时磁盘首次构造也需要
* region / bucket 等配置。
*/
public function test_s3_url_concat(): void
{
$this->seedUploadConfigs(self::S3_KV);
$url = Filesystem::disk('s3')->url('upload/20260817/abc.png');
$this->assertSame('https://s3-test.example.com/upload/20260817/abc.png', $url);
}
/**
* 用例 4WebDAV url() 拼接 = webdav_domain + '/' + path.
*
* 同理本用例内重新播种全部五项url() 实时读 sysconfig + 单独运行时
* 构造 Client 需要 url/username/password
*/
public function test_webdav_url_concat(): void
{
$this->seedUploadConfigs(self::WEBDAV_KV);
$url = Filesystem::disk('webdav')->url('upload/20260817/abc.png');
$this->assertSame('https://webdav-test.example.com/upload/20260817/abc.png', $url);
}
/**
* 用例 5UploadService 对 s3 / webdav 真正通过 in:{upload_allow_type} 白名单闸门.
*
* 走 validateException 全链(白名单 + fileExt + fileSize + PHP 注入检查),
* 而非仅构造 UploadService只构造不校验是恒真断言不可取
* webdav 构造只读 uploadType 属性不连磁盘,安全。
*/
public function test_upload_service_accepts_new_types(): void
{
$this->seedUploadConfigs([
'upload_type' => 's3',
'upload_allow_type' => self::ALLOW_TYPE_ALL,
'upload_allow_ext' => 'jpg,png',
'upload_allow_size' => '1024000',
]);
$tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ulthon_t8_test.png';
file_put_contents($tmpPath, 'pngdata');
try {
$file = new File($tmpPath);
$this->assertTrue(
(new UploadService('s3'))->validateException($file),
'upload_type=s3 应通过 in:{upload_allow_type} 白名单与文件校验'
);
$this->assertTrue(
(new UploadService('webdav'))->validateException($file),
'upload_type=webdav 应通过 in:{upload_allow_type} 白名单与文件校验'
);
} finally {
@unlink($tmpPath);
}
}
/**
* 附加用例(任务允许的可选白名单拒绝场景):白名单不含 s3 时闸门应拒绝.
*
* 锁死 in:{upload_allow_type} 的拒绝方向:防止有人把白名单校验改坏后
* 用例 5 仍绿。expectException 场景下 tearDown回滚 + 清缓存)照常执行。
*/
public function test_upload_service_rejects_type_not_in_whitelist(): void
{
$this->seedUploadConfigs([
'upload_allow_type' => 'local_public,alioss,qnoss,txcos',
'upload_allow_ext' => 'jpg,png',
'upload_allow_size' => '1024000',
]);
$tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ulthon_t8_reject.png';
file_put_contents($tmpPath, 'pngdata');
try {
$file = new File($tmpPath);
$this->expectException(ValidateException::class);
$this->expectExceptionMessage('指定上传类型有误');
(new UploadService('s3'))->validateException($file);
} finally {
@unlink($tmpPath);
}
}
}