api/api_test.go

257 lines
7.2 KiB
Go
Raw Permalink Normal View History

package api_test
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"apigo.cc/go/api"
"apigo.cc/go/encoding"
)
// =========================================================================
// 1. 配置继承与合并测试
// =========================================================================
func TestConfigInheritance(t *testing.T) {
api.GlobalConfigs = map[string]any{} // 确保环境干净
api.GlobalConfigs["api"] = map[string]any{
"mockSvc": map[string]any{
"url": "https://api.mock.com",
"signer": "mockSigner",
"config": map[string]any{
"secretId": "globalId",
},
"actions": map[string]any{
"subSvc": map[string]any{
"config": map[string]any{
"service": "subSvc",
},
"actions": map[string]any{
"doAction": map[string]any{
"url": "/subSvc/doAction",
"config": map[string]any{
"secretId": "overrideId",
},
},
},
},
},
},
}
cfg, _ := api.GetActionConfig("mockSvc.subSvc.doAction")
if cfg == nil {
t.Fatal("config not found")
}
if cfg["url"] != "/subSvc/doAction" {
t.Errorf("expected url /subSvc/doAction, got %v", cfg["url"])
}
if cfg["signer"] != "mockSigner" {
t.Errorf("expected signer mockSigner, got %v", cfg["signer"])
}
actionConfig, ok := cfg["config"].(map[string]any)
if !ok {
t.Fatal("action config not found or invalid type")
}
if actionConfig["secretId"] != "overrideId" {
t.Errorf("expected secretId overrideId, got %v", actionConfig["secretId"])
}
if actionConfig["service"] != "subSvc" {
t.Errorf("expected service subSvc, got %v", actionConfig["service"])
}
}
// =========================================================================
// 2. 签名器逻辑测试
// =========================================================================
func TestBuiltinSigners(t *testing.T) {
// 测试 Basic 签名器
req := &api.HttpRequest{}
config := map[string]any{
"username": "admin",
"password": "123",
}
signer := api.GetSigner("basic")
if signer == nil {
t.Fatal("basic signer not found")
}
if err := signer.Sign(req, config); err != nil {
t.Fatal(err)
}
expected := "Basic " + encoding.Base64ToString([]byte("admin:123"))
if req.GetHeader("Authorization") != expected {
t.Errorf("expected %s, got %s", expected, req.GetHeader("Authorization"))
}
// 测试 Bearer 签名器
req = &api.HttpRequest{}
config = map[string]any{
"token": "secret-token",
}
signer = api.GetSigner("bearer")
if signer == nil {
t.Fatal("bearer signer not found")
}
signer.Sign(req, config)
if req.GetHeader("Authorization") != "Bearer secret-token" {
t.Errorf("expected Bearer secret-token, got %s", req.GetHeader("Authorization"))
}
}
// =========================================================================
// 3. 完整调用与参数注入测试 (无状态网络环境)
// =========================================================================
// 模拟外部专有签名器
type mockExtSigner struct{}
func (s *mockExtSigner) Sign(req *api.HttpRequest, rawConfig map[string]any) error {
// 获取动态配置信息
service, _ := rawConfig["service"].(string)
// 签名器内部处理 URL 动态重写
if strings.Contains(req.Url, "{{.service}}") {
req.Url = strings.ReplaceAll(req.Url, "{{.service}}", service)
}
// 添加模拟签名
req.SetHeader("Authorization", "Mock-Signature-Pass")
return nil
}
func init() {
api.RegisterSigner("mockExtSigner", &mockExtSigner{})
}
// 模拟 Action 定义
type MockCallAction struct {
Name string
Age int
AppId string // 预期由引擎的 Fill 机制自动注入
Secret string // 预期由引擎的 Fill 机制自动注入
}
func (MockCallAction) ActionName() string { return "mockPlatform.mockAction" }
func (MockCallAction) SignerName() string { return "mockExtSigner" }
func (MockCallAction) Config() map[string]any {
return map[string]any{
"service": "mockService",
}
}
type MockResponse struct {
Status string
Echo string
}
func TestFullCallFlow(t *testing.T) {
// 启动本地截获 HTTP 的 Mock Server
var receivedURL string
var receivedAuth string
var receivedMethod string
var receivedBody []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedURL = r.URL.String()
receivedAuth = r.Header.Get("Authorization")
receivedMethod = r.Method
receivedBody, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "success", "echo": "Hello from mock server"}`))
}))
defer ts.Close()
// 初始化隔离的全局配置
api.GlobalConfigs = map[string]any{}
api.GlobalConfigs["api"] = map[string]any{
"mockPlatform": map[string]any{
"url": "{{.service}}.example.com", // 带占位符的 host (会被 signer 替换)
"appId": "inject-app-id",
"actions": map[string]any{
"mockAction": map[string]any{
"url": ts.URL + "?host={{.service}}.example.com", // 为了让流量指向 ts我们在 query 模拟
"secret": "inject-secret",
},
},
},
}
action := &MockCallAction{
Name: "Alice",
Age: 30,
}
// 执行调度调用
resp, err := api.Call[MockResponse](action)
if err != nil {
t.Fatalf("Call failed: %v", err)
}
// 1. 验证 Fill 参数注入
if action.AppId != "inject-app-id" {
t.Errorf("expected injected AppId inject-app-id, got %s", action.AppId)
}
if action.Secret != "inject-secret" {
t.Errorf("expected injected Secret inject-secret, got %s", action.Secret)
}
if action.Name != "Alice" {
t.Errorf("Fill should not overwrite existing value, expected Alice, got %s", action.Name)
}
// 2. 验证响应绑定
if resp.Status != "success" || resp.Echo != "Hello from mock server" {
t.Errorf("Response bind failed, got %+v", resp)
}
// 3. 验证 HTTP 透传数据
if receivedMethod != "POST" {
t.Errorf("expected POST method, got %s", receivedMethod)
}
if receivedAuth != "Mock-Signature-Pass" {
t.Errorf("expected signature header, got %s", receivedAuth)
}
// 验证 Signer 中 URL 占位符已被替换
if !strings.Contains(receivedURL, "host=mockService.example.com") {
t.Errorf("expected Signer to replace URL placeholder, got URL: %s", receivedURL)
}
// 验证 Payload 的 JSON 序列化 (由底层 apigo.cc/go/http 处理)
var bodyMap map[string]any
json.Unmarshal(receivedBody, &bodyMap)
if bodyMap["name"] != "Alice" || bodyMap["appId"] != "inject-app-id" {
t.Errorf("Payload body serialization incorrect: %s", string(receivedBody))
}
}
// 性能测试:验证纯逻辑开销 (无网络)
func BenchmarkCallEngineLogic(b *testing.B) {
api.GlobalConfigs = map[string]any{
"api": map[string]any{
"benchPlatform": map[string]any{
"appId": "bench-id",
},
},
}
actionConfig, _ := api.GetActionConfig("benchPlatform")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// 仅测试引擎准备阶段的开销 (合并、校验、注入),不走网络
api.MergeMap(map[string]any{}, actionConfig)
// 这里由于 fill 仅限于 package 内部,我们不能直接调 fill()
// 但我们实际上想衡量的是整个准备流程。
// 由于 Call 会触发网络,这里只压测 GetActionConfig (最核心合并解析逻辑)
_, _ = api.GetActionConfig("mockPlatform.mockAction")
}
}