rwadurian/backend/services/authorization-service/docs/TESTING.md

1053 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Authorization Service 测试文档
## 目录
1. [测试架构概述](#测试架构概述)
2. [测试金字塔](#测试金字塔)
3. [单元测试](#单元测试)
4. [集成测试](#集成测试)
5. [端到端测试](#端到端测试)
6. [Docker 测试环境](#docker-测试环境)
7. [测试覆盖率](#测试覆盖率)
8. [CI/CD 集成](#cicd-集成)
9. [手动测试指南](#手动测试指南)
---
## 测试架构概述
Authorization Service 采用多层测试策略,确保代码质量和系统稳定性。
### 测试框架
| 框架 | 用途 |
|------|------|
| Jest | 测试运行器和断言库 |
| @nestjs/testing | NestJS 测试工具 |
| supertest | HTTP 请求测试 |
| ts-jest | TypeScript 支持 |
### 测试类型
```
┌─────────────────────────────────────────────┐
│ E2E Tests │ ← 少量,验证完整流程
│ (test/*.e2e-spec.ts) │
├─────────────────────────────────────────────┤
│ Integration Tests │ ← 中等,验证模块协作
│ (test/*.integration-spec.ts) │
├─────────────────────────────────────────────┤
│ Unit Tests │ ← 大量,验证单个组件
│ (src/**/*.spec.ts) │
└─────────────────────────────────────────────┘
```
---
## 测试金字塔
### 测试分布
| 层级 | 数量 | 执行时间 | 覆盖范围 |
|------|------|----------|----------|
| 单元测试 | 33+ | ~15秒 | 领域层核心逻辑 |
| 集成测试 | 30+ | ~25秒 | 领域服务、值对象交互 |
| E2E测试 | 6+ | ~20秒 | API端点、认证流程 |
### 运行所有测试
```bash
# 运行所有测试
npm run test:all
# 或分别运行
npm run test:unit # 单元测试
npm run test:integration # 集成测试
npm run test:e2e # E2E 测试
```
---
## 单元测试
### 测试位置
单元测试与源代码放在一起:
```
src/
├── domain/
│ ├── aggregates/
│ │ ├── authorization-role.aggregate.ts
│ │ └── authorization-role.aggregate.spec.ts ← 单元测试
│ ├── entities/
│ │ ├── ladder-target-rule.entity.ts
│ │ └── ladder-target-rule.entity.spec.ts
│ └── value-objects/
│ ├── month.vo.ts
│ └── month.vo.spec.ts
```
### 测试配置
```json
// jest.config.js 或 package.json
{
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/$1"
},
"testEnvironment": "node"
}
}
```
### 聚合根测试示例
```typescript
// src/domain/aggregates/authorization-role.aggregate.spec.ts
import { AuthorizationRole } from './authorization-role.aggregate'
import { UserId, AdminUserId } from '@/domain/value-objects'
import { RoleType, AuthorizationStatus } from '@/domain/enums'
import { DomainError } from '@/shared/exceptions'
describe('AuthorizationRole Aggregate', () => {
describe('createAuthProvinceCompany', () => {
it('should create a pending province company authorization', () => {
const userId = UserId.create('user-123')
const auth = AuthorizationRole.createAuthProvinceCompany({
userId,
provinceCode: '430000',
provinceName: '湖南省',
})
expect(auth.userId.value).toBe('user-123')
expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY)
expect(auth.status).toBe(AuthorizationStatus.PENDING)
expect(auth.provinceCode).toBe('430000')
expect(auth.provinceName).toBe('湖南省')
})
it('should emit AuthorizationAppliedEvent', () => {
const userId = UserId.create('user-123')
const auth = AuthorizationRole.createAuthProvinceCompany({
userId,
provinceCode: '430000',
provinceName: '湖南省',
})
expect(auth.domainEvents).toHaveLength(1)
expect(auth.domainEvents[0].eventName).toBe('authorization.applied')
})
})
describe('authorize', () => {
it('should change status to APPROVED when pending', () => {
const auth = createPendingAuthorization()
const adminId = AdminUserId.create('admin-001')
auth.authorize(adminId)
expect(auth.status).toBe(AuthorizationStatus.APPROVED)
expect(auth.authorizedBy?.value).toBe('admin-001')
})
it('should throw error when already approved', () => {
const auth = createApprovedAuthorization()
const adminId = AdminUserId.create('admin-001')
expect(() => auth.authorize(adminId)).toThrow(DomainError)
})
})
describe('activate', () => {
it('should change status to ACTIVE when approved', () => {
const auth = createApprovedAuthorization()
auth.activate()
expect(auth.status).toBe(AuthorizationStatus.ACTIVE)
expect(auth.activatedAt).toBeDefined()
})
})
describe('revoke', () => {
it('should change status to REVOKED with reason', () => {
const auth = createActiveAuthorization()
const adminId = AdminUserId.create('admin-001')
auth.revoke(adminId, '违规操作')
expect(auth.status).toBe(AuthorizationStatus.REVOKED)
expect(auth.revokedBy?.value).toBe('admin-001')
expect(auth.revokeReason).toBe('违规操作')
})
})
})
// 测试辅助函数
function createPendingAuthorization(): AuthorizationRole {
return AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-123'),
provinceCode: '430000',
provinceName: '湖南省',
})
}
function createApprovedAuthorization(): AuthorizationRole {
const auth = createPendingAuthorization()
auth.authorize(AdminUserId.create('admin-001'))
return auth
}
function createActiveAuthorization(): AuthorizationRole {
const auth = createApprovedAuthorization()
auth.activate()
return auth
}
```
### 值对象测试示例
```typescript
// src/domain/value-objects/month.vo.spec.ts
import { Month } from './month.vo'
describe('Month Value Object', () => {
describe('create', () => {
it('should create month from valid string', () => {
const month = Month.create('2024-03')
expect(month.value).toBe('2024-03')
})
it('should throw error for invalid format', () => {
expect(() => Month.create('2024/03')).toThrow()
expect(() => Month.create('03-2024')).toThrow()
expect(() => Month.create('invalid')).toThrow()
})
})
describe('current', () => {
it('should create current month', () => {
const month = Month.current()
const now = new Date()
const expected = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
expect(month.value).toBe(expected)
})
})
describe('navigation', () => {
it('should get next month', () => {
expect(Month.create('2024-03').next().value).toBe('2024-04')
expect(Month.create('2024-12').next().value).toBe('2025-01')
})
it('should get previous month', () => {
expect(Month.create('2024-03').previous().value).toBe('2024-02')
expect(Month.create('2024-01').previous().value).toBe('2023-12')
})
})
describe('comparison', () => {
it('should compare months correctly', () => {
const m1 = Month.create('2024-03')
const m2 = Month.create('2024-06')
expect(m1.isBefore(m2)).toBe(true)
expect(m2.isAfter(m1)).toBe(true)
expect(m1.equals(Month.create('2024-03'))).toBe(true)
})
})
})
```
### 实体测试示例
```typescript
// src/domain/entities/ladder-target-rule.entity.spec.ts
import { LadderTargetRule } from './ladder-target-rule.entity'
import { RoleType } from '@/domain/enums'
describe('LadderTargetRule Entity', () => {
describe('getTarget', () => {
describe('province company', () => {
it.each([
[1, 150, 150],
[2, 300, 450],
[3, 600, 1050],
[5, 2400, 4650],
[9, 11750, 50000],
[12, 11750, 50000], // 超过9月使用第9月目标
])('month %i should have monthly=%i, cumulative=%i', (month, monthly, cumulative) => {
const target = LadderTargetRule.getTarget(RoleType.AUTH_PROVINCE_COMPANY, month)
expect(target.monthlyTarget).toBe(monthly)
expect(target.cumulativeTarget).toBe(cumulative)
})
})
describe('city company', () => {
it('should return correct targets', () => {
const target = LadderTargetRule.getTarget(RoleType.AUTH_CITY_COMPANY, 1)
expect(target.monthlyTarget).toBe(30)
expect(target.cumulativeTarget).toBe(30)
})
})
describe('community', () => {
it('should return fixed target', () => {
const target = LadderTargetRule.getTarget(RoleType.COMMUNITY, 1)
expect(target.monthlyTarget).toBe(10)
expect(target.cumulativeTarget).toBe(10)
})
})
})
describe('getFinalTarget', () => {
it('should return 50000 for province', () => {
expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_PROVINCE_COMPANY)).toBe(50000)
})
it('should return 10000 for city', () => {
expect(LadderTargetRule.getFinalTarget(RoleType.AUTH_CITY_COMPANY)).toBe(10000)
})
})
})
```
---
## 集成测试
### 测试位置
集成测试位于 `test/` 目录:
```
test/
├── jest-integration.json # 集成测试配置
├── setup-integration.ts # 测试环境设置
└── domain-services.integration-spec.ts # 领域服务集成测试
```
### 测试配置
```json
// test/jest-integration.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".integration-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/../src/$1"
},
"setupFilesAfterEnv": ["<rootDir>/setup-integration.ts"],
"testTimeout": 30000
}
```
### 领域服务集成测试
```typescript
// test/domain-services.integration-spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { AuthorizationValidatorService, IReferralRepository } from '@/domain/services/authorization-validator.service'
import { AssessmentCalculatorService, ITeamStatisticsRepository } from '@/domain/services/assessment-calculator.service'
import { IAuthorizationRoleRepository } from '@/domain/repositories'
import { UserId, RegionCode, Month } from '@/domain/value-objects'
import { RoleType } from '@/domain/enums'
import { AuthorizationRole } from '@/domain/aggregates'
describe('Domain Services Integration Tests', () => {
let module: TestingModule
let validatorService: AuthorizationValidatorService
let calculatorService: AssessmentCalculatorService
// Mock repositories
const mockAuthorizationRoleRepository: jest.Mocked<IAuthorizationRoleRepository> = {
save: jest.fn(),
findById: jest.fn(),
findByUserIdAndRoleType: jest.fn(),
findByUserIdRoleTypeAndRegion: jest.fn(),
findByUserId: jest.fn(),
findActiveByRoleTypeAndRegion: jest.fn(),
findAllActive: jest.fn(),
findPendingByUserId: jest.fn(),
findByStatus: jest.fn(),
delete: jest.fn(),
}
const mockReferralRepository: jest.Mocked<IReferralRepository> = {
findByUserId: jest.fn(),
getAllAncestors: jest.fn(),
getAllDescendants: jest.fn(),
}
const mockTeamStatisticsRepository: jest.Mocked<ITeamStatisticsRepository> = {
findByUserId: jest.fn(),
}
beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
AuthorizationValidatorService,
AssessmentCalculatorService,
{
provide: 'AUTHORIZATION_ROLE_REPOSITORY',
useValue: mockAuthorizationRoleRepository,
},
{
provide: 'REFERRAL_REPOSITORY',
useValue: mockReferralRepository,
},
{
provide: 'TEAM_STATISTICS_REPOSITORY',
useValue: mockTeamStatisticsRepository,
},
],
}).compile()
validatorService = module.get(AuthorizationValidatorService)
calculatorService = module.get(AssessmentCalculatorService)
})
afterAll(async () => {
await module.close()
})
beforeEach(() => {
jest.clearAllMocks()
})
describe('AuthorizationValidatorService', () => {
describe('validateAuthorizationRequest', () => {
it('should return success when no conflicts', async () => {
const userId = UserId.create('user-123')
const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.findByUserId.mockResolvedValue(null)
const result = await validatorService.validateAuthorizationRequest(
userId,
roleType,
regionCode,
mockReferralRepository,
mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(true)
})
it('should return failure when user already has authorization', async () => {
const userId = UserId.create('user-123')
const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
const existingAuth = AuthorizationRole.createAuthProvinceCompany({
userId,
provinceCode: '440000',
provinceName: '广东省',
})
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(existingAuth)
const result = await validatorService.validateAuthorizationRequest(
userId,
roleType,
regionCode,
mockReferralRepository,
mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(false)
expect(result.errorMessage).toContain('只能申请一个省代或市代授权')
})
it('should return failure when team member has same region authorization', async () => {
const userId = UserId.create('user-123')
const ancestorUserId = UserId.create('ancestor-user')
const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
mockAuthorizationRoleRepository.findByUserIdAndRoleType.mockResolvedValue(null)
mockReferralRepository.findByUserId.mockResolvedValue({ parentId: 'ancestor-user' })
mockReferralRepository.getAllAncestors.mockResolvedValue([ancestorUserId])
mockReferralRepository.getAllDescendants.mockResolvedValue([])
const existingAuth = AuthorizationRole.createAuthProvinceCompany({
userId: ancestorUserId,
provinceCode: '430000',
provinceName: '湖南省',
})
mockAuthorizationRoleRepository.findByUserIdRoleTypeAndRegion.mockResolvedValue(existingAuth)
const result = await validatorService.validateAuthorizationRequest(
userId,
roleType,
regionCode,
mockReferralRepository,
mockAuthorizationRoleRepository,
)
expect(result.isValid).toBe(false)
expect(result.errorMessage).toContain('本团队已有人申请')
})
})
})
describe('AssessmentCalculatorService', () => {
describe('assessAndRankRegion', () => {
it('should calculate assessments and rank by exceed ratio', async () => {
const roleType = RoleType.AUTH_PROVINCE_COMPANY
const regionCode = RegionCode.create('430000')
const assessmentMonth = Month.current()
// Setup: Two authorizations
const auth1 = AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-1'),
provinceCode: '430000',
provinceName: '湖南省',
})
auth1.authorize(AdminUserId.create('admin'))
const auth2 = AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-2'),
provinceCode: '430000',
provinceName: '湖南省',
})
auth2.authorize(AdminUserId.create('admin'))
mockAuthorizationRoleRepository.findActiveByRoleTypeAndRegion.mockResolvedValue([auth1, auth2])
// User 1 has better stats
mockTeamStatisticsRepository.findByUserId
.mockResolvedValueOnce({
userId: 'user-1',
totalTeamPlantingCount: 200,
getProvinceTeamCount: () => 70,
})
.mockResolvedValueOnce({
userId: 'user-2',
totalTeamPlantingCount: 100,
getProvinceTeamCount: () => 35,
})
const assessments = await calculatorService.assessAndRankRegion(
roleType,
regionCode,
assessmentMonth,
mockAuthorizationRoleRepository,
mockTeamStatisticsRepository,
)
expect(assessments.length).toBe(2)
expect(assessments[0].userId.value).toBe('user-1')
expect(assessments[0].rankingInRegion).toBe(1)
expect(assessments[0].isFirstPlace).toBe(true)
})
})
})
})
```
---
## 端到端测试
### 测试位置
```
test/
├── jest-e2e.json # E2E 测试配置
└── app.e2e-spec.ts # E2E 测试
```
### E2E 测试配置
```json
// test/jest-e2e.json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/../src/$1"
},
"testTimeout": 60000
}
```
### E2E 测试示例
```typescript
// test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '@/app.module'
import { GlobalExceptionFilter } from '@/shared/filters'
import { TransformInterceptor } from '@/shared/interceptors'
import { PrismaClient } from '@prisma/client'
describe('Authorization Service E2E Tests', () => {
let app: INestApplication
let prisma: PrismaClient
const isDockerEnv = process.env.NODE_ENV === 'test' && process.env.DATABASE_URL?.includes('test-db')
beforeAll(async () => {
try {
// Initialize Prisma
prisma = new PrismaClient()
await prisma.$connect()
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
app.setGlobalPrefix('api/v1')
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
app.useGlobalFilters(new GlobalExceptionFilter())
app.useGlobalInterceptors(new TransformInterceptor())
await app.init()
console.log('E2E tests initialized successfully')
} catch (error) {
if (isDockerEnv) {
throw new Error(`E2E tests failed in Docker: ${error}`)
}
console.log('E2E tests skipped: Infrastructure not available')
}
}, 60000)
afterAll(async () => {
if (app) await app.close()
if (prisma) await prisma.$disconnect()
})
describe('Authentication Tests', () => {
it('should return 401 for unauthorized access to /api/v1/authorizations/my', async () => {
if (!app) return
return request(app.getHttpServer())
.get('/api/v1/authorizations/my')
.expect(401)
})
it('should return 401 for unauthorized admin access', async () => {
if (!app) return
return request(app.getHttpServer())
.post('/api/v1/admin/authorizations/province-company')
.send({ userId: 'test', provinceCode: '430000', provinceName: '湖南省' })
.expect(401)
})
})
describe('Validation Tests', () => {
it('should return 401 for community authorization without token', async () => {
if (!app) return
return request(app.getHttpServer())
.post('/api/v1/authorizations/community')
.send({ communityName: '测试社区' })
.expect(401)
})
it('should return 401 for province authorization without token', async () => {
if (!app) return
return request(app.getHttpServer())
.post('/api/v1/authorizations/province')
.send({ provinceCode: '430000', provinceName: '湖南省' })
.expect(401)
})
it('should return 401 for city authorization without token', async () => {
if (!app) return
return request(app.getHttpServer())
.post('/api/v1/authorizations/city')
.send({ cityCode: '430100', cityName: '长沙市' })
.expect(401)
})
})
})
```
---
## Docker 测试环境
### Docker Compose 配置
```yaml
# docker-compose.test.yml
services:
test-db:
image: postgres:15-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: authorization_test
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test -d authorization_test"]
interval: 5s
timeout: 5s
retries: 5
test-redis:
image: redis:7-alpine
ports:
- "6380:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
test-kafka:
image: apache/kafka:3.7.0
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://test-kafka:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@test-kafka:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
ports:
- "9093:9092"
healthcheck:
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
authorization-service-test:
build:
context: .
dockerfile: Dockerfile.test
environment:
DATABASE_URL: postgresql://test:test@test-db:5432/authorization_test
REDIS_HOST: test-redis
REDIS_PORT: 6379
KAFKA_BROKERS: test-kafka:9092
JWT_SECRET: test-jwt-secret-key
JWT_EXPIRES_IN: 1h
NODE_ENV: test
depends_on:
test-db:
condition: service_healthy
test-redis:
condition: service_healthy
test-kafka:
condition: service_healthy
volumes:
- ./coverage:/app/coverage
command: sh -c "npx prisma migrate deploy && npm run test:unit && npm run test:integration && npm run test:e2e"
networks:
default:
driver: bridge
```
### 测试 Dockerfile
```dockerfile
# Dockerfile.test
FROM node:20-alpine
WORKDIR /app
# Install OpenSSL for Prisma
RUN apk add --no-cache openssl openssl-dev libc6-compat
# Copy package files
COPY package*.json ./
COPY prisma ./prisma/
# Install dependencies
RUN npm ci
# Generate Prisma client
RUN npx prisma generate
# Copy source code and tests
COPY . .
# Default command
CMD ["npm", "run", "test:all"]
```
### 运行 Docker 测试
```bash
# 使用 Makefile
make test-docker-all
# 或直接使用 docker compose
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
```
---
## 测试覆盖率
### 生成覆盖率报告
```bash
# 运行测试并生成覆盖率
npm run test:cov
# 覆盖率报告位置
coverage/
├── lcov-report/ # HTML 报告
│ └── index.html
├── lcov.info # LCOV 格式
└── coverage-summary.json
```
### 覆盖率目标
| 指标 | 目标 | 说明 |
|------|------|------|
| 行覆盖率 | ≥80% | 代码行被执行的比例 |
| 分支覆盖率 | ≥70% | 条件分支被覆盖的比例 |
| 函数覆盖率 | ≥80% | 函数被调用的比例 |
---
## CI/CD 集成
### GitHub Actions 示例
```yaml
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: authorization_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/authorization_test
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/authorization_test
REDIS_HOST: localhost
REDIS_PORT: 6379
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
```
---
## 手动测试指南
### 使用 curl 测试 API
```bash
# 获取 JWT Token假设从 Identity Service
TOKEN="eyJhbGciOiJIUzI1NiIs..."
# 获取我的授权
curl -X GET http://localhost:3002/api/v1/authorizations/my \
-H "Authorization: Bearer $TOKEN"
# 申请省代授权
curl -X POST http://localhost:3002/api/v1/authorizations/province \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provinceCode": "430000",
"provinceName": "湖南省"
}'
# 管理员审核授权
curl -X POST http://localhost:3002/api/v1/admin/authorizations/auth-123/review \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"approved": true,
"reason": "审核通过"
}'
```
### 使用 REST Client (VSCode)
创建 `.http` 文件:
```http
### Variables
@baseUrl = http://localhost:3002/api/v1
@token = your-jwt-token
### Get my authorizations
GET {{baseUrl}}/authorizations/my
Authorization: Bearer {{token}}
### Apply for province authorization
POST {{baseUrl}}/authorizations/province
Authorization: Bearer {{token}}
Content-Type: application/json
{
"provinceCode": "430000",
"provinceName": "湖南省"
}
### Admin: Review authorization
POST {{baseUrl}}/admin/authorizations/auth-123/review
Authorization: Bearer {{adminToken}}
Content-Type: application/json
{
"approved": true,
"reason": "审核通过"
}
```
---
## 测试最佳实践
### 1. 遵循 AAA 模式
```typescript
it('should do something', () => {
// Arrange - 准备测试数据
const input = createTestInput()
// Act - 执行被测试的操作
const result = service.doSomething(input)
// Assert - 验证结果
expect(result).toBe(expected)
})
```
### 2. 使用描述性测试名称
```typescript
// Good
it('should return failure when user already has province authorization')
// Bad
it('test validation')
```
### 3. 隔离测试
- 每个测试应该独立运行
- 使用 `beforeEach` 重置状态
- 避免测试之间的依赖
### 4. Mock 外部依赖
```typescript
const mockRepository: jest.Mocked<IRepository> = {
find: jest.fn(),
save: jest.fn(),
}
```
### 5. 测试边界条件
- 空值处理
- 边界值
- 异常情况