//go:build legacytests // +build legacytests package e2e import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "time" . "github.com/smartystreets/goconvey/convey" "quyun/v2/app" "quyun/v2/app/config" ) // TestAPIHealth 测试 API 健康检查 func TestAPIHealth(t *testing.T) { Convey("API 健康检查测试", t, func() { var server *httptest.Server var testConfig *config.Config Convey("当启动测试服务器时", func() { testConfig = &config.Config{ App: config.AppConfig{ Mode: "test", BaseURI: "http://localhost:8080", }, Http: config.HttpConfig{ Port: 8080, }, Log: config.LogConfig{ Level: "debug", Format: "text", EnableCaller: true, }, } app := app.New(testConfig) server = httptest.NewServer(app) Convey("服务器应该成功启动", func() { So(server, ShouldNotBeNil) So(server.URL, ShouldNotBeEmpty) }) }) Convey("当访问健康检查端点时", func() { resp, err := http.Get(server.URL + "/health") So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusOK) defer resp.Body.Close() var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) So(err, ShouldBeNil) Convey("响应应该包含正确的状态", func() { So(result["status"], ShouldEqual, "ok") }) Convey("响应应该包含时间戳", func() { So(result, ShouldContainKey, "timestamp") }) Convey("响应应该是 JSON 格式", func() { So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8") }) }) Convey("当访问不存在的端点时", func() { resp, err := http.Get(server.URL + "/api/nonexistent") So(err, ShouldBeNil) So(resp.StatusCode, ShouldEqual, http.StatusNotFound) defer resp.Body.Close() var result map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&result) So(err, ShouldBeNil) Convey("响应应该包含错误信息", func() { So(result, ShouldContainKey, "error") }) }) Convey("当测试 CORS 支持", func() { req, err := http.NewRequest("OPTIONS", server.URL+"/api/test", nil) So(err, ShouldBeNil) req.Header.Set("Origin", "http://localhost:3000") req.Header.Set("Access-Control-Request-Method", "POST") req.Header.Set("Access-Control-Request-Headers", "Content-Type,Authorization") resp, err := http.DefaultClient.Do(req) So(err, ShouldBeNil) defer resp.Body.Close() Convey("应该返回正确的 CORS 头", func() { So(resp.StatusCode, ShouldEqual, http.StatusOK) So(resp.Header.Get("Access-Control-Allow-Origin"), ShouldContainSubstring, "localhost") So(resp.Header.Get("Access-Control-Allow-Methods"), ShouldContainSubstring, "POST") }) }) Reset(func() { if server != nil { server.Close() } }) }) } // TestAPIPerformance 测试 API 性能 func TestAPIPerformance(t *testing.T) { Convey("API 性能测试", t, func() { var server *httptest.Server var testConfig *config.Config Convey("当准备性能测试时", func() { testConfig = &config.Config{ App: config.AppConfig{ Mode: "test", BaseURI: "http://localhost:8080", }, Http: config.HttpConfig{ Port: 8080, }, Log: config.LogConfig{ Level: "error", // 减少日志输出以提升性能 Format: "text", }, } app := app.New(testConfig) server = httptest.NewServer(app) }) Convey("当测试响应时间时", func() { start := time.Now() resp, err := http.Get(server.URL + "/health") So(err, ShouldBeNil) defer resp.Body.Close() duration := time.Since(start) So(resp.StatusCode, ShouldEqual, http.StatusOK) Convey("响应时间应该在合理范围内", func() { So(duration, ShouldBeLessThan, 100*time.Millisecond) }) }) Convey("当测试并发请求时", func() { const numRequests = 50 const maxConcurrency = 10 const timeout = 5 * time.Second var wg sync.WaitGroup successCount := 0 errorCount := 0 var mu sync.Mutex // 使用信号量控制并发数 sem := make(chan struct{}, maxConcurrency) start := time.Now() for i := 0; i < numRequests; i++ { wg.Add(1) go func(requestID int) { defer wg.Done() // 获取信号量 sem <- struct{}{} defer func() { <-sem }() ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", server.URL+"/health", nil) if err != nil { mu.Lock() errorCount++ mu.Unlock() return } client := &http.Client{ Timeout: timeout, } resp, err := client.Do(req) if err != nil { mu.Lock() errorCount++ mu.Unlock() return } defer resp.Body.Close() mu.Lock() if resp.StatusCode == http.StatusOK { successCount++ } else { errorCount++ } mu.Unlock() }(i) } wg.Wait() duration := time.Since(start) Convey("所有请求都应该完成", func() { So(successCount+errorCount, ShouldEqual, numRequests) }) Convey("所有请求都应该成功", func() { So(errorCount, ShouldEqual, 0) }) Convey("总耗时应该在合理范围内", func() { So(duration, ShouldBeLessThan, 10*time.Second) }) Convey("并发性能应该良好", func() { avgTime := duration / numRequests So(avgTime, ShouldBeLessThan, 200*time.Millisecond) }) }) Reset(func() { if server != nil { server.Close() } }) }) } // TestAPIBehavior 测试 API 行为 func TestAPIBehavior(t *testing.T) { Convey("API 行为测试", t, func() { var server *httptest.Server var testConfig *config.Config Convey("当准备行为测试时", func() { testConfig = &config.Config{ App: config.AppConfig{ Mode: "test", BaseURI: "http://localhost:8080", }, Http: config.HttpConfig{ Port: 8080, }, Log: config.LogConfig{ Level: "debug", Format: "text", EnableCaller: true, }, } app := app.New(testConfig) server = httptest.NewServer(app) }) Convey("当测试不同 HTTP 方法时", func() { testURL := server.URL + "/health" Convey("GET 请求应该成功", func() { resp, err := http.Get(testURL) So(err, ShouldBeNil) defer resp.Body.Close() So(resp.StatusCode, ShouldEqual, http.StatusOK) }) Convey("POST 请求应该被处理", func() { resp, err := http.Post(testURL, "application/json", bytes.NewBuffer([]byte{})) So(err, ShouldBeNil) defer resp.Body.Close() // 健康检查端点通常支持所有方法 So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) }) Convey("PUT 请求应该被处理", func() { req, err := http.NewRequest("PUT", testURL, bytes.NewBuffer([]byte{})) So(err, ShouldBeNil) resp, err := http.DefaultClient.Do(req) So(err, ShouldBeNil) defer resp.Body.Close() So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) }) Convey("DELETE 请求应该被处理", func() { req, err := http.NewRequest("DELETE", testURL, nil) So(err, ShouldBeNil) resp, err := http.DefaultClient.Do(req) So(err, ShouldBeNil) defer resp.Body.Close() So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed}) }) }) Convey("当测试自定义请求头时", func() { req, err := http.NewRequest("GET", server.URL+"/health", nil) So(err, ShouldBeNil) // 设置各种请求头 req.Header.Set("User-Agent", "E2E-Test-Agent/1.0") req.Header.Set("Accept", "application/json") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") req.Header.Set("X-Custom-Header", "test-value") req.Header.Set("X-Request-ID", "test-request-123") req.Header.Set("Authorization", "Bearer test-token") resp, err := http.DefaultClient.Do(req) So(err, ShouldBeNil) defer resp.Body.Close() Convey("请求应该成功", func() { So(resp.StatusCode, ShouldEqual, http.StatusOK) }) Convey("响应应该是 JSON 格式", func() { So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8") }) }) Convey("当测试错误处理时", func() { Convey("访问不存在的路径应该返回 404", func() { resp, err := http.Get(server.URL + "/api/v1/nonexistent") So(err, ShouldBeNil) defer resp.Body.Close() So(resp.StatusCode, ShouldEqual, http.StatusNotFound) }) Convey("访问非法路径应该返回 404", func() { resp, err := http.Get(server.URL + "/../etc/passwd") So(err, ShouldBeNil) defer resp.Body.Close() So(resp.StatusCode, ShouldEqual, http.StatusNotFound) }) }) Reset(func() { if server != nil { server.Close() } }) }) } // TestAPIDocumentation 测试 API 文档 func TestAPIDocumentation(t *testing.T) { Convey("API 文档测试", t, func() { var server *httptest.Server var testConfig *config.Config Convey("当准备文档测试时", func() { testConfig = &config.Config{ App: config.AppConfig{ Mode: "test", BaseURI: "http://localhost:8080", }, Http: config.HttpConfig{ Port: 8080, }, Log: config.LogConfig{ Level: "debug", Format: "text", EnableCaller: true, }, } app := app.New(testConfig) server = httptest.NewServer(app) }) Convey("当访问 Swagger UI 时", func() { resp, err := http.Get(server.URL + "/swagger/index.html") So(err, ShouldBeNil) defer resp.Body.Close() Convey("应该能够访问 Swagger UI", func() { So(resp.StatusCode, ShouldEqual, http.StatusOK) }) Convey("响应应该是 HTML 格式", func() { contentType := resp.Header.Get("Content-Type") So(contentType, ShouldContainSubstring, "text/html") }) }) Convey("当访问 OpenAPI 规范时", func() { resp, err := http.Get(server.URL + "/swagger/doc.json") So(err, ShouldBeNil) defer resp.Body.Close() Convey("应该能够访问 OpenAPI 规范", func() { // 如果存在则返回 200,不存在则返回 404 So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusNotFound}) }) Convey("如果存在,响应应该是 JSON 格式", func() { if resp.StatusCode == http.StatusOK { contentType := resp.Header.Get("Content-Type") So(contentType, ShouldContainSubstring, "application/json") } }) }) Reset(func() { if server != nil { server.Close() } }) }) }