Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
186 lines
7.3 KiB
JavaScript
186 lines
7.3 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { promises as fs } from 'node:fs'
|
||
import path from 'node:path'
|
||
import test from 'node:test'
|
||
import {
|
||
scanLegacyStorageBoundaries,
|
||
TEMP_ROOT,
|
||
} from './check-legacy-storage-boundaries.mjs'
|
||
|
||
async function withFixture(files, callback) {
|
||
const root = await fs.mkdtemp(path.join(TEMP_ROOT, 'legacy-storage-boundary-'))
|
||
try {
|
||
for (const [relativePath, content] of Object.entries(files)) {
|
||
const absolutePath = path.join(root, relativePath)
|
||
await fs.mkdir(path.dirname(absolutePath), { recursive: true })
|
||
await fs.writeFile(absolutePath, content, 'utf8')
|
||
}
|
||
await callback(root)
|
||
} finally {
|
||
await fs.rm(root, { recursive: true, force: true })
|
||
}
|
||
}
|
||
|
||
test('拦截新 migration、Dockerfile 和 PowerShell 中的旧域名', async () => {
|
||
await withFixture({
|
||
'prisma/migrations/20260812_bad/migration.sql': "ALTER TABLE x SET DEFAULT 'https://s3.1080p.hk/x'",
|
||
Dockerfile: 'ENV STORAGE=https://s3.yydd.ai',
|
||
'scripts/deploy.ps1': "$target = 'https://s3.1080p.hk'",
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.deepEqual(
|
||
violations.map(item => item.relativePath).sort(),
|
||
['Dockerfile', 'prisma/migrations/20260812_bad/migration.sql', 'scripts/deploy.ps1'],
|
||
)
|
||
})
|
||
})
|
||
|
||
test('旧域名匹配不区分大小写,并覆盖常见前端与容器文件', async () => {
|
||
await withFixture({
|
||
'Dockerfile.production': 'ENV STORAGE=https://S3.1080P.HK',
|
||
'Containerfile.worker': 'ENV STORAGE=https://S3.YYDD.AI',
|
||
'components/legacy.jsx': "const host='https://S3.1080P.HK'",
|
||
'styles/legacy.scss': 'body{background:url(https://S3.YYDD.AI/a.png)}',
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.deepEqual(
|
||
violations.map(item => item.relativePath).sort(),
|
||
[
|
||
'Containerfile.worker',
|
||
'Dockerfile.production',
|
||
'components/legacy.jsx',
|
||
'styles/legacy.scss',
|
||
],
|
||
)
|
||
})
|
||
})
|
||
|
||
test('只跳过仓库根 uploads 数据,不得漏扫源码中的 uploads 路由', async () => {
|
||
await withFixture({
|
||
'uploads/runtime-data.json': '{"legacy":"https://s3.1080p.hk/data"}',
|
||
'app/api/uploads/[...path]/route.ts': "const target='https://s3.1080p.hk/source'",
|
||
'app/uploads/[...path]/route.ts': "const target='https://s3.yydd.ai/source'",
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.deepEqual(
|
||
violations.map(item => item.relativePath).sort(),
|
||
['app/api/uploads/[...path]/route.ts', 'app/uploads/[...path]/route.ts'],
|
||
)
|
||
})
|
||
})
|
||
|
||
test('allowlist 文件缺标记或精确计数变化时也会失败', async () => {
|
||
await withFixture({
|
||
'lib/network-detection.ts': "// LEGACY_READ_ONLY\nconst hosts=['s3.1080p.hk']",
|
||
'lib/url-rewrite.ts': "const target='s3.1080p.hk'",
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.equal(violations.length, 2)
|
||
})
|
||
})
|
||
|
||
test('精确只读兼容和唯一历史 migration 可以通过', async () => {
|
||
await withFixture({
|
||
'lib/network-detection.ts': "// LEGACY_READ_ONLY\nconst hosts=['s3.1080p.hk','s3.yydd.ai']",
|
||
'prisma/migrations/20251026015433_add_material_type/migration.sql': "DEFAULT 'https://s3.1080p.hk/logo.png'",
|
||
}, async root => {
|
||
assert.deepEqual(await scanLegacyStorageBoundaries(root), [])
|
||
})
|
||
})
|
||
|
||
test('品牌 migration 精确清洗旧品牌值并中和三个 Logo 默认值', async () => {
|
||
const migrationPath = path.join(
|
||
process.cwd(),
|
||
'prisma/migrations/20260811_update_brand_logo_defaults/migration.sql',
|
||
)
|
||
const content = await fs.readFile(migrationPath, 'utf8')
|
||
|
||
assert.equal((content.match(/s3\\\.1080p\\\.hk/gi) || []).length, 2)
|
||
assert.match(content, /LEGACY_DATA_REMEDIATION/)
|
||
assert.match(content, /UPDATE "system_settings"\s+SET "headerLogoUrl" = '\/brand\/header-logo\.png'/)
|
||
assert.match(content, /UPDATE "system_settings"\s+SET "loginLogoUrl" = '\/brand\/login-logo\.png'/)
|
||
assert.doesNotMatch(content, /DEFAULT[^;\n]*s3\.1080p\.hk/i)
|
||
assert.match(content, /"headerLogoUrl"[\s\S]*DEFAULT '\/brand\/header-logo\.png'/)
|
||
assert.match(content, /"loginLogoUrl"[\s\S]*DEFAULT '\/brand\/login-logo\.png'/)
|
||
assert.match(content, /ALTER COLUMN "logoUrl" SET DEFAULT '\/brand\/header-logo\.png'/)
|
||
|
||
await withFixture({
|
||
'prisma/migrations/20260811_update_brand_logo_defaults/migration.sql': content,
|
||
}, async root => {
|
||
assert.deepEqual(await scanLegacyStorageBoundaries(root), [])
|
||
})
|
||
})
|
||
|
||
test('品牌数据修复标记不能放行默认值回写或任意新 migration', async () => {
|
||
const shapedButUnsafe = `-- LEGACY_DATA_REMEDIATION
|
||
UPDATE "system_settings"
|
||
SET "headerLogoUrl" = '/brand/header-logo.png'
|
||
WHERE "headerLogoUrl" ~* '^https?://s3\\.1080p\\.hk(?:/|$)';
|
||
UPDATE "system_settings"
|
||
SET "loginLogoUrl" = '/brand/login-logo.png'
|
||
WHERE "loginLogoUrl" ~* '^https?://s3\\.1080p\\.hk(?:/|$)';
|
||
ALTER TABLE "system_settings" ALTER COLUMN "logoUrl" SET DEFAULT 'https://s3.1080p.hk/logo.png';`
|
||
await withFixture({
|
||
'prisma/migrations/20260811_update_brand_logo_defaults/migration.sql': shapedButUnsafe,
|
||
'prisma/migrations/20260812_bad/migration.sql': shapedButUnsafe,
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.deepEqual(
|
||
violations.map(item => item.relativePath).sort(),
|
||
[
|
||
'prisma/migrations/20260811_update_brand_logo_defaults/migration.sql',
|
||
'prisma/migrations/20260812_bad/migration.sql',
|
||
],
|
||
)
|
||
})
|
||
})
|
||
|
||
test('默认 Compose 拒绝相对 uploads,并要求容器路径和禁止自动建目录', async () => {
|
||
await withFixture({
|
||
'docker-compose.yml': `services:
|
||
app:
|
||
environment:
|
||
- UPLOAD_DIR=./uploads
|
||
volumes:
|
||
- ./uploads:/app/uploads
|
||
`,
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.equal(violations.length, 1)
|
||
assert.equal(violations[0].relativePath, 'docker-compose.yml')
|
||
assert.match(violations[0].reasons.join('\n'), /relative uploads bind/)
|
||
assert.match(violations[0].reasons.join('\n'), /UPLOAD_DIR=\/app\/uploads/)
|
||
assert.match(violations[0].reasons.join('\n'), /GONGXIANGP2_UPLOADS_MOUNT/)
|
||
assert.match(violations[0].reasons.join('\n'), /automatic host-path creation/)
|
||
})
|
||
})
|
||
|
||
test('默认 Compose 只接受显式宿主机挂载变量与固定容器路径', async () => {
|
||
await withFixture({
|
||
'docker-compose.yml': `services:
|
||
app:
|
||
environment:
|
||
UPLOAD_DIR: /app/uploads
|
||
volumes:
|
||
- type: bind
|
||
source: \${GONGXIANGP2_UPLOADS_MOUNT:?required}
|
||
target: /app/uploads
|
||
bind:
|
||
create_host_path: false
|
||
`,
|
||
}, async root => {
|
||
assert.deepEqual(await scanLegacyStorageBoundaries(root), [])
|
||
})
|
||
})
|
||
|
||
test('相对 uploads 只允许出现在名称明确的非生产 Compose', async () => {
|
||
await withFixture({
|
||
'docker-compose.dev.yml': `services:\n app:\n volumes:\n - ./uploads:/app/uploads\n`,
|
||
'docker-compose.production.yml': `services:\n app:\n volumes:\n - \${GONGXIANGP2_UPLOADS_MOUNT:-./uploads}:/app/uploads\n`,
|
||
'docker-compose.shadow.yml': `services:\n app:\n volumes:\n - ./uploads-shadow:/app/uploads\n`,
|
||
}, async root => {
|
||
const violations = await scanLegacyStorageBoundaries(root)
|
||
assert.deepEqual(violations.map(item => item.relativePath), ['docker-compose.production.yml'])
|
||
})
|
||
})
|