# 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. 测试边界条件

- 空值处理
- 边界值
- 异常情况
