257 lines
7.3 KiB
Go
257 lines
7.3 KiB
Go
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{Headers: make(map[string]string)}
|
||
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.Headers["Authorization"] != expected {
|
||
t.Errorf("expected %s, got %s", expected, req.Headers["Authorization"])
|
||
}
|
||
|
||
// 测试 Bearer 签名器
|
||
req = &api.HttpRequest{Headers: make(map[string]string)}
|
||
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.Headers["Authorization"] != "Bearer secret-token" {
|
||
t.Errorf("expected Bearer secret-token, got %s", req.Headers["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.Headers["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")
|
||
}
|
||
}
|