chore: 完成v1.0.0最终验收,更新测试报告与文档

新增CSP安全配置与对应测试用例,补充并发负载测试,更新README进度与统计数据,新增最终验收报告与测试计划文档,修复已知测试问题
This commit is contained in:
2026-08-13 09:31:11 +08:00
parent 17ada52928
commit 7d93c13b18
7 changed files with 1035 additions and 21 deletions
@@ -72,4 +72,26 @@ test.describe('安全验证 E2E 测试', () => {
}
})
})
test.describe('TC-SEC-E2E-004: 内容安全策略 (CSP) 验证', () => {
test('页面不应加载未知外部资源', async ({ page }) => {
const requests: string[] = []
page.on('request', request => {
const url = request.url()
// 允许本地资源、data URI 和 uni-app 框架已知 CDN 资源
if (
!url.startsWith('http://localhost') &&
!url.startsWith('data:') &&
!url.startsWith('https://cdn.dcloud.net.cn')
) {
requests.push(url)
}
})
await page.goto('/')
await page.waitForLoadState('networkidle')
expect(requests).toHaveLength(0)
})
})
})
+1
View File
@@ -12,6 +12,7 @@ server {
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'none'; object-src 'none'";
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
@@ -92,4 +92,60 @@ describe('TC-SEC-005: 应用配置安全', () => {
expect(iosPrivacy).toBeDefined()
expect(iosPrivacy.NSPrivacyTracking).toBe(false)
})
})
describe('TC-SEC-006: 内容安全策略 (CSP)', () => {
it('nginx.conf 应包含 CSP header', () => {
const nginxPath = path.resolve(__dirname, '../../../nginx.conf')
const nginx = fs.readFileSync(nginxPath, 'utf-8')
expect(nginx).toContain('add_header Content-Security-Policy')
expect(nginx).toContain("default-src 'self'")
expect(nginx).toContain("script-src 'self'")
})
})
describe('TC-SEC-007: 代码安全审计', () => {
it('源码中不应包含 eval 调用', () => {
const srcDir = path.resolve(__dirname, '..')
const findEval = (dir: string): string[] => {
const results: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '__tests__' && entry.name !== 'node_modules') {
results.push(...findEval(fullPath))
} else if (entry.isFile() && /\.(ts|vue)$/.test(entry.name)) {
const content = fs.readFileSync(fullPath, 'utf-8')
if (content.includes('eval(')) {
results.push(fullPath)
}
}
}
return results
}
const evalUses = findEval(srcDir)
expect(evalUses).toHaveLength(0)
})
it('源码中不应包含 innerHTML 赋值', () => {
const srcDir = path.resolve(__dirname, '..')
const findInnerHTML = (dir: string): string[] => {
const results: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '__tests__' && entry.name !== 'node_modules') {
results.push(...findInnerHTML(fullPath))
} else if (entry.isFile() && /\.(ts|vue)$/.test(entry.name)) {
const content = fs.readFileSync(fullPath, 'utf-8')
if (content.includes('.innerHTML =') || content.includes('innerHTML=')) {
results.push(fullPath)
}
}
}
return results
}
const innerHTMLUses = findInnerHTML(srcDir)
expect(innerHTMLUses).toHaveLength(0)
})
})
@@ -1,9 +1,11 @@
/**
* TC-PERF-005: 长时间使用稳定性测试
* TC-PERF-006: 并发负载测试
*
* 模拟用户持续使用场景,验证:
* 1. 性能不会随着重复调用而退化
* 2. 长时间运行稳定性
* 3. 高并发场景下的响应性能
*/
import { describe, it, expect } from 'vitest'
import { calculateAlmanac } from '../almanac'
@@ -134,4 +136,126 @@ describe('TC-PERF-005: 长时间使用稳定性', () => {
expect(avg).toBeLessThan(1000)
})
})
/**
* TC-PERF-006: 并发负载测试
*
* 模拟高并发场景,验证:
* 1. 并发请求下的响应时间
* 2. 并发请求的正确性
* 3. 混合并发场景的稳定性
*/
describe('TC-PERF-006: 并发负载测试', () => {
const ziweiService = new ZiweiService()
const fortuneService = new FortuneService()
it('并发10次紫微排盘应全部成功且耗时 < 2000ms', async () => {
const CONCURRENT = 10
const start = performance.now()
const results = await Promise.all(
Array.from({ length: CONCURRENT }, (_, i) =>
ziweiService.generateChart({
birthTime: `1990-05-${String(15 + i).padStart(2, '0')}T08:30:00`,
gender: i % 2 === 0 ? 'male' : 'female',
timezone: 'Asia/Shanghai',
longitude: 116.4 + i,
latitude: 39.9 + i,
birthPlace: '北京市',
})
)
)
const duration = performance.now() - start
expect(results).toHaveLength(CONCURRENT)
results.forEach((result) => {
expect(result).toBeDefined()
expect(result.palaces).toHaveLength(12)
})
expect(duration).toBeLessThan(2000)
})
it('并发20次黄历计算应全部成功且耗时 < 2000ms', async () => {
const CONCURRENT = 20
const start = performance.now()
const results = await Promise.all(
Array.from({ length: CONCURRENT }, (_, i) =>
Promise.resolve(calculateAlmanac(new Date(2026, 0, 1 + i)))
)
)
const duration = performance.now() - start
expect(results).toHaveLength(CONCURRENT)
results.forEach((result) => {
expect(result.suitable).toBeInstanceOf(Array)
expect(result.unsuitable).toBeInstanceOf(Array)
})
expect(duration).toBeLessThan(2000)
})
it('混合并发:5组排盘+黄历+运势组合应全部成功且耗时 < 3000ms', async () => {
const CONCURRENT = 5
const start = performance.now()
const results = await Promise.all(
Array.from({ length: CONCURRENT }, async (_, i) => {
const chart = await ziweiService.generateChart({
birthTime: `1990-05-${String(15 + i).padStart(2, '0')}T08:30:00`,
gender: 'male',
timezone: 'Asia/Shanghai',
longitude: 116.4,
latitude: 39.9,
birthPlace: '北京市',
})
const almanac = calculateAlmanac(new Date(2026, 7, 13 + i))
const daily = await fortuneService.getDailyFortune(chart, '2026-08-13')
return { chart, almanac, daily }
})
)
const duration = performance.now() - start
expect(results).toHaveLength(CONCURRENT)
results.forEach((result) => {
expect(result.chart.palaces).toHaveLength(12)
expect(result.almanac.suitable.length).toBeGreaterThan(0)
expect(result.daily!.overallScore).toBeGreaterThanOrEqual(0)
})
expect(duration).toBeLessThan(3000)
})
it('高负载:20次并发运势计算应全部成功且无数据错误', async () => {
const chart = await ziweiService.generateChart({
birthTime: '1990-05-15T08:30:00',
gender: 'male',
timezone: 'Asia/Shanghai',
longitude: 116.4,
latitude: 39.9,
birthPlace: '北京市',
})
const start = performance.now()
const dates = Array.from({ length: 20 }, (_, i) => {
const d = new Date(2026, 0, 1 + i)
return d.toISOString().split('T')[0]
})
const results = await Promise.all(
dates.map((date) => fortuneService.getDailyFortune(chart, date))
)
const duration = performance.now() - start
expect(results).toHaveLength(20)
results.forEach((result) => {
expect(result).toBeDefined()
expect(result!.overallScore).toBeGreaterThanOrEqual(0)
expect(result!.overallScore).toBeLessThanOrEqual(100)
})
expect(duration).toBeLessThan(2000)
})
})