# 📚 API 与测试文档

> **💡 推荐使用 OpenAPI 规范**
> 
> 本项目现在提供标准的 **OpenAPI 3.0 规范**！
> 
> - 📖 **交互式 API 文档**: [http://localhost:8080/api/docs](http://localhost:8080/api/docs) (Swagger UI)
> - 📄 **OpenAPI 规范文件**: [openapi.yaml](./openapi.yaml)
> - 🔧 **在线测试**: 在 Swagger UI 中直接测试所有 API
> - 🚀 **客户端生成**: 使用 OpenAPI 规范自动生成客户端代码
> 
> **本文档 (Markdown 版本)** 作为参考文档保留，内容与 OpenAPI 规范保持一致。
> 如需最新的 API 定义，请参考 [openapi.yaml](./openapi.yaml)。

---

## 目录

### Part 1: API 文档
- [基础信息](#基础信息)
- [认证 API](#认证-api)
- [集群管理 API](#集群管理-api)
- [Registry 管理 API](#registry-管理-api)
- [Artifact 浏览 API](#artifact-浏览-api)
- [实例管理 API](#实例管理-api)
- [监控 API](#监控-api)
- [响应格式](#响应格式)
- [错误处理](#错误处理)

### Part 2: 测试文档
- [测试策略](#测试策略)
- [单元测试](#单元测试)
- [集成测试](#集成测试)
- [API 测试](#api-测试)
- [E2E 测试](#e2e-测试)
- [Mock 测试](#mock-测试)
- [测试工具](#测试工具)
- [测试最佳实践](#测试最佳实践)

---

# Part 1: API 文档

## 基础信息

### Base URL

```
http://localhost:8080/api/v1
```

### 健康检查

```bash
GET /health

# 响应
{
  "status": "healthy"
}
```

### 通用请求头

```
Content-Type: application/json
Authorization: Bearer <JWT_TOKEN>  # 部分接口需要
```

---

## 认证 API

### 用户注册

```bash
POST /api/v1/auth/register
Content-Type: application/json

{
  "username": "john",
  "password": "secret123",
  "email": "john@example.com"
}

# 响应 201
{
  "id": "user-123",
  "username": "john",
  "email": "john@example.com",
  "createdAt": "2025-11-09T10:00:00Z"
}
```

### 用户登录

```bash
POST /api/v1/auth/login
Content-Type: application/json

{
  "username": "john",
  "password": "secret123"
}

# 响应 200
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "userId": "user-123",
  "username": "john"
}
```

### 刷新 Token

```bash
POST /api/v1/auth/refresh
Content-Type: application/json

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

# 响应 200
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "userId": "user-123",
  "username": "john"
}
```

---

## 集群管理 API

### 创建集群

```bash
POST /api/v1/clusters
Content-Type: application/json

{
  "name": "Production Cluster",
  "host": "https://k8s.example.com:6443",
  "description": "生产环境集群",
  "caData": "LS0tLS1CRUdJTi0...",      # Base64 编码的 CA 证书
  "certData": "LS0tLS1CRUdJTi0...",    # Base64 编码的客户端证书
  "keyData": "LS0tLS1CRUdJTi0..."      # Base64 编码的客户端密钥
}

# 响应 201
{
  "id": "cluster-abc123",
  "name": "Production Cluster",
  "host": "https://k8s.example.com:6443",
  "description": "生产环境集群",
  "status": "healthy",
  "createdAt": "2025-11-09T10:00:00Z",
  "updatedAt": "2025-11-09T10:00:00Z"
}
```

### 列出所有集群

```bash
GET /api/v1/clusters

# 响应 200
[
  {
    "id": "cluster-abc123",
    "name": "Production Cluster",
    "host": "https://k8s.example.com:6443",
    "description": "生产环境集群",
    "status": "healthy",
    "createdAt": "2025-11-09T10:00:00Z"
  }
]
```

### 获取集群详情

```bash
GET /api/v1/clusters/{clusterId}

# 响应 200
{
  "id": "cluster-abc123",
  "name": "Production Cluster",
  "host": "https://k8s.example.com:6443",
  "description": "生产环境集群",
  "status": "healthy",
  "version": "v1.28.0",
  "nodeCount": 5,
  "createdAt": "2025-11-09T10:00:00Z",
  "updatedAt": "2025-11-09T10:00:00Z"
}
```

### 更新集群

```bash
PUT /api/v1/clusters/{clusterId}
Content-Type: application/json

{
  "name": "Production Cluster (Updated)",
  "description": "更新后的描述"
}

# 响应 200
{
  "id": "cluster-abc123",
  "name": "Production Cluster (Updated)",
  "description": "更新后的描述",
  "updatedAt": "2025-11-09T11:00:00Z"
}
```

### 删除集群

```bash
DELETE /api/v1/clusters/{clusterId}

# 响应 204 No Content
```

### 集群健康检查

```bash
GET /api/v1/clusters/{clusterId}/health

# 响应 200
{
  "clusterId": "cluster-abc123",
  "status": "healthy",
  "version": "v1.28.0",
  "nodeCount": 5,
  "readyNodes": 5,
  "cpuCapacity": "40 cores",
  "memoryCapacity": "160Gi",
  "checkedAt": "2025-11-09T12:00:00Z"
}
```

---

## Registry 管理 API

> **OCI 标准**: 所有 Registry 都遵循 OCI (Open Container Initiative) 标准，支持 Harbor, Docker Hub, GHCR, Nexus, 以及任何兼容 OCI Distribution Spec 的 Registry。

### 创建 Registry

```bash
POST /api/v1/registries
Content-Type: application/json

{
  "name": "Harbor Production",
  "url": "https://harbor.example.com",
  "description": "生产环境 Harbor 仓库",
  "username": "admin",
  "password": "secret",
  "insecure": false
}

# 响应 201
{
  "id": "registry-123",
  "name": "Harbor Production",
  "url": "https://harbor.example.com",
  "description": "生产环境 Harbor 仓库",
  "username": "admin",
  "insecure": false,
  "createdAt": "2025-11-09T10:00:00Z",
  "updatedAt": "2025-11-09T10:00:00Z"
}
```

**字段说明**:
- `url`: Registry URL，所有 Registry 都使用 OCI Distribution API
- `username/password`: 可选，用于私有 Registry 认证
- `insecure`: 是否跳过 TLS 验证（开发环境可用）

> **注意**: 密码不会在响应中返回，存储时会自动加密。

### 列出所有 Registries

```bash
GET /api/v1/registries

# 响应 200
[
  {
    "id": "registry-123",
    "name": "Harbor Production",
    "url": "https://harbor.example.com",
    "description": "生产环境 Harbor 仓库",
    "username": "admin",
    "insecure": false,
    "createdAt": "2025-11-09T10:00:00Z"
  }
]
```

### 获取 Registry 详情

```bash
GET /api/v1/registries/{registryId}

# 响应 200
{
  "id": "registry-123",
  "name": "Harbor Production",
  "url": "https://harbor.example.com",
  "description": "生产环境 Harbor 仓库",
  "username": "admin",
  "insecure": false,
  "createdAt": "2025-11-09T10:00:00Z",
  "updatedAt": "2025-11-09T10:00:00Z"
}
```

### 更新 Registry

```bash
PUT /api/v1/registries/{registryId}
Content-Type: application/json

{
  "name": "Harbor Production (Updated)",
  "url": "https://new-harbor.example.com",
  "password": "new-secret"  # 可选，只在需要更新密码时提供
}

# 响应 200
{
  "id": "registry-123",
  "name": "Harbor Production (Updated)",
  "url": "https://new-harbor.example.com",
  "updatedAt": "2025-11-09T11:00:00Z"
}
```

### 删除 Registry

```bash
DELETE /api/v1/registries/{registryId}

# 响应 204 No Content
```

### Registry 健康检查

```bash
GET /api/v1/registries/{registryId}/health

# 响应 200
{
  "registryId": "registry-123",
  "status": "healthy",
  "url": "https://harbor.example.com",
  "reachable": true,
  "authenticated": true,
  "responseTime": 125,  # 毫秒
  "checkedAt": "2025-11-09T12:00:00Z"
}

# 响应 503 (不健康)
{
  "registryId": "registry-123",
  "status": "unhealthy",
  "url": "https://harbor.example.com",
  "reachable": false,
  "error": "connection timeout",
  "checkedAt": "2025-11-09T12:00:00Z"
}
```

---

## Artifact 浏览 API

### 列出 Repositories

```bash
GET /api/v1/registries/{registryId}/repositories

# 响应 200
{
  "registryId": "registry-123",
  "registryUrl": "https://harbor.example.com",
  "repositories": [
    "charts/nginx",
    "charts/redis",
    "charts/vllm-serve",
    "library/alpine"
  ],
  "total": 4,
  "catalogSupported": true,
  "source": "catalog"
}
```

> **注意**: 需要 Registry 支持 `_catalog` API（OCI Distribution Spec）。

### 列出 Artifacts

```bash
GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts

# 示例（需要 URL 编码）
GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts

# 响应 200
{
  "repositoryName": "charts/nginx",
  "tags": [
    {
      "name": "1.0.0",
      "digest": "sha256:abc123def456...",
      "type": "chart",
      "size": 12345678,
      "createdAt": "2025-11-01T10:00:00Z"
    }
  ],
  "total": 1
}
```

**Artifact 类型识别**:
- `chart`: Helm Chart
- `image`: Docker Image / OCI Image
- `other`: 其他类型

### 获取 Artifact 详情

```bash
GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference}

# reference 可以是 tag 或 digest
GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0

# 响应 200
{
  "repositoryName": "charts/nginx",
  "tag": "1.0.0",
  "digest": "sha256:abc123def456...",
  "type": "chart",
  "size": 12345678,
  "createdAt": "2025-11-01T10:00:00Z"
}
```

**字段说明**:
- `repositoryName`: 仓库名称
- `tag`: 标签名称（如果使用 tag 引用）
- `digest`: SHA256 摘要
- `type`: 制品类型，从 mediaType 自动识别：
  - `chart`: Helm Chart（mediaType 包含 `helm.config` 或 `helm.chart`）
  - `image`: Docker/OCI Image（mediaType 包含 `docker.container.image` 或 `oci.image`）
  - `other`: 其他类型
- `size`: 总大小（字节）
- `createdAt`: 创建时间（ISO 8601 格式）

### 获取 Helm Chart Values Schema

```bash
GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference}/values-schema

# 仅支持 Helm Chart 类型
GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0/values-schema

# 响应 200
{
  "repositoryName": "charts/nginx",
  "tag": "1.0.0",
  "valuesSchema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
      "replicaCount": {
        "type": "integer",
        "default": 1,
        "description": "Number of replicas"
      },
      "image": {
        "type": "object",
        "properties": {
          "repository": {
            "type": "string",
            "default": "nginx"
          },
          "tag": {
            "type": "string",
            "default": "latest"
          }
        }
      }
    }
  }
}
```

> **URL 编码提示**: Repository 名称包含斜杠时（如 `charts/nginx`），需要编码为 `charts%2Fnginx`。

---

## 实例管理 API

### 安装应用

```bash
POST /api/v1/clusters/{clusterId}/instances
Content-Type: application/json

# 方式 1: 使用 JSON values
{
  "name": "my-nginx",
  "namespace": "default",
  "registryId": "registry-123",
  "repository": "charts/nginx",
  "chart": "nginx",
  "version": "1.0.0",
  "description": "My NGINX deployment",
  "values": {
    "replicaCount": 2,
    "image": {
      "tag": "1.21.0"
    }
  }
}

# 响应 201
{
  "id": "instance-xyz789",
  "name": "my-nginx",
  "namespace": "default",
  "clusterId": "cluster-abc123",
  "registryId": "registry-123",
  "chart": "nginx",
  "version": "1.0.0",
  "status": "deployed",
  "revision": 1,
  "description": "My NGINX deployment",
  "createdAt": "2025-11-09T10:00:00Z",
  "updatedAt": "2025-11-09T10:00:00Z"
}
```

### 列出应用实例

```bash
GET /api/v1/clusters/{clusterId}/instances

# 响应 200
[
  {
    "id": "instance-xyz789",
    "name": "my-nginx",
    "namespace": "default",
    "chart": "nginx",
    "version": "1.0.0",
    "status": "deployed",
    "revision": 1,
    "createdAt": "2025-11-09T10:00:00Z"
  }
]
```

### 升级应用

```bash
PUT /api/v1/clusters/{clusterId}/instances/{instanceId}
Content-Type: application/json

{
  "version": "1.1.0",
  "values": {
    "replicaCount": 3
  },
  "description": "Upgrade to v1.1.0"
}

# 响应 200
{
  "id": "instance-xyz789",
  "name": "my-nginx",
  "version": "1.1.0",
  "status": "deployed",
  "revision": 4,
  "updatedAt": "2025-11-09T12:00:00Z"
}
```

### 卸载应用

```bash
DELETE /api/v1/clusters/{clusterId}/instances/{instanceId}

# 响应 204 No Content
```

### 获取实例访问入口

```bash
GET /api/v1/clusters/{clusterId}/instances/{instanceId}/entries

# 响应 200
[
  {
    "kind": "Service",
    "name": "test-nginx",
    "namespace": "default",
    "type": "ClusterIP",
    "clusterIP": "10.43.120.15",
    "ports": [
      {
        "name": "http",
        "protocol": "TCP",
        "port": 80,
        "targetPort": "http"
      }
    ]
  },
  {
    "kind": "Ingress",
    "name": "test-nginx",
    "namespace": "default",
    "type": "nginx",
    "hosts": [
      {
        "host": "nginx.example.com",
        "paths": [
          {
            "path": "/",
            "serviceName": "test-nginx",
            "servicePort": "http"
          }
        ]
      }
    ],
    "loadBalancerIngress": [
      "34.120.0.12"
    ]
  }
]
```

---

## 监控 API

### 列出集群监控信息

```bash
GET /api/v1/monitoring/clusters

# 响应 200
[
  {
    "clusterId": "cluster-abc123",
    "clusterName": "Production Cluster",
    "status": "healthy",
    "cpuUsage": 45.2,
    "memoryUsage": 62.8,
    "nodeCount": 5,
    "readyNodes": 5,
    "podCount": 120,
    "timestamp": "2025-11-09T12:00:00Z"
  }
]
```

### 获取监控摘要

```bash
GET /api/v1/monitoring/summary

# 响应 200
{
  "totalClusters": 3,
  "healthyClusters": 2,
  "unhealthyClusters": 1,
  "totalNodes": 15,
  "totalInstances": 45,
  "averageCpuUsage": 42.3,
  "averageMemoryUsage": 58.6,
  "timestamp": "2025-11-09T12:00:00Z"
}
```

---

## 响应格式

### 成功响应

**单个资源**:

```json
{
  "id": "resource-123",
  "name": "Resource Name",
  "status": "active",
  "createdAt": "2025-11-09T10:00:00Z"
}
```

**资源列表**:

```json
[
  {
    "id": "resource-123",
    "name": "Resource 1"
  },
  {
    "id": "resource-456",
    "name": "Resource 2"
  }
]
```

### HTTP 状态码

| 状态码 | 说明 | 使用场景 |
|--------|------|----------|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 删除成功（无返回内容） |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证或 Token 无效 |
| 403 | Forbidden | 无权限访问 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突（如重复创建） |
| 500 | Internal Server Error | 服务器内部错误 |
| 503 | Service Unavailable | 服务不可用 |

---

## 错误处理

### 错误响应格式

```json
{
  "error": "Error Title",
  "message": "Detailed error message",
  "code": "ERROR_CODE",
  "details": {...}
}
```

### 常见错误示例

#### 400 Bad Request

```json
{
  "error": "Invalid Request",
  "message": "Field 'name' is required"
}
```

#### 401 Unauthorized

```json
{
  "error": "Unauthorized",
  "message": "Invalid or expired token"
}
```

#### 404 Not Found

```json
{
  "error": "Not Found",
  "message": "Cluster with ID 'cluster-123' not found"
}
```

#### 500 Internal Server Error

```json
{
  "error": "Internal Server Error",
  "message": "Failed to connect to database"
}
```

---

# Part 2: 测试文档

## 测试策略

OCDP Backend 采用多层测试策略，确保代码质量和系统稳定性。

### 测试金字塔

```
       ┌────────────┐
      ╱    E2E     ╱│    少量（慢速、昂贵）
     ╱ 集成测试  ╱ │    中等（中速、适度）
    ╱  单元测试 ╱  │    大量（快速、便宜）
   └──────────┘   │
    │            │
    └────────────┘
```

### 测试类型

| 测试类型 | 范围 | 速度 | 依赖 | 执行频率 |
|---------|------|------|------|----------|
| **单元测试** | Domain Layer | 快 | 无 | 每次提交 |
| **集成测试** | 跨层交互 | 中 | Mock | 每次提交 |
| **API 测试** | HTTP 接口 | 中 | Mock | 每次 PR |
| **E2E 测试** | 完整流程 | 慢 | 真实环境 | 发布前 |

---

## 单元测试

### 测试 Domain Service

单元测试专注于业务逻辑，使用 Mock Repository。

#### 示例：测试 ClusterService

```go
// internal/domain/service/cluster_service_test.go
package service_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"

    "ocdp-backend/internal/adapter/output/persistence/mock"
    "ocdp-backend/internal/domain/service"
)

func TestClusterService_CreateCluster(t *testing.T) {
    // Arrange
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    
    ctx := context.Background()
    name := "Test Cluster"
    host := "https://k8s.test:6443"
    
    // Act
    cluster, err := svc.CreateCluster(ctx, name, host, "desc", "ca", "cert", "key")
    
    // Assert
    require.NoError(t, err)
    assert.NotEmpty(t, cluster.ID)
    assert.Equal(t, name, cluster.Name)
    assert.Equal(t, host, cluster.Host)
}

func TestClusterService_GetCluster(t *testing.T) {
    // Arrange
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    // 先创建集群
    created, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    
    // Act
    cluster, err := svc.GetCluster(ctx, created.ID)
    
    // Assert
    require.NoError(t, err)
    assert.Equal(t, created.ID, cluster.ID)
    assert.Equal(t, created.Name, cluster.Name)
}

func TestClusterService_GetCluster_NotFound(t *testing.T) {
    // Arrange
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    // Act
    cluster, err := svc.GetCluster(ctx, "non-existent-id")
    
    // Assert
    assert.Error(t, err)
    assert.Nil(t, cluster)
}

func TestClusterService_ListClusters(t *testing.T) {
    // Arrange
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    // 创建多个集群
    svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "")
    svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "")
    
    // Act
    clusters, err := svc.ListClusters(ctx)
    
    // Assert
    require.NoError(t, err)
    assert.Len(t, clusters, 2)
}

func TestClusterService_DeleteCluster(t *testing.T) {
    // Arrange
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    
    // Act
    err := svc.DeleteCluster(ctx, cluster.ID)
    
    // Assert
    require.NoError(t, err)
    
    // 验证已删除
    _, err = svc.GetCluster(ctx, cluster.ID)
    assert.Error(t, err)
}
```

#### 示例：测试 AuthService

```go
// internal/domain/service/auth_service_test.go
package service_test

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"

    "ocdp-backend/internal/adapter/output/persistence/mock"
    "ocdp-backend/internal/domain/service"
    "ocdp-backend/internal/pkg/password"
    "ocdp-backend/internal/pkg/jwt"
)

func TestAuthService_Register(t *testing.T) {
    // Arrange
    userRepo := mock.NewUserRepositoryMock()
    hasher := password.NewBcryptHasher()
    jwtGen := jwt.NewJWTGenerator("test-secret")
    svc := service.NewAuthService(userRepo, hasher, jwtGen)
    
    ctx := context.Background()
    
    // Act
    user, err := svc.Register(ctx, "testuser", "password123", "test@example.com")
    
    // Assert
    require.NoError(t, err)
    assert.NotEmpty(t, user.ID)
    assert.Equal(t, "testuser", user.Username)
    assert.NotEqual(t, "password123", user.PasswordHash) // 密码已哈希
}

func TestAuthService_Login(t *testing.T) {
    // Arrange
    userRepo := mock.NewUserRepositoryMock()
    hasher := password.NewBcryptHasher()
    jwtGen := jwt.NewJWTGenerator("test-secret")
    svc := service.NewAuthService(userRepo, hasher, jwtGen)
    
    ctx := context.Background()
    
    // 先注册用户
    svc.Register(ctx, "testuser", "password123", "test@example.com")
    
    // Act
    response, err := svc.Login(ctx, "testuser", "password123")
    
    // Assert
    require.NoError(t, err)
    assert.NotEmpty(t, response.AccessToken)
    assert.NotEmpty(t, response.RefreshToken)
    assert.Equal(t, "testuser", response.User.Username)
}

func TestAuthService_Login_WrongPassword(t *testing.T) {
    // Arrange
    userRepo := mock.NewUserRepositoryMock()
    hasher := password.NewBcryptHasher()
    jwtGen := jwt.NewJWTGenerator("test-secret")
    svc := service.NewAuthService(userRepo, hasher, jwtGen)
    
    ctx := context.Background()
    svc.Register(ctx, "testuser", "password123", "test@example.com")
    
    // Act
    response, err := svc.Login(ctx, "testuser", "wrongpassword")
    
    // Assert
    assert.Error(t, err)
    assert.Nil(t, response)
}
```

### 运行单元测试

```bash
# 运行所有单元测试
go test ./internal/domain/...

# 运行特定包
go test ./internal/domain/service

# 带覆盖率
go test -cover ./internal/domain/...

# 详细输出
go test -v ./internal/domain/...

# 生成覆盖率报告
go test -coverprofile=coverage.out ./internal/domain/...
go tool cover -html=coverage.out
```

---

## 集成测试

集成测试验证跨层交互，使用 Mock 适配器。

### 示例：测试 REST Handler + Service

```go
// internal/adapter/input/http/rest/cluster_handler_test.go
package rest_test

import (
    "bytes"
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gorilla/mux"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"

    "ocdp-backend/internal/adapter/input/http/dto"
    "ocdp-backend/internal/adapter/input/http/rest"
    "ocdp-backend/internal/adapter/output/persistence/mock"
    "ocdp-backend/internal/domain/service"
)

func setupClusterHandler() (*rest.ClusterHandler, *service.ClusterService) {
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    handler := rest.NewClusterHandler(svc)
    return handler, svc
}

func TestClusterHandler_CreateCluster(t *testing.T) {
    // Arrange
    handler, _ := setupClusterHandler()
    
    reqBody := dto.CreateClusterRequest{
        Name:        "Test Cluster",
        Host:        "https://k8s.test:6443",
        Description: "Test cluster",
        CAData:      "ca-data",
        CertData:    "cert-data",
        KeyData:     "key-data",
    }
    
    body, _ := json.Marshal(reqBody)
    req := httptest.NewRequest(http.MethodPost, "/api/v1/clusters", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    rec := httptest.NewRecorder()
    
    // Act
    handler.CreateCluster(rec, req)
    
    // Assert
    assert.Equal(t, http.StatusCreated, rec.Code)
    
    var response map[string]interface{}
    json.Unmarshal(rec.Body.Bytes(), &response)
    
    assert.NotEmpty(t, response["id"])
    assert.Equal(t, "Test Cluster", response["name"])
}

func TestClusterHandler_GetAllClusters(t *testing.T) {
    // Arrange
    handler, svc := setupClusterHandler()
    
    // 创建测试数据
    ctx := context.Background()
    svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "")
    svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "")
    
    req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters", nil)
    rec := httptest.NewRecorder()
    
    // Act
    handler.GetAllClusters(rec, req)
    
    // Assert
    assert.Equal(t, http.StatusOK, rec.Code)
    
    var clusters []map[string]interface{}
    json.Unmarshal(rec.Body.Bytes(), &clusters)
    
    assert.Len(t, clusters, 2)
}

func TestClusterHandler_GetCluster(t *testing.T) {
    // Arrange
    handler, svc := setupClusterHandler()
    ctx := context.Background()
    
    cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    
    req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters/"+cluster.ID, nil)
    req = mux.SetURLVars(req, map[string]string{"clusterId": cluster.ID})
    rec := httptest.NewRecorder()
    
    // Act
    handler.GetCluster(rec, req)
    
    // Assert
    assert.Equal(t, http.StatusOK, rec.Code)
    
    var response map[string]interface{}
    json.Unmarshal(rec.Body.Bytes(), &response)
    
    assert.Equal(t, cluster.ID, response["id"])
}
```

### 运行集成测试

```bash
# 使用 Mock 模式运行所有测试
ADAPTER_MODE=mock go test ./...

# 测试特定模块
go test ./internal/adapter/input/http/rest/...

# 并行测试
go test -parallel 4 ./...
```

---

## API 测试

使用 HTTP 客户端测试完整的 API 流程。

### 使用 cURL 测试

```bash
#!/bin/bash
# scripts/test-api.sh

BASE_URL="http://localhost:8080/api/v1"

# 健康检查
echo "=== Health Check ==="
curl -X GET http://localhost:8080/health

# 注册用户
echo "\n=== Register User ==="
curl -X POST $BASE_URL/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "test123",
    "email": "test@example.com"
  }'

# 登录
echo "\n=== Login ==="
LOGIN_RESPONSE=$(curl -X POST $BASE_URL/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "test123"
  }')

TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.accessToken')

# 创建集群
echo "\n=== Create Cluster ==="
curl -X POST $BASE_URL/clusters \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "Test Cluster",
    "host": "https://k8s.test:6443",
    "description": "Test cluster",
    "caData": "LS0tLS...",
    "certData": "LS0tLS...",
    "keyData": "LS0tLS..."
  }'

# 列出集群
echo "\n=== List Clusters ==="
curl -X GET $BASE_URL/clusters \
  -H "Authorization: Bearer $TOKEN"

# 创建 Registry
echo "\n=== Create Registry ==="
curl -X POST $BASE_URL/registries \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "Test Harbor",
    "url": "https://harbor.test.com",
    "username": "admin",
    "password": "secret"
  }'

# 列出 Registries
echo "\n=== List Registries ==="
curl -X GET $BASE_URL/registries \
  -H "Authorization: Bearer $TOKEN"
```

### 使用 Go 测试 HTTP 接口

```go
// test/api/api_test.go
package api_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

const baseURL = "http://localhost:8080/api/v1"

func TestAPI_HealthCheck(t *testing.T) {
    resp, err := http.Get("http://localhost:8080/health")
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusOK, resp.StatusCode)
}

func TestAPI_AuthFlow(t *testing.T) {
    // 1. 注册
    registerBody := map[string]string{
        "username": "apitest",
        "password": "test123",
        "email":    "apitest@example.com",
    }
    
    body, _ := json.Marshal(registerBody)
    resp, err := http.Post(baseURL+"/auth/register", "application/json", bytes.NewBuffer(body))
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
    
    // 2. 登录
    loginBody := map[string]string{
        "username": "apitest",
        "password": "test123",
    }
    
    body, _ = json.Marshal(loginBody)
    resp, err = http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body))
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusOK, resp.StatusCode)
    
    var loginResponse map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&loginResponse)
    
    assert.NotEmpty(t, loginResponse["accessToken"])
    assert.NotEmpty(t, loginResponse["refreshToken"])
}

func TestAPI_ClusterCRUD(t *testing.T) {
    // 先获取 token
    token := getAuthToken(t)
    
    // 1. 创建集群
    clusterBody := map[string]string{
        "name":        "API Test Cluster",
        "host":        "https://k8s.test:6443",
        "description": "Created by API test",
        "caData":      "test-ca",
        "certData":    "test-cert",
        "keyData":     "test-key",
    }
    
    body, _ := json.Marshal(clusterBody)
    req, _ := http.NewRequest(http.MethodPost, baseURL+"/clusters", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)
    
    resp, err := http.DefaultClient.Do(req)
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
    
    var cluster map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&cluster)
    clusterID := cluster["id"].(string)
    
    // 2. 获取集群
    req, _ = http.NewRequest(http.MethodGet, baseURL+"/clusters/"+clusterID, nil)
    req.Header.Set("Authorization", "Bearer "+token)
    
    resp, err = http.DefaultClient.Do(req)
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusOK, resp.StatusCode)
    
    // 3. 删除集群
    req, _ = http.NewRequest(http.MethodDelete, baseURL+"/clusters/"+clusterID, nil)
    req.Header.Set("Authorization", "Bearer "+token)
    
    resp, err = http.DefaultClient.Do(req)
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}

func getAuthToken(t *testing.T) string {
    // 登录并返回 token
    loginBody := map[string]string{
        "username": "admin",
        "password": "admin123",
    }
    
    body, _ := json.Marshal(loginBody)
    resp, _ := http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body))
    defer resp.Body.Close()
    
    var response map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&response)
    
    return response["accessToken"].(string)
}
```

---

## E2E 测试

端到端测试验证完整的用户场景。

### E2E 测试示例

```go
// test/e2e/deployment_test.go
package e2e_test

import (
    "context"
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestE2E_CompleteDeploymentFlow(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping E2E test in short mode")
    }
    
    ctx := context.Background()
    
    // 1. 注册用户
    t.Log("Step 1: Register user")
    user := registerUser(t, "e2euser", "password123")
    assert.NotEmpty(t, user.ID)
    
    // 2. 登录获取 token
    t.Log("Step 2: Login")
    token := login(t, "e2euser", "password123")
    assert.NotEmpty(t, token)
    
    // 3. 创建 Registry
    t.Log("Step 3: Create registry")
    registry := createRegistry(t, token, "Test Harbor", "https://harbor.test.com")
    assert.NotEmpty(t, registry.ID)
    
    // 4. 创建 Cluster
    t.Log("Step 4: Create cluster")
    cluster := createCluster(t, token, "Test K8s", "https://k8s.test:6443")
    assert.NotEmpty(t, cluster.ID)
    
    // 5. 部署应用
    t.Log("Step 5: Deploy application")
    instance := deployApp(t, token, cluster.ID, registry.ID, "nginx", "1.0.0")
    assert.Equal(t, "deployed", instance.Status)
    
    // 6. 等待应用就绪
    t.Log("Step 6: Wait for application ready")
    time.Sleep(10 * time.Second)
    
    // 7. 检查应用状态
    t.Log("Step 7: Check application status")
    status := getInstanceStatus(t, token, cluster.ID, instance.ID)
    assert.Equal(t, "deployed", status)
    
    // 8. 升级应用
    t.Log("Step 8: Upgrade application")
    upgraded := upgradeApp(t, token, cluster.ID, instance.ID, "1.1.0")
    assert.Equal(t, 2, upgraded.Revision)
    
    // 9. 卸载应用
    t.Log("Step 9: Uninstall application")
    err := uninstallApp(t, token, cluster.ID, instance.ID)
    assert.NoError(t, err)
    
    // 10. 清理
    t.Log("Step 10: Cleanup")
    deleteCluster(t, token, cluster.ID)
    deleteRegistry(t, token, registry.ID)
}
```

### 运行 E2E 测试

```bash
# 使用 Production 模式运行 E2E 测试
ADAPTER_MODE=production DATABASE_URL="..." go test ./test/e2e/... -timeout 30m

# 跳过 E2E 测试
go test -short ./...
```

---

## Mock 测试

### Mock Repository 示例

所有 Repository 都有 Mock 实现，位于 `internal/adapter/output/persistence/mock/`。

```go
// internal/adapter/output/persistence/mock/cluster_repository_mock.go
package mock

import (
    "context"
    "fmt"
    "sync"

    "ocdp-backend/internal/domain/entity"
    "ocdp-backend/internal/domain/repository"
)

type ClusterRepositoryMock struct {
    clusters map[string]*entity.Cluster
    mu       sync.RWMutex
}

func NewClusterRepositoryMock() repository.ClusterRepository {
    return &ClusterRepositoryMock{
        clusters: make(map[string]*entity.Cluster),
    }
}

func (r *ClusterRepositoryMock) Create(ctx context.Context, cluster *entity.Cluster) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, exists := r.clusters[cluster.ID]; exists {
        return fmt.Errorf("cluster already exists")
    }
    
    r.clusters[cluster.ID] = cluster
    return nil
}

func (r *ClusterRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    cluster, exists := r.clusters[id]
    if !exists {
        return nil, fmt.Errorf("cluster not found")
    }
    
    return cluster, nil
}

func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    clusters := make([]*entity.Cluster, 0, len(r.clusters))
    for _, cluster := range r.clusters {
        clusters = append(clusters, cluster)
    }
    
    return clusters, nil
}

func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    if _, exists := r.clusters[id]; !exists {
        return fmt.Errorf("cluster not found")
    }
    
    delete(r.clusters, id)
    return nil
}
```

---

## 测试工具

### 推荐的测试库

```go
import (
    "testing"
    
    "github.com/stretchr/testify/assert"     // 断言
    "github.com/stretchr/testify/require"    // 必需条件
    "github.com/stretchr/testify/mock"       // Mock 对象
    "github.com/stretchr/testify/suite"      // 测试套件
)
```

### 安装测试依赖

```bash
go get github.com/stretchr/testify
```

### 测试覆盖率

```bash
# 生成覆盖率报告
go test -coverprofile=coverage.out ./...

# 查看覆盖率
go tool cover -func=coverage.out

# HTML 报告
go tool cover -html=coverage.out -o coverage.html
```

### 性能测试

```go
func BenchmarkClusterService_CreateCluster(b *testing.B) {
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    }
}

// 运行性能测试
// go test -bench=. -benchmem ./internal/domain/service/
```

---

## 测试最佳实践

### 1. 测试命名规范

```go
// ✅ 好的命名
func TestClusterService_CreateCluster(t *testing.T)
func TestClusterService_CreateCluster_DuplicateName(t *testing.T)
func TestClusterHandler_GetCluster_NotFound(t *testing.T)

// ❌ 不好的命名
func TestCreate(t *testing.T)
func Test1(t *testing.T)
```

### 2. AAA 模式 (Arrange-Act-Assert)

```go
func TestExample(t *testing.T) {
    // Arrange - 准备测试数据
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    
    // Act - 执行操作
    cluster, err := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    
    // Assert - 验证结果
    require.NoError(t, err)
    assert.NotEmpty(t, cluster.ID)
}
```

### 3. 表驱动测试

```go
func TestClusterValidation(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    bool
        wantErr bool
    }{
        {"valid URL", "https://k8s.example.com:6443", true, false},
        {"invalid URL", "not-a-url", false, true},
        {"empty URL", "", false, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := validateClusterURL(tt.input)
            
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                require.NoError(t, err)
                assert.Equal(t, tt.want, got)
            }
        })
    }
}
```

### 4. 使用测试辅助函数

```go
// test/helpers/helpers.go
package helpers

func CreateTestCluster(t *testing.T, svc *service.ClusterService) *entity.Cluster {
    t.Helper()
    
    cluster, err := svc.CreateCluster(
        context.Background(),
        "Test Cluster",
        "https://k8s.test:6443",
        "", "", "", "",
    )
    
    require.NoError(t, err)
    return cluster
}

// 在测试中使用
func TestSomething(t *testing.T) {
    cluster := helpers.CreateTestCluster(t, svc)
    // ...
}
```

### 5. 清理测试数据

```go
func TestWithCleanup(t *testing.T) {
    repo := mock.NewClusterRepositoryMock()
    svc := service.NewClusterService(repo)
    ctx := context.Background()
    
    cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "")
    
    // 确保清理
    t.Cleanup(func() {
        svc.DeleteCluster(ctx, cluster.ID)
    })
    
    // 测试逻辑
    // ...
}
```

### 6. 并行测试

```go
func TestParallel(t *testing.T) {
    t.Parallel() // 标记为可并行
    
    // 测试逻辑
}
```

### 7. 跳过测试

```go
func TestSomething(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping test in short mode")
    }
    
    // 长时间运行的测试
}

// 运行: go test -short
```

---

## CI/CD 集成

### GitHub Actions 示例

```yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: ocdp_test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Install dependencies
        run: go mod download
      
      - name: Run unit tests
        run: go test -short -cover ./internal/domain/...
      
      - name: Run integration tests (Mock)
        env:
          ADAPTER_MODE: mock
        run: go test -short ./...
      
      - name: Run integration tests (Production)
        env:
          ADAPTER_MODE: production
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ocdp_test?sslmode=disable
        run: go test ./...
      
      - name: Generate coverage report
        run: |
          go test -coverprofile=coverage.out ./...
          go tool cover -func=coverage.out
```

---

## 相关文档

- [架构文档](architecture.md)
- [部署文档](deployment.md)
- [主 README](../README.md)

---

**Last Updated**: 2025-11-09  
**API Version**: v1
