chore: 完成v1.0.0最终验收,更新测试报告与文档
新增CSP安全配置与对应测试用例,补充并发负载测试,更新README进度与统计数据,新增最终验收报告与测试计划文档,修复已知测试问题
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user