mirror of
https://github.com/alibaba/higress.git
synced 2026-03-17 08:50:46 +08:00
feat(wasm-go): add wasm go plugin unit test and ci workflow (#2809)
This commit is contained in:
@@ -148,6 +148,49 @@ spec:
|
||||
|
||||
所有规则会按上面配置的顺序一次执行匹配,当有一个规则匹配时,就停止匹配,并选择匹配的配置执行插件逻辑。
|
||||
|
||||
## 单元测试
|
||||
|
||||
在开发wasm插件时,建议同时编写单元测试来验证插件功能。详细的单元测试编写指南请参考 [wasm plugin unit test](https://github.com/higress-group/wasm-go/blob/main/pkg/test/README.md)。
|
||||
|
||||
### 单元测试样例
|
||||
|
||||
```go
|
||||
func TestMyPlugin(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 1. 创建测试主机
|
||||
config := json.RawMessage(`{"key": "value"}`)
|
||||
host, status := test.NewTestHost(config)
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
defer host.Reset()
|
||||
|
||||
// 2. 设置请求头
|
||||
headers := [][2]string{
|
||||
{":method", "GET"},
|
||||
{":path", "/test"},
|
||||
{":authority", "test.com"},
|
||||
}
|
||||
|
||||
// 3. 调用插件请求头处理方法
|
||||
action := host.CallOnHttpRequestHeaders(headers)
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 4. 模拟外部调用响应(如果需要)
|
||||
|
||||
// host.CallOnRedisCall(0, test.CreateRedisRespString("OK"))
|
||||
|
||||
// host.CallOnHttpCall([][2]string{{":status", "200"}}, []byte(`{"result": "success"}`))
|
||||
|
||||
// 5. 完成请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 6. 验证结果(如果插件里返回了响应)
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
assert.Equal(t, uint32(200), localResponse.StatusCode)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## E2E测试
|
||||
|
||||
当你完成一个GO语言的插件功能时, 可以同时创建关联的e2e test cases, 并在本地对插件功能完成测试验证。
|
||||
|
||||
@@ -139,6 +139,52 @@ spec:
|
||||
The rules will be matched in the order of configuration. If one match is found, it will stop, and the matching configuration will take effect.
|
||||
|
||||
|
||||
## Unit Testing
|
||||
|
||||
When developing wasm plugins, it's recommended to write unit tests to verify plugin functionality. For detailed unit testing guidelines, please refer to [wasm plugin unit test](https://github.com/higress-group/wasm-go/blob/main/pkg/test/README.md).
|
||||
|
||||
### Unit Test Structure Example
|
||||
|
||||
```go
|
||||
func TestMyPlugin(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 1. Create test host
|
||||
config := json.RawMessage(`{"key": "value"}`)
|
||||
host, status := test.NewTestHost(config)
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
defer host.Reset()
|
||||
|
||||
// 2. Set request headers
|
||||
headers := [][2]string{
|
||||
{":method", "GET"},
|
||||
{":path", "/test"},
|
||||
{":authority", "test.com"},
|
||||
}
|
||||
|
||||
// 3. Call plugin request header processing method
|
||||
action := host.CallOnHttpRequestHeaders(headers)
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 4. Simulate external call responses (if needed)
|
||||
|
||||
// host.CallOnRedisCall(0, test.CreateRedisRespString("OK"))
|
||||
|
||||
// host.CallOnHttpCall([][2]string{{":status", "200"}}, []byte(`{"result": "success"}`))
|
||||
|
||||
// 5. Complete request
|
||||
host.CompleteHttp()
|
||||
|
||||
// 6. Verify results (if the plugin returns a response)
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
assert.Equal(t, uint32(200), localResponse.StatusCode)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This example shows the basic test structure including configuration parsing, request processing flow, and result verification.
|
||||
|
||||
|
||||
## E2E test
|
||||
|
||||
When you complete a GO plug-in function, you can create associated e2e test cases at the same time, and complete the test verification of the plug-in function locally.
|
||||
|
||||
@@ -5,15 +5,21 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545 h1:qb/Rhhfm1gzr/stim/L0cKNo0MPatdo0Rd8iYOAPWE0=
|
||||
github.com/higress-group/wasm-go v0.0.0-20250628101008-bea7da01a545/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
|
||||
1835
plugins/wasm-go/extensions/ai-agent/main_test.go
Normal file
1835
plugins/wasm-go/extensions/ai-agent/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,14 +8,20 @@ toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
// github.com/weaviate/weaviate-go-client/v4 v4.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
1195
plugins/wasm-go/extensions/ai-cache/main_test.go
Normal file
1195
plugins/wasm-go/extensions/ai-cache/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,10 +1,129 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本Redis配置
|
||||
var basicRedisConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"redis": map[string]interface{}{
|
||||
"serviceName": "redis.static",
|
||||
"servicePort": 6379,
|
||||
"timeout": 1000,
|
||||
"database": 0,
|
||||
},
|
||||
"questionFrom": map[string]interface{}{
|
||||
"requestBody": "messages.@reverse.0.content",
|
||||
},
|
||||
"answerValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.message.content",
|
||||
},
|
||||
"answerStreamValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.delta.content",
|
||||
},
|
||||
"cacheKeyPrefix": "higress-ai-history:",
|
||||
"identityHeader": "Authorization",
|
||||
"fillHistoryCnt": 3,
|
||||
"cacheTTL": 3600,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:最小Redis配置(使用默认值)
|
||||
var minimalRedisConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"redis": map[string]interface{}{
|
||||
"serviceName": "redis.static",
|
||||
},
|
||||
"questionFrom": map[string]interface{}{
|
||||
"requestBody": "messages.@reverse.0.content",
|
||||
},
|
||||
"answerValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.message.content",
|
||||
},
|
||||
"answerStreamValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.delta.content",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义Redis配置
|
||||
var customRedisConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"redis": map[string]interface{}{
|
||||
"serviceName": "custom-redis.dns",
|
||||
"servicePort": 6380,
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
"timeout": 2000,
|
||||
"database": 1,
|
||||
},
|
||||
"questionFrom": map[string]interface{}{
|
||||
"requestBody": "query.text",
|
||||
},
|
||||
"answerValueFrom": map[string]interface{}{
|
||||
"responseBody": "response.content",
|
||||
},
|
||||
"answerStreamValueFrom": map[string]interface{}{
|
||||
"responseBody": "response.delta.content",
|
||||
},
|
||||
"cacheKeyPrefix": "custom-history:",
|
||||
"identityHeader": "X-User-ID",
|
||||
"fillHistoryCnt": 5,
|
||||
"cacheTTL": 7200,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带认证的Redis配置
|
||||
var authRedisConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"redis": map[string]interface{}{
|
||||
"serviceName": "auth-redis.static",
|
||||
"servicePort": 6379,
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"timeout": 1500,
|
||||
"database": 2,
|
||||
},
|
||||
"questionFrom": map[string]interface{}{
|
||||
"requestBody": "messages.@reverse.0.content",
|
||||
},
|
||||
"answerValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.message.content",
|
||||
},
|
||||
"answerStreamValueFrom": map[string]interface{}{
|
||||
"responseBody": "choices.0.delta.content",
|
||||
},
|
||||
"cacheKeyPrefix": "auth-history:",
|
||||
"identityHeader": "X-Auth-Token",
|
||||
"fillHistoryCnt": 4,
|
||||
"cacheTTL": 1800,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestDistinctChat(t *testing.T) {
|
||||
type args struct {
|
||||
chat []ChatHistory
|
||||
@@ -34,3 +153,627 @@ func TestDistinctChat(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本Redis配置解析
|
||||
t.Run("basic redis config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// 类型断言
|
||||
pluginConfig, ok := config.(*PluginConfig)
|
||||
require.True(t, ok, "config should be *PluginConfig")
|
||||
|
||||
// 验证Redis配置字段
|
||||
require.Equal(t, "redis.static", pluginConfig.RedisInfo.ServiceName)
|
||||
require.Equal(t, 6379, pluginConfig.RedisInfo.ServicePort)
|
||||
require.Equal(t, 1000, pluginConfig.RedisInfo.Timeout)
|
||||
require.Equal(t, 0, pluginConfig.RedisInfo.Database)
|
||||
require.Equal(t, "", pluginConfig.RedisInfo.Username)
|
||||
require.Equal(t, "", pluginConfig.RedisInfo.Password)
|
||||
|
||||
// 验证问题提取配置
|
||||
require.Equal(t, "messages.@reverse.0.content", pluginConfig.QuestionFrom.RequestBody)
|
||||
require.Equal(t, "", pluginConfig.QuestionFrom.ResponseBody)
|
||||
|
||||
// 验证答案提取配置
|
||||
require.Equal(t, "", pluginConfig.AnswerValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.message.content", pluginConfig.AnswerValueFrom.ResponseBody)
|
||||
|
||||
// 验证流式答案提取配置
|
||||
require.Equal(t, "", pluginConfig.AnswerStreamValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.delta.content", pluginConfig.AnswerStreamValueFrom.ResponseBody)
|
||||
|
||||
// 验证其他配置字段
|
||||
require.Equal(t, "higress-ai-history:", pluginConfig.CacheKeyPrefix)
|
||||
require.Equal(t, "Authorization", pluginConfig.IdentityHeader)
|
||||
require.Equal(t, 3, pluginConfig.FillHistoryCnt)
|
||||
require.Equal(t, 3600, pluginConfig.CacheTTL)
|
||||
})
|
||||
|
||||
// 测试最小Redis配置解析(使用默认值)
|
||||
t.Run("minimal redis config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimalRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// 类型断言
|
||||
pluginConfig, ok := config.(*PluginConfig)
|
||||
require.True(t, ok, "config should be *PluginConfig")
|
||||
|
||||
// 验证Redis配置字段(使用默认值)
|
||||
require.Equal(t, "redis.static", pluginConfig.RedisInfo.ServiceName)
|
||||
require.Equal(t, 80, pluginConfig.RedisInfo.ServicePort) // 对于.static服务,默认端口是80
|
||||
require.Equal(t, 1000, pluginConfig.RedisInfo.Timeout) // 默认超时
|
||||
require.Equal(t, 0, pluginConfig.RedisInfo.Database) // 默认数据库
|
||||
require.Equal(t, "", pluginConfig.RedisInfo.Username)
|
||||
require.Equal(t, "", pluginConfig.RedisInfo.Password)
|
||||
|
||||
// 验证问题提取配置(使用默认值)
|
||||
require.Equal(t, "messages.@reverse.0.content", pluginConfig.QuestionFrom.RequestBody)
|
||||
require.Equal(t, "", pluginConfig.QuestionFrom.ResponseBody)
|
||||
|
||||
// 验证答案提取配置(使用默认值)
|
||||
require.Equal(t, "", pluginConfig.AnswerValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.message.content", pluginConfig.AnswerValueFrom.ResponseBody)
|
||||
|
||||
// 验证流式答案提取配置(使用默认值)
|
||||
require.Equal(t, "", pluginConfig.AnswerStreamValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.delta.content", pluginConfig.AnswerStreamValueFrom.ResponseBody)
|
||||
|
||||
// 验证其他配置字段(使用默认值)
|
||||
require.Equal(t, "higress-ai-history:", pluginConfig.CacheKeyPrefix)
|
||||
require.Equal(t, "Authorization", pluginConfig.IdentityHeader)
|
||||
require.Equal(t, 3, pluginConfig.FillHistoryCnt)
|
||||
require.Equal(t, 0, pluginConfig.CacheTTL) // 默认永不过期
|
||||
})
|
||||
|
||||
// 测试自定义Redis配置解析
|
||||
t.Run("custom redis config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// 类型断言
|
||||
pluginConfig, ok := config.(*PluginConfig)
|
||||
require.True(t, ok, "config should be *PluginConfig")
|
||||
|
||||
// 验证Redis配置字段
|
||||
require.Equal(t, "custom-redis.dns", pluginConfig.RedisInfo.ServiceName)
|
||||
require.Equal(t, 6380, pluginConfig.RedisInfo.ServicePort)
|
||||
require.Equal(t, 2000, pluginConfig.RedisInfo.Timeout)
|
||||
require.Equal(t, 1, pluginConfig.RedisInfo.Database)
|
||||
require.Equal(t, "admin", pluginConfig.RedisInfo.Username)
|
||||
require.Equal(t, "password123", pluginConfig.RedisInfo.Password)
|
||||
|
||||
// 验证问题提取配置(插件硬编码,不从配置读取)
|
||||
require.Equal(t, "messages.@reverse.0.content", pluginConfig.QuestionFrom.RequestBody)
|
||||
require.Equal(t, "", pluginConfig.QuestionFrom.ResponseBody)
|
||||
|
||||
// 验证答案提取配置(插件硬编码,不从配置读取)
|
||||
require.Equal(t, "", pluginConfig.AnswerValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.message.content", pluginConfig.AnswerValueFrom.ResponseBody)
|
||||
|
||||
// 验证流式答案提取配置(插件硬编码,不从配置读取)
|
||||
require.Equal(t, "", pluginConfig.AnswerStreamValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.delta.content", pluginConfig.AnswerStreamValueFrom.ResponseBody)
|
||||
|
||||
// 验证其他配置字段
|
||||
require.Equal(t, "custom-history:", pluginConfig.CacheKeyPrefix)
|
||||
require.Equal(t, "X-User-ID", pluginConfig.IdentityHeader)
|
||||
require.Equal(t, 5, pluginConfig.FillHistoryCnt)
|
||||
require.Equal(t, 7200, pluginConfig.CacheTTL)
|
||||
})
|
||||
|
||||
// 测试带认证的Redis配置解析
|
||||
t.Run("auth redis config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(authRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// 类型断言
|
||||
pluginConfig, ok := config.(*PluginConfig)
|
||||
require.True(t, ok, "config should be *PluginConfig")
|
||||
|
||||
// 验证Redis配置字段
|
||||
require.Equal(t, "auth-redis.static", pluginConfig.RedisInfo.ServiceName)
|
||||
require.Equal(t, 6379, pluginConfig.RedisInfo.ServicePort)
|
||||
require.Equal(t, 1500, pluginConfig.RedisInfo.Timeout)
|
||||
require.Equal(t, 2, pluginConfig.RedisInfo.Database)
|
||||
require.Equal(t, "user", pluginConfig.RedisInfo.Username)
|
||||
require.Equal(t, "pass", pluginConfig.RedisInfo.Password)
|
||||
|
||||
// 验证问题提取配置
|
||||
require.Equal(t, "messages.@reverse.0.content", pluginConfig.QuestionFrom.RequestBody)
|
||||
require.Equal(t, "", pluginConfig.QuestionFrom.ResponseBody)
|
||||
|
||||
// 验证答案提取配置
|
||||
require.Equal(t, "", pluginConfig.AnswerValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.message.content", pluginConfig.AnswerValueFrom.ResponseBody)
|
||||
|
||||
// 验证流式答案提取配置
|
||||
require.Equal(t, "", pluginConfig.AnswerStreamValueFrom.RequestBody)
|
||||
require.Equal(t, "choices.0.delta.content", pluginConfig.AnswerStreamValueFrom.ResponseBody)
|
||||
|
||||
// 验证其他配置字段
|
||||
require.Equal(t, "auth-history:", pluginConfig.CacheKeyPrefix)
|
||||
require.Equal(t, "X-Auth-Token", pluginConfig.IdentityHeader)
|
||||
require.Equal(t, 4, pluginConfig.FillHistoryCnt)
|
||||
require.Equal(t, 1800, pluginConfig.CacheTTL)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试JSON内容类型的请求头处理
|
||||
t.Run("JSON content type headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置JSON内容类型的请求头,包含身份标识
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,因为需要读取请求体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
|
||||
// 测试非JSON内容类型的请求头处理
|
||||
t.Run("non-JSON content type headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置非JSON内容类型的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "text/plain"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,但不会读取请求体
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试缺少身份标识的请求头处理
|
||||
t.Run("missing identity header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置缺少身份标识的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,因为缺少身份标识
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试自定义身份标识头
|
||||
t.Run("custom identity header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置自定义身份标识头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"x-user-id", "user456"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试缓存命中的请求体处理
|
||||
t.Run("cache hit request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 构造请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,请介绍一下自己"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待Redis响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis缓存命中响应
|
||||
cacheResponse := `[{"role":"user","content":"之前的问题"},{"role":"assistant","content":"之前的回答"}]`
|
||||
resp := test.CreateRedisRespString(cacheResponse)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试缓存未命中的请求体处理
|
||||
t.Run("cache miss request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 构造请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气怎么样?"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待Redis响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis缓存未命中响应
|
||||
resp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试流式请求的请求体处理
|
||||
t.Run("streaming request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 构造流式请求体
|
||||
requestBody := `{
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "请用流式方式回答"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待Redis响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis缓存未命中响应
|
||||
resp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试查询历史请求的请求体处理
|
||||
t.Run("query history request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/ai-history/query?cnt=2"},
|
||||
{":method", "GET"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 构造请求体(需要包含messages字段,因为插件会尝试提取问题)
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "查询历史"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待Redis响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis缓存命中响应
|
||||
cacheResponse := `[{"role":"user","content":"问题1"},{"role":"assistant","content":"回答1"},{"role":"user","content":"问题2"},{"role":"assistant","content":"回答2"}]`
|
||||
resp := test.CreateRedisRespString(cacheResponse)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试流式响应头处理
|
||||
t.Run("streaming response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 必须先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 设置流式响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试非流式响应头处理
|
||||
t.Run("non-streaming response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 必须先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 设置非流式响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpStreamResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试流式响应体处理 - 非流式模式
|
||||
t.Run("non-streaming mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "测试问题"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理,设置必要的上下文
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 模拟Redis缓存未命中,设置QuestionContextKey
|
||||
resp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 测试非流式响应体处理
|
||||
chunk := []byte(`{"choices":[{"message":{"content":"测试回答"}}]}`)
|
||||
action := host.CallOnHttpStreamingResponseBody(chunk, true)
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试流式响应体处理 - 流式模式
|
||||
t.Run("streaming mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 设置流式请求体
|
||||
requestBody := `{
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "测试流式问题"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理,设置必要的上下文
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 模拟Redis缓存未命中,设置QuestionContextKey
|
||||
resp := test.CreateRedisRespNull()
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 设置流式响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 测试流式响应体处理 - 非最后一个chunk
|
||||
chunk1 := []byte("data: {\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\n\n")
|
||||
action1 := host.CallOnHttpStreamingResponseBody(chunk1, false)
|
||||
require.Equal(t, types.ActionContinue, action1)
|
||||
|
||||
// 测试流式响应体处理 - 最后一个chunk
|
||||
chunk2 := []byte("data: {\"choices\":[{\"delta\":{\"content\":\" World\"}}]}\n\n")
|
||||
action2 := host.CallOnHttpStreamingResponseBody(chunk2, true)
|
||||
require.Equal(t, types.ActionContinue, action2)
|
||||
})
|
||||
|
||||
// 测试查询历史路径的流式响应体处理
|
||||
t.Run("query history path", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/ai-history/query?cnt=2"},
|
||||
{":method", "GET"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "查询历史"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理,设置必要的上下文
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 模拟Redis缓存命中,设置QuestionContextKey
|
||||
cacheResponse := `[{"role":"user","content":"问题1"},{"role":"assistant","content":"回答1"}]`
|
||||
resp := test.CreateRedisRespString(cacheResponse)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 测试查询历史路径的响应体处理
|
||||
chunk := []byte("test chunk")
|
||||
action := host.CallOnHttpStreamingResponseBody(chunk, true)
|
||||
|
||||
// 应该直接返回chunk,不进行处理
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试没有QuestionContextKey的情况
|
||||
t.Run("no question context", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicRedisConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
{"authorization", "Bearer user123"},
|
||||
})
|
||||
|
||||
// 不调用请求体处理,所以没有QuestionContextKey
|
||||
|
||||
// 测试没有QuestionContextKey的响应体处理
|
||||
chunk := []byte("test chunk")
|
||||
action := host.CallOnHttpStreamingResponseBody(chunk, true)
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,13 +5,21 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
616
plugins/wasm-go/extensions/ai-image-reader/main_test.go
Normal file
616
plugins/wasm-go/extensions/ai-image-reader/main_test.go
Normal file
@@ -0,0 +1,616 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本DashScope OCR配置
|
||||
var basicDashScopeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"apiKey": "test-api-key-123",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"servicePort": 443,
|
||||
"timeout": 10000,
|
||||
"model": "qwen-vl-ocr",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:最小DashScope配置(使用默认值)
|
||||
var minimalDashScopeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"apiKey": "minimal-api-key",
|
||||
"serviceName": "ocr-service",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义端口和超时配置
|
||||
var customPortTimeoutConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"apiKey": "custom-api-key",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "custom.dashscope.com",
|
||||
"servicePort": 8443,
|
||||
"timeout": 30000,
|
||||
"model": "qwen-vl-ocr",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义模型配置
|
||||
var customModelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"apiKey": "model-api-key",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"servicePort": 443,
|
||||
"timeout": 15000,
|
||||
"model": "custom-ocr-model",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本DashScope配置解析
|
||||
t.Run("basic dashscope config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试最小DashScope配置解析(使用默认值)
|
||||
t.Run("minimal dashscope config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimalDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义端口和超时配置解析
|
||||
t.Run("custom port timeout config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customPortTimeoutConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义模型配置解析
|
||||
t.Run("custom model config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试JSON内容类型的请求头处理
|
||||
t.Run("JSON content type headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置JSON内容类型的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,因为禁用了重路由但允许继续处理
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试非JSON内容类型的请求头处理
|
||||
t.Run("non-JSON content type headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置非JSON内容类型的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue,但不会读取请求体
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试缺少content-type的请求头处理
|
||||
t.Run("missing content type headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置缺少content-type的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试包含单张图片的请求体处理
|
||||
t.Run("single image request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造包含单张图片的请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这张图片里有什么?"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image1.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待OCR响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟OCR服务响应
|
||||
ocrResponse := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "图片中包含一些文字内容"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(ocrResponse))
|
||||
|
||||
modifiedBody := host.GetRequestBody()
|
||||
require.NotNil(t, modifiedBody)
|
||||
require.Contains(t, string(modifiedBody), "图片中包含一些文字内容")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试包含多张图片的请求体处理
|
||||
t.Run("multiple images request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造包含多张图片的请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这些图片里有什么?"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image2.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待OCR响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟第一张图片的OCR响应
|
||||
ocrResponse1 := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "第一张图片包含文字A"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟第二张图片的OCR响应
|
||||
ocrResponse2 := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "第二张图片包含文字B"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟第一个HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(ocrResponse1))
|
||||
|
||||
// 模拟第二个HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(ocrResponse2))
|
||||
|
||||
modifiedBody := host.GetRequestBody()
|
||||
require.NotNil(t, modifiedBody)
|
||||
require.Contains(t, string(modifiedBody), "第一张图片包含文字A")
|
||||
require.Contains(t, string(modifiedBody), "第二张图片包含文字B")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不包含图片的请求体处理
|
||||
t.Run("no image request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造不包含图片的请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "你好,请介绍一下自己"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionContinue,因为没有图片需要处理
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 测试配置验证
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试缺少type配置
|
||||
t.Run("missing type", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"servicePort": 443,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的type
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试缺少apiKey配置
|
||||
t.Run("missing apiKey", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"servicePort": 443,
|
||||
"timeout": 10000,
|
||||
"model": "qwen-vl-ocr",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的apiKey
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试缺少serviceName配置
|
||||
t.Run("missing serviceName", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "dashscope",
|
||||
"apiKey": "test-api-key",
|
||||
"serviceHost": "dashscope.aliyuncs.com",
|
||||
"servicePort": 443,
|
||||
"timeout": 10000,
|
||||
"model": "qwen-vl-ocr",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的serviceName
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试未知的provider类型
|
||||
t.Run("unknown provider type", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "unknown-provider",
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ocr-service",
|
||||
"serviceHost": "example.com",
|
||||
"servicePort": 443,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为provider类型未知
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 测试边界情况
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试空请求体
|
||||
t.Run("empty request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 调用请求体处理 - 空请求体
|
||||
action := host.CallOnHttpRequestBody([]byte{})
|
||||
|
||||
// 应该返回ActionContinue,因为没有图片需要处理
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试无效JSON请求体
|
||||
t.Run("invalid JSON request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 调用请求体处理 - 无效JSON
|
||||
invalidJSON := []byte(`{"messages": [{"role": "user", "content": "test"}`)
|
||||
action := host.CallOnHttpRequestBody(invalidJSON)
|
||||
|
||||
// 应该返回ActionContinue,因为JSON解析失败
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试OCR服务错误响应
|
||||
t.Run("OCR service error response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造包含图片的请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这张图片里有什么?"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image1.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟OCR服务错误响应
|
||||
errorResponse := `{
|
||||
"error": "Service unavailable",
|
||||
"message": "OCR service is down"
|
||||
}`
|
||||
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "503"},
|
||||
}, []byte(errorResponse))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试OCR服务返回空结果
|
||||
t.Run("OCR service empty response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicDashScopeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造包含图片的请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这张图片里有什么?"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://example.com/image1.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟OCR服务返回空结果
|
||||
emptyResponse := `{
|
||||
"choices": []
|
||||
}`
|
||||
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(emptyResponse))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -7,14 +7,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
531
plugins/wasm-go/extensions/ai-intent/main_test.go
Normal file
531
plugins/wasm-go/extensions/ai-intent/main_test.go
Normal file
@@ -0,0 +1,531 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本意图识别配置
|
||||
var basicIntentConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "金融|电商|法律|Higress",
|
||||
"prompt": "你是一个智能类别识别助手,负责根据用户提出的问题和预设的类别,确定问题属于哪个预设的类别,并给出相应的类别。用户提出的问题为:'%s',预设的类别为'%s',直接返回一种具体类别,如果没有找到就返回'NotFound'。",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
"proxyUrl": "http://ai.example.com/v1/chat/completions",
|
||||
"proxyModel": "qwen-long",
|
||||
"proxyPort": 80,
|
||||
"proxyDomain": "ai.example.com",
|
||||
"proxyTimeout": 10000,
|
||||
"proxyApiKey": "test-api-key",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{
|
||||
"requestBody": "messages.@reverse.0.content",
|
||||
"responseBody": "choices.0.message.content",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义提示词配置
|
||||
var customPromptConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "技术|产品|运营|设计",
|
||||
"prompt": "请分析以下问题属于哪个技术领域:%s,可选领域:%s,请直接返回领域名称。",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
"proxyUrl": "https://ai.example.com/v1/chat/completions",
|
||||
"proxyModel": "gpt-3.5-turbo",
|
||||
"proxyPort": 443,
|
||||
"proxyDomain": "ai.example.com",
|
||||
"proxyTimeout": 15000,
|
||||
"proxyApiKey": "custom-api-key",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{
|
||||
"requestBody": "query",
|
||||
"responseBody": "result",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:最小配置(使用默认值)
|
||||
var minimalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "A|B|C",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
"proxyUrl": "http://ai.example.com/v1/chat/completions",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:HTTPS配置
|
||||
var httpsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "客服|销售|技术支持",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
"proxyUrl": "https://ai.example.com:8443/v1/chat/completions",
|
||||
"proxyModel": "claude-3",
|
||||
"proxyTimeout": 20000,
|
||||
"proxyApiKey": "https-api-key",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{
|
||||
"requestBody": "input.text",
|
||||
"responseBody": "output.classification",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本意图识别配置解析
|
||||
t.Run("basic intent config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义提示词配置解析
|
||||
t.Run("custom prompt config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customPromptConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试最小配置解析(使用默认值)
|
||||
t.Run("minimal config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(minimalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试HTTPS配置解析
|
||||
t.Run("https config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(httpsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求头处理
|
||||
t.Run("request headers processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,因为禁用了重路由
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求体处理 - 金融类问题
|
||||
t.Run("financial question processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体 - 金融类问题
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{"role": "user", "content": "今天股市怎么样?"}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause,因为需要等待LLM响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟LLM响应 - 返回"金融"类别
|
||||
llmResponse := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "金融"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(llmResponse))
|
||||
|
||||
// 验证插件是否正确处理了LLM响应
|
||||
// 插件应该将"金融"类别设置到Property中
|
||||
// 通过host.GetProperty验证意图类别是否被正确设置
|
||||
intentCategory, err := host.GetProperty([]string{"intent_category"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "金融", string(intentCategory))
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试请求体处理 - 电商类问题
|
||||
t.Run("ecommerce question processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体 - 电商类问题
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{"role": "user", "content": "这个商品什么时候发货?"}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟LLM响应 - 返回"电商"类别
|
||||
llmResponse := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "电商"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(llmResponse))
|
||||
|
||||
// 验证插件是否正确处理了LLM响应
|
||||
// 插件应该将"电商"类别设置到Property中
|
||||
// 通过host.GetProperty验证意图类别是否被正确设置
|
||||
intentCategory, err := host.GetProperty([]string{"intent_category"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "电商", string(intentCategory))
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试请求体处理 - 未找到类别
|
||||
t.Run("category not found processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体 - 不相关的问题
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{"role": "user", "content": "今天天气怎么样?"}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟LLM响应 - 返回"NotFound"
|
||||
llmResponse := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "NotFound"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
// 模拟HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(llmResponse))
|
||||
|
||||
_, err := host.GetProperty([]string{"intent_category"})
|
||||
// 应该返回错误,因为没有设置该Property
|
||||
require.Error(t, err)
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 测试配置验证
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试缺少scene.category配置
|
||||
t.Run("missing scene.category", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"prompt": "test prompt",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
"proxyUrl": "http://ai.example.com/v1/chat/completions",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的scene.category
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试缺少llm.proxyServiceName配置
|
||||
t.Run("missing llm.proxyServiceName", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "A|B|C",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyUrl": "http://ai.example.com/v1/chat/completions",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的llm.proxyServiceName
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试缺少llm.proxyUrl配置
|
||||
t.Run("missing llm.proxyUrl", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "A|B|C",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
},
|
||||
"keyFrom": map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的llm.proxyUrl
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
|
||||
// 测试缺少必需字段的配置
|
||||
t.Run("missing required fields", func(t *testing.T) {
|
||||
invalidConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"scene": map[string]interface{}{
|
||||
"category": "A|B|C",
|
||||
},
|
||||
"llm": map[string]interface{}{
|
||||
"proxyServiceName": "ai-service",
|
||||
// 故意不设置proxyUrl,这是必需的
|
||||
},
|
||||
"keyFrom": map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
// 应该返回错误状态,因为缺少必需的proxyUrl
|
||||
require.NotEqual(t, types.OnPluginStartStatusOK, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 测试边界情况
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
|
||||
// 测试无效JSON请求体
|
||||
t.Run("invalid JSON request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 调用请求体处理 - 无效JSON
|
||||
invalidJSON := []byte(`{"messages": [{"role": "user", "content": "test"}`)
|
||||
action := host.CallOnHttpRequestBody(invalidJSON)
|
||||
|
||||
// 应该返回ActionPause,因为需要等待LLM响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟LLM响应
|
||||
llmResponse := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "NotFound"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(llmResponse))
|
||||
|
||||
// 验证插件是否正确处理了LLM响应
|
||||
// 由于返回"NotFound",插件不会设置任何意图类别到Property中
|
||||
// 验证没有设置意图类别Property
|
||||
_, err := host.GetProperty([]string{"intent_category"})
|
||||
// 应该返回错误,因为没有设置该Property
|
||||
require.Error(t, err)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试LLM服务错误响应
|
||||
t.Run("LLM service error response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicIntentConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 构造请求体
|
||||
requestBody := `{
|
||||
"messages": [
|
||||
{"role": "user", "content": "今天股市怎么样?"}
|
||||
]
|
||||
}`
|
||||
|
||||
// 调用请求体处理
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟LLM服务错误响应
|
||||
errorResponse := `{
|
||||
"error": "Service unavailable",
|
||||
"message": "LLM service is down"
|
||||
}`
|
||||
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"content-type", "application/json"},
|
||||
{":status", "503"},
|
||||
}, []byte(errorResponse))
|
||||
|
||||
// 验证插件是否正确处理了LLM错误响应
|
||||
// 由于状态码不是200,插件不会设置任何意图类别到Property中
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,17 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -2,16 +2,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
|
||||
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -21,5 +24,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
892
plugins/wasm-go/extensions/ai-json-resp/main_test.go
Normal file
892
plugins/wasm-go/extensions/ai-json-resp/main_test.go
Normal file
@@ -0,0 +1,892 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/santhosh-tekuri/jsonschema"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"serviceDomain": "api.openai.com",
|
||||
"servicePort": 443,
|
||||
"servicePath": "/v1/chat/completions",
|
||||
"apiKey": "sk-test123",
|
||||
"serviceTimeout": 30000,
|
||||
"maxRetry": 3,
|
||||
"contentPath": "choices.0.message.content",
|
||||
"enableContentDisposition": true,
|
||||
// 添加一个简单的JSON Schema,避免编译失败
|
||||
"jsonSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:使用serviceUrl的配置
|
||||
var serviceUrlConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"serviceUrl": "https://api.openai.com/v1/chat/completions",
|
||||
"apiKey": "sk-test456",
|
||||
"serviceTimeout": 50000,
|
||||
"maxRetry": 5,
|
||||
"contentPath": "choices.0.message.content",
|
||||
"enableContentDisposition": false,
|
||||
// 添加一个简单的JSON Schema,避免编译失败
|
||||
"jsonSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:包含JSON Schema的配置
|
||||
var jsonSchemaConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"serviceDomain": "api.openai.com",
|
||||
"servicePort": 443,
|
||||
"apiKey": "sk-test789",
|
||||
"jsonSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
"age": map[string]interface{}{
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": []string{"name"},
|
||||
},
|
||||
"enableSwagger": true,
|
||||
"enableOas3": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:启用OAS3的配置
|
||||
var oas3Config = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"serviceDomain": "api.openai.com",
|
||||
"servicePort": 443,
|
||||
"apiKey": "sk-test101",
|
||||
"jsonSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"title": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
"content": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"enableSwagger": false,
|
||||
"enableOas3": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效的JSON Schema配置
|
||||
var invalidJsonSchemaConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"serviceDomain": "api.openai.com",
|
||||
"servicePort": 443,
|
||||
"apiKey": "sk-test303",
|
||||
"jsonSchema": "invalid-schema",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少必需字段的配置
|
||||
var missingRequiredConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test404",
|
||||
"serviceTimeout": 30000,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
require.Equal(t, "ai-service", pluginConfig.serviceName)
|
||||
require.Equal(t, "api.openai.com", pluginConfig.serviceDomain)
|
||||
require.Equal(t, 443, pluginConfig.servicePort)
|
||||
require.Equal(t, "/v1/chat/completions", pluginConfig.servicePath)
|
||||
require.Equal(t, "sk-test123", pluginConfig.apiKey)
|
||||
require.Equal(t, 30000, pluginConfig.serviceTimeout)
|
||||
require.Equal(t, 3, pluginConfig.maxRetry)
|
||||
require.Equal(t, "choices.0.message.content", pluginConfig.contentPath)
|
||||
require.True(t, pluginConfig.enableContentDisposition)
|
||||
require.NotNil(t, pluginConfig.jsonSchema)
|
||||
require.Equal(t, jsonschema.Draft7, pluginConfig.draft)
|
||||
require.True(t, pluginConfig.enableJsonSchemaValidation)
|
||||
})
|
||||
|
||||
// 测试使用serviceUrl的配置解析
|
||||
t.Run("serviceUrl config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(serviceUrlConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
require.Equal(t, "ai-service", pluginConfig.serviceName)
|
||||
require.Equal(t, "api.openai.com", pluginConfig.serviceDomain)
|
||||
require.Equal(t, 443, pluginConfig.servicePort)
|
||||
require.Equal(t, "/v1/chat/completions", pluginConfig.servicePath)
|
||||
require.Equal(t, "sk-test456", pluginConfig.apiKey)
|
||||
require.Equal(t, 50000, pluginConfig.serviceTimeout)
|
||||
require.Equal(t, 5, pluginConfig.maxRetry)
|
||||
require.False(t, pluginConfig.enableContentDisposition)
|
||||
})
|
||||
|
||||
// 测试包含JSON Schema的配置解析
|
||||
t.Run("jsonSchema config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(jsonSchemaConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
require.NotNil(t, pluginConfig.jsonSchema)
|
||||
require.Equal(t, jsonschema.Draft4, pluginConfig.draft)
|
||||
require.True(t, pluginConfig.enableJsonSchemaValidation)
|
||||
require.NotNil(t, pluginConfig.compile)
|
||||
})
|
||||
|
||||
// 测试启用OAS3的配置解析
|
||||
t.Run("oas3 config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(oas3Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
require.Equal(t, jsonschema.Draft7, pluginConfig.draft)
|
||||
require.True(t, pluginConfig.enableJsonSchemaValidation)
|
||||
})
|
||||
|
||||
// 测试无效的JSON Schema配置
|
||||
t.Run("invalid jsonSchema config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidJsonSchemaConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
// 根据插件的实际行为,无效的JSON Schema会导致编译失败
|
||||
require.Equal(t, uint32(JSON_SCHEMA_COMPILE_FAILED_CODE), pluginConfig.rejectStruct.RejectCode)
|
||||
})
|
||||
|
||||
// 测试缺少必需字段的配置
|
||||
t.Run("missing required config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingRequiredConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, _ := host.GetMatchConfig()
|
||||
require.NotNil(t, config)
|
||||
|
||||
pluginConfig := config.(*PluginConfig)
|
||||
// 根据插件的实际行为,缺少serviceDomain会导致JSON Schema编译失败
|
||||
require.Equal(t, uint32(JSON_SCHEMA_COMPILE_FAILED_CODE), pluginConfig.rejectStruct.RejectCode)
|
||||
require.Contains(t, pluginConfig.rejectStruct.RejectMsg, "Json Schema compile failed")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试正常请求头处理
|
||||
t.Run("normal request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Authorization", "Bearer sk-user123"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"Content-Length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试来自插件的请求头处理
|
||||
t.Run("request from this plugin", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置来自插件的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{EXTEND_HEADER_KEY, "true"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试没有Authorization头的请求
|
||||
t.Run("no authorization header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置没有Authorization的请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"Content-Length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试配置错误的请求头处理
|
||||
t.Run("config error", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingRequiredConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试来自插件的请求(应该直接继续)
|
||||
t.Run("request from this plugin", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含EXTEND_HEADER_KEY来标记请求来自插件
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
{EXTEND_HEADER_KEY, "true"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionContinue,因为请求来自插件
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试配置错误的请求体处理
|
||||
t.Run("config error", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingRequiredConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionContinue,因为配置有错误
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试正常请求体处理 - 成功响应
|
||||
t.Run("normal request with successful response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务返回成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "{\"definition\": \"AI is artificial intelligence\", \"examples\": [\"machine learning\", \"natural language processing\"]}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "definition")
|
||||
require.Contains(t, string(response.Data), "examples")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试正常请求体处理 - 需要重试的响应
|
||||
t.Run("normal request with retry response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务返回需要重试的响应(content字段不是有效JSON)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "AI is artificial intelligence. It includes machine learning and natural language processing."
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
// 由于content不是有效JSON,插件会进行重试
|
||||
// 模拟重试请求的响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-456",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "{\"definition\": \"AI is artificial intelligence\", \"examples\": [\"machine learning\", \"natural language processing\"]}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
// 验证最终响应体是提取的JSON内容
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "definition")
|
||||
require.Contains(t, string(response.Data), "examples")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试外部服务返回无效响应体
|
||||
t.Run("external service returns invalid response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务返回无效的响应体
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`invalid json response`))
|
||||
|
||||
// 验证响应体包含错误信息
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "invalid json response")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试外部服务返回缺少content字段的响应
|
||||
t.Run("external service returns response without content field", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务返回缺少content字段的响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
// 验证响应体包含错误信息
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "response body does not contain the content")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试使用自定义servicePath的请求
|
||||
t.Run("request with custom service path", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(serviceUrlConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/custom/chat"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务返回成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "{\"answer\": \"AI is artificial intelligence\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
|
||||
// 验证响应体是提取的JSON内容
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "answer")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试达到最大重试次数的情况
|
||||
t.Run("max retry count exceeded", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What is AI?"}
|
||||
]
|
||||
}`
|
||||
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
// 应该返回ActionPause,等待外部服务响应
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟多次重试,每次都返回无效的content
|
||||
for i := 0; i < 4; i++ { // 超过最大重试次数3次
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "AI is artificial intelligence"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`))
|
||||
}
|
||||
|
||||
// 验证最终响应体包含重试次数超限的错误信息
|
||||
response := host.GetLocalResponse()
|
||||
require.NotNil(t, response)
|
||||
require.Contains(t, string(response.Data), "retry count exceeds max retry count")
|
||||
|
||||
// 完成HTTP请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRejectStruct(t *testing.T) {
|
||||
// 测试RejectStruct的GetBytes方法
|
||||
t.Run("GetBytes", func(t *testing.T) {
|
||||
reject := RejectStruct{
|
||||
RejectCode: 1001,
|
||||
RejectMsg: "Test error message",
|
||||
}
|
||||
|
||||
bytes := reject.GetBytes()
|
||||
require.NotNil(t, bytes)
|
||||
|
||||
// 验证JSON格式
|
||||
var result RejectStruct
|
||||
err := json.Unmarshal(bytes, &result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1001), result.RejectCode)
|
||||
require.Equal(t, "Test error message", result.RejectMsg)
|
||||
})
|
||||
|
||||
// 测试RejectStruct的GetShortMsg方法
|
||||
t.Run("GetShortMsg", func(t *testing.T) {
|
||||
reject := RejectStruct{
|
||||
RejectCode: 1001,
|
||||
RejectMsg: "Json Schema is not valid: invalid format",
|
||||
}
|
||||
|
||||
shortMsg := reject.GetShortMsg()
|
||||
require.Equal(t, "ai-json-resp.Json Schema is not valid", shortMsg)
|
||||
})
|
||||
|
||||
// 测试RejectStruct的GetShortMsg方法 - 没有冒号的情况
|
||||
t.Run("GetShortMsg no colon", func(t *testing.T) {
|
||||
reject := RejectStruct{
|
||||
RejectCode: 1001,
|
||||
RejectMsg: "Simple error message",
|
||||
}
|
||||
|
||||
shortMsg := reject.GetShortMsg()
|
||||
require.Equal(t, "ai-json-resp.Simple error message", shortMsg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateBody(t *testing.T) {
|
||||
// 创建测试配置
|
||||
config := &PluginConfig{
|
||||
contentPath: "choices.0.message.content",
|
||||
jsonSchema: nil, // 明确设置为nil,禁用JSON Schema验证
|
||||
enableJsonSchemaValidation: false, // 禁用JSON Schema验证
|
||||
}
|
||||
|
||||
// 测试有效的响应体
|
||||
t.Run("valid response body", func(t *testing.T) {
|
||||
validBody := []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello, how can I help you?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
err := config.ValidateBody(validBody)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// 测试无效的JSON响应体
|
||||
t.Run("invalid JSON response body", func(t *testing.T) {
|
||||
invalidBody := []byte(`invalid json content`)
|
||||
|
||||
err := config.ValidateBody(invalidBody)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
|
||||
require.Contains(t, config.rejectStruct.RejectMsg, "service unavailable")
|
||||
})
|
||||
|
||||
// 测试缺少content字段的响应体
|
||||
t.Run("missing content field", func(t *testing.T) {
|
||||
missingContentBody := []byte(`{
|
||||
"id": "chatcmpl-123",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
err := config.ValidateBody(missingContentBody)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
|
||||
require.Contains(t, config.rejectStruct.RejectMsg, "response body does not contain the content")
|
||||
})
|
||||
|
||||
// 测试空的响应体
|
||||
t.Run("empty response body", func(t *testing.T) {
|
||||
emptyBody := []byte{}
|
||||
|
||||
err := config.ValidateBody(emptyBody)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, uint32(SERVICE_UNAVAILABLE_CODE), config.rejectStruct.RejectCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractJson(t *testing.T) {
|
||||
// 创建测试配置
|
||||
config := &PluginConfig{
|
||||
jsonSchema: nil, // 明确设置为nil,禁用JSON Schema验证
|
||||
enableJsonSchemaValidation: false, // 禁用JSON Schema验证
|
||||
}
|
||||
|
||||
// 测试提取有效的JSON
|
||||
t.Run("extract valid JSON", func(t *testing.T) {
|
||||
content := `Here is the response: {"name": "John", "age": 30} and some other text`
|
||||
|
||||
jsonStr, err := config.ExtractJson(content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"name": "John", "age": 30}`, jsonStr)
|
||||
})
|
||||
|
||||
// 测试提取嵌套JSON
|
||||
t.Run("extract nested JSON", func(t *testing.T) {
|
||||
content := `Response: {"user": {"name": "John", "profile": {"age": 30, "city": "NYC"}}}`
|
||||
|
||||
jsonStr, err := config.ExtractJson(content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{"user": {"name": "John", "profile": {"age": 30, "city": "NYC"}}}`, jsonStr)
|
||||
})
|
||||
|
||||
// 测试没有JSON的内容
|
||||
t.Run("no JSON in content", func(t *testing.T) {
|
||||
content := `This is just plain text without any JSON content`
|
||||
|
||||
_, err := config.ExtractJson(content)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot find json in the response body")
|
||||
})
|
||||
|
||||
// 测试只有开始括号的内容
|
||||
t.Run("only opening brace", func(t *testing.T) {
|
||||
content := `Here is the start: { but no closing brace`
|
||||
|
||||
_, err := config.ExtractJson(content)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot find json in the response body")
|
||||
})
|
||||
|
||||
// 测试只有结束括号的内容
|
||||
t.Run("only closing brace", func(t *testing.T) {
|
||||
content := `Here is the end: } but no opening brace`
|
||||
|
||||
_, err := config.ExtractJson(content)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "cannot find json in the response body")
|
||||
})
|
||||
|
||||
// 测试无效的JSON格式
|
||||
t.Run("invalid JSON format", func(t *testing.T) {
|
||||
content := `Here is invalid JSON: {"name": "John", "age": 30,}`
|
||||
|
||||
_, err := config.ExtractJson(content)
|
||||
require.Error(t, err)
|
||||
// ExtractJson会提取到{"name": "John", "age": 30,},但json.Unmarshal会失败
|
||||
// 因为JSON格式无效(末尾有多余的逗号)
|
||||
require.Contains(t, err.Error(), "invalid character '}' looking for beginning of object key string")
|
||||
})
|
||||
|
||||
// 测试多个JSON对象(应该提取第一个完整的)
|
||||
t.Run("multiple JSON objects", func(t *testing.T) {
|
||||
content := `First: {"name": "John"} Second: {"age": 30}`
|
||||
|
||||
_, err := config.ExtractJson(content)
|
||||
require.Error(t, err)
|
||||
// ExtractJson会提取到{"name": "John"} Second: {"age": 30}
|
||||
// 这不是有效的JSON,因为"Second: {"age": 30}"不是有效的JSON语法
|
||||
require.Contains(t, err.Error(), "invalid character 'S' after top-level value")
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250628101008-bea7da01a545
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
@@ -21,3 +21,5 @@ require (
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
require github.com/tidwall/sjson v1.2.5 // indirect
|
||||
|
||||
@@ -4,10 +4,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250628101008-bea7da01a545 h1:zPXEonKCAeLvXI1IpwGpIeVSvLY5AZ9h9uTJnOuiA3Q=
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250628101008-bea7da01a545/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -18,6 +18,7 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -27,6 +28,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
|
||||
@@ -5,11 +5,19 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
511
plugins/wasm-go/extensions/ai-prompt-decorator/main_test.go
Normal file
511
plugins/wasm-go/extensions/ai-prompt-decorator/main_test.go
Normal file
@@ -0,0 +1,511 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础装饰器配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"prepend": []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant from ${geo-country}.",
|
||||
},
|
||||
},
|
||||
"append": []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Please provide context about ${geo-city}.",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:只有前置消息的配置
|
||||
var prependOnlyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"prepend": []map[string]interface{}{
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are located in ${geo-province}, ${geo-country}.",
|
||||
},
|
||||
},
|
||||
"append": []map[string]interface{}{}, // 显式定义空的append字段
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:空配置
|
||||
var emptyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"prepend": []map[string]interface{}{},
|
||||
"append": []map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础装饰器配置解析
|
||||
t.Run("basic decorator config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
decoratorConfig := config.(*AIPromptDecoratorConfig)
|
||||
require.NotNil(t, decoratorConfig.Prepend)
|
||||
require.NotNil(t, decoratorConfig.Append)
|
||||
require.Len(t, decoratorConfig.Prepend, 1)
|
||||
require.Len(t, decoratorConfig.Append, 1)
|
||||
require.Equal(t, "system", decoratorConfig.Prepend[0].Role)
|
||||
require.Equal(t, "You are a helpful assistant from ${geo-country}.", decoratorConfig.Prepend[0].Content)
|
||||
require.Equal(t, "system", decoratorConfig.Append[0].Role)
|
||||
require.Equal(t, "Please provide context about ${geo-city}.", decoratorConfig.Append[0].Content)
|
||||
})
|
||||
|
||||
// 测试只有前置消息的配置解析
|
||||
t.Run("prepend only config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(prependOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
decoratorConfig := config.(*AIPromptDecoratorConfig)
|
||||
require.NotNil(t, decoratorConfig.Prepend)
|
||||
require.NotNil(t, decoratorConfig.Append)
|
||||
require.Len(t, decoratorConfig.Prepend, 1)
|
||||
require.Len(t, decoratorConfig.Append, 0)
|
||||
require.Equal(t, "system", decoratorConfig.Prepend[0].Role)
|
||||
require.Equal(t, "You are located in ${geo-province}, ${geo-country}.", decoratorConfig.Prepend[0].Content)
|
||||
})
|
||||
|
||||
// 测试空配置解析
|
||||
t.Run("empty config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
decoratorConfig := config.(*AIPromptDecoratorConfig)
|
||||
require.NotNil(t, decoratorConfig.Prepend)
|
||||
require.NotNil(t, decoratorConfig.Append)
|
||||
require.Len(t, decoratorConfig.Prepend, 0)
|
||||
require.Len(t, decoratorConfig.Append, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求头处理
|
||||
t.Run("request headers processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"content-length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基础消息装饰
|
||||
t.Run("basic message decoration", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置地理变量属性,供插件使用
|
||||
host.SetProperty([]string{"geo-country"}, []byte("China"))
|
||||
host.SetProperty([]string{"geo-province"}, []byte("Beijing"))
|
||||
host.SetProperty([]string{"geo-city"}, []byte("Beijing"))
|
||||
host.SetProperty([]string{"geo-isp"}, []byte("China Mobile"))
|
||||
|
||||
// 设置请求体,包含消息
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证消息装饰是否成功
|
||||
modifiedBody := host.GetRequestBody()
|
||||
require.NotEmpty(t, modifiedBody)
|
||||
|
||||
// 解析修改后的请求体
|
||||
var modifiedRequest map[string]interface{}
|
||||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证messages字段存在
|
||||
messages, exists := modifiedRequest["messages"].([]interface{})
|
||||
require.True(t, exists, "messages field should exist")
|
||||
require.NotNil(t, messages)
|
||||
|
||||
// 验证消息数量:前置消息(1) + 原始消息(1) + 后置消息(1) = 3
|
||||
require.Len(t, messages, 3, "should have 3 messages: prepend + original + append")
|
||||
|
||||
// 验证第一个消息是前置消息(地理变量已被替换)
|
||||
firstMessage := messages[0].(map[string]interface{})
|
||||
require.Equal(t, "system", firstMessage["role"])
|
||||
require.Equal(t, "You are a helpful assistant from China.", firstMessage["content"])
|
||||
|
||||
// 验证第二个消息是原始用户消息
|
||||
secondMessage := messages[1].(map[string]interface{})
|
||||
require.Equal(t, "user", secondMessage["role"])
|
||||
require.Equal(t, "Hello, how are you?", secondMessage["content"])
|
||||
|
||||
// 验证第三个消息是后置消息(地理变量已被替换)
|
||||
thirdMessage := messages[2].(map[string]interface{})
|
||||
require.Equal(t, "system", thirdMessage["role"])
|
||||
require.Equal(t, "Please provide context about Beijing.", thirdMessage["content"])
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试只有前置消息的装饰
|
||||
t.Run("prepend only decoration", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(prependOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置地理变量属性,供插件使用
|
||||
host.SetProperty([]string{"geo-country"}, []byte("China"))
|
||||
host.SetProperty([]string{"geo-province"}, []byte("Shanghai"))
|
||||
host.SetProperty([]string{"geo-city"}, []byte("Shanghai"))
|
||||
host.SetProperty([]string{"geo-isp"}, []byte("China Telecom"))
|
||||
|
||||
// 设置请求体,包含消息
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "What's the weather like?"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证消息装饰是否成功
|
||||
modifiedBody := host.GetRequestBody()
|
||||
require.NotEmpty(t, modifiedBody)
|
||||
|
||||
// 解析修改后的请求体
|
||||
var modifiedRequest map[string]interface{}
|
||||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证messages字段存在
|
||||
messages, exists := modifiedRequest["messages"].([]interface{})
|
||||
require.True(t, exists, "messages field should exist")
|
||||
require.NotNil(t, messages)
|
||||
|
||||
// 验证消息数量:前置消息(1) + 原始消息(1) = 2
|
||||
require.Len(t, messages, 2, "should have 2 messages: prepend + original")
|
||||
|
||||
// 验证第一个消息是前置消息(地理变量已被替换)
|
||||
firstMessage := messages[0].(map[string]interface{})
|
||||
require.Equal(t, "system", firstMessage["role"])
|
||||
require.Equal(t, "You are located in Shanghai, China.", firstMessage["content"])
|
||||
|
||||
// 验证第二个消息是原始用户消息
|
||||
secondMessage := messages[1].(map[string]interface{})
|
||||
require.Equal(t, "user", secondMessage["role"])
|
||||
require.Equal(t, "What's the weather like?", secondMessage["content"])
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空消息的情况
|
||||
t.Run("empty messages", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体,不包含messages字段
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo"
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多个消息的装饰
|
||||
t.Run("multiple messages decoration", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置地理变量属性,供插件使用
|
||||
host.SetProperty([]string{"geo-country"}, []byte("USA"))
|
||||
host.SetProperty([]string{"geo-province"}, []byte("California"))
|
||||
host.SetProperty([]string{"geo-city"}, []byte("San Francisco"))
|
||||
host.SetProperty([]string{"geo-isp"}, []byte("Comcast"))
|
||||
|
||||
// 设置请求体,包含多个消息
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a helpful assistant"},
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证消息装饰是否成功
|
||||
modifiedBody := host.GetRequestBody()
|
||||
require.NotEmpty(t, modifiedBody)
|
||||
|
||||
// 解析修改后的请求体
|
||||
var modifiedRequest map[string]interface{}
|
||||
err := json.Unmarshal(modifiedBody, &modifiedRequest)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证messages字段存在
|
||||
messages, exists := modifiedRequest["messages"].([]interface{})
|
||||
require.True(t, exists, "messages field should exist")
|
||||
require.NotNil(t, messages)
|
||||
|
||||
// 验证消息数量:前置消息(1) + 原始消息(3) + 后置消息(1) = 5
|
||||
require.Len(t, messages, 5, "should have 5 messages: prepend + original(3) + append")
|
||||
|
||||
// 验证第一个消息是前置消息(地理变量已被替换)
|
||||
firstMessage := messages[0].(map[string]interface{})
|
||||
require.Equal(t, "system", firstMessage["role"])
|
||||
require.Equal(t, "You are a helpful assistant from USA.", firstMessage["content"])
|
||||
|
||||
// 验证原始消息保持顺序
|
||||
originalMessages := messages[1:4]
|
||||
require.Equal(t, "system", originalMessages[0].(map[string]interface{})["role"])
|
||||
require.Equal(t, "You are a helpful assistant", originalMessages[0].(map[string]interface{})["content"])
|
||||
require.Equal(t, "user", originalMessages[1].(map[string]interface{})["role"])
|
||||
require.Equal(t, "Hello", originalMessages[1].(map[string]interface{})["content"])
|
||||
require.Equal(t, "assistant", originalMessages[2].(map[string]interface{})["role"])
|
||||
require.Equal(t, "Hi there!", originalMessages[2].(map[string]interface{})["content"])
|
||||
|
||||
// 验证最后一个消息是后置消息(地理变量已被替换)
|
||||
lastMessage := messages[4].(map[string]interface{})
|
||||
require.Equal(t, "system", lastMessage["role"])
|
||||
require.Equal(t, "Please provide context about San Francisco.", lastMessage["content"])
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructs(t *testing.T) {
|
||||
// 测试Message结构体
|
||||
t.Run("Message struct", func(t *testing.T) {
|
||||
message := Message{
|
||||
Role: "system",
|
||||
Content: "You are a helpful assistant from ${geo-country}.",
|
||||
}
|
||||
require.Equal(t, "system", message.Role)
|
||||
require.Equal(t, "You are a helpful assistant from ${geo-country}.", message.Content)
|
||||
})
|
||||
|
||||
// 测试AIPromptDecoratorConfig结构体
|
||||
t.Run("AIPromptDecoratorConfig struct", func(t *testing.T) {
|
||||
config := &AIPromptDecoratorConfig{
|
||||
Prepend: []Message{
|
||||
{Role: "system", Content: "Prepend message"},
|
||||
},
|
||||
Append: []Message{
|
||||
{Role: "system", Content: "Append message"},
|
||||
},
|
||||
}
|
||||
require.NotNil(t, config.Prepend)
|
||||
require.NotNil(t, config.Append)
|
||||
require.Len(t, config.Prepend, 1)
|
||||
require.Len(t, config.Append, 1)
|
||||
require.Equal(t, "Prepend message", config.Prepend[0].Content)
|
||||
require.Equal(t, "Append message", config.Append[0].Content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGeographicVariableReplacement(t *testing.T) {
|
||||
// 测试地理变量替换逻辑
|
||||
t.Run("geographic variable replacement", func(t *testing.T) {
|
||||
config := &AIPromptDecoratorConfig{
|
||||
Prepend: []Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: "Location: ${geo-country}/${geo-province}/${geo-city}, ISP: ${geo-isp}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 验证地理变量在内容中的存在
|
||||
content := config.Prepend[0].Content
|
||||
require.Contains(t, content, "${geo-country}")
|
||||
require.Contains(t, content, "${geo-province}")
|
||||
require.Contains(t, content, "${geo-city}")
|
||||
require.Contains(t, content, "${geo-isp}")
|
||||
|
||||
// 测试变量替换逻辑
|
||||
geoVariables := []string{"geo-country", "geo-province", "geo-city", "geo-isp"}
|
||||
for _, geo := range geoVariables {
|
||||
require.Contains(t, content, fmt.Sprintf("${%s}", geo))
|
||||
}
|
||||
})
|
||||
|
||||
// 测试混合内容的地理变量
|
||||
t.Run("mixed content geographic variables", func(t *testing.T) {
|
||||
config := &AIPromptDecoratorConfig{
|
||||
Append: []Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: "User from ${geo-country} with ISP ${geo-isp}. Context: ${geo-province}, ${geo-city}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
content := config.Append[0].Content
|
||||
require.Contains(t, content, "${geo-country}")
|
||||
require.Contains(t, content, "${geo-isp}")
|
||||
require.Contains(t, content, "${geo-province}")
|
||||
require.Contains(t, content, "${geo-city}")
|
||||
})
|
||||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试空前置和后置消息
|
||||
t.Run("empty prepend and append", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Test message"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效JSON请求体
|
||||
t.Run("invalid JSON body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置无效的请求体
|
||||
body := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
// Missing closing brace
|
||||
`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
424
plugins/wasm-go/extensions/ai-prompt-template/main_test.go
Normal file
424
plugins/wasm-go/extensions/ai-prompt-template/main_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础模板配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"templates": []map[string]interface{}{
|
||||
{
|
||||
"name": "greeting",
|
||||
"template": "Hello {{name}}, welcome to {{company}}!",
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"template": "Here is a summary of {{topic}}: {{content}}",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:单个模板配置
|
||||
var singleTemplateConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"templates": []map[string]interface{}{
|
||||
{
|
||||
"name": "simple",
|
||||
"template": "This is a {{adjective}} {{noun}}.",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:空模板配置
|
||||
var emptyTemplatesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"templates": []map[string]interface{}{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:复杂模板配置
|
||||
var complexTemplateConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"templates": []map[string]interface{}{
|
||||
{
|
||||
"name": "email",
|
||||
"template": "Dear {{recipient}},\n\n{{greeting}}\n\n{{body}}\n\nBest regards,\n{{sender}}",
|
||||
},
|
||||
{
|
||||
"name": "report",
|
||||
"template": "Report: {{title}}\nDate: {{date}}\nAuthor: {{author}}\n\n{{content}}\n\nConclusion: {{conclusion}}",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础模板配置解析
|
||||
t.Run("basic templates config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
promptConfig := config.(*AIPromptTemplateConfig)
|
||||
require.NotNil(t, promptConfig.templates)
|
||||
require.Len(t, promptConfig.templates, 2)
|
||||
// 由于gjson.Get("template").Raw返回JSON原始值,包含引号
|
||||
require.Equal(t, "\"Hello {{name}}, welcome to {{company}}!\"", promptConfig.templates["greeting"])
|
||||
require.Equal(t, "\"Here is a summary of {{topic}}: {{content}}\"", promptConfig.templates["summary"])
|
||||
})
|
||||
|
||||
// 测试单个模板配置解析
|
||||
t.Run("single template config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(singleTemplateConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
promptConfig := config.(*AIPromptTemplateConfig)
|
||||
require.NotNil(t, promptConfig.templates)
|
||||
require.Len(t, promptConfig.templates, 1)
|
||||
// 由于gjson.Get("template").Raw返回JSON原始值,包含引号
|
||||
require.Equal(t, "\"This is a {{adjective}} {{noun}}.\"", promptConfig.templates["simple"])
|
||||
})
|
||||
|
||||
// 测试空模板配置解析
|
||||
t.Run("empty templates config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyTemplatesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
promptConfig := config.(*AIPromptTemplateConfig)
|
||||
require.NotNil(t, promptConfig.templates)
|
||||
require.Len(t, promptConfig.templates, 0)
|
||||
})
|
||||
|
||||
// 测试复杂模板配置解析
|
||||
t.Run("complex templates config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(complexTemplateConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
promptConfig := config.(*AIPromptTemplateConfig)
|
||||
require.NotNil(t, promptConfig.templates)
|
||||
require.Len(t, promptConfig.templates, 2)
|
||||
// 由于gjson.Get("template").Raw返回JSON原始值,包含引号和转义字符
|
||||
require.Equal(t, "\"Dear {{recipient}},\\n\\n{{greeting}}\\n\\n{{body}}\\n\\nBest regards,\\n{{sender}}\"", promptConfig.templates["email"])
|
||||
require.Equal(t, "\"Report: {{title}}\\nDate: {{date}}\\nAuthor: {{author}}\\n\\n{{content}}\\n\\nConclusion: {{conclusion}}\"", promptConfig.templates["report"])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试启用模板的情况
|
||||
t.Run("template enabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,启用模板
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
{"content-length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试禁用模板的情况
|
||||
t.Run("template disabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,禁用模板
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "false"},
|
||||
{"content-length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试没有template-enable头的情况
|
||||
t.Run("no template-enable header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含template-enable
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"content-length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基础模板替换
|
||||
t.Run("basic template replacement", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
})
|
||||
|
||||
// 设置请求体,包含模板和属性
|
||||
body := `{
|
||||
"template": "greeting",
|
||||
"properties": {
|
||||
"name": "Alice",
|
||||
"company": "TechCorp"
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试复杂模板替换
|
||||
t.Run("complex template replacement", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(complexTemplateConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
})
|
||||
|
||||
// 设置请求体,包含复杂模板和属性
|
||||
body := `{
|
||||
"template": "email",
|
||||
"properties": {
|
||||
"recipient": "John Doe",
|
||||
"greeting": "I hope this email finds you well",
|
||||
"body": "Please find attached the quarterly report",
|
||||
"sender": "Jane Smith"
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试没有模板的情况
|
||||
t.Run("no template in body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
})
|
||||
|
||||
// 设置请求体,不包含模板
|
||||
body := `{
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试没有属性的情况
|
||||
t.Run("no properties in body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
})
|
||||
|
||||
// 设置请求体,包含模板但不包含属性
|
||||
body := `{
|
||||
"template": "greeting"
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试部分属性替换
|
||||
t.Run("partial properties replacement", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"template-enable", "true"},
|
||||
})
|
||||
|
||||
// 设置请求体,只包含部分属性
|
||||
body := `{
|
||||
"template": "greeting",
|
||||
"properties": {
|
||||
"name": "Bob"
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructs(t *testing.T) {
|
||||
// 测试AIPromptTemplateConfig结构体
|
||||
t.Run("AIPromptTemplateConfig struct", func(t *testing.T) {
|
||||
config := &AIPromptTemplateConfig{
|
||||
templates: map[string]string{
|
||||
"test": "This is a {{test}} template",
|
||||
},
|
||||
}
|
||||
require.NotNil(t, config.templates)
|
||||
require.Len(t, config.templates, 1)
|
||||
require.Equal(t, "This is a {{test}} template", config.templates["test"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateReplacementLogic(t *testing.T) {
|
||||
// 测试模板变量替换逻辑
|
||||
t.Run("template variable replacement", func(t *testing.T) {
|
||||
config := &AIPromptTemplateConfig{
|
||||
templates: map[string]string{
|
||||
"greeting": "Hello {{name}}, welcome to {{company}}!",
|
||||
},
|
||||
}
|
||||
|
||||
// 模拟模板替换逻辑
|
||||
template := config.templates["greeting"]
|
||||
require.Equal(t, "Hello {{name}}, welcome to {{company}}!", template)
|
||||
|
||||
// 测试变量替换
|
||||
properties := map[string]string{
|
||||
"name": "Alice",
|
||||
"company": "TechCorp",
|
||||
}
|
||||
|
||||
for key, value := range properties {
|
||||
template = strings.ReplaceAll(template, fmt.Sprintf("{{%s}}", key), value)
|
||||
}
|
||||
|
||||
require.Equal(t, "Hello Alice, welcome to TechCorp!", template)
|
||||
})
|
||||
|
||||
// 测试嵌套变量替换
|
||||
t.Run("nested variable replacement", func(t *testing.T) {
|
||||
config := &AIPromptTemplateConfig{
|
||||
templates: map[string]string{
|
||||
"nested": "{{greeting}} {{name}}, {{message}}",
|
||||
},
|
||||
}
|
||||
|
||||
template := config.templates["nested"]
|
||||
require.Equal(t, "{{greeting}} {{name}}, {{message}}", template)
|
||||
|
||||
// 测试嵌套替换
|
||||
properties := map[string]string{
|
||||
"greeting": "Hello",
|
||||
"name": "World",
|
||||
"message": "welcome!",
|
||||
}
|
||||
|
||||
for key, value := range properties {
|
||||
template = strings.ReplaceAll(template, fmt.Sprintf("{{%s}}", key), value)
|
||||
}
|
||||
|
||||
require.Equal(t, "Hello World, welcome!", template)
|
||||
})
|
||||
}
|
||||
@@ -16,4 +16,3 @@
|
||||
!*/
|
||||
|
||||
/out
|
||||
/test
|
||||
|
||||
@@ -7,12 +7,14 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250819092116-2fd2b083a8e2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250819092116-2fd2b083a8e2 h1:2wlbNpFJCQNbPBFYgswz7Zvxo9O3L0PH0AJxwiCc5lk=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250819092116-2fd2b083a8e2/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/provider"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/test"
|
||||
)
|
||||
|
||||
func Test_getApiName(t *testing.T) {
|
||||
@@ -57,3 +58,48 @@ func Test_getApiName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAi360(t *testing.T) {
|
||||
test.RunAi360ParseConfigTests(t)
|
||||
test.RunAi360OnHttpRequestHeadersTests(t)
|
||||
test.RunAi360OnHttpRequestBodyTests(t)
|
||||
test.RunAi360OnHttpResponseHeadersTests(t)
|
||||
test.RunAi360OnHttpResponseBodyTests(t)
|
||||
test.RunAi360OnStreamingResponseBodyTests(t)
|
||||
}
|
||||
|
||||
func TestOpenAI(t *testing.T) {
|
||||
test.RunOpenAIParseConfigTests(t)
|
||||
test.RunOpenAIOnHttpRequestHeadersTests(t)
|
||||
test.RunOpenAIOnHttpRequestBodyTests(t)
|
||||
test.RunOpenAIOnHttpResponseHeadersTests(t)
|
||||
test.RunOpenAIOnHttpResponseBodyTests(t)
|
||||
test.RunOpenAIOnStreamingResponseBodyTests(t)
|
||||
}
|
||||
|
||||
func TestQwen(t *testing.T) {
|
||||
test.RunQwenParseConfigTests(t)
|
||||
test.RunQwenOnHttpRequestHeadersTests(t)
|
||||
test.RunQwenOnHttpRequestBodyTests(t)
|
||||
test.RunQwenOnHttpResponseHeadersTests(t)
|
||||
test.RunQwenOnHttpResponseBodyTests(t)
|
||||
test.RunQwenOnStreamingResponseBodyTests(t)
|
||||
}
|
||||
|
||||
func TestGemini(t *testing.T) {
|
||||
test.RunGeminiParseConfigTests(t)
|
||||
test.RunGeminiOnHttpRequestHeadersTests(t)
|
||||
test.RunGeminiOnHttpRequestBodyTests(t)
|
||||
test.RunGeminiOnHttpResponseHeadersTests(t)
|
||||
test.RunGeminiOnHttpResponseBodyTests(t)
|
||||
test.RunGeminiOnStreamingResponseBodyTests(t)
|
||||
test.RunGeminiGetImageURLTests(t)
|
||||
}
|
||||
|
||||
func TestAzure(t *testing.T) {
|
||||
test.RunAzureParseConfigTests(t)
|
||||
test.RunAzureOnHttpRequestHeadersTests(t)
|
||||
test.RunAzureOnHttpRequestBodyTests(t)
|
||||
test.RunAzureOnHttpResponseHeadersTests(t)
|
||||
test.RunAzureOnHttpResponseBodyTests(t)
|
||||
}
|
||||
|
||||
718
plugins/wasm-go/extensions/ai-proxy/test/ai360.go
Normal file
718
plugins/wasm-go/extensions/ai-proxy/test/ai360.go
Normal file
@@ -0,0 +1,718 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本ai360配置
|
||||
var basicAi360Config = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "ai360",
|
||||
"apiTokens": []string{"sk-ai360-test123456789"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "360GPT_S2_V9",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:ai360多模型配置
|
||||
var ai360MultiModelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "ai360",
|
||||
"apiTokens": []string{"sk-ai360-multi-model"},
|
||||
"modelMapping": map[string]string{
|
||||
"gpt-3.5-turbo": "360GPT_S2_V9",
|
||||
"gpt-4": "360GPT_S2_V9",
|
||||
"text-embedding-ada-002": "360Embedding_Text_V1",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效ai360配置(缺少apiToken)
|
||||
var invalidAi360Config = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "ai360",
|
||||
// 缺少apiTokens
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:ai360自定义域名配置
|
||||
var ai360CustomDomainConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "ai360",
|
||||
"apiTokens": []string{"sk-ai360-custom-domain"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "360GPT_S2_V9",
|
||||
},
|
||||
"openaiCustomUrl": "https://custom.ai360.cn/v1",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:ai360完整配置(包含failover等字段)
|
||||
var completeAi360Config = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "ai360",
|
||||
"apiTokens": []string{"sk-ai360-complete"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "360GPT_S2_V9",
|
||||
},
|
||||
"failover": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"retryOnFailure": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunAi360ParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本ai360配置解析
|
||||
t.Run("basic ai360 config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试ai360多模型配置解析
|
||||
t.Run("ai360 multi model config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(ai360MultiModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效ai360配置(缺少apiToken)
|
||||
t.Run("invalid ai360 config - missing api token", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试ai360自定义域名配置解析
|
||||
t.Run("ai360 custom domain config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(ai360CustomDomainConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试ai360完整配置解析
|
||||
t.Run("ai360 complete config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(completeAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAi360OnHttpRequestHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试ai360请求头处理(聊天完成接口)
|
||||
t.Run("ai360 chat completion request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,因为需要处理请求体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证请求头是否被正确处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host是否被改为ai360域名
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "api.360.cn", hostValue, "Host should be changed to ai360 domain")
|
||||
|
||||
// 验证Authorization是否被设置
|
||||
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
|
||||
require.True(t, hasAuth, "Authorization header should exist")
|
||||
require.Contains(t, authValue, "sk-ai360-test123456789", "Authorization should contain ai360 API token")
|
||||
|
||||
// 验证Path是否被正确处理
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
// ai360应该支持聊天完成接口,路径可能被转换
|
||||
require.Contains(t, pathValue, "/v1/chat/completions", "Path should contain chat completions endpoint")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasAi360Logs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "ai360") {
|
||||
hasAi360Logs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasAi360Logs, "Should have ai360 processing logs")
|
||||
})
|
||||
|
||||
// 测试ai360请求头处理(嵌入接口)
|
||||
t.Run("ai360 embeddings request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证嵌入接口的请求头处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host转换
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost)
|
||||
require.Equal(t, "api.360.cn", hostValue)
|
||||
|
||||
// 验证Path转换
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath)
|
||||
require.Contains(t, pathValue, "/v1/embeddings", "Path should contain embeddings endpoint")
|
||||
|
||||
// 验证Authorization设置
|
||||
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
|
||||
require.True(t, hasAuth, "Authorization header should exist for embeddings")
|
||||
require.Contains(t, authValue, "sk-ai360-test123456789", "Authorization should contain ai360 API token")
|
||||
})
|
||||
|
||||
// 测试ai360请求头处理(不支持的接口)
|
||||
t.Run("ai360 unsupported api request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/images/generations"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证不支持的接口处理
|
||||
// 即使是不支持的接口,基本的请求头转换仍然应该执行
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// Host仍然应该被转换
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost)
|
||||
require.Equal(t, "api.360.cn", hostValue)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAi360OnHttpRequestBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试ai360请求体处理(聊天完成接口)
|
||||
t.Run("ai360 chat completion request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体是否被正确处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证模型名称是否被正确映射
|
||||
// ai360 provider会将模型名称从gpt-3.5-turbo映射为360GPT_S2_V9
|
||||
require.Contains(t, string(processedBody), "360GPT_S2_V9", "Model name should be mapped to ai360 format")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
infoLogs := host.GetInfoLogs()
|
||||
|
||||
// 验证是否有ai360相关的处理日志
|
||||
hasAi360Logs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "ai360") {
|
||||
hasAi360Logs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, log := range infoLogs {
|
||||
if strings.Contains(log, "ai360") {
|
||||
hasAi360Logs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasAi360Logs, "Should have ai360 processing logs")
|
||||
})
|
||||
|
||||
// 测试ai360请求体处理(嵌入接口)
|
||||
t.Run("ai360 embeddings request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证嵌入接口的请求体处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证模型名称映射
|
||||
// ai360 provider会将模型名称从text-embedding-ada-002映射为360GPT_S2_V9
|
||||
require.Contains(t, string(processedBody), "360GPT_S2_V9", "Model name should be mapped to ai360 format")
|
||||
|
||||
// 检查处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasEmbeddingLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "embeddings") || strings.Contains(log, "ai360") {
|
||||
hasEmbeddingLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasEmbeddingLogs, "Should have embedding processing logs")
|
||||
})
|
||||
|
||||
// 测试ai360请求体处理(不支持的接口)
|
||||
t.Run("ai360 unsupported api request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/images/generations"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"dall-e-3","prompt":"test image"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证不支持的接口处理
|
||||
|
||||
// 验证请求体没有被意外修改
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
require.Contains(t, string(processedBody), "dall-e-3", "Request body should not be modified for unsupported APIs")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAi360OnHttpResponseHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试ai360响应头处理(聊天完成接口)
|
||||
t.Run("ai360 chat completion response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Request-Id", "req-123"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头是否被正确处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证状态码
|
||||
statusValue, hasStatus := test.GetHeaderValue(processedResponseHeaders, ":status")
|
||||
require.True(t, hasStatus, "Status header should exist")
|
||||
require.Equal(t, "200", statusValue, "Status should be 200")
|
||||
|
||||
// 验证Content-Type
|
||||
contentTypeValue, hasContentType := test.GetHeaderValue(processedResponseHeaders, "Content-Type")
|
||||
require.True(t, hasContentType, "Content-Type header should exist")
|
||||
require.Equal(t, "application/json", contentTypeValue, "Content-Type should be application/json")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasResponseLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "response") || strings.Contains(log, "ai360") {
|
||||
hasResponseLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasResponseLogs, "Should have response processing logs")
|
||||
})
|
||||
|
||||
// 测试ai360响应头处理(嵌入接口)
|
||||
t.Run("ai360 embeddings response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Embedding-Model", "360Embedding_Text_V1"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证嵌入模型信息
|
||||
modelValue, hasModel := test.GetHeaderValue(processedResponseHeaders, "X-Embedding-Model")
|
||||
require.True(t, hasModel, "Embedding model header should exist")
|
||||
require.Equal(t, "360Embedding_Text_V1", modelValue, "Embedding model should match configuration")
|
||||
})
|
||||
|
||||
// 测试ai360响应头处理(错误响应)
|
||||
t.Run("ai360 error response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置错误响应头
|
||||
errorResponseHeaders := [][2]string{
|
||||
{":status", "429"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"Retry-After", "60"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(errorResponseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证错误响应头处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证错误状态码
|
||||
statusValue, hasStatus := test.GetHeaderValue(processedResponseHeaders, ":status")
|
||||
require.True(t, hasStatus, "Status header should exist")
|
||||
require.Equal(t, "429", statusValue, "Status should be 429 (Too Many Requests)")
|
||||
|
||||
// 验证重试信息
|
||||
retryValue, hasRetry := test.GetHeaderValue(processedResponseHeaders, "Retry-After")
|
||||
require.True(t, hasRetry, "Retry-After header should exist")
|
||||
require.Equal(t, "60", retryValue, "Retry-After should be 60 seconds")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAi360OnHttpResponseBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试ai360响应体处理(聊天完成接口)
|
||||
t.Run("ai360 chat completion response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-3.5-turbo",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello! How can I help you today?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 12,
|
||||
"total_tokens": 21
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体是否被正确处理
|
||||
processedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, processedResponseBody)
|
||||
|
||||
// 验证响应体内容
|
||||
responseStr := string(processedResponseBody)
|
||||
require.Contains(t, responseStr, "chat.completion", "Response should contain chat completion object")
|
||||
require.Contains(t, responseStr, "assistant", "Response should contain assistant role")
|
||||
require.Contains(t, responseStr, "usage", "Response should contain usage information")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasResponseBodyLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "response") || strings.Contains(log, "body") || strings.Contains(log, "ai360") {
|
||||
hasResponseBodyLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasResponseBodyLogs, "Should have response body processing logs")
|
||||
})
|
||||
|
||||
// 测试ai360响应体处理(嵌入接口)
|
||||
t.Run("ai360 embeddings response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"object": "embedding",
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
"index": 0
|
||||
}],
|
||||
"model": "text-embedding-ada-002",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"total_tokens": 5
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体处理
|
||||
processedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, processedResponseBody)
|
||||
|
||||
// 验证嵌入响应内容
|
||||
responseStr := string(processedResponseBody)
|
||||
require.Contains(t, responseStr, "embedding", "Response should contain embedding object")
|
||||
require.Contains(t, responseStr, "0.1", "Response should contain embedding vector")
|
||||
require.Contains(t, responseStr, "text-embedding-ada-002", "Response should contain model name")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func RunAi360OnStreamingResponseBodyTests(t *testing.T) {
|
||||
// 测试ai360响应体处理(流式响应)
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("ai360 streaming response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAi360Config)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置流式请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}],"stream":true}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置流式响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "text/event-stream"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 模拟流式响应体
|
||||
chunk1 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk2 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk3 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"!"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk4 := `data: [DONE]
|
||||
|
||||
`
|
||||
|
||||
// 处理流式响应体
|
||||
action1 := host.CallOnHttpStreamingResponseBody([]byte(chunk1), false)
|
||||
require.Equal(t, types.ActionContinue, action1)
|
||||
|
||||
action2 := host.CallOnHttpStreamingResponseBody([]byte(chunk2), false)
|
||||
require.Equal(t, types.ActionContinue, action2)
|
||||
|
||||
action3 := host.CallOnHttpStreamingResponseBody([]byte(chunk3), false)
|
||||
require.Equal(t, types.ActionContinue, action3)
|
||||
|
||||
action4 := host.CallOnHttpStreamingResponseBody([]byte(chunk4), true)
|
||||
require.Equal(t, types.ActionContinue, action4)
|
||||
|
||||
// 验证流式响应处理
|
||||
// 注意:流式响应可能不会在GetResponseBody中累积,需要检查日志或其他方式验证
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasStreamingLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "streaming") || strings.Contains(log, "chunk") || strings.Contains(log, "ai360") {
|
||||
hasStreamingLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasStreamingLogs, "Should have streaming response processing logs")
|
||||
})
|
||||
})
|
||||
}
|
||||
600
plugins/wasm-go/extensions/ai-proxy/test/azure.go
Normal file
600
plugins/wasm-go/extensions/ai-proxy/test/azure.go
Normal file
@@ -0,0 +1,600 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本Azure OpenAI配置
|
||||
var basicAzureConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-test123456789",
|
||||
},
|
||||
"azureServiceUrl": "https://test-resource.openai.azure.com/openai/deployments/test-deployment/chat/completions?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI完整路径配置
|
||||
var azureFullPathConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-fullpath",
|
||||
},
|
||||
"azureServiceUrl": "https://fullpath-resource.openai.azure.com/openai/deployments/fullpath-deployment/chat/completions?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]string{
|
||||
"gpt-3.5-turbo": "gpt-3.5-turbo",
|
||||
"gpt-4": "gpt-4",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI仅部署配置
|
||||
var azureDeploymentOnlyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-deployment",
|
||||
},
|
||||
"azureServiceUrl": "https://deployment-resource.openai.azure.com/openai/deployments/deployment-only?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI仅域名配置
|
||||
var azureDomainOnlyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-domain",
|
||||
},
|
||||
"azureServiceUrl": "https://domain-resource.openai.azure.com?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI多模型配置
|
||||
var azureMultiModelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-multi",
|
||||
},
|
||||
"azureServiceUrl": "https://multi-resource.openai.azure.com/openai/deployments/multi-deployment?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]string{
|
||||
"gpt-3.5-turbo": "gpt-3.5-turbo",
|
||||
"gpt-4": "gpt-4",
|
||||
"text-embedding-ada-002": "text-embedding-ada-002",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI无效配置(缺少azureServiceUrl)
|
||||
var azureInvalidConfigMissingUrl = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-invalid",
|
||||
},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI无效配置(缺少api-version)
|
||||
var azureInvalidConfigMissingApiVersion = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"apiTokens": []string{
|
||||
"sk-azure-invalid",
|
||||
},
|
||||
"azureServiceUrl": "https://invalid-resource.openai.azure.com/openai/deployments/invalid-deployment/chat/completions",
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:Azure OpenAI无效配置(缺少apiToken)
|
||||
var azureInvalidConfigMissingToken = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "azure",
|
||||
"azureServiceUrl": "https://invalid-resource.openai.azure.com/openai/deployments/invalid-deployment/chat/completions?api-version=2024-02-15-preview",
|
||||
"modelMapping": map[string]interface{}{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunAzureParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本Azure OpenAI配置解析
|
||||
t.Run("basic azure config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAzureConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI完整路径配置解析
|
||||
t.Run("azure full path config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureFullPathConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI仅部署配置解析
|
||||
t.Run("azure deployment only config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureDeploymentOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI仅域名配置解析
|
||||
t.Run("azure domain only config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureDomainOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI多模型配置解析
|
||||
t.Run("azure multi model config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureMultiModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI无效配置(缺少azureServiceUrl)
|
||||
t.Run("azure invalid config missing url", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureInvalidConfigMissingUrl)
|
||||
defer host.Reset()
|
||||
// 应该失败,因为缺少azureServiceUrl
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI无效配置(缺少api-version)
|
||||
t.Run("azure invalid config missing api version", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureInvalidConfigMissingApiVersion)
|
||||
defer host.Reset()
|
||||
// 应该失败,因为缺少api-version
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI无效配置(缺少apiToken)
|
||||
t.Run("azure invalid config missing token", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureInvalidConfigMissingToken)
|
||||
defer host.Reset()
|
||||
// 应该失败,因为缺少apiToken
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAzureOnHttpRequestHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试Azure OpenAI请求头处理(聊天完成接口)
|
||||
t.Run("azure chat completion request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAzureConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,因为需要处理请求体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证请求头是否被正确处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host是否被改为Azure服务域名
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "test-resource.openai.azure.com", hostValue, "Host should be changed to Azure service domain")
|
||||
|
||||
// 验证api-key是否被设置
|
||||
apiKeyValue, hasApiKey := test.GetHeaderValue(requestHeaders, "api-key")
|
||||
require.True(t, hasApiKey, "api-key header should exist")
|
||||
require.Equal(t, "sk-azure-test123456789", apiKeyValue, "api-key should contain Azure API token")
|
||||
|
||||
// 验证Path是否被正确处理
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.Contains(t, pathValue, "/openai/deployments/test-deployment/chat/completions", "Path should contain Azure deployment path")
|
||||
|
||||
// 验证Content-Length是否被删除
|
||||
_, hasContentLength := test.GetHeaderValue(requestHeaders, "Content-Length")
|
||||
require.False(t, hasContentLength, "Content-Length header should be deleted")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasAzureLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "azureProvider") {
|
||||
hasAzureLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasAzureLogs, "Should have Azure provider debug logs")
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI请求头处理(完整路径配置)
|
||||
t.Run("azure full path request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureFullPathConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证请求头是否被正确处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host是否被改为Azure服务域名
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "fullpath-resource.openai.azure.com", hostValue, "Host should be changed to Azure service domain")
|
||||
|
||||
// 验证api-key是否被设置
|
||||
apiKeyValue, hasApiKey := test.GetHeaderValue(requestHeaders, "api-key")
|
||||
require.True(t, hasApiKey, "api-key header should exist")
|
||||
require.Equal(t, "sk-azure-fullpath", apiKeyValue, "api-key should contain Azure API token")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAzureOnHttpRequestBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试Azure OpenAI请求体处理(聊天完成接口)
|
||||
t.Run("azure chat completion request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAzureConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, how are you?"
|
||||
}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}`
|
||||
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体是否被正确处理
|
||||
transformedBody := host.GetRequestBody()
|
||||
require.NotNil(t, transformedBody)
|
||||
|
||||
// 验证模型映射是否生效
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(transformedBody, &bodyMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
model, exists := bodyMap["model"]
|
||||
require.True(t, exists, "Model should exist in request body")
|
||||
require.Equal(t, "gpt-3.5-turbo", model, "Model should be mapped correctly")
|
||||
|
||||
// 验证请求路径是否被正确转换
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.Contains(t, pathValue, "/openai/deployments/test-deployment/chat/completions", "Path should contain Azure deployment path")
|
||||
require.Contains(t, pathValue, "api-version=2024-02-15-preview", "Path should contain API version")
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI请求体处理(不同模型)
|
||||
t.Run("azure different model request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureMultiModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Explain quantum computing"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体是否被正确处理
|
||||
transformedBody := host.GetRequestBody()
|
||||
require.NotNil(t, transformedBody)
|
||||
|
||||
var bodyMap map[string]interface{}
|
||||
err := json.Unmarshal(transformedBody, &bodyMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
model, exists := bodyMap["model"]
|
||||
require.True(t, exists, "Model should exist in request body")
|
||||
require.Equal(t, "gpt-4", model, "Model should be mapped correctly")
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI请求体处理(仅部署配置)
|
||||
t.Run("azure deployment only request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureDeploymentOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Test message"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求路径是否使用默认部署
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.Contains(t, pathValue, "/openai/deployments/deployment-only/chat/completions", "Path should use default deployment")
|
||||
})
|
||||
|
||||
// 测试Azure OpenAI请求体处理(仅域名配置)
|
||||
t.Run("azure domain only request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(azureDomainOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Test message"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求路径是否使用模型占位符
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.Contains(t, pathValue, "/openai/deployments/gpt-3.5-turbo/chat/completions", "Path should use model from request body")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAzureOnHttpResponseHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试Azure OpenAI响应头处理
|
||||
t.Run("azure response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAzureConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello"
|
||||
}
|
||||
]
|
||||
}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头是否被正确处理
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, responseHeaders)
|
||||
|
||||
// 验证状态码
|
||||
statusValue, hasStatus := test.GetHeaderValue(responseHeaders, ":status")
|
||||
require.True(t, hasStatus, "Status header should exist")
|
||||
require.Equal(t, "200", statusValue, "Status should be 200")
|
||||
|
||||
// 验证Content-Type
|
||||
contentTypeValue, hasContentType := test.GetHeaderValue(responseHeaders, "Content-Type")
|
||||
require.True(t, hasContentType, "Content-Type header should exist")
|
||||
require.Equal(t, "application/json", contentTypeValue, "Content-Type should be application/json")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunAzureOnHttpResponseBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试Azure OpenAI响应体处理
|
||||
t.Run("azure response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicAzureConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello"
|
||||
}
|
||||
]
|
||||
}`
|
||||
action = host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理响应体
|
||||
responseBody := `{
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": "Hello! How can I help you?"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
action = host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体是否被正确处理
|
||||
transformedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, transformedResponseBody)
|
||||
|
||||
// 验证响应体内容
|
||||
var responseMap map[string]interface{}
|
||||
err := json.Unmarshal(transformedResponseBody, &responseMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
choices, exists := responseMap["choices"]
|
||||
require.True(t, exists, "Choices should exist in response body")
|
||||
require.NotNil(t, choices, "Choices should not be nil")
|
||||
})
|
||||
})
|
||||
}
|
||||
1335
plugins/wasm-go/extensions/ai-proxy/test/gemini.go
Normal file
1335
plugins/wasm-go/extensions/ai-proxy/test/gemini.go
Normal file
File diff suppressed because it is too large
Load Diff
866
plugins/wasm-go/extensions/ai-proxy/test/openai.go
Normal file
866
plugins/wasm-go/extensions/ai-proxy/test/openai.go
Normal file
@@ -0,0 +1,866 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本OpenAI配置
|
||||
var basicOpenAIConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "openai",
|
||||
"apiTokens": []string{"sk-openai-test123456789"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:OpenAI多模型配置
|
||||
var openAIMultiModelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "openai",
|
||||
"apiTokens": []string{"sk-openai-multi-model"},
|
||||
"modelMapping": map[string]string{
|
||||
"gpt-3.5-turbo": "gpt-3.5-turbo",
|
||||
"gpt-4": "gpt-4",
|
||||
"text-embedding-ada-002": "text-embedding-ada-002",
|
||||
"dall-e-3": "dall-e-3",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:OpenAI自定义域名配置(直接路径)
|
||||
var openAICustomDomainDirectPathConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "openai",
|
||||
"apiTokens": []string{"sk-openai-custom-domain"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
"openaiCustomUrl": "https://custom.openai.com/v1",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:OpenAI自定义域名配置(间接路径)
|
||||
var openAICustomDomainIndirectPathConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "openai",
|
||||
"apiTokens": []string{"sk-openai-custom-domain"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
"openaiCustomUrl": "https://custom.openai.com/api",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:OpenAI完整配置(包含responseJsonSchema等字段)
|
||||
var completeOpenAIConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "openai",
|
||||
"apiTokens": []string{"sk-openai-complete"},
|
||||
"modelMapping": map[string]string{
|
||||
"*": "gpt-3.5-turbo",
|
||||
},
|
||||
"responseJsonSchema": map[string]interface{}{
|
||||
"type": "json_object",
|
||||
},
|
||||
"failover": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"retryOnFailure": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunOpenAIParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本OpenAI配置解析
|
||||
t.Run("basic openai config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试OpenAI多模型配置解析
|
||||
t.Run("openai multi model config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(openAIMultiModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试OpenAI自定义域名配置(直接路径)
|
||||
t.Run("openai custom domain direct path config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(openAICustomDomainDirectPathConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试OpenAI自定义域名配置(间接路径)
|
||||
t.Run("openai custom domain indirect path config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(openAICustomDomainIndirectPathConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试OpenAI完整配置解析
|
||||
t.Run("openai complete config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(completeOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunOpenAIOnHttpRequestHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试OpenAI请求头处理(聊天完成接口)
|
||||
t.Run("openai chat completion request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration,因为需要处理请求体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证请求头是否被正确处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host是否被改为OpenAI默认域名
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost, "Host header should exist")
|
||||
require.Equal(t, "api.openai.com", hostValue, "Host should be changed to OpenAI default domain")
|
||||
|
||||
// 验证Authorization是否被设置
|
||||
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
|
||||
require.True(t, hasAuth, "Authorization header should exist")
|
||||
require.Contains(t, authValue, "sk-openai-test123456789", "Authorization should contain OpenAI API token")
|
||||
|
||||
// 验证Path是否被正确处理
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath, "Path header should exist")
|
||||
require.Contains(t, pathValue, "/v1/chat/completions", "Path should contain chat completions endpoint")
|
||||
|
||||
// 验证Content-Length是否被删除
|
||||
_, hasContentLength := test.GetHeaderValue(requestHeaders, "Content-Length")
|
||||
require.False(t, hasContentLength, "Content-Length header should be deleted")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasOpenAILogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "openai") {
|
||||
hasOpenAILogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasOpenAILogs, "Should have OpenAI processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI请求头处理(嵌入接口)
|
||||
t.Run("openai embeddings request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证嵌入接口的请求头处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host转换
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost)
|
||||
require.Equal(t, "api.openai.com", hostValue)
|
||||
|
||||
// 验证Path转换
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath)
|
||||
require.Contains(t, pathValue, "/v1/embeddings", "Path should contain embeddings endpoint")
|
||||
|
||||
// 验证Authorization设置
|
||||
authValue, hasAuth := test.GetHeaderValue(requestHeaders, "Authorization")
|
||||
require.True(t, hasAuth, "Authorization header should exist for embeddings")
|
||||
require.Contains(t, authValue, "sk-openai-test123456789", "Authorization should contain OpenAI API token")
|
||||
})
|
||||
|
||||
// 测试OpenAI请求头处理(图像生成接口)
|
||||
t.Run("openai image generation request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/images/generations"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证图像生成接口的请求头处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host转换
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost)
|
||||
require.Equal(t, "api.openai.com", hostValue)
|
||||
|
||||
// 验证Path转换
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath)
|
||||
require.Contains(t, pathValue, "/v1/images/generations", "Path should contain image generations endpoint")
|
||||
})
|
||||
|
||||
// 测试OpenAI自定义域名请求头处理
|
||||
t.Run("openai custom domain request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(openAICustomDomainDirectPathConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 验证自定义域名的请求头处理
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.NotNil(t, requestHeaders)
|
||||
|
||||
// 验证Host是否被改为自定义域名
|
||||
hostValue, hasHost := test.GetHeaderValue(requestHeaders, ":authority")
|
||||
require.True(t, hasHost)
|
||||
require.Equal(t, "custom.openai.com", hostValue, "Host should be changed to custom domain")
|
||||
|
||||
// 验证Path是否被正确处理
|
||||
pathValue, hasPath := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, hasPath)
|
||||
// 对于直接路径,应该保持原有路径
|
||||
require.Contains(t, pathValue, "/v1/chat/completions", "Path should be preserved for direct custom path")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunOpenAIOnHttpRequestBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试OpenAI请求体处理(聊天完成接口)
|
||||
t.Run("openai chat completion request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体是否被正确处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证模型名称是否被正确映射
|
||||
require.Contains(t, string(processedBody), "gpt-3.5-turbo", "Original model name should be preserved or mapped")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
infoLogs := host.GetInfoLogs()
|
||||
|
||||
// 验证是否有OpenAI相关的处理日志
|
||||
hasOpenAILogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "openai") {
|
||||
hasOpenAILogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, log := range infoLogs {
|
||||
if strings.Contains(log, "openai") {
|
||||
hasOpenAILogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasOpenAILogs, "Should have OpenAI processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI请求体处理(嵌入接口)
|
||||
t.Run("openai embeddings request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证嵌入接口的请求体处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证模型名称映射
|
||||
// 由于使用了通配符映射 "*": "gpt-3.5-turbo",text-embedding-ada-002 会被映射为 gpt-3.5-turbo
|
||||
require.Contains(t, string(processedBody), "gpt-3.5-turbo", "Model name should be mapped via wildcard")
|
||||
|
||||
// 检查处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasEmbeddingLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "embeddings") || strings.Contains(log, "openai") {
|
||||
hasEmbeddingLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasEmbeddingLogs, "Should have embedding processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI请求体处理(图像生成接口)
|
||||
t.Run("openai image generation request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/images/generations"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"dall-e-3","prompt":"test image"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证图像生成接口的请求体处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证模型名称映射
|
||||
// 由于使用了通配符映射 "*": "gpt-3.5-turbo",dall-e-3 会被映射为 gpt-3.5-turbo
|
||||
require.Contains(t, string(processedBody), "gpt-3.5-turbo", "Model name should be mapped via wildcard")
|
||||
|
||||
// 检查处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasImageLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "image") || strings.Contains(log, "openai") {
|
||||
hasImageLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasImageLogs, "Should have image generation processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI请求体处理(带responseJsonSchema配置)
|
||||
t.Run("openai request body with responseJsonSchema", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(completeOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体是否被正确处理
|
||||
processedBody := host.GetRequestBody()
|
||||
require.NotNil(t, processedBody)
|
||||
|
||||
// 验证responseJsonSchema是否被应用
|
||||
// 注意:由于test框架的限制,我们可能需要检查日志或其他方式来验证处理结果
|
||||
require.Contains(t, string(processedBody), "gpt-3.5-turbo", "Model name should be preserved")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasSchemaLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "response format") || strings.Contains(log, "openai") {
|
||||
hasSchemaLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasSchemaLogs, "Should have response format processing logs")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunOpenAIOnHttpResponseHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试OpenAI响应头处理(聊天完成接口)
|
||||
t.Run("openai chat completion response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Request-Id", "req-123"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头是否被正确处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证状态码
|
||||
statusValue, hasStatus := test.GetHeaderValue(processedResponseHeaders, ":status")
|
||||
require.True(t, hasStatus, "Status header should exist")
|
||||
require.Equal(t, "200", statusValue, "Status should be 200")
|
||||
|
||||
// 验证Content-Type
|
||||
contentTypeValue, hasContentType := test.GetHeaderValue(processedResponseHeaders, "Content-Type")
|
||||
require.True(t, hasContentType, "Content-Type header should exist")
|
||||
require.Equal(t, "application/json", contentTypeValue, "Content-Type should be application/json")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasResponseLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "response") || strings.Contains(log, "openai") {
|
||||
hasResponseLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasResponseLogs, "Should have response processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI响应头处理(嵌入接口)
|
||||
t.Run("openai embeddings response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"X-Embedding-Model", "text-embedding-ada-002"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证嵌入模型信息
|
||||
modelValue, hasModel := test.GetHeaderValue(processedResponseHeaders, "X-Embedding-Model")
|
||||
require.True(t, hasModel, "Embedding model header should exist")
|
||||
require.Equal(t, "text-embedding-ada-002", modelValue, "Embedding model should match configuration")
|
||||
})
|
||||
|
||||
// 测试OpenAI响应头处理(错误响应)
|
||||
t.Run("openai error response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置错误响应头
|
||||
errorResponseHeaders := [][2]string{
|
||||
{":status", "429"},
|
||||
{"Content-Type", "application/json"},
|
||||
{"Retry-After", "60"},
|
||||
}
|
||||
action := host.CallOnHttpResponseHeaders(errorResponseHeaders)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证错误响应头处理
|
||||
processedResponseHeaders := host.GetResponseHeaders()
|
||||
require.NotNil(t, processedResponseHeaders)
|
||||
|
||||
// 验证错误状态码
|
||||
statusValue, hasStatus := test.GetHeaderValue(processedResponseHeaders, ":status")
|
||||
require.True(t, hasStatus, "Status header should exist")
|
||||
require.Equal(t, "429", statusValue, "Status should be 429 (Too Many Requests)")
|
||||
|
||||
// 验证重试信息
|
||||
retryValue, hasRetry := test.GetHeaderValue(processedResponseHeaders, "Retry-After")
|
||||
require.True(t, hasRetry, "Retry-After header should exist")
|
||||
require.Equal(t, "60", retryValue, "Retry-After should be 60 seconds")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunOpenAIOnHttpResponseBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试OpenAI响应体处理(聊天完成接口)
|
||||
t.Run("openai chat completion response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{
|
||||
"id": "chatcmpl-123",
|
||||
"object": "chat.completion",
|
||||
"created": 1677652288,
|
||||
"model": "gpt-3.5-turbo",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello! How can I help you today?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 9,
|
||||
"completion_tokens": 12,
|
||||
"total_tokens": 21
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体是否被正确处理
|
||||
processedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, processedResponseBody)
|
||||
|
||||
// 验证响应体内容
|
||||
responseStr := string(processedResponseBody)
|
||||
require.Contains(t, responseStr, "chat.completion", "Response should contain chat completion object")
|
||||
require.Contains(t, responseStr, "assistant", "Response should contain assistant role")
|
||||
require.Contains(t, responseStr, "usage", "Response should contain usage information")
|
||||
|
||||
// 检查是否有相关的处理日志
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasResponseBodyLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "response") || strings.Contains(log, "body") || strings.Contains(log, "openai") {
|
||||
hasResponseBodyLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasResponseBodyLogs, "Should have response body processing logs")
|
||||
})
|
||||
|
||||
// 测试OpenAI响应体处理(嵌入接口)
|
||||
t.Run("openai embeddings response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/embeddings"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"text-embedding-ada-002","input":"test text"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{
|
||||
"object": "list",
|
||||
"data": [{
|
||||
"object": "embedding",
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
"index": 0
|
||||
}],
|
||||
"model": "text-embedding-ada-002",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"total_tokens": 5
|
||||
}
|
||||
}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体处理
|
||||
processedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, processedResponseBody)
|
||||
|
||||
// 验证嵌入响应内容
|
||||
responseStr := string(processedResponseBody)
|
||||
require.Contains(t, responseStr, "embedding", "Response should contain embedding object")
|
||||
require.Contains(t, responseStr, "0.1", "Response should contain embedding vector")
|
||||
require.Contains(t, responseStr, "text-embedding-ada-002", "Response should contain model name")
|
||||
})
|
||||
|
||||
// 测试OpenAI响应体处理(图像生成接口)
|
||||
t.Run("openai image generation response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/images/generations"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := `{"model":"dall-e-3","prompt":"test image"}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 设置响应体
|
||||
responseBody := `{
|
||||
"created": 1677652288,
|
||||
"data": [{
|
||||
"url": "https://example.com/image1.png",
|
||||
"revised_prompt": "test image"
|
||||
}]
|
||||
}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体处理
|
||||
processedResponseBody := host.GetResponseBody()
|
||||
require.NotNil(t, processedResponseBody)
|
||||
|
||||
// 验证图像生成响应内容
|
||||
responseStr := string(processedResponseBody)
|
||||
require.Contains(t, responseStr, "data", "Response should contain data array")
|
||||
require.Contains(t, responseStr, "url", "Response should contain image URL")
|
||||
require.Contains(t, responseStr, "revised_prompt", "Response should contain revised prompt")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunOpenAIOnStreamingResponseBodyTests(t *testing.T) {
|
||||
// 测试OpenAI响应体处理(流式响应)
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("openai streaming response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicOpenAIConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置流式请求体
|
||||
requestBody := `{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"test"}],"stream":true}`
|
||||
host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 设置流式响应头
|
||||
responseHeaders := [][2]string{
|
||||
{":status", "200"},
|
||||
{"Content-Type", "text/event-stream"},
|
||||
}
|
||||
host.CallOnHttpResponseHeaders(responseHeaders)
|
||||
|
||||
// 模拟流式响应体
|
||||
chunk1 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk2 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk3 := `data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"!"},"index":0}]}
|
||||
|
||||
`
|
||||
chunk4 := `data: [DONE]
|
||||
|
||||
`
|
||||
|
||||
// 处理流式响应体
|
||||
action1 := host.CallOnHttpStreamingResponseBody([]byte(chunk1), false)
|
||||
require.Equal(t, types.ActionContinue, action1)
|
||||
|
||||
action2 := host.CallOnHttpStreamingResponseBody([]byte(chunk2), false)
|
||||
require.Equal(t, types.ActionContinue, action2)
|
||||
|
||||
action3 := host.CallOnHttpStreamingResponseBody([]byte(chunk3), false)
|
||||
require.Equal(t, types.ActionContinue, action3)
|
||||
|
||||
action4 := host.CallOnHttpStreamingResponseBody([]byte(chunk4), true)
|
||||
require.Equal(t, types.ActionContinue, action4)
|
||||
|
||||
// 验证流式响应处理
|
||||
// 注意:流式响应可能不会在GetResponseBody中累积,需要检查日志或其他方式验证
|
||||
debugLogs := host.GetDebugLogs()
|
||||
hasStreamingLogs := false
|
||||
for _, log := range debugLogs {
|
||||
if strings.Contains(log, "streaming") || strings.Contains(log, "chunk") || strings.Contains(log, "openai") {
|
||||
hasStreamingLogs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, hasStreamingLogs, "Should have streaming response processing logs")
|
||||
})
|
||||
})
|
||||
}
|
||||
1213
plugins/wasm-go/extensions/ai-proxy/test/qwen.go
Normal file
1213
plugins/wasm-go/extensions/ai-proxy/test/qwen.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.1
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY=
|
||||
github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
328
plugins/wasm-go/extensions/ai-quota/main_test.go
Normal file
328
plugins/wasm-go/extensions/ai-quota/main_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"admin_consumer": "admin",
|
||||
"redis_key_prefix": "chat_quota:",
|
||||
"admin_path": "/quota",
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
"timeout": 1000,
|
||||
"database": 0,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少admin_consumer
|
||||
var missingAdminConsumerConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
quotaConfig := config.(*QuotaConfig)
|
||||
require.Equal(t, "admin", quotaConfig.AdminConsumer)
|
||||
require.Equal(t, "chat_quota:", quotaConfig.RedisKeyPrefix)
|
||||
require.Equal(t, "/quota", quotaConfig.AdminPath)
|
||||
})
|
||||
|
||||
// 测试缺少admin_consumer的配置
|
||||
t.Run("missing admin_consumer", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingAdminConsumerConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试聊天完成模式的请求头处理
|
||||
t.Run("chat completion mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含consumer信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 由于需要调用Redis检查配额,应该返回HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟Redis调用响应(有足够配额)
|
||||
resp := test.CreateRedisResp(1000)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试管理员查询模式的请求头处理
|
||||
t.Run("admin query mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含admin consumer信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions/quota?consumer=consumer1"},
|
||||
{":method", "GET"},
|
||||
{"x-mse-consumer", "admin"},
|
||||
})
|
||||
|
||||
// 管理员查询模式应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis调用响应
|
||||
resp := test.CreateRedisResp(500)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(http.StatusOK), response.StatusCode)
|
||||
require.Equal(t, "{\"consumer\":\"consumer1\",\"quota\":500}", string(response.Data))
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无consumer的情况
|
||||
t.Run("no consumer", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含consumer信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 无consumer应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试管理员刷新模式的请求体处理
|
||||
t.Run("admin refresh mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions/quota/refresh"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "admin"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := "consumer=consumer1"a=1000"
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 管理员刷新模式应该返回ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟Redis调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{"OK"})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(http.StatusOK), response.StatusCode)
|
||||
require.Equal(t, "refresh quota successful", string(response.Data))
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试聊天完成模式的请求体处理
|
||||
t.Run("chat completion mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 聊天完成模式应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpStreamingResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试聊天完成模式的流式响应体处理
|
||||
t.Run("chat completion mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 测试流式响应体处理
|
||||
data := []byte(`{"choices": [{"delta": {"content": "Hello"}}]}`)
|
||||
action := host.CallOnHttpStreamingResponseBody(data, false)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
result := host.GetResponseBody()
|
||||
// 非结束流应该返回原始数据
|
||||
require.Equal(t, data, result)
|
||||
|
||||
// 测试结束流
|
||||
action = host.CallOnHttpStreamingResponseBody(data, true)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
result = host.GetResponseBody()
|
||||
// 结束流应该返回原始数据
|
||||
require.Equal(t, data, result)
|
||||
|
||||
// 模拟Redis调用响应(减少配额)
|
||||
resp := test.CreateRedisRespArray([]interface{}{30})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非聊天完成模式的流式响应体处理
|
||||
t.Run("non-chat completion mode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/other/path"},
|
||||
{":method", "GET"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 测试流式响应体处理
|
||||
data := []byte("response data")
|
||||
action := host.CallOnHttpStreamingResponseBody(data, false)
|
||||
|
||||
// 非聊天完成模式应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
result := host.GetResponseBody()
|
||||
require.Equal(t, data, result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOperationMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
adminPath string
|
||||
chatMode ChatMode
|
||||
adminMode AdminMode
|
||||
}{
|
||||
{
|
||||
name: "chat completion mode",
|
||||
path: "/v1/chat/completions",
|
||||
adminPath: "/quota",
|
||||
chatMode: ChatModeCompletion,
|
||||
adminMode: AdminModeNone,
|
||||
},
|
||||
{
|
||||
name: "admin query mode",
|
||||
path: "/v1/chat/completions/quota",
|
||||
adminPath: "/quota",
|
||||
chatMode: ChatModeAdmin,
|
||||
adminMode: AdminModeQuery,
|
||||
},
|
||||
{
|
||||
name: "admin refresh mode",
|
||||
path: "/v1/chat/completions/quota/refresh",
|
||||
adminPath: "/quota",
|
||||
chatMode: ChatModeAdmin,
|
||||
adminMode: AdminModeRefresh,
|
||||
},
|
||||
{
|
||||
name: "admin delta mode",
|
||||
path: "/v1/chat/completions/quota/delta",
|
||||
adminPath: "/quota",
|
||||
chatMode: ChatModeAdmin,
|
||||
adminMode: AdminModeDelta,
|
||||
},
|
||||
{
|
||||
name: "none mode",
|
||||
path: "/other/path",
|
||||
adminPath: "/quota",
|
||||
chatMode: ChatModeNone,
|
||||
adminMode: AdminModeNone,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
chatMode, adminMode := getOperationMode(tt.path, tt.adminPath)
|
||||
require.Equal(t, tt.chatMode, chatMode)
|
||||
require.Equal(t, tt.adminMode, adminMode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ func SendResponse(statusCode uint32, statusCodeDetails string, contentType, body
|
||||
}
|
||||
|
||||
func CreateHeaders(kvs ...string) [][2]string {
|
||||
if len(kvs)%2 != 0 {
|
||||
kvs = kvs[:len(kvs)-1]
|
||||
}
|
||||
headers := make([][2]string, 0, len(kvs)/2)
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
headers = append(headers, [2]string{kvs[i], kvs[i+1]})
|
||||
|
||||
84
plugins/wasm-go/extensions/ai-quota/util/http_test.go
Normal file
84
plugins/wasm-go/extensions/ai-quota/util/http_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCreateHeaders 测试CreateHeaders函数
|
||||
func TestCreateHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
kvs []string
|
||||
expected [][2]string
|
||||
}{
|
||||
{
|
||||
name: "single header",
|
||||
kvs: []string{"Content-Type", "text/plain"},
|
||||
expected: [][2]string{
|
||||
{"Content-Type", "text/plain"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple headers",
|
||||
kvs: []string{"Content-Type", "application/json", "Authorization", "Bearer token"},
|
||||
expected: [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{"Authorization", "Bearer token"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
kvs: []string{},
|
||||
expected: [][2]string{},
|
||||
},
|
||||
{
|
||||
name: "odd number of elements",
|
||||
kvs: []string{"Content-Type", "text/plain", "Authorization"},
|
||||
expected: [][2]string{
|
||||
{"Content-Type", "text/plain"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CreateHeaders(tt.kvs...)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConstants 测试常量定义
|
||||
func TestConstants(t *testing.T) {
|
||||
require.Equal(t, "Content-Type", HeaderContentType)
|
||||
require.Equal(t, "text/plain", MimeTypeTextPlain)
|
||||
require.Equal(t, "application/json", MimeTypeApplicationJson)
|
||||
}
|
||||
|
||||
// TestSendResponse 测试SendResponse函数
|
||||
// 注意:这个函数调用了proxywasm SDK,在单元测试中我们主要验证函数签名和基本逻辑
|
||||
func TestSendResponse(t *testing.T) {
|
||||
// 由于SendResponse函数调用了proxywasm SDK,在单元测试环境中可能无法完全执行
|
||||
// 但我们仍然可以测试函数的存在性和基本结构
|
||||
t.Run("function exists", func(t *testing.T) {
|
||||
// 验证函数存在且可以调用(即使可能失败)
|
||||
// 在实际的proxy-wasm环境中,这个函数应该能正常工作
|
||||
require.NotNil(t, SendResponse)
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
393
plugins/wasm-go/extensions/ai-rag/main_test.go
Normal file
393
plugins/wasm-go/extensions/ai-rag/main_test.go
Normal file
@@ -0,0 +1,393 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"ai-rag/dashscope"
|
||||
"ai-rag/dashvector"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础RAG配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"dashscope": map[string]interface{}{
|
||||
"apiKey": "test-dashscope-key",
|
||||
"serviceFQDN": "dashscope-service",
|
||||
"servicePort": 8080,
|
||||
"serviceHost": "dashscope.example.com",
|
||||
},
|
||||
"dashvector": map[string]interface{}{
|
||||
"apiKey": "test-dashvector-key",
|
||||
"collection": "test-collection",
|
||||
"serviceFQDN": "dashvector-service",
|
||||
"servicePort": 8081,
|
||||
"serviceHost": "dashvector.example.com",
|
||||
"topk": 5,
|
||||
"threshold": 0.8,
|
||||
"field": "content",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少必需字段
|
||||
var missingRequiredConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"dashscope": map[string]interface{}{
|
||||
"apiKey": "test-dashscope-key",
|
||||
},
|
||||
"dashvector": map[string]interface{}{
|
||||
"apiKey": "test-dashvector-key",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
ragConfig := config.(*AIRagConfig)
|
||||
require.Equal(t, "test-dashscope-key", ragConfig.DashScopeAPIKey)
|
||||
require.Equal(t, "test-dashvector-key", ragConfig.DashVectorAPIKey)
|
||||
require.Equal(t, "test-collection", ragConfig.DashVectorCollection)
|
||||
require.Equal(t, int32(5), ragConfig.DashVectorTopK)
|
||||
require.Equal(t, 0.8, ragConfig.DashVectorThreshold)
|
||||
require.Equal(t, "content", ragConfig.DashVectorField)
|
||||
})
|
||||
|
||||
// 测试缺少必需字段的配置
|
||||
t.Run("missing required config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingRequiredConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求头处理
|
||||
t.Run("request headers processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"content-length", "100"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试空消息的请求体
|
||||
t.Run("empty messages", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置空消息的请求体
|
||||
body := `{"model": "gpt-3.5-turbo", "messages": []}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 空消息应该直接通过
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试正常RAG流程
|
||||
t.Run("normal rag flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置包含消息的请求体
|
||||
body := `{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "What is AI?"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionPause,等待RAG流程完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟DashScope嵌入服务响应
|
||||
embeddingResponse := `{
|
||||
"output": {
|
||||
"embeddings": [{
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
"text_index": 0
|
||||
}]
|
||||
},
|
||||
"usage": {"total_tokens": 10},
|
||||
"request_id": "req-123"
|
||||
}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(embeddingResponse))
|
||||
|
||||
// 模拟DashVector向量搜索响应
|
||||
vectorResponse := `{
|
||||
"code": 200,
|
||||
"request_id": "req-456",
|
||||
"message": "success",
|
||||
"output": [{
|
||||
"id": "doc1",
|
||||
"fields": {"raw": "AI is artificial intelligence"},
|
||||
"score": 0.75
|
||||
}]
|
||||
}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(vectorResponse))
|
||||
|
||||
// 获取修改后的请求体
|
||||
requestBody := host.GetRequestBody()
|
||||
require.NotEmpty(t, requestBody)
|
||||
|
||||
// 解析修改后的请求体,验证RAG增强
|
||||
var modifiedRequest Request
|
||||
err := json.Unmarshal(requestBody, &modifiedRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "gpt-3.5-turbo", modifiedRequest.Model)
|
||||
|
||||
// 验证消息数量:检索文档(1) + 问题提示(1) = 2
|
||||
// 注意:原始消息被清空了,因为 messageLength-1 = 0
|
||||
require.Len(t, modifiedRequest.Messages, 2)
|
||||
|
||||
// 验证第一个消息(检索到的文档)
|
||||
require.Equal(t, "user", modifiedRequest.Messages[0].Role)
|
||||
require.Equal(t, "AI is artificial intelligence", modifiedRequest.Messages[0].Content)
|
||||
|
||||
// 验证第二个消息(问题提示)
|
||||
require.Equal(t, "user", modifiedRequest.Messages[1].Role)
|
||||
require.Equal(t, "现在,请回答以下问题:\nWhat is AI?", modifiedRequest.Messages[1].Content)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试RAG召回标记
|
||||
t.Run("rag recall header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "What is AI?"}]}`
|
||||
host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 模拟DashScope嵌入服务响应
|
||||
embeddingResponse := `{
|
||||
"output": {
|
||||
"embeddings": [{
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
"text_index": 0
|
||||
}]
|
||||
},
|
||||
"usage": {"total_tokens": 10},
|
||||
"request_id": "req-123"
|
||||
}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(embeddingResponse))
|
||||
|
||||
// 模拟DashVector向量搜索响应
|
||||
vectorResponse := `{
|
||||
"code": 200,
|
||||
"request_id": "req-456",
|
||||
"message": "success",
|
||||
"output": [{
|
||||
"id": "doc1",
|
||||
"fields": {"raw": "AI is artificial intelligence"},
|
||||
"score": 0.75
|
||||
}]
|
||||
}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(vectorResponse))
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应头包含RAG召回标记
|
||||
require.True(t, test.HasHeaderWithValue(host.GetResponseHeaders(), "x-envoy-rag-recall", "true"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructs(t *testing.T) {
|
||||
// 测试Request结构体
|
||||
t.Run("Request struct", func(t *testing.T) {
|
||||
request := Request{
|
||||
Model: "gpt-3.5-turbo",
|
||||
Messages: []Message{{Role: "user", Content: "Hello"}},
|
||||
FrequencyPenalty: 0.0,
|
||||
PresencePenalty: 0.0,
|
||||
Stream: false,
|
||||
Temperature: 0.7,
|
||||
Topp: 1,
|
||||
}
|
||||
require.Equal(t, "gpt-3.5-turbo", request.Model)
|
||||
require.Len(t, request.Messages, 1)
|
||||
require.Equal(t, "user", request.Messages[0].Role)
|
||||
require.Equal(t, "Hello", request.Messages[0].Content)
|
||||
require.Equal(t, 0.7, request.Temperature)
|
||||
})
|
||||
|
||||
// 测试Message结构体
|
||||
t.Run("Message struct", func(t *testing.T) {
|
||||
message := Message{
|
||||
Role: "assistant",
|
||||
Content: "Hello! How can I help you?",
|
||||
}
|
||||
require.Equal(t, "assistant", message.Role)
|
||||
require.Equal(t, "Hello! How can I help you?", message.Content)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashScopeTypes(t *testing.T) {
|
||||
// 测试DashScope Request结构体
|
||||
t.Run("DashScope Request", func(t *testing.T) {
|
||||
request := dashscope.Request{
|
||||
Model: "text-embedding-v2",
|
||||
Input: dashscope.Input{
|
||||
Texts: []string{"Hello, world"},
|
||||
},
|
||||
Parameter: dashscope.Parameter{
|
||||
TextType: "query",
|
||||
},
|
||||
}
|
||||
require.Equal(t, "text-embedding-v2", request.Model)
|
||||
require.Len(t, request.Input.Texts, 1)
|
||||
require.Equal(t, "Hello, world", request.Input.Texts[0])
|
||||
require.Equal(t, "query", request.Parameter.TextType)
|
||||
})
|
||||
|
||||
// 测试DashScope Response结构体
|
||||
t.Run("DashScope Response", func(t *testing.T) {
|
||||
response := dashscope.Response{
|
||||
Output: dashscope.Output{
|
||||
Embeddings: []dashscope.Embedding{
|
||||
{
|
||||
Embedding: []float32{0.1, 0.2, 0.3},
|
||||
TextIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
Usage: dashscope.Usage{
|
||||
TotalTokens: 10,
|
||||
},
|
||||
RequestID: "req-123",
|
||||
}
|
||||
require.Equal(t, "req-123", response.RequestID)
|
||||
require.Equal(t, int32(10), response.Usage.TotalTokens)
|
||||
require.Len(t, response.Output.Embeddings, 1)
|
||||
require.Len(t, response.Output.Embeddings[0].Embedding, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashVectorTypes(t *testing.T) {
|
||||
// 测试DashVector Request结构体
|
||||
t.Run("DashVector Request", func(t *testing.T) {
|
||||
request := dashvector.Request{
|
||||
TopK: 5,
|
||||
OutputFileds: []string{"content", "title"},
|
||||
Vector: []float32{0.1, 0.2, 0.3, 0.4, 0.5},
|
||||
}
|
||||
require.Equal(t, int32(5), request.TopK)
|
||||
require.Len(t, request.OutputFileds, 2)
|
||||
require.Len(t, request.Vector, 5)
|
||||
})
|
||||
|
||||
// 测试DashVector Response结构体
|
||||
t.Run("DashVector Response", func(t *testing.T) {
|
||||
response := dashvector.Response{
|
||||
Code: 200,
|
||||
RequestID: "req-456",
|
||||
Message: "success",
|
||||
Output: []dashvector.OutputObject{
|
||||
{
|
||||
ID: "doc1",
|
||||
Fields: dashvector.FieldObject{
|
||||
Raw: "AI is artificial intelligence",
|
||||
},
|
||||
Score: 0.75,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Equal(t, int32(200), response.Code)
|
||||
require.Equal(t, "req-456", response.RequestID)
|
||||
require.Equal(t, "success", response.Message)
|
||||
require.Len(t, response.Output, 1)
|
||||
require.Equal(t, "doc1", response.Output[0].ID)
|
||||
require.Equal(t, "AI is artificial intelligence", response.Output[0].Fields.Raw)
|
||||
require.Equal(t, float32(0.75), response.Output[0].Score)
|
||||
})
|
||||
}
|
||||
@@ -6,8 +6,8 @@ toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250714125049-cb970b4561fa
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
)
|
||||
|
||||
@@ -9,10 +9,10 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250714125049-cb970b4561fa h1:GnYXjsG9/nRJ4+GQeJBKS8/a28N8yAt1pUmGZcxnHd4=
|
||||
github.com/higress-group/wasm-go v1.0.1-0.20250714125049-cb970b4561fa/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
|
||||
@@ -5,15 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950 h1:X4a+wzGEuLkCcAX2XiDf/vcVOIdZWxtEo0YkT+F/mcM=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
416
plugins/wasm-go/extensions/ai-security-guard/main_test.go
Normal file
416
plugins/wasm-go/extensions/ai-security-guard/main_test.go
Normal file
@@ -0,0 +1,416 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基础安全配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "security-service",
|
||||
"servicePort": 8080,
|
||||
"serviceHost": "security.example.com",
|
||||
"accessKey": "test-ak",
|
||||
"secretKey": "test-sk",
|
||||
"checkRequest": true,
|
||||
"checkResponse": true,
|
||||
"riskLevelBar": "high",
|
||||
"timeout": 2000,
|
||||
"bufferLimit": 1000,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:仅检查请求
|
||||
var requestOnlyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "security-service",
|
||||
"servicePort": 8080,
|
||||
"serviceHost": "security.example.com",
|
||||
"accessKey": "test-ak",
|
||||
"secretKey": "test-sk",
|
||||
"checkRequest": true,
|
||||
"checkResponse": false,
|
||||
"riskLevelBar": "medium",
|
||||
"timeout": 1000,
|
||||
"bufferLimit": 500,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少必需字段
|
||||
var missingRequiredConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"accessKey": "test-ak",
|
||||
"secretKey": "test-sk",
|
||||
// 故意缺少必需字段:serviceName, servicePort, serviceHost
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少服务配置字段
|
||||
var missingServiceConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"accessKey": "test-ak",
|
||||
"secretKey": "test-sk",
|
||||
"checkRequest": true,
|
||||
"checkResponse": true,
|
||||
// 缺少 serviceName, servicePort, serviceHost
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少认证字段
|
||||
var missingAuthConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"serviceName": "security-service",
|
||||
"servicePort": 8080,
|
||||
"serviceHost": "security.example.com",
|
||||
"checkRequest": true,
|
||||
"checkResponse": true,
|
||||
// 缺少 accessKey, secretKey
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基础配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
securityConfig := config.(*AISecurityConfig)
|
||||
require.Equal(t, "test-ak", securityConfig.ak)
|
||||
require.Equal(t, "test-sk", securityConfig.sk)
|
||||
require.Equal(t, true, securityConfig.checkRequest)
|
||||
require.Equal(t, true, securityConfig.checkResponse)
|
||||
require.Equal(t, "high", securityConfig.riskLevelBar)
|
||||
require.Equal(t, uint32(2000), securityConfig.timeout)
|
||||
require.Equal(t, 1000, securityConfig.bufferLimit)
|
||||
})
|
||||
|
||||
// 测试仅检查请求的配置
|
||||
t.Run("request only config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
securityConfig := config.(*AISecurityConfig)
|
||||
require.Equal(t, true, securityConfig.checkRequest)
|
||||
require.Equal(t, false, securityConfig.checkResponse)
|
||||
require.Equal(t, "medium", securityConfig.riskLevelBar)
|
||||
})
|
||||
|
||||
// 测试缺少必需字段的配置
|
||||
t.Run("missing required config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingRequiredConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试缺少服务配置字段
|
||||
t.Run("missing service config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingServiceConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试缺少认证字段
|
||||
t.Run("missing auth config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试启用请求检查的情况
|
||||
t.Run("request checking enabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试禁用请求检查的情况
|
||||
t.Run("request checking disabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求体安全检查通过
|
||||
t.Run("request body security check pass", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
body := `{"messages": [{"role": "user", "content": "Hello, how are you?"}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 应该返回ActionPause,等待安全检查结果
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟安全检查服务响应(通过)
|
||||
securityResponse := `{"Code": 200, "Message": "Success", "RequestId": "req-123", "Data": {"RiskLevel": "low"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(securityResponse))
|
||||
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空请求内容
|
||||
t.Run("empty request content", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置空内容的请求体
|
||||
body := `{"messages": [{"role": "user", "content": ""}]}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
|
||||
// 空内容应该直接通过
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试启用响应检查的情况
|
||||
t.Run("response checking enabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回HeaderStopIteration
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
|
||||
// 测试禁用响应检查的情况
|
||||
t.Run("response checking disabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试非200状态码
|
||||
t.Run("non-200 status code", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置非200响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "500"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRiskLevelFunctions(t *testing.T) {
|
||||
// 测试风险等级转换函数
|
||||
t.Run("risk level conversion", func(t *testing.T) {
|
||||
require.Equal(t, 4, riskLevelToInt(MaxRisk))
|
||||
require.Equal(t, 3, riskLevelToInt(HighRisk))
|
||||
require.Equal(t, 2, riskLevelToInt(MediumRisk))
|
||||
require.Equal(t, 1, riskLevelToInt(LowRisk))
|
||||
require.Equal(t, 0, riskLevelToInt(NoRisk))
|
||||
require.Equal(t, -1, riskLevelToInt("invalid"))
|
||||
})
|
||||
|
||||
// 测试风险等级比较
|
||||
t.Run("risk level comparison", func(t *testing.T) {
|
||||
require.True(t, riskLevelToInt(HighRisk) >= riskLevelToInt(MediumRisk))
|
||||
require.True(t, riskLevelToInt(MediumRisk) >= riskLevelToInt(LowRisk))
|
||||
require.True(t, riskLevelToInt(LowRisk) >= riskLevelToInt(NoRisk))
|
||||
require.False(t, riskLevelToInt(LowRisk) >= riskLevelToInt(HighRisk))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUtilityFunctions(t *testing.T) {
|
||||
// 测试URL编码函数
|
||||
t.Run("url encoding", func(t *testing.T) {
|
||||
original := "test+string:with=special&chars@$"
|
||||
encoded := urlEncoding(original)
|
||||
require.NotEqual(t, original, encoded)
|
||||
require.Contains(t, encoded, "%2B") // + 应该被编码
|
||||
require.Contains(t, encoded, "%3A") // : 应该被编码
|
||||
require.Contains(t, encoded, "%3D") // = 应该被编码
|
||||
require.Contains(t, encoded, "%26") // & 应该被编码
|
||||
})
|
||||
|
||||
// 测试HMAC-SHA1签名函数
|
||||
t.Run("hmac sha1", func(t *testing.T) {
|
||||
message := "test message"
|
||||
secret := "test secret"
|
||||
signature := hmacSha1(message, secret)
|
||||
require.NotEmpty(t, signature)
|
||||
require.NotEqual(t, message, signature)
|
||||
})
|
||||
|
||||
// 测试签名生成函数
|
||||
t.Run("signature generation", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
params := map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
}
|
||||
secret := "test-secret"
|
||||
signature := getSign(params, secret)
|
||||
require.NotEmpty(t, signature)
|
||||
})
|
||||
|
||||
// 测试十六进制ID生成函数
|
||||
t.Run("hex id generation", func(t *testing.T) {
|
||||
id, err := generateHexID(16)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, id, 16)
|
||||
require.Regexp(t, "^[0-9a-f]+$", id)
|
||||
})
|
||||
|
||||
// 测试随机ID生成函数
|
||||
t.Run("random id generation", func(t *testing.T) {
|
||||
id := generateRandomID()
|
||||
require.NotEmpty(t, id)
|
||||
require.Contains(t, id, "chatcmpl-")
|
||||
require.Len(t, id, 38) // "chatcmpl-" + 29 random chars
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarshalFunctions(t *testing.T) {
|
||||
// 测试marshalStr函数
|
||||
t.Run("marshal string", func(t *testing.T) {
|
||||
testStr := "Hello, World!"
|
||||
marshalled := marshalStr(testStr)
|
||||
require.Equal(t, testStr, marshalled)
|
||||
})
|
||||
|
||||
// 测试extractMessageFromStreamingBody函数
|
||||
t.Run("extract streaming body", func(t *testing.T) {
|
||||
// 使用正确的分隔符,每个chunk之间用双换行符分隔
|
||||
streamingData := []byte(`{"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}
|
||||
|
||||
{"choices":[{"index":0,"delta":{"role":"assistant","content":" World"}}]}
|
||||
|
||||
{"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`)
|
||||
|
||||
extracted := extractMessageFromStreamingBody(streamingData, "choices.0.delta.content")
|
||||
require.Equal(t, "Hello World", extracted)
|
||||
})
|
||||
}
|
||||
@@ -5,15 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950 h1:X4a+wzGEuLkCcAX2XiDf/vcVOIdZWxtEo0YkT+F/mcM=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250729071413-2478fd585950/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
983
plugins/wasm-go/extensions/ai-statistics/main_test.go
Normal file
983
plugins/wasm-go/extensions/ai-statistics/main_test.go
Normal file
@@ -0,0 +1,983 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本统计配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
{
|
||||
"key": "request_id",
|
||||
"value_source": "request_header",
|
||||
"value": "x-request-id",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": false,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "api_version",
|
||||
"value_source": "fixed_value",
|
||||
"value": "v1",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "model",
|
||||
"value_source": "request_body",
|
||||
"value": "model",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "input_token",
|
||||
"value_source": "response_body",
|
||||
"value": "usage.prompt_tokens",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "output_token",
|
||||
"value_source": "response_body",
|
||||
"value": "usage.completion_tokens",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "total_token",
|
||||
"value_source": "response_body",
|
||||
"value": "usage.total_tokens",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
},
|
||||
"disable_openai_usage": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:流式响应体属性配置
|
||||
var streamingBodyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
{
|
||||
"key": "response_content",
|
||||
"value_source": "response_streaming_body",
|
||||
"value": "choices.0.message.content",
|
||||
"rule": "first",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": false,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "model_name",
|
||||
"value_source": "response_streaming_body",
|
||||
"value": "model",
|
||||
"rule": "replace",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
},
|
||||
"disable_openai_usage": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:请求体属性配置
|
||||
var requestBodyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
{
|
||||
"key": "user_message_count",
|
||||
"value_source": "request_body",
|
||||
"value": "messages.#(role==\"user\")",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": false,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "request_model",
|
||||
"value_source": "request_body",
|
||||
"value": "model",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
},
|
||||
"disable_openai_usage": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:响应体属性配置
|
||||
var responseBodyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
{
|
||||
"key": "response_status",
|
||||
"value_source": "response_body",
|
||||
"value": "status",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": false,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
{
|
||||
"key": "response_message",
|
||||
"value_source": "response_body",
|
||||
"value": "message",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": true,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
},
|
||||
"disable_openai_usage": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:禁用 OpenAI 使用统计
|
||||
var disableOpenaiUsageConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
{
|
||||
"key": "custom_attribute",
|
||||
"value_source": "fixed_value",
|
||||
"value": "custom_value",
|
||||
"apply_to_log": true,
|
||||
"apply_to_span": false,
|
||||
"as_separate_log_field": false,
|
||||
},
|
||||
},
|
||||
"disable_openai_usage": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:空属性配置
|
||||
var emptyAttributesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{},
|
||||
"disable_openai_usage": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本统计配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试流式响应体属性配置解析
|
||||
t.Run("streaming body config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试请求体属性配置解析
|
||||
t.Run("request body config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试响应体属性配置解析
|
||||
t.Run("response body config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试禁用 OpenAI 使用统计配置解析
|
||||
t.Run("disable openai usage config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(disableOpenaiUsageConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试空属性配置解析
|
||||
t.Run("empty attributes config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyAttributesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本请求头处理
|
||||
t.Run("basic request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-request-id", "req-123"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试包含 consumer 的请求头处理
|
||||
t.Run("request headers with consumer", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-request-id", "req-456"},
|
||||
{"x-mse-consumer", "consumer2"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不包含 consumer 的请求头处理
|
||||
t.Run("request headers without consumer", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-request-id", "req-789"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本请求体处理
|
||||
t.Run("basic request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there"},
|
||||
{"role": "user", "content": "How are you?"}
|
||||
]
|
||||
}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 Google Gemini 格式的请求体处理
|
||||
t.Run("gemini request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/v1/models/gemini-pro:generateContent"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "Hello"}]},
|
||||
{"parts": [{"text": "Hi there"}]}
|
||||
]
|
||||
}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不包含消息的请求体处理
|
||||
t.Run("request body without messages", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"temperature": 0.7
|
||||
}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本响应头处理
|
||||
t.Run("basic response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试流式响应头处理
|
||||
t.Run("streaming response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置流式响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpStreamingBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试流式响应体处理
|
||||
t.Run("streaming response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置流式响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 处理第一个流式块
|
||||
firstChunk := []byte(`data: {"choices":[{"message":{"content":"Hello"}}],"model":"gpt-3.5-turbo"}`)
|
||||
action := host.CallOnHttpStreamingResponseBody(firstChunk, false)
|
||||
|
||||
result := host.GetResponseBody()
|
||||
require.Equal(t, firstChunk, result)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理最后一个流式块
|
||||
lastChunk := []byte(`data: {"choices":[{"message":{"content":"How can I help you?"}}],"model":"gpt-3.5-turbo"}`)
|
||||
action = host.CallOnHttpStreamingResponseBody(lastChunk, true)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result = host.GetResponseBody()
|
||||
require.Equal(t, lastChunk, result)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不包含 token 统计的流式响应体处理
|
||||
t.Run("streaming body without token usage", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置流式响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 处理流式响应体
|
||||
chunk := []byte(`data: {"message": "Hello world"}`)
|
||||
action := host.CallOnHttpStreamingResponseBody(chunk, true)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result := host.GetResponseBody()
|
||||
require.Equal(t, chunk, result)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本响应体处理
|
||||
t.Run("basic response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := []byte(`{
|
||||
"status": "success",
|
||||
"message": "Hello, how can I help you?",
|
||||
"choices": [{"message": {"content": "Hello"}}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25},
|
||||
"model": "gpt-3.5-turbo"
|
||||
}`)
|
||||
action := host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不包含 token 统计的响应体处理
|
||||
t.Run("response body without token usage", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := []byte(`{
|
||||
"status": "success",
|
||||
"message": "Hello world"
|
||||
}`)
|
||||
action := host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试指标收集
|
||||
t.Run("test token usage metrics", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置路由和集群名称
|
||||
host.SetRouteName("api-v1")
|
||||
host.SetClusterName("cluster-1")
|
||||
|
||||
// 1. 处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "user1"},
|
||||
})
|
||||
|
||||
// 2. 处理请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
}`)
|
||||
host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 3. 处理响应体
|
||||
responseBody := []byte(`{
|
||||
"choices": [{"message": {"content": "Hello, how can I help you?"}}],
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 8, "total_tokens": 13},
|
||||
"model": "gpt-3.5-turbo"
|
||||
}`)
|
||||
host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 4. 完成请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 5. 验证指标值
|
||||
// 检查输入 token 指标
|
||||
inputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.input_token"
|
||||
inputTokenValue, err := host.GetCounterMetric(inputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(5), inputTokenValue)
|
||||
|
||||
// 检查输出 token 指标
|
||||
outputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.output_token"
|
||||
outputTokenValue, err := host.GetCounterMetric(outputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(8), outputTokenValue)
|
||||
|
||||
// 检查总 token 指标
|
||||
totalTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.total_token"
|
||||
totalTokenValue, err := host.GetCounterMetric(totalTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(13), totalTokenValue)
|
||||
|
||||
// 检查服务时长指标
|
||||
serviceDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.llm_service_duration"
|
||||
serviceDurationValue, err := host.GetCounterMetric(serviceDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, serviceDurationValue, uint64(0))
|
||||
|
||||
// 检查请求计数指标
|
||||
durationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.llm_duration_count"
|
||||
durationCountValue, err := host.GetCounterMetric(durationCountMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), durationCountValue)
|
||||
})
|
||||
|
||||
// 测试流式响应指标
|
||||
t.Run("test streaming metrics", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置路由和集群名称
|
||||
host.SetRouteName("api-v1")
|
||||
host.SetClusterName("cluster-1")
|
||||
|
||||
// 1. 处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "user2"},
|
||||
})
|
||||
|
||||
// 2. 处理请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 3. 处理流式响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 4. 处理流式响应体 - 添加 usage 信息
|
||||
firstChunk := []byte(`data: {"choices":[{"message":{"content":"Hello"}}],"model":"gpt-4","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`)
|
||||
action = host.CallOnHttpStreamingResponseBody(firstChunk, false)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result := host.GetResponseBody()
|
||||
require.Equal(t, firstChunk, result)
|
||||
|
||||
// 5. 处理最后一个流式块 - 添加 usage 信息
|
||||
lastChunk := []byte(`data: {"choices":[{"message":{"content":"How can I help you?"}}],"model":"gpt-4","usage":{"prompt_tokens":5,"completion_tokens":8,"total_tokens":13}}`)
|
||||
action = host.CallOnHttpStreamingResponseBody(lastChunk, true)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result = host.GetResponseBody()
|
||||
require.Equal(t, lastChunk, result)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 6. 完成请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 7. 验证流式响应指标
|
||||
// 检查首 token 延迟指标
|
||||
firstTokenDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.llm_first_token_duration"
|
||||
firstTokenDurationValue, err := host.GetCounterMetric(firstTokenDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, firstTokenDurationValue, uint64(0))
|
||||
|
||||
// 检查流式请求计数指标
|
||||
streamDurationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.llm_stream_duration_count"
|
||||
streamDurationCountValue, err := host.GetCounterMetric(streamDurationCountMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), streamDurationCountValue)
|
||||
|
||||
// 检查服务时长指标
|
||||
serviceDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.llm_service_duration"
|
||||
serviceDurationValue, err := host.GetCounterMetric(serviceDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, serviceDurationValue, uint64(0))
|
||||
|
||||
// 检查 token 指标
|
||||
inputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.input_token"
|
||||
inputTokenValue, err := host.GetCounterMetric(inputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(5), inputTokenValue)
|
||||
|
||||
outputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.output_token"
|
||||
outputTokenValue, err := host.GetCounterMetric(outputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(8), outputTokenValue)
|
||||
|
||||
totalTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user2.metric.total_token"
|
||||
totalTokenValue, err := host.GetCounterMetric(totalTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(13), totalTokenValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试完整的统计流程
|
||||
t.Run("complete statistics flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置路由和集群名称
|
||||
host.SetRouteName("api-v1")
|
||||
host.SetClusterName("cluster-1")
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-request-id", "req-123"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 2. 处理请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`)
|
||||
action = host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 3. 处理响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 4. 处理响应体
|
||||
responseBody := []byte(`{
|
||||
"choices": [{"message": {"content": "Hello, how can I help you?"}}],
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 8, "total_tokens": 13},
|
||||
"model": "gpt-3.5-turbo"
|
||||
}`)
|
||||
action = host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 5. 完成请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 6. 验证指标值
|
||||
// 检查输入 token 指标
|
||||
inputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.consumer1.metric.input_token"
|
||||
inputTokenValue, err := host.GetCounterMetric(inputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(5), inputTokenValue)
|
||||
|
||||
// 检查输出 token 指标
|
||||
outputTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.consumer1.metric.output_token"
|
||||
outputTokenValue, err := host.GetCounterMetric(outputTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(8), outputTokenValue)
|
||||
|
||||
// 检查总 token 指标
|
||||
totalTokenMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.consumer1.metric.total_token"
|
||||
totalTokenValue, err := host.GetCounterMetric(totalTokenMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(13), totalTokenValue)
|
||||
|
||||
// 检查服务时长指标
|
||||
serviceDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.consumer1.metric.llm_service_duration"
|
||||
serviceDurationValue, err := host.GetCounterMetric(serviceDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, serviceDurationValue, uint64(0))
|
||||
|
||||
// 检查请求计数指标
|
||||
durationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.consumer1.metric.llm_duration_count"
|
||||
durationCountValue, err := host.GetCounterMetric(durationCountMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), durationCountValue)
|
||||
})
|
||||
|
||||
// 测试流式响应的完整流程
|
||||
t.Run("complete streaming flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(streamingBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置路由和集群名称
|
||||
host.SetRouteName("api-v1")
|
||||
host.SetClusterName("cluster-1")
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "consumer2"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 2. 处理请求体
|
||||
requestBody := []byte(`{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`)
|
||||
action = host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 3. 处理流式响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/event-stream"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 4. 处理流式响应体 - 添加 usage 信息
|
||||
firstChunk := []byte(`data: {"choices":[{"message":{"content":"Hello"}}],"model":"gpt-4","usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`)
|
||||
action = host.CallOnHttpStreamingResponseBody(firstChunk, false)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result := host.GetResponseBody()
|
||||
require.Equal(t, firstChunk, result)
|
||||
|
||||
// 5. 处理最后一个流式块 - 添加 usage 信息
|
||||
lastChunk := []byte(`data: {"choices":[{"message":{"content":"How can I help you?"}}],"model":"gpt-4","usage":{"prompt_tokens":5,"completion_tokens":8,"total_tokens":13}}`)
|
||||
action = host.CallOnHttpStreamingResponseBody(lastChunk, true)
|
||||
|
||||
// 应该返回原始数据
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
result = host.GetResponseBody()
|
||||
require.Equal(t, lastChunk, result)
|
||||
|
||||
// 添加延迟,确保有足够的时间间隔来计算 llm_service_duration
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// 6. 完成请求
|
||||
host.CompleteHttp()
|
||||
|
||||
// 7. 验证流式响应指标
|
||||
// 检查首 token 延迟指标
|
||||
firstTokenDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.consumer2.metric.llm_first_token_duration"
|
||||
firstTokenDurationValue, err := host.GetCounterMetric(firstTokenDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, firstTokenDurationValue, uint64(0))
|
||||
|
||||
// 检查流式请求计数指标
|
||||
streamDurationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.consumer2.metric.llm_stream_duration_count"
|
||||
streamDurationCountValue, err := host.GetCounterMetric(streamDurationCountMetric)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), streamDurationCountValue)
|
||||
|
||||
// 检查服务时长指标
|
||||
serviceDurationMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.consumer2.metric.llm_service_duration"
|
||||
serviceDurationValue, err := host.GetCounterMetric(serviceDurationMetric)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, serviceDurationValue, uint64(0))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.1
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
@@ -18,7 +18,9 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,14 +4,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY=
|
||||
github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -21,6 +24,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M=
|
||||
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
557
plugins/wasm-go/extensions/ai-token-ratelimit/main_test.go
Normal file
557
plugins/wasm-go/extensions/ai-token-ratelimit/main_test.go
Normal file
@@ -0,0 +1,557 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:全局限流配置
|
||||
var globalThresholdConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-global-limit",
|
||||
"global_threshold": map[string]interface{}{
|
||||
"token_per_minute": 1000,
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
"timeout": 1000,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "Too many AI token requests",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于请求头的限流配置
|
||||
var headerLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-header-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_header": "x-api-key",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "test-key-123",
|
||||
"token_per_minute": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "API key rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于请求参数的限流配置
|
||||
var paramLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-param-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_param": "apikey",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "param-key-456",
|
||||
"token_per_minute": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "Parameter rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 Consumer 的限流配置
|
||||
var consumerLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-consumer-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_consumer": "",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "consumer1",
|
||||
"token_per_minute": 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "Consumer rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 Cookie 的限流配置
|
||||
var cookieLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-cookie-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_cookie": "session-id",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "session-789",
|
||||
"token_per_minute": 75,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "Session rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 IP 的限流配置
|
||||
var ipLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-ip-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_per_ip": "from-remote-addr",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "192.168.1.0/24",
|
||||
"token_per_minute": 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "IP rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:正则表达式限流配置
|
||||
var regexpLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "ai-token-regexp-limit",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_per_header": "x-user-id",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "regexp:^user-\\d+$",
|
||||
"token_per_minute": 150,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "User ID rate limit exceeded",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试全局限流配置解析
|
||||
t.Run("global threshold config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试基于请求头的限流配置解析
|
||||
t.Run("header limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试基于请求参数的限流配置解析
|
||||
t.Run("param limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(paramLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试基于 Consumer 的限流配置解析
|
||||
t.Run("consumer limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(consumerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试基于 Cookie 的限流配置解析
|
||||
t.Run("cookie limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(cookieLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试基于 IP 的限流配置解析
|
||||
t.Run("ip limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(ipLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试正则表达式限流配置解析
|
||||
t.Run("regexp limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(regexpLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试全局限流请求头处理
|
||||
t.Run("global threshold request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
// 返回 [count, remaining, ttl] 格式
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于请求头的限流请求头处理
|
||||
t.Run("header limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含限流键
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"x-api-key", "test-key-123"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{100, 99, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于请求参数的限流请求头处理
|
||||
t.Run("param limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(paramLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test?apikey=param-key-456"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{50, 49, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于 Consumer 的限流请求头处理
|
||||
t.Run("consumer limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(consumerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 consumer 信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{200, 199, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于 Cookie 的限流请求头处理
|
||||
t.Run("cookie limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(cookieLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 cookie
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"cookie", "session-id=session-789; other=value"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{75, 74, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试限流触发的情况
|
||||
t.Run("rate limit exceeded", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(触发限流)
|
||||
// 返回 [count, remaining, ttl] 格式,remaining < 0 表示触发限流
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, -1, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 检查是否发送了限流响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(429), localResponse.StatusCode)
|
||||
require.Contains(t, string(localResponse.Data), "Too many AI token requests")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试没有匹配到限流规则的情况
|
||||
t.Run("no matching limit rule", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,但不包含限流键
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
// 不包含 x-api-key 头
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为没有匹配到限流规则
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpStreamingBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试流式响应体处理(包含 token 统计)
|
||||
t.Run("streaming body with token usage", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 处理流式响应体
|
||||
// 模拟包含 token 统计信息的响应体
|
||||
responseBody := []byte(`{"choices":[{"message":{"content":"Hello, how can I help you?"}}],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25}}`)
|
||||
action := host.CallOnHttpStreamingRequestBody(responseBody, false) // 不是最后一个块
|
||||
|
||||
result := host.GetRequestBody()
|
||||
require.Equal(t, responseBody, result)
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理最后一个块
|
||||
lastChunk := []byte(`{"choices":[{"message":{"content":"How can I help you?"}}],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25}}`)
|
||||
action = host.CallOnHttpStreamingRequestBody(lastChunk, true) // 最后一个块
|
||||
|
||||
result = host.GetRequestBody()
|
||||
require.Equal(t, lastChunk, result)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试流式响应体处理(不包含 token 统计)
|
||||
t.Run("streaming body without token usage", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 处理流式响应体
|
||||
// 模拟不包含 token 统计信息的响应体
|
||||
responseBody := []byte(`{"message": "Hello, world!"}`)
|
||||
action := host.CallOnHttpStreamingRequestBody(responseBody, true) // 最后一个块
|
||||
|
||||
result := host.GetRequestBody()
|
||||
require.Equal(t, responseBody, result)
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试完整的限流流程
|
||||
t.Run("complete rate limit flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"x-api-key", "test-key-123"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 2. 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{100, 99, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 3. 处理流式响应体
|
||||
responseBody := []byte(`{"choices":[{"message":{"content":"AI response"}}],"usage":{"prompt_tokens":5,"completion_tokens":8,"total_tokens":13}}`)
|
||||
action = host.CallOnHttpStreamingRequestBody(responseBody, true)
|
||||
|
||||
result := host.GetRequestBody()
|
||||
require.Equal(t, responseBody, result)
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 4. 完成请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,11 +5,19 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
575
plugins/wasm-go/extensions/ai-transformer/main_test.go
Normal file
575
plugins/wasm-go/extensions/ai-transformer/main_test.go
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:启用请求转换
|
||||
var requestTransformConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "将请求转换为JSON格式",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:启用响应转换
|
||||
var responseTransformConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "将响应转换为XML格式",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:同时启用请求和响应转换
|
||||
var bothTransformConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "将请求转换为JSON格式",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "将响应转换为XML格式",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:禁用所有转换
|
||||
var noTransformConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:缺少API密钥
|
||||
var missingAPIKeyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "将请求转换为JSON格式",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试请求转换配置解析
|
||||
t.Run("request transform config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试响应转换配置解析
|
||||
t.Run("response transform config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试同时启用请求和响应转换的配置解析
|
||||
t.Run("both transform config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(bothTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试禁用所有转换的配置解析
|
||||
t.Run("no transform config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(noTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试缺少API密钥的配置解析
|
||||
t.Run("missing API key config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingAPIKeyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试启用请求转换时的请求头处理
|
||||
t.Run("request transform enabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 HeaderStopIteration,因为需要读取请求体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
|
||||
// 测试禁用请求转换时的请求头处理
|
||||
t.Run("request transform disabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(noTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为不需要转换
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试启用请求转换但缺少提示词时的请求头处理
|
||||
t.Run("request transform enabled but no prompt", func(t *testing.T) {
|
||||
// 创建缺少提示词的配置
|
||||
noPromptConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(noPromptConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为提示词为空
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求体转换
|
||||
t.Run("request body transformation", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"name": "test", "value": "data"}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 AI 服务调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟 AI 服务的 HTTP 调用响应(仅包含头与空行,再跟随 body 的 HTTP 帧)
|
||||
// 注意:每个头部行必须有 key: value 格式,否则 extraceHttpFrame 会解析失败
|
||||
aiResponse := `{"output": {"text": "Host: example.com\nContent-Type: application/json\n\n{\"transformed\": true, \"data\": \"converted\"}"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(aiResponse))
|
||||
|
||||
// 完成外呼回调后,应继续处理
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证请求体已被替换为 AI 返回的内容
|
||||
expected := []byte(`{"transformed": true, "data": "converted"}`)
|
||||
got := host.GetRequestBody()
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 AI 服务返回无效 HTTP 帧的情况
|
||||
t.Run("invalid HTTP frame from AI service", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(requestTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"name": "test", "value": "data"}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟 AI 服务返回格式错误但不会导致 panic 的响应
|
||||
// 返回一个包含 \n\n 但格式不正确的响应,这样 extraceHttpFrame 会返回错误但不会 panic
|
||||
invalidResponse := `{"output": {"text": "invalid\n\nhttp frame"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(invalidResponse))
|
||||
|
||||
// 完成外呼回调后,应继续处理
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 由于解析失败,请求体应该保持原样
|
||||
expected := requestBody
|
||||
got := host.GetRequestBody()
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试启用响应转换时的响应头处理
|
||||
t.Run("response transform enabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 HeaderStopIteration,因为需要读取响应体
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
})
|
||||
|
||||
// 测试禁用响应转换时的响应头处理
|
||||
t.Run("response transform disabled", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(noTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为不需要转换
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
|
||||
// 测试启用响应转换但缺少提示词时的响应头处理
|
||||
t.Run("response transform enabled but no prompt", func(t *testing.T) {
|
||||
// 创建缺少提示词的配置
|
||||
noPromptConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"request": map[string]interface{}{
|
||||
"enable": false,
|
||||
"prompt": "",
|
||||
},
|
||||
"response": map[string]interface{}{
|
||||
"enable": true,
|
||||
"prompt": "",
|
||||
},
|
||||
"provider": map[string]interface{}{
|
||||
"apiKey": "test-api-key",
|
||||
"serviceName": "ai-service",
|
||||
"domain": "ai.example.com",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(noPromptConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为提示词为空
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应体转换
|
||||
t.Run("response body transformation", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := []byte(`{"status": "success", "data": "test"}`)
|
||||
action := host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 AI 服务调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟 AI 服务的 HTTP 调用响应
|
||||
// 返回一个有效的 HTTP 帧格式,确保每个头部行都有 key: value 格式
|
||||
// 注意:不要包含状态行(如 HTTP/1.1 200 OK),只包含头部行
|
||||
aiResponse := `{"output": {"text": "Content-Type: application/xml\n\n<response><status>success</status><data>test</data></response>"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(aiResponse))
|
||||
|
||||
// 完成外呼回调后,应继续处理
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体已被替换为 AI 返回的内容
|
||||
expected := []byte(`<response><status>success</status><data>test</data></response>`)
|
||||
got := host.GetResponseBody()
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 AI 服务返回无效 HTTP 帧的情况
|
||||
t.Run("invalid HTTP frame from AI service for response", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(responseTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先设置响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 设置响应体
|
||||
responseBody := []byte(`{"status": "success", "data": "test"}`)
|
||||
action := host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟 AI 服务返回格式错误但不会导致 panic 的响应
|
||||
// 返回一个包含 \n\n 但格式不正确的响应,这样 extraceHttpFrame 会返回错误但不会 panic
|
||||
invalidResponse := `{"output": {"text": "invalid\n\nhttp frame"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(invalidResponse))
|
||||
|
||||
// 完成外呼回调后,应继续处理
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 由于解析失败,响应体应该保持原样
|
||||
expected := responseBody
|
||||
got := host.GetResponseBody()
|
||||
require.Equal(t, expected, got)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试完整的请求和响应转换流程
|
||||
t.Run("complete request and response transformation", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(bothTransformConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 HeaderStopIteration
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 2. 处理请求体
|
||||
requestBody := []byte(`{"name": "test", "value": "data"}`)
|
||||
action = host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 3. 模拟 AI 服务对请求的响应
|
||||
// 确保头部行格式正确,避免 extraceHttpFrame 解析失败
|
||||
requestAIResponse := `{"output": {"text": "Host: example.com\nContent-Type: application/json\n\n{\"transformed\": true, \"data\": \"converted\"}"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(requestAIResponse))
|
||||
|
||||
// 4. 处理响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 HeaderStopIteration
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
// 5. 处理响应体
|
||||
responseBody := []byte(`{"status": "success", "data": "test"}`)
|
||||
action = host.CallOnHttpResponseBody(responseBody)
|
||||
|
||||
// 应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 6. 模拟 AI 服务对响应的响应
|
||||
// 确保头部行格式正确,避免 extraceHttpFrame 解析失败
|
||||
// 注意:不要包含状态行,只包含头部行
|
||||
responseAIResponse := `{"output": {"text": "Content-Type: application/xml\n\n<response><status>success</status><data>test</data></response>"}}`
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(responseAIResponse))
|
||||
|
||||
// 验证请求和响应都被正确转换
|
||||
// 检查请求体转换结果
|
||||
expectedRequestBody := []byte(`{"transformed": true, "data": "converted"}`)
|
||||
gotRequestBody := host.GetRequestBody()
|
||||
require.Equal(t, expectedRequestBody, gotRequestBody)
|
||||
|
||||
// 检查响应体转换结果
|
||||
expectedResponseBody := []byte(`<response><status>success</status><data>test</data></response>`)
|
||||
gotResponseBody := host.GetResponseBody()
|
||||
require.Equal(t, expectedResponseBody, gotResponseBody)
|
||||
|
||||
// 7. 完成请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,15 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -22,5 +24,7 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
435
plugins/wasm-go/extensions/api-workflow/main_test.go
Normal file
435
plugins/wasm-go/extensions/api-workflow/main_test.go
Normal file
@@ -0,0 +1,435 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本工作流配置
|
||||
var basicWorkflowConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"env": map[string]interface{}{
|
||||
"timeout": 5000,
|
||||
"max_depth": 100,
|
||||
},
|
||||
"workflow": map[string]interface{}{
|
||||
"edges": []map[string]interface{}{
|
||||
{
|
||||
"source": "start",
|
||||
"target": "A",
|
||||
},
|
||||
{
|
||||
"source": "A",
|
||||
"target": "end",
|
||||
},
|
||||
},
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"name": "A",
|
||||
"service_name": "test-service.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/test",
|
||||
"service_method": "POST",
|
||||
"service_body_tmpl": map[string]interface{}{
|
||||
"message": "hello",
|
||||
"data": "",
|
||||
},
|
||||
"service_body_replace_keys": []map[string]interface{}{
|
||||
{
|
||||
"from": "start||message",
|
||||
"to": "data",
|
||||
},
|
||||
},
|
||||
"service_headers": []map[string]interface{}{
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:条件分支工作流配置
|
||||
var conditionalWorkflowConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"env": map[string]interface{}{
|
||||
"timeout": 3000,
|
||||
"max_depth": 50,
|
||||
},
|
||||
"workflow": map[string]interface{}{
|
||||
"edges": []map[string]interface{}{
|
||||
{
|
||||
"source": "start",
|
||||
"target": "A",
|
||||
},
|
||||
{
|
||||
"source": "A",
|
||||
"target": "end",
|
||||
"conditional": "gt {{A||score}} 0.5",
|
||||
},
|
||||
{
|
||||
"source": "A",
|
||||
"target": "B",
|
||||
"conditional": "lt {{A||score}} 0.5",
|
||||
},
|
||||
{
|
||||
"source": "B",
|
||||
"target": "end",
|
||||
},
|
||||
},
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"name": "A",
|
||||
"service_name": "service-a.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/score",
|
||||
"service_method": "GET",
|
||||
},
|
||||
{
|
||||
"name": "B",
|
||||
"service_name": "service-b.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/fallback",
|
||||
"service_method": "POST",
|
||||
"service_body_tmpl": map[string]interface{}{
|
||||
"fallback": "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:并行执行工作流配置
|
||||
var parallelWorkflowConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"env": map[string]interface{}{
|
||||
"timeout": 5000,
|
||||
"max_depth": 100,
|
||||
},
|
||||
"workflow": map[string]interface{}{
|
||||
"edges": []map[string]interface{}{
|
||||
{
|
||||
"source": "start",
|
||||
"target": "A",
|
||||
},
|
||||
{
|
||||
"source": "start",
|
||||
"target": "B",
|
||||
},
|
||||
{
|
||||
"source": "start",
|
||||
"target": "C",
|
||||
},
|
||||
{
|
||||
"source": "A",
|
||||
"target": "D",
|
||||
},
|
||||
{
|
||||
"source": "B",
|
||||
"target": "D",
|
||||
},
|
||||
{
|
||||
"source": "C",
|
||||
"target": "D",
|
||||
},
|
||||
{
|
||||
"source": "D",
|
||||
"target": "end",
|
||||
},
|
||||
},
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"name": "A",
|
||||
"service_name": "service-a.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/a",
|
||||
"service_method": "GET",
|
||||
},
|
||||
{
|
||||
"name": "B",
|
||||
"service_name": "service-b.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/b",
|
||||
"service_method": "GET",
|
||||
},
|
||||
{
|
||||
"name": "C",
|
||||
"service_name": "service-c.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/c",
|
||||
"service_method": "GET",
|
||||
},
|
||||
{
|
||||
"name": "D",
|
||||
"service_name": "service-d.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/d",
|
||||
"service_method": "POST",
|
||||
"service_body_tmpl": map[string]interface{}{
|
||||
"a_result": "",
|
||||
"b_result": "",
|
||||
"c_result": "",
|
||||
},
|
||||
"service_body_replace_keys": []map[string]interface{}{
|
||||
{
|
||||
"from": "A||result",
|
||||
"to": "a_result",
|
||||
},
|
||||
{
|
||||
"from": "B||result",
|
||||
"to": "b_result",
|
||||
},
|
||||
{
|
||||
"from": "C||result",
|
||||
"to": "c_result",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:continue 工作流配置
|
||||
var continueWorkflowConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"env": map[string]interface{}{
|
||||
"timeout": 5000,
|
||||
"max_depth": 100,
|
||||
},
|
||||
"workflow": map[string]interface{}{
|
||||
"edges": []map[string]interface{}{
|
||||
{
|
||||
"source": "start",
|
||||
"target": "A",
|
||||
},
|
||||
{
|
||||
"source": "A",
|
||||
"target": "continue",
|
||||
},
|
||||
},
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"name": "A",
|
||||
"service_name": "service-a.static",
|
||||
"service_port": 80,
|
||||
"service_path": "/api/process",
|
||||
"service_method": "POST",
|
||||
"service_body_tmpl": map[string]interface{}{
|
||||
"processed": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本工作流配置解析
|
||||
t.Run("basic workflow config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试条件分支工作流配置解析
|
||||
t.Run("conditional workflow config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(conditionalWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试并行执行工作流配置解析
|
||||
t.Run("parallel workflow config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(parallelWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试 continue 工作流配置解析
|
||||
t.Run("continue workflow config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(continueWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本工作流执行
|
||||
t.Run("basic workflow execution", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"message": "test message"}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 HTTP 调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务的 HTTP 调用响应
|
||||
// 模拟成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"result": "success", "data": "processed"}`))
|
||||
|
||||
// 检查插件的响应状态
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
// 如果插件发送了响应,验证响应内容
|
||||
require.Equal(t, uint32(200), localResponse.StatusCode)
|
||||
require.Contains(t, string(localResponse.Data), "success")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试条件分支工作流执行
|
||||
t.Run("conditional workflow execution", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(conditionalWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"input": "test"}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 HTTP 调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务的 HTTP 调用响应
|
||||
// 模拟成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"score": 0.8}`))
|
||||
|
||||
// 检查插件的响应状态
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
// 如果插件发送了响应,验证响应内容
|
||||
require.Equal(t, uint32(200), localResponse.StatusCode)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试并行执行工作流执行
|
||||
t.Run("parallel workflow execution", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(parallelWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"data": "test data"}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 HTTP 调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务的 HTTP 调用响应
|
||||
// 模拟 A 服务的响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"result": "a_result"}`))
|
||||
|
||||
// 模拟 B 服务的响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"result": "b_result"}`))
|
||||
|
||||
// 模拟 C 服务的响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"result": "c_result"}`))
|
||||
|
||||
// 模拟 D 服务的响应(这是汇聚节点)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"final_result": "success"}`))
|
||||
|
||||
// 检查插件的响应状态
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
// 如果插件发送了响应,验证响应内容
|
||||
require.Equal(t, uint32(200), localResponse.StatusCode)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 continue 工作流执行
|
||||
t.Run("continue workflow execution", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(continueWorkflowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求体
|
||||
requestBody := []byte(`{"process": true}`)
|
||||
action := host.CallOnHttpRequestBody(requestBody)
|
||||
|
||||
// 应该返回 ActionPause,因为需要等待外部 HTTP 调用完成
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部服务的 HTTP 调用响应
|
||||
// 模拟成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"processed": true, "status": "success"}`))
|
||||
|
||||
// 检查插件的响应状态
|
||||
action = host.GetHttpStreamAction()
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,15 +5,21 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,16 +2,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -21,5 +24,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
871
plugins/wasm-go/extensions/basic-auth/main_test.go
Normal file
871
plugins/wasm-go/extensions/basic-auth/main_test.go
Normal file
@@ -0,0 +1,871 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本全局配置
|
||||
var basicGlobalConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:全局认证开启配置
|
||||
var globalAuthTrueConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:路由鉴权配置
|
||||
var routeAuthConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"allow": []string{
|
||||
"consumer1",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:域名鉴权配置
|
||||
var domainAuthConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"allow": []string{
|
||||
"consumer2",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(缺少 consumers)
|
||||
var invalidConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(空的 consumers)
|
||||
var emptyConsumersConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(重复的 credential)
|
||||
var duplicateCredentialConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "admin:123456", // 重复的 credential
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(无效的 credential 格式)
|
||||
var invalidCredentialFormatConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin", // 缺少密码部分
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(缺少 consumer name)
|
||||
var missingConsumerNameConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"credential": "admin:123456",
|
||||
// 缺少 name
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(空的 consumer name)
|
||||
var emptyConsumerNameConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(空的 credential)
|
||||
var emptyCredentialConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(空的 allow 列表)
|
||||
var emptyAllowConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow": []string{},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:路由级别配置(使用 _rules_ 和 _match_route_)
|
||||
var routeLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_route_": []string{"route-a", "route-b"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
{
|
||||
"_match_route_": []string{"route-c"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:域名级别配置(使用 _rules_ 和 _match_domain_)
|
||||
var domainLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_domain_": []string{"*.example.com", "test.com"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
{
|
||||
"_match_domain_": []string{"api.example.com"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:服务级别配置(使用 _rules_ 和 _match_service_)
|
||||
var serviceLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_service_": []string{"service-a:8080", "service-b"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
{
|
||||
"_match_service_": []string{"service-c:9090"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:路由前缀级别配置(使用 _rules_ 和 _match_route_prefix_)
|
||||
var routePrefixLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_route_prefix_": []string{"api-", "web-"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
{
|
||||
"_match_route_prefix_": []string{"admin-", "internal-"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:路由和服务组合配置(使用 _rules_、_match_route_ 和 _match_service_)
|
||||
var routeAndServiceLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_route_": []string{"route-a"},
|
||||
"_match_service_": []string{"service-a:8080"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
{
|
||||
"_match_route_": []string{"route-b"},
|
||||
"_match_service_": []string{"service-b:9090"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:混合级别配置
|
||||
var mixedLevelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
{
|
||||
"name": "consumer2",
|
||||
"credential": "guest:abc",
|
||||
},
|
||||
{
|
||||
"name": "consumer3",
|
||||
"credential": "user:def",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_route_": []string{"api-route"},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
{
|
||||
"_match_domain_": []string{"*.example.com"},
|
||||
"allow": []string{"consumer2"},
|
||||
},
|
||||
{
|
||||
"_match_service_": []string{"internal-service:8080"},
|
||||
"allow": []string{"consumer3"},
|
||||
},
|
||||
{
|
||||
"_match_route_prefix_": []string{"web-"},
|
||||
"allow": []string{"consumer1", "consumer2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效规则配置(缺少匹配条件)
|
||||
var invalidRuleConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"allow": []string{"consumer1"},
|
||||
// 缺少匹配条件
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效规则配置(空的匹配条件)
|
||||
var emptyMatchConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"consumers": []map[string]interface{}{
|
||||
{
|
||||
"name": "consumer1",
|
||||
"credential": "admin:123456",
|
||||
},
|
||||
},
|
||||
"global_auth": false,
|
||||
"_rules_": []map[string]interface{}{
|
||||
{
|
||||
"_match_route_": []string{},
|
||||
"allow": []string{"consumer1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseGlobalConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本全局配置解析
|
||||
t.Run("basic global config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGlobalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试全局认证开启配置解析
|
||||
t.Run("global auth true config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效配置(缺少 consumers)
|
||||
t.Run("invalid config - missing consumers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(空的 consumers)
|
||||
t.Run("invalid config - empty consumers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConsumersConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(重复的 credential)
|
||||
t.Run("invalid config - duplicate credential", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(duplicateCredentialConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(无效的 credential 格式)
|
||||
t.Run("invalid config - invalid credential format", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidCredentialFormatConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(缺少 consumer name)
|
||||
t.Run("invalid config - missing consumer name", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(missingConsumerNameConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(空的 consumer name)
|
||||
t.Run("invalid config - empty consumer name", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConsumerNameConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置(空的 credential)
|
||||
t.Run("invalid config - empty credential", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyCredentialConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseOverrideRuleConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试路由鉴权配置解析
|
||||
t.Run("route auth config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(routeAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试域名鉴权配置解析
|
||||
t.Run("domain auth config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(domainAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效配置(空的 allow 列表)
|
||||
t.Run("invalid config - empty allow list", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyAllowConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseRuleConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试路由级别配置解析
|
||||
t.Run("route level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(routeLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试域名级别配置解析
|
||||
t.Run("domain level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(domainLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试服务级别配置解析
|
||||
t.Run("service level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(serviceLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试路由前缀级别配置解析
|
||||
t.Run("route prefix level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(routePrefixLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试路由和服务组合配置解析
|
||||
t.Run("route and service level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(routeAndServiceLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试混合级别配置解析
|
||||
t.Run("mixed level config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(mixedLevelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效规则配置(缺少匹配条件)
|
||||
t.Run("invalid rule config - missing match conditions", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidRuleConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效规则配置(空的匹配条件)
|
||||
t.Run("invalid rule config - empty match conditions", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试缺少 Authorization 头的情况
|
||||
t.Run("missing authorization header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGlobalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含 Authorization
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 false 且没有配置 allow
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空的 Authorization 头的情况
|
||||
t.Run("empty authorization header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGlobalConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含空的 Authorization
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", ""},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 false 且没有配置 allow
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效的 Authorization 头格式(缺少 Basic 前缀)
|
||||
t.Run("invalid authorization format - missing basic prefix", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含无效的 Authorization 格式
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Bearer token123"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效的 Authorization 头格式(无效的 base64)
|
||||
t.Run("invalid authorization format - invalid base64", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含无效的 base64 编码
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic invalid-base64"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效的凭证格式(缺少密码部分)
|
||||
t.Run("invalid credential format - missing password", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含无效的凭证格式
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("admin"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效的用户名(未配置的用户名)
|
||||
t.Run("invalid username - not configured", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含未配置的用户名
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("unknown:password"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效的密码(错误的密码)
|
||||
t.Run("invalid password - wrong password", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含错误的密码
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("admin:wrongpassword"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试有效的凭证(全局认证开启,无 allow 配置)
|
||||
t.Run("valid credentials - global auth true, no allow config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含有效的凭证
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("admin:123456"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为凭证有效
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 注意:在测试框架中,proxywasm.AddHttpRequestHeader 可能不会立即反映在 host.GetRequestHeaders() 中
|
||||
// 这是因为测试框架可能没有完全模拟插件的执行环境
|
||||
// 我们主要验证插件的行为逻辑,而不是具体的请求头修改
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试有效的凭证(全局认证关闭,有 allow 配置)
|
||||
t.Run("valid credentials - global auth false, with allow config", func(t *testing.T) {
|
||||
// 这里需要先设置全局配置,然后设置路由配置
|
||||
// 由于测试框架的限制,我们直接测试路由配置
|
||||
host, status := test.NewTestHost(routeAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含有效的凭证
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("admin:123456"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为凭证有效且在 allow 列表中
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试有效的凭证但不在 allow 列表中的情况
|
||||
t.Run("valid credentials but not in allow list", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(routeAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含有效的凭证但不在 allow 列表中
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("guest:abc"))
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为凭证有效但不在 allow 列表中
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete basic auth flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalAuthTrueConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 测试缺少认证信息的情况
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为 global_auth 为 true
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
|
||||
// 2. 测试有效认证的情况
|
||||
encodedCredential := base64.StdEncoding.EncodeToString([]byte("admin:123456"))
|
||||
action = host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Basic " + encodedCredential},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为凭证有效
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了 X-Mse-Consumer 请求头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "X-Mse-Consumer", "consumer1"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
@@ -15,8 +15,10 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
444
plugins/wasm-go/extensions/bot-detect/main_test.go
Normal file
444
plugins/wasm-go/extensions/bot-detect/main_test.go
Normal file
@@ -0,0 +1,444 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本配置(默认值)
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义阻止状态码和消息
|
||||
var customBlockConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"blocked_code": 429,
|
||||
"blocked_message": "Too Many Requests - Bot Detected",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:允许规则配置
|
||||
var allowRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow": []string{
|
||||
".*Go-http-client.*",
|
||||
".*Python-requests.*",
|
||||
".*curl.*",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:拒绝规则配置
|
||||
var denyRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"deny": []string{
|
||||
"spd-tools.*",
|
||||
"malicious-bot.*",
|
||||
".*scraper.*",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:混合规则配置
|
||||
var mixedRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow": []string{
|
||||
".*Go-http-client.*",
|
||||
".*Python-requests.*",
|
||||
},
|
||||
"deny": []string{
|
||||
"spd-tools.*",
|
||||
"malicious-bot.*",
|
||||
},
|
||||
"blocked_code": 418,
|
||||
"blocked_message": "I'm a teapot - Bot Detected",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效正则表达式配置
|
||||
var invalidRegexConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"deny": []string{
|
||||
"[invalid-regex",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析(默认值)
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义阻止状态码和消息配置解析
|
||||
t.Run("custom block config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customBlockConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试允许规则配置解析
|
||||
t.Run("allow rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(allowRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试拒绝规则配置解析
|
||||
t.Run("deny rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(denyRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试混合规则配置解析
|
||||
t.Run("mixed rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(mixedRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效正则表达式配置解析
|
||||
t.Run("invalid regex config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidRegexConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试正常 User-Agent 请求头处理
|
||||
t.Run("normal user agent", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含正常的 User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试默认爬虫检测(Googlebot)
|
||||
t.Run("default bot detection - googlebot", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 Googlebot User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被识别为爬虫
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(403), localResponse.StatusCode)
|
||||
require.Equal(t, "Invalid User-Agent", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试默认爬虫检测(BaiduSpider)
|
||||
t.Run("default bot detection - baiduspider", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 BaiduSpider User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被识别为爬虫
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(403), localResponse.StatusCode)
|
||||
require.Equal(t, "Invalid User-Agent", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试允许规则(Go-http-client)
|
||||
t.Run("allow rule - go-http-client", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(allowRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 Go-http-client User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Go-http-client/1.1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为被允许规则匹配
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试允许规则(Python-requests)
|
||||
t.Run("allow rule - python-requests", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(allowRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 Python-requests User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "python-requests/2.28.1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为被允许规则匹配
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试拒绝规则(spd-tools)
|
||||
t.Run("deny rule - spd-tools", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(denyRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 spd-tools User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "spd-tools/1.1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被拒绝规则匹配
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(403), localResponse.StatusCode)
|
||||
require.Equal(t, "Invalid User-Agent", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试拒绝规则(malicious-bot)
|
||||
t.Run("deny rule - malicious-bot", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(denyRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 malicious-bot User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "malicious-bot/2.0"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被拒绝规则匹配
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(403), localResponse.StatusCode)
|
||||
require.Equal(t, "Invalid User-Agent", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试混合规则配置
|
||||
t.Run("mixed rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(mixedRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 测试允许规则(Go-http-client)
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Go-http-client/1.1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为被允许规则匹配
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
|
||||
// 测试拒绝规则(spd-tools)
|
||||
action = host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "spd-tools/1.1"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被拒绝规则匹配
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了自定义阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(418), localResponse.StatusCode)
|
||||
require.Equal(t, "I'm a teapot - Bot Detected", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试缺少 User-Agent 的情况
|
||||
t.Run("missing user agent", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含 User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为缺少 User-Agent
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空 User-Agent 的情况
|
||||
t.Run("empty user agent", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含空的 User-Agent
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", ""},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为 User-Agent 为空
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete bot detection flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(mixedRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 测试正常请求通过
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
|
||||
// 2. 测试爬虫请求被阻止
|
||||
action = host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"user-agent", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionPause,因为被识别为爬虫
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 验证是否发送了阻止响应
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(418), localResponse.StatusCode)
|
||||
require.Equal(t, "I'm a teapot - Bot Detected", string(localResponse.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
460
plugins/wasm-go/extensions/cache-control/main_test.go
Normal file
460
plugins/wasm-go/extensions/cache-control/main_test.go
Normal file
@@ -0,0 +1,460 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本配置(数字过期时间)
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"suffix": "jpg|png|jpeg",
|
||||
"expires": "3600",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:最大缓存时间配置
|
||||
var maxExpiresConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"suffix": "css|js",
|
||||
"expires": "max",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:不缓存配置
|
||||
var epochExpiresConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"suffix": "html|htm",
|
||||
"expires": "epoch",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无后缀限制配置
|
||||
var noSuffixConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"expires": "7200",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:单后缀配置
|
||||
var singleSuffixConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"suffix": "pdf",
|
||||
"expires": "1800",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:空后缀配置
|
||||
var emptySuffixConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]string{
|
||||
"suffix": "",
|
||||
"expires": "3600",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试最大缓存时间配置解析
|
||||
t.Run("max expires config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(maxExpiresConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试不缓存配置解析
|
||||
t.Run("epoch expires config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(epochExpiresConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无后缀限制配置解析
|
||||
t.Run("no suffix config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(noSuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试单后缀配置解析
|
||||
t.Run("single suffix config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(singleSuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试空后缀配置解析
|
||||
t.Run("empty suffix config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptySuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本请求头处理(带查询参数)
|
||||
t.Run("request headers with query params", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/images/photo.jpg?size=large"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试请求头处理(无查询参数)
|
||||
t.Run("request headers without query params", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/images/photo.png"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试请求头处理(复杂路径)
|
||||
t.Run("request headers with complex path", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含复杂路径
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/static/css/main.css?v=1.0.0&theme=dark"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试匹配后缀的响应头处理(数字过期时间)
|
||||
t.Run("matching suffix with numeric expires", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/images/photo.jpg"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "image/jpeg"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试匹配后缀的响应头处理(最大缓存时间)
|
||||
t.Run("matching suffix with max expires", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(maxExpiresConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/static/main.css"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/css"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=315360000"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试匹配后缀的响应头处理(不缓存)
|
||||
t.Run("matching suffix with epoch expires", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(epochExpiresConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/page.html"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/html"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "no-cache"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不匹配后缀的响应头处理
|
||||
t.Run("non-matching suffix", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data.json"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否没有添加缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.False(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.False(t, test.HasHeader(responseHeaders, "cache-control"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无后缀限制的响应头处理
|
||||
t.Run("no suffix restriction", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(noSuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/any/file.txt"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=7200"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试单后缀匹配
|
||||
t.Run("single suffix match", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(singleSuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/documents/report.pdf"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/pdf"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=1800"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空后缀配置
|
||||
t.Run("empty suffix config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptySuffixConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/any/file.xyz"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/octet-stream"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了缓存控制头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete cache control flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/images/logo.png"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 2. 处理响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "image/png"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 3. 验证完整的缓存控制流程
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
|
||||
// 验证是否添加了必要的缓存控制响应头
|
||||
require.True(t, test.HasHeader(responseHeaders, "expires"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
390
plugins/wasm-go/extensions/chatgpt-proxy/main_test.go
Normal file
390
plugins/wasm-go/extensions/chatgpt-proxy/main_test.go
Normal file
@@ -0,0 +1,390 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "prompt",
|
||||
"model": "text-davinci-003",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义模型配置
|
||||
var customModelConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "text",
|
||||
"model": "curie",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义提示参数配置
|
||||
var customPromptParamConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "question",
|
||||
"model": "text-davinci-003",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义 ChatGPT URI 配置
|
||||
var customUriConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "prompt",
|
||||
"model": "text-davinci-003",
|
||||
"chatgptUri": "https://custom-ai.example.com/v1/chat/completions",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:自定义 Human ID 和 AI ID 配置
|
||||
var customIdsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "prompt",
|
||||
"model": "text-davinci-003",
|
||||
"HumainId": "User:",
|
||||
"AIId": "Assistant:",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(缺少 API Key)
|
||||
var invalidConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"promptParam": "prompt",
|
||||
"model": "text-davinci-003",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效 URI 配置
|
||||
var invalidUriConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"apiKey": "sk-test123456789",
|
||||
"promptParam": "prompt",
|
||||
"model": "text-davinci-003",
|
||||
"chatgptUri": "://invalid-uri",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义模型配置解析
|
||||
t.Run("custom model config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customModelConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义提示参数配置解析
|
||||
t.Run("custom prompt param config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customPromptParamConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义 URI 配置解析
|
||||
t.Run("custom uri config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customUriConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试自定义 ID 配置解析
|
||||
t.Run("custom ids config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customIdsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效配置(缺少 API Key)
|
||||
t.Run("invalid config - missing api key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效 URI 配置
|
||||
t.Run("invalid config - invalid uri", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidUriConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本请求头处理(带查询参数)
|
||||
t.Run("basic request headers with query params", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?prompt=Hello, how are you?"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 AI 服务响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"choices":[{"text":"I'm doing well, thank you for asking!"}]}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), response.StatusCode)
|
||||
require.Equal(t, `{"choices":[{"text":"I'm doing well, thank you for asking!"}]}`, string(response.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试自定义提示参数请求头处理
|
||||
t.Run("custom prompt param request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(customPromptParamConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,使用自定义提示参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?question=What is the weather like?"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 AI 服务响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"choices":[{"text":"I don't have access to real-time weather information."}]}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), response.StatusCode)
|
||||
require.Equal(t, `{"choices":[{"text":"I don't have access to real-time weather information."}]}`, string(response.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试缺少查询参数的情况
|
||||
t.Run("missing query params", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为缺少查询参数
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试缺少提示参数的情况
|
||||
t.Run("missing prompt param", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数但不包含提示参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?other=value"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue,因为缺少提示参数
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空提示参数的情况
|
||||
t.Run("empty prompt param", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含空的提示参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?prompt="},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 AI 服务响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"choices":[{"text":"Empty prompt response"}]}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), response.StatusCode)
|
||||
require.Equal(t, `{"choices":[{"text":"Empty prompt response"}]}`, string(response.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试外部服务调用成功的情况
|
||||
t.Run("external service call success", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?prompt=Tell me a joke"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 AI 服务成功响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"choices":[{"text":"Why don't scientists trust atoms? Because they make up everything!"}]}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), response.StatusCode)
|
||||
require.Equal(t, `{"choices":[{"text":"Why don't scientists trust atoms? Because they make up everything!"}]}`, string(response.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试外部服务调用失败的情况
|
||||
t.Run("external service call failure", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?prompt=Hello"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 AI 服务失败响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "429"},
|
||||
}, []byte(`{"error":"Rate limit exceeded"}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(429), response.StatusCode)
|
||||
require.Equal(t, `{"error":"Rate limit exceeded"}`, string(response.Data))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete chatgpt proxy flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/chat?prompt=What is artificial intelligence?"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 AI 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 2. 模拟外部 AI 服务响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
{":status", "200"},
|
||||
}, []byte(`{"choices":[{"text":"Artificial Intelligence (AI) is a branch of computer science that aims to create systems capable of performing tasks that typically require human intelligence."}]}`))
|
||||
|
||||
response := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(200), response.StatusCode)
|
||||
require.Equal(t, `{"choices":[{"text":"Artificial Intelligence (AI) is a branch of computer science that aims to create systems capable of performing tasks that typically require human intelligence."}]}`, string(response.Data))
|
||||
|
||||
// 3. 完成请求
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.1
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/resp v0.1.1
|
||||
@@ -18,7 +18,9 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,14 +4,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY=
|
||||
github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -21,6 +24,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M=
|
||||
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
666
plugins/wasm-go/extensions/cluster-key-rate-limit/main_test.go
Normal file
666
plugins/wasm-go/extensions/cluster-key-rate-limit/main_test.go
Normal file
@@ -0,0 +1,666 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"cluster-key-rate-limit/config"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:全局限流配置
|
||||
var globalThresholdConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-global-limit-rule",
|
||||
"global_threshold": map[string]interface{}{
|
||||
"query_per_minute": 1000,
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
"timeout": 1000,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
"rejected_code": 429,
|
||||
"rejected_msg": "Too many requests",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于请求参数的限流配置
|
||||
var paramLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-request-param-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_param": "apikey",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "9a342114-ba8a-11ec-b1bf-00163e1250b5",
|
||||
"query_per_minute": 10,
|
||||
},
|
||||
{
|
||||
"key": "a6a6d7f2-ba8a-11ec-bec2-00163e1250b5",
|
||||
"query_per_hour": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于请求头的限流配置
|
||||
var headerLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-request-header-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_header": "x-ca-key",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "102234",
|
||||
"query_per_minute": 10,
|
||||
},
|
||||
{
|
||||
"key": "308239",
|
||||
"query_per_hour": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 Consumer 的限流配置
|
||||
var consumerLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-consumer-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_consumer": "",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "consumer1",
|
||||
"query_per_second": 10,
|
||||
},
|
||||
{
|
||||
"key": "consumer2",
|
||||
"query_per_hour": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 Cookie 的限流配置
|
||||
var cookieLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-cookie-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_cookie": "key1",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "value1",
|
||||
"query_per_minute": 10,
|
||||
},
|
||||
{
|
||||
"key": "value2",
|
||||
"query_per_hour": 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
"rejected_code": 200,
|
||||
"rejected_msg": `{"code":-1,"msg":"Too many requests"}`,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:基于 IP 的限流配置
|
||||
var ipLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-client-ip-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_per_ip": "from-header-x-forwarded-for",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "1.1.1.1",
|
||||
"query_per_day": 10,
|
||||
},
|
||||
{
|
||||
"key": "1.1.1.0/24",
|
||||
"query_per_day": 100,
|
||||
},
|
||||
{
|
||||
"key": "0.0.0.0/0",
|
||||
"query_per_day": 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:正则表达式限流配置
|
||||
var regexpLimitConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-regexp-limit-rule",
|
||||
"rule_items": []map[string]interface{}{
|
||||
{
|
||||
"limit_by_per_param": "apikey",
|
||||
"limit_keys": []map[string]interface{}{
|
||||
{
|
||||
"key": "regexp:^a.*",
|
||||
"query_per_second": 10,
|
||||
},
|
||||
{
|
||||
"key": "regexp:^b.*",
|
||||
"query_per_minute": 100,
|
||||
},
|
||||
{
|
||||
"key": "*",
|
||||
"query_per_hour": 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": true,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试全局限流配置解析
|
||||
t.Run("global threshold config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-global-limit-rule", parsedConfig.RuleName)
|
||||
require.NotNil(t, parsedConfig.GlobalThreshold)
|
||||
require.Equal(t, int64(1000), parsedConfig.GlobalThreshold.Count)
|
||||
require.Equal(t, int64(60), parsedConfig.GlobalThreshold.TimeWindow)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
require.Equal(t, uint32(429), parsedConfig.RejectedCode)
|
||||
require.Equal(t, "Too many requests", parsedConfig.RejectedMsg)
|
||||
})
|
||||
|
||||
// 测试基于请求参数的限流配置解析
|
||||
t.Run("param limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(paramLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-request-param-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByParamType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.Equal(t, "apikey", parsedConfig.RuleItems[0].Key)
|
||||
require.Len(t, parsedConfig.RuleItems[0].ConfigItems, 2)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
|
||||
// 测试基于请求头的限流配置解析
|
||||
t.Run("header limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-request-header-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByHeaderType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.Equal(t, "x-ca-key", parsedConfig.RuleItems[0].Key)
|
||||
require.Len(t, parsedConfig.RuleItems[0].ConfigItems, 2)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
|
||||
// 测试基于 Consumer 的限流配置解析
|
||||
t.Run("consumer limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(consumerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-consumer-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByConsumerType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.Len(t, parsedConfig.RuleItems[0].ConfigItems, 2)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
|
||||
// 测试基于 Cookie 的限流配置解析
|
||||
t.Run("cookie limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(cookieLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-cookie-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByCookieType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.Equal(t, "key1", parsedConfig.RuleItems[0].Key)
|
||||
require.Len(t, parsedConfig.RuleItems[0].ConfigItems, 2)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
|
||||
// 测试基于 IP 的限流配置解析
|
||||
t.Run("ip limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(ipLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-client-ip-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByPerIpType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.NotNil(t, parsedConfig.RuleItems[0].LimitByPerIp)
|
||||
require.Equal(t, config.HeaderSourceType, parsedConfig.RuleItems[0].LimitByPerIp.SourceType)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
|
||||
// 测试正则表达式限流配置解析
|
||||
t.Run("regexp limit config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(regexpLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
cfg, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cfg)
|
||||
|
||||
// 验证配置内容
|
||||
parsedConfig := cfg.(*config.ClusterKeyRateLimitConfig)
|
||||
require.Equal(t, "routeA-regexp-limit-rule", parsedConfig.RuleName)
|
||||
require.Len(t, parsedConfig.RuleItems, 1)
|
||||
require.Equal(t, config.LimitByPerParamType, parsedConfig.RuleItems[0].LimitType)
|
||||
require.Equal(t, "apikey", parsedConfig.RuleItems[0].Key)
|
||||
require.Len(t, parsedConfig.RuleItems[0].ConfigItems, 3)
|
||||
require.Equal(t, config.RegexpType, parsedConfig.RuleItems[0].ConfigItems[0].ConfigType)
|
||||
require.True(t, parsedConfig.ShowLimitQuotaHeader)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试全局限流请求头处理
|
||||
t.Run("global threshold request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于请求参数的限流请求头处理
|
||||
t.Run("param limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(paramLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于请求头的限流请求头处理
|
||||
t.Run("header limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含限流键
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"x-ca-key", "102234"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于 Consumer 的限流请求头处理
|
||||
t.Run("consumer limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(consumerLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 consumer 信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"x-mse-consumer", "consumer1"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{10, 9, 1})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于 Cookie 的限流请求头处理
|
||||
t.Run("cookie limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(cookieLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 cookie
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "key1=value1; other=value"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试基于 IP 的限流请求头处理
|
||||
t.Run("ip limit request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(ipLimitConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 IP 信息
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"x-forwarded-for", "1.1.1.1"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(允许请求)
|
||||
resp := test.CreateRedisRespArray([]interface{}{10, 9, 86400})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试限流触发的情况
|
||||
t.Run("rate limit exceeded", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟 Redis 调用响应(触发限流)
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, -1, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试显示限流配额的响应头处理
|
||||
t.Run("show limit quota headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了限流配额响应头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-limit"))
|
||||
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-remaining"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不显示限流配额的响应头处理
|
||||
t.Run("hide limit quota headers", func(t *testing.T) {
|
||||
// 创建不显示限流配额的配置
|
||||
hideQuotaConfig := func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rule_name": "routeA-global-limit-rule",
|
||||
"global_threshold": map[string]interface{}{
|
||||
"query_per_minute": 1000,
|
||||
},
|
||||
"redis": map[string]interface{}{
|
||||
"service_name": "redis.static",
|
||||
"service_port": 6379,
|
||||
},
|
||||
"show_limit_quota_header": false,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
host, status := test.NewTestHost(hideQuotaConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否没有添加限流配额响应头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.False(t, test.HasHeader(responseHeaders, "x-ratelimit-limit"))
|
||||
require.False(t, test.HasHeader(responseHeaders, "x-ratelimit-remaining"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete rate limit flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(globalThresholdConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用 Redis,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 2. 模拟 Redis 调用响应
|
||||
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
|
||||
host.CallOnRedisCall(0, resp)
|
||||
|
||||
// 3. 处理响应头
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证完整的限流流程
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
|
||||
// 验证是否添加了必要的限流响应头
|
||||
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-limit"))
|
||||
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-remaining"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
@@ -15,8 +15,10 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
432
plugins/wasm-go/extensions/cors/main_test.go
Normal file
432
plugins/wasm-go/extensions/cors/main_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本 CORS 配置
|
||||
var basicCorsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow_origins": []string{
|
||||
"http://example.com",
|
||||
"https://example.com",
|
||||
},
|
||||
"allow_methods": []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"OPTIONS",
|
||||
},
|
||||
"allow_headers": []string{
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
},
|
||||
"expose_headers": []string{
|
||||
"X-Custom-Header",
|
||||
},
|
||||
"allow_credentials": false,
|
||||
"max_age": 3600,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:允许所有 Origin 的配置
|
||||
var allowAllOriginsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow_origins": []string{
|
||||
"*",
|
||||
},
|
||||
"allow_methods": []string{
|
||||
"*",
|
||||
},
|
||||
"allow_headers": []string{
|
||||
"*",
|
||||
},
|
||||
"expose_headers": []string{
|
||||
"*",
|
||||
},
|
||||
"allow_credentials": false,
|
||||
"max_age": 7200,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带模式匹配的配置
|
||||
var patternMatchConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow_origin_patterns": []string{
|
||||
"http://*.example.com",
|
||||
"http://*.example.org:[8080,9090]",
|
||||
},
|
||||
"allow_methods": []string{
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
},
|
||||
"allow_headers": []string{
|
||||
"Content-Type",
|
||||
"Token",
|
||||
"Authorization",
|
||||
},
|
||||
"expose_headers": []string{
|
||||
"X-Custom-Header",
|
||||
"X-Env-UTM",
|
||||
},
|
||||
"allow_credentials": true,
|
||||
"max_age": 1800,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:允许凭据的配置
|
||||
var credentialsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"allow_origin_patterns": []string{
|
||||
"*",
|
||||
},
|
||||
"allow_methods": []string{
|
||||
"GET",
|
||||
"POST",
|
||||
},
|
||||
"allow_headers": []string{
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
},
|
||||
"expose_headers": []string{
|
||||
"X-Custom-Header",
|
||||
},
|
||||
"allow_credentials": true,
|
||||
"max_age": 86400,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:默认值配置
|
||||
var defaultConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本 CORS 配置解析
|
||||
t.Run("basic cors config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试允许所有 Origin 的配置解析
|
||||
t.Run("allow all origins config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(allowAllOriginsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带模式匹配的配置解析
|
||||
t.Run("pattern match config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(patternMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试允许凭据的配置解析
|
||||
t.Run("credentials config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(credentialsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试默认值配置解析
|
||||
t.Run("default config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(defaultConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试简单 CORS 请求头处理
|
||||
t.Run("simple cors request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含 Origin
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://example.com"},
|
||||
})
|
||||
|
||||
// 有效的 CORS 请求应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试预检请求头处理
|
||||
t.Run("preflight request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置预检请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://example.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
{"access-control-request-headers", "Content-Type, Authorization"},
|
||||
})
|
||||
|
||||
// 预检请求应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无效 Origin 的请求头处理
|
||||
t.Run("invalid origin request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含无效的 Origin
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://invalid.com"},
|
||||
})
|
||||
|
||||
// 无效的 CORS 请求应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试允许所有 Origin 的请求头处理
|
||||
t.Run("allow all origins request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(allowAllOriginsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含任意 Origin
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://any-domain.com"},
|
||||
})
|
||||
|
||||
// 允许所有 Origin 的配置应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试模式匹配的请求头处理
|
||||
t.Run("pattern match request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(patternMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含匹配模式的 Origin
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://sub.example.com"},
|
||||
})
|
||||
|
||||
// 匹配模式的 Origin 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非 CORS 请求头处理
|
||||
t.Run("non-cors request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含 Origin
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 非 CORS 请求应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试 CORS 响应头处理
|
||||
t.Run("cors response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://example.com"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了 CORS 响应头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "access-control-allow-origin"))
|
||||
require.True(t, test.HasHeader(responseHeaders, "access-control-expose-headers"))
|
||||
|
||||
// 对于简单请求,不添加 AllowMethods 和 AllowHeaders(这些只在预检请求时添加)
|
||||
require.False(t, test.HasHeader(responseHeaders, "access-control-allow-methods"))
|
||||
require.False(t, test.HasHeader(responseHeaders, "access-control-allow-headers"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非 CORS 请求的响应头处理
|
||||
t.Run("non-cors response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头,不包含 Origin
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否没有添加 CORS 响应头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.False(t, test.HasHeader(responseHeaders, "access-control-allow-origin"))
|
||||
require.False(t, test.HasHeader(responseHeaders, "access-control-expose-headers"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试允许凭据的响应头处理
|
||||
t.Run("credentials response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(credentialsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://any-domain.com"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了允许凭据的响应头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, "access-control-allow-credentials", "true"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试预检请求的响应头处理
|
||||
t.Run("preflight response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理预检请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://example.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
{"access-control-request-headers", "Content-Type, Authorization"},
|
||||
})
|
||||
|
||||
// 预检请求应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_prefixMatchCode(t *testing.T) {
|
||||
@@ -78,3 +97,442 @@ func TestIsValidPrefixString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试配置:基本配置(老版本)
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"status_code": 200,
|
||||
"headers": []string{
|
||||
"Content-Type=application/json",
|
||||
"Hello=World",
|
||||
},
|
||||
"body": `{"hello":"world"}`,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带状态码匹配的配置(老版本)
|
||||
var statusMatchConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"status_code": 302,
|
||||
"headers": []string{
|
||||
"Location=https://example.com",
|
||||
},
|
||||
"body": "Redirect to example.com",
|
||||
"enable_on_status": []string{
|
||||
"429",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:新版本多规则配置
|
||||
var multiRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"body": `{"hello":"world 200"}`,
|
||||
"enable_on_status": []string{
|
||||
"200",
|
||||
"201",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
{
|
||||
"body": `{"hello":"world 404"}`,
|
||||
"enable_on_status": []string{
|
||||
"404",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:模糊匹配配置
|
||||
var fuzzyMatchConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"body": `{"hello":"world 200"}`,
|
||||
"enable_on_status": []string{
|
||||
"200",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
{
|
||||
"body": `{"hello":"world 40x"}`,
|
||||
"enable_on_status": []string{
|
||||
"40x",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
{
|
||||
"body": `{"hello":"world 4xx"}`,
|
||||
"enable_on_status": []string{
|
||||
"4xx",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带默认规则的配置
|
||||
var defaultRuleConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"body": `{"hello":"world default"}`,
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
{
|
||||
"body": `{"hello":"world 404"}`,
|
||||
"enable_on_status": []string{
|
||||
"404",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:纯默认规则配置(没有 enable_on_status)
|
||||
var pureDefaultRuleConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"body": `{"hello":"world pure default"}`,
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
"key2=value2",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置
|
||||
var invalidConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"body": `{"hello":"world"}`,
|
||||
"enable_on_status": []string{
|
||||
"invalid",
|
||||
},
|
||||
"headers": []string{
|
||||
"key1=value1",
|
||||
},
|
||||
"status_code": 200,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析(老版本)
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试状态码匹配配置解析(老版本)
|
||||
t.Run("status match config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(statusMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试多规则配置解析(新版本)
|
||||
t.Run("multi rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试模糊匹配配置解析
|
||||
t.Run("fuzzy match config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(fuzzyMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带默认规则的配置解析
|
||||
t.Run("default rule config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(defaultRuleConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效配置解析
|
||||
t.Run("invalid config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本配置的请求头处理(应该使用默认规则)
|
||||
t.Run("basic config request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于没有 enable_on_status 规则,应该使用默认规则并返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带状态码匹配的请求头处理(不应该在请求头阶段处理)
|
||||
t.Run("status match config request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(statusMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于有 enable_on_status 规则,应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多规则配置的请求头处理(不应该在请求头阶段处理)
|
||||
t.Run("multi rules config request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于有 enable_on_status 规则,应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带默认规则的请求头处理(由于有 enable_on_status 规则,应该返回 ActionContinue)
|
||||
t.Run("default rule config request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(defaultRuleConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于有 enable_on_status 规则,应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试纯默认规则的请求头处理(应该使用默认规则并返回 ActionPause)
|
||||
t.Run("pure default rule config request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(pureDefaultRuleConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于没有 enable_on_status 规则,应该使用默认规则并返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试状态码匹配的响应头处理
|
||||
t.Run("status match response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(statusMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头,状态码为 429(应该匹配规则)
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "429"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多规则配置的响应头处理
|
||||
t.Run("multi rules response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头,状态码为 200(应该匹配第一个规则)
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试模糊匹配的响应头处理
|
||||
t.Run("fuzzy match response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(fuzzyMatchConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头,状态码为 404(应该匹配 4xx 规则)
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "404"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不匹配状态码的响应头处理
|
||||
t.Run("no match response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头,状态码为 500(不应该匹配任何规则)
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "500"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
// 应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
@@ -15,8 +15,10 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
372
plugins/wasm-go/extensions/de-graphql/main_test.go
Normal file
372
plugins/wasm-go/extensions/de-graphql/main_test.go
Normal file
@@ -0,0 +1,372 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"gql": `query ($owner: String!, $name: String!) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
name
|
||||
forkCount
|
||||
description
|
||||
}
|
||||
}`,
|
||||
"endpoint": "/graphql",
|
||||
"timeout": 5000,
|
||||
"domain": "api.github.com",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带不同类型变量的配置
|
||||
var multiTypeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"gql": `query ($id: Int!, $enabled: Boolean!, $score: Float!, $title: String!) {
|
||||
item(id: $id, enabled: $enabled, score: $score, title: $title) {
|
||||
id
|
||||
name
|
||||
status
|
||||
}
|
||||
}`,
|
||||
"endpoint": "/api/graphql",
|
||||
"timeout": 3000,
|
||||
"domain": "example.com",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:可选参数配置
|
||||
var optionalParamsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"gql": `query ($id: String, $name: String) {
|
||||
user(id: $id, name: $name) {
|
||||
id
|
||||
name
|
||||
email
|
||||
}
|
||||
}`,
|
||||
"endpoint": "/graphql",
|
||||
"timeout": 5000,
|
||||
"domain": "api.example.com",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:默认值配置
|
||||
var defaultConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"gql": `query ($owner: String!) {
|
||||
repository(owner: $owner) {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效 GraphQL 配置
|
||||
var invalidGqlConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"gql": "",
|
||||
"endpoint": "/graphql",
|
||||
"timeout": 5000,
|
||||
"domain": "api.github.com",
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试多类型变量配置解析
|
||||
t.Run("multi type config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiTypeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试可选参数配置解析
|
||||
t.Run("optional params config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(optionalParamsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试默认值配置解析
|
||||
t.Run("default config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(defaultConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效 GraphQL 配置解析
|
||||
t.Run("invalid gql config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidGqlConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本 GraphQL 查询请求头处理
|
||||
t.Run("basic graphql query", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?owner=alibaba&name=higress"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Bearer token123"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 GraphQL 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 GraphQL 服务的HTTP调用响应
|
||||
// 模拟成功响应(200状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"data":{"repository":{"name":"higress","forkCount":149,"description":"Next-generation Cloud Native Gateway"}}}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多类型变量查询请求头处理
|
||||
t.Run("multi type variables query", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multiTypeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含不同类型的查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?id=123&enabled=true&score=95.5&title=Test Item"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 GraphQL 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 GraphQL 服务的HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"data":{"item":{"id":123,"name":"Test Item","status":"active"}}}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试可选参数查询请求头处理
|
||||
t.Run("optional parameters query", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(optionalParamsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,只包含部分查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?name=john"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 GraphQL 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 GraphQL 服务的HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"data":{"user":{"id":"user123","name":"john","email":"john@example.com"}}}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试无查询参数的请求头处理
|
||||
t.Run("no query parameters", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,不包含查询参数
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 GraphQL 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 GraphQL 服务的HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"data":{"repository":null}}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 POST 请求的请求头处理
|
||||
t.Run("POST request", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,POST 请求
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?owner=alibaba&name=higress"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部 GraphQL 服务,应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
// 模拟外部 GraphQL 服务的HTTP调用响应
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"data":{"repository":{"name":"higress","forkCount":149,"description":"Next-generation Cloud Native Gateway"}}}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试请求体处理
|
||||
t.Run("request body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?owner=alibaba&name=higress"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 处理请求体
|
||||
requestBody := `{"additional": "data"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 请求体处理应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应头处理
|
||||
t.Run("response headers processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?owner=alibaba&name=higress"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 响应头处理应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应体处理
|
||||
t.Run("response body processing", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api?owner=alibaba&name=higress"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 处理响应体
|
||||
responseBody := `{"data":{"repository":{"name":"higress","forkCount":149,"description":"Next-generation Cloud Native Gateway"}}}`
|
||||
action := host.CallOnHttpResponseBody([]byte(responseBody))
|
||||
|
||||
// 响应体处理应该返回 ActionContinue
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,8 +5,8 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
@@ -15,8 +15,10 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
529
plugins/wasm-go/extensions/ext-auth/main_test.go
Normal file
529
plugins/wasm-go/extensions/ext-auth/main_test.go
Normal file
@@ -0,0 +1,529 @@
|
||||
// Copyright (c) 2024 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本 envoy 模式配置
|
||||
var basicEnvoyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path_prefix": "/auth",
|
||||
},
|
||||
"timeout": 1000,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:forward_auth 模式配置
|
||||
var forwardAuthConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "forward_auth",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path": "/auth",
|
||||
"request_method": "POST",
|
||||
},
|
||||
"timeout": 1000,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带请求头过滤的配置
|
||||
var headersConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path_prefix": "/auth",
|
||||
},
|
||||
"timeout": 1000,
|
||||
"authorization_request": map[string]interface{}{
|
||||
"allowed_headers": []map[string]interface{}{
|
||||
{"exact": "x-auth-version"},
|
||||
{"prefix": "x-custom"},
|
||||
},
|
||||
"headers_to_add": map[string]interface{}{
|
||||
"x-envoy-header": "true",
|
||||
},
|
||||
},
|
||||
"authorization_response": map[string]interface{}{
|
||||
"allowed_upstream_headers": []map[string]interface{}{
|
||||
{"exact": "x-user-id"},
|
||||
{"exact": "x-auth-version"},
|
||||
},
|
||||
"allowed_client_headers": []map[string]interface{}{
|
||||
{"exact": "x-auth-failed"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带请求体的配置
|
||||
var withRequestBodyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path_prefix": "/auth",
|
||||
},
|
||||
"timeout": 1000,
|
||||
"authorization_request": map[string]interface{}{
|
||||
"with_request_body": true,
|
||||
"max_request_body_bytes": 1024,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带黑白名单的配置
|
||||
var matchRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path_prefix": "/auth",
|
||||
},
|
||||
"timeout": 1000,
|
||||
},
|
||||
"match_type": "whitelist",
|
||||
"match_list": []map[string]interface{}{
|
||||
{
|
||||
"match_rule_domain": "api.example.com",
|
||||
"match_rule_path": "/public",
|
||||
"match_rule_type": "prefix",
|
||||
},
|
||||
{
|
||||
"match_rule_method": []string{"GET"},
|
||||
"match_rule_path": "/health",
|
||||
"match_rule_type": "exact",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:失败模式配置
|
||||
var failureModeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"http_service": map[string]interface{}{
|
||||
"endpoint_mode": "envoy",
|
||||
"endpoint": map[string]interface{}{
|
||||
"service_name": "ext-auth.backend.svc.cluster.local",
|
||||
"service_port": 8090,
|
||||
"path_prefix": "/auth",
|
||||
},
|
||||
"timeout": 1000,
|
||||
},
|
||||
"failure_mode_allow": true,
|
||||
"failure_mode_allow_header_add": true,
|
||||
"status_on_error": 500,
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本 envoy 模式配置解析
|
||||
t.Run("basic envoy config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicEnvoyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试 forward_auth 模式配置解析
|
||||
t.Run("forward auth config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(forwardAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带请求头过滤的配置解析
|
||||
t.Run("headers config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headersConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带请求体的配置解析
|
||||
t.Run("with request body config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(withRequestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带黑白名单的配置解析
|
||||
t.Run("match rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(matchRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试失败模式配置解析
|
||||
t.Run("failure mode config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(failureModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本 envoy 模式请求头处理
|
||||
t.Run("basic envoy request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicEnvoyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
{"x-custom-header", "value"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟成功响应(200状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"x-user-id", "user123"},
|
||||
{"x-auth-version", "1.0"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"authorized": true, "user": "user123"}`))
|
||||
|
||||
// 验证请求是否被恢复
|
||||
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction())
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 forward_auth 模式请求头处理
|
||||
t.Run("forward auth request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(forwardAuthConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "GET"},
|
||||
{"authorization", "Bearer token123"},
|
||||
{"x-custom-header", "value"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟成功响应(200状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "200"},
|
||||
{"x-user-id", "user456"},
|
||||
{"x-auth-version", "1.0"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"authorized": true, "user": "user456"}`))
|
||||
|
||||
// 验证请求是否被恢复
|
||||
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction())
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带请求头过滤的请求头处理
|
||||
t.Run("headers filtered request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(headersConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
{"x-auth-version", "1.0"},
|
||||
{"x-custom-header", "value"},
|
||||
{"x-ignored-header", "ignored"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带请求体的请求头处理
|
||||
t.Run("with request body request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(withRequestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 由于需要读取请求体,应该返回 HeaderStopIteration
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试黑白名单匹配的请求头处理
|
||||
t.Run("match rules request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(matchRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 测试白名单匹配的请求(应该跳过认证)
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/public/users"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
|
||||
// 白名单匹配的请求应该直接通过
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试黑白名单不匹配的请求头处理
|
||||
t.Run("match rules no match request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(matchRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 测试不在白名单中的请求(应该进行认证)
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/private/users"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
|
||||
// 不在白名单中的请求应该进行认证
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟认证失败响应(401状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "401"},
|
||||
{"x-auth-failed", "true"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"authorized": false, "message": "Invalid token"}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试认证失败的情况
|
||||
t.Run("authentication failed", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicEnvoyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer invalid-token"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟认证失败响应(403状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "403"},
|
||||
{"x-auth-failed", "true"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"authorized": false, "message": "Access denied"}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试认证服务返回5xx错误的情况
|
||||
t.Run("authentication service error", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicEnvoyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟服务错误响应(500状态码)
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "500"},
|
||||
{"x-auth-error", "true"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"error": "Internal server error"}`))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试失败模式允许的情况
|
||||
t.Run("failure mode allow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(failureModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
})
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 HeaderStopAllIterationAndWatermark
|
||||
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
|
||||
|
||||
// 模拟外部认证服务的HTTP调用响应
|
||||
// 模拟服务错误响应(500状态码),但由于配置了失败模式允许,请求应该通过
|
||||
host.CallOnHttpCall([][2]string{
|
||||
{":status", "500"},
|
||||
{"x-auth-error", "true"},
|
||||
{"content-type", "application/json"},
|
||||
}, []byte(`{"error": "Internal server error"}`))
|
||||
|
||||
// 验证请求是否被恢复(失败模式允许的情况下)
|
||||
require.Equal(t, types.ActionContinue, host.GetHttpStreamAction())
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试带请求体的请求体处理
|
||||
t.Run("with request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(withRequestBodyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 处理请求体
|
||||
requestBody := `{"username": "test", "password": "password123"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 由于需要调用外部认证服务,应该返回 DataStopIterationAndBuffer
|
||||
require.Equal(t, types.DataStopIterationAndBuffer, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试不带请求体的请求体处理
|
||||
t.Run("without request body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicEnvoyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/users"},
|
||||
{":method", "POST"},
|
||||
{"authorization", "Bearer token123"},
|
||||
})
|
||||
|
||||
// 处理请求体
|
||||
requestBody := `{"username": "test", "password": "password123"}`
|
||||
action := host.CallOnHttpRequestBody([]byte(requestBody))
|
||||
|
||||
// 不带请求体配置的请求应该直接通过
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -7,8 +7,8 @@ toolchain go1.24.4
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
@@ -16,8 +16,10 @@ require (
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,14 +4,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -21,6 +24,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
569
plugins/wasm-go/extensions/frontend-gray/main_test.go
Normal file
569
plugins/wasm-go/extensions/frontend-gray/main_test.go
Normal file
@@ -0,0 +1,569 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本灰度配置
|
||||
var basicGrayConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"grayKey": "userid",
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000001",
|
||||
"00000005",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "beta-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000002",
|
||||
"00000003",
|
||||
},
|
||||
"grayTagKey": "level",
|
||||
"grayTagValue": []string{
|
||||
"level3",
|
||||
"level5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"baseDeployment": map[string]interface{}{
|
||||
"version": "base",
|
||||
"backendVersion": "base-backend",
|
||||
},
|
||||
"grayDeployments": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"version": "gray",
|
||||
"enabled": true,
|
||||
"backendVersion": "gray-backend",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:按比例灰度配置
|
||||
var weightGrayConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"grayKey": "userid",
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000001",
|
||||
"00000005",
|
||||
},
|
||||
},
|
||||
},
|
||||
"baseDeployment": map[string]interface{}{
|
||||
"version": "base",
|
||||
"backendVersion": "base-backend",
|
||||
},
|
||||
"grayDeployments": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"version": "gray",
|
||||
"enabled": true,
|
||||
"backendVersion": "gray-backend",
|
||||
"weight": 80,
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带重写的配置
|
||||
var rewriteConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"grayKey": "userid",
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000001",
|
||||
"00000005",
|
||||
},
|
||||
},
|
||||
},
|
||||
"rewrite": map[string]interface{}{
|
||||
"host": "frontend-gray.example.com",
|
||||
"indexRouting": map[string]interface{}{
|
||||
"/app1": "/mfe/app1/{version}/index.html",
|
||||
"/": "/mfe/app1/{version}/index.html",
|
||||
},
|
||||
"fileRouting": map[string]interface{}{
|
||||
"/": "/mfe/app1/{version}",
|
||||
"/app1/": "/mfe/app1/{version}",
|
||||
},
|
||||
},
|
||||
"baseDeployment": map[string]interface{}{
|
||||
"version": "base",
|
||||
"backendVersion": "base-backend",
|
||||
},
|
||||
"grayDeployments": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"version": "gray",
|
||||
"enabled": true,
|
||||
"backendVersion": "gray-backend",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带注入的配置
|
||||
var injectionConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"grayKey": "userid",
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000001",
|
||||
"00000005",
|
||||
},
|
||||
},
|
||||
},
|
||||
"baseDeployment": map[string]interface{}{
|
||||
"version": "base",
|
||||
"backendVersion": "base-backend",
|
||||
},
|
||||
"grayDeployments": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"version": "gray",
|
||||
"enabled": true,
|
||||
"backendVersion": "gray-backend",
|
||||
},
|
||||
},
|
||||
"injection": map[string]interface{}{
|
||||
"head": []string{
|
||||
"<script>console.log('Header')</script>",
|
||||
},
|
||||
"body": map[string]interface{}{
|
||||
"first": []string{
|
||||
"<script>console.log('hello world before')</script>",
|
||||
},
|
||||
"last": []string{
|
||||
"<script>console.log('hello world after')</script>",
|
||||
},
|
||||
},
|
||||
"globalConfig": map[string]interface{}{
|
||||
"enabled": true,
|
||||
"key": "TEST_CONFIG",
|
||||
"featureKey": "FEATURE_STATUS",
|
||||
"value": "testValue",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:带跳过路径的配置
|
||||
var skippedPathsConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"grayKey": "userid",
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"grayKeyValue": []string{
|
||||
"00000001",
|
||||
"00000005",
|
||||
},
|
||||
},
|
||||
},
|
||||
"skippedPaths": []string{
|
||||
"/api/**",
|
||||
"/static/**",
|
||||
},
|
||||
"indexPaths": []string{
|
||||
"/app1/**",
|
||||
"/index.html",
|
||||
},
|
||||
"baseDeployment": map[string]interface{}{
|
||||
"version": "base",
|
||||
"backendVersion": "base-backend",
|
||||
},
|
||||
"grayDeployments": []map[string]interface{}{
|
||||
{
|
||||
"name": "inner-user",
|
||||
"version": "gray",
|
||||
"enabled": true,
|
||||
"backendVersion": "gray-backend",
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本灰度配置解析
|
||||
t.Run("basic gray config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试按比例灰度配置解析
|
||||
t.Run("weight gray config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(weightGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带重写的配置解析
|
||||
t.Run("rewrite config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(rewriteConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带注入的配置解析
|
||||
t.Run("injection config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(injectionConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试带跳过路径的配置解析
|
||||
t.Run("skipped paths config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(skippedPathsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本灰度请求头处理
|
||||
t.Run("basic gray request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置请求头,包含灰度用户 ID
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了版本标签头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeader(requestHeaders, "x-higress-tag"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试按比例灰度请求头处理
|
||||
t.Run("weight gray request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(weightGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了版本标签头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeader(requestHeaders, "x-higress-tag"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带重写的请求头处理
|
||||
t.Run("rewrite request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(rewriteConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/app1"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了版本标签头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeader(requestHeaders, "x-higress-tag"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试跳过路径的请求头处理
|
||||
t.Run("skipped paths request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(skippedPathsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 测试跳过路径
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/users"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 跳过路径不应该添加版本标签头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.False(t, test.HasHeader(requestHeaders, "x-higress-tag"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非 HTML 请求的请求头处理
|
||||
t.Run("non-html request headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 非 HTML 请求也应该添加版本标签头
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeader(requestHeaders, "x-higress-tag"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeader(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本灰度响应头处理
|
||||
t.Run("basic gray response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/html"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证是否添加了 Set-Cookie 头
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeader(responseHeaders, "Set-Cookie"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试 404 状态码的响应头处理
|
||||
t.Run("404 status response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "404"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非首页请求的响应头处理
|
||||
t.Run("non-index response headers", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试基本灰度响应体处理
|
||||
t.Run("basic gray response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/html"},
|
||||
})
|
||||
|
||||
// 处理响应体
|
||||
htmlBody := "<html><head><title>Test</title></head><body><h1>Hello World</h1></body></html>"
|
||||
action := host.CallOnHttpResponseBody([]byte(htmlBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试带注入的响应体处理
|
||||
t.Run("injection response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(injectionConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "text/html"},
|
||||
})
|
||||
|
||||
// 处理响应体
|
||||
htmlBody := "<html><head><title>Test</title></head><body><h1>Hello World</h1></body></html>"
|
||||
action := host.CallOnHttpResponseBody([]byte(htmlBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试非 HTML 请求的响应体处理
|
||||
t.Run("non-html response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicGrayConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理请求头
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/data"},
|
||||
{":method", "GET"},
|
||||
{"cookie", "userid=00000001"},
|
||||
})
|
||||
|
||||
// 处理响应头
|
||||
host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
|
||||
// 处理响应体
|
||||
jsonBody := `{"message": "Hello World"}`
|
||||
action := host.CallOnHttpResponseBody([]byte(jsonBody))
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,15 +5,23 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require github.com/tidwall/resp v0.1.1 // indirect
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
527
plugins/wasm-go/extensions/gw-error-format/main_test.go
Normal file
527
plugins/wasm-go/extensions/gw-error-format/main_test.go
Normal file
@@ -0,0 +1,527 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 测试配置:基本配置
|
||||
var basicConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"statuscode": "403",
|
||||
"responsebody": "RBAC: access denied",
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
"statuscode": "200",
|
||||
"responsebody": `{"code":401,"message":"User is not authenticated"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"set_header": []map[string]interface{}{
|
||||
{"content-type": "application/json;charset=UTF-8"},
|
||||
{"custom-header": "test-value"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:多个规则配置
|
||||
var multipleRulesConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"statuscode": "403",
|
||||
"responsebody": "RBAC: access denied",
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
"statuscode": "200",
|
||||
"responsebody": `{"code":401,"message":"User is not authenticated"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"statuscode": "503",
|
||||
"responsebody": "no healthy upstream",
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
"statuscode": "200",
|
||||
"responsebody": `{"code":404,"message":"No Healthy Service"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"set_header": []map[string]interface{}{
|
||||
{"content-type": "application/json;charset=UTF-8"},
|
||||
{"access-control-allow-origin": "*"},
|
||||
{"access-control-allow-methods": "GET,POST,PUT,DELETE"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(缺少 match.statuscode)
|
||||
var invalidConfigMissingStatusCode = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"responsebody": "RBAC: access denied",
|
||||
// 缺少 statuscode
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
"statuscode": "200",
|
||||
"responsebody": `{"code":401,"message":"User is not authenticated"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:无效配置(缺少 replace.statuscode)
|
||||
var invalidConfigMissingReplaceStatusCode = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"statuscode": "403",
|
||||
"responsebody": "RBAC: access denied",
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
// 缺少 statuscode
|
||||
"responsebody": `{"code":401,"message":"User is not authenticated"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:空配置
|
||||
var emptyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{})
|
||||
return data
|
||||
}()
|
||||
|
||||
// 测试配置:只有规则,没有响应头
|
||||
var rulesOnlyConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"rules": []map[string]interface{}{
|
||||
{
|
||||
"match": map[string]interface{}{
|
||||
"statuscode": "403",
|
||||
"responsebody": "RBAC: access denied",
|
||||
},
|
||||
"replace": map[string]interface{}{
|
||||
"statuscode": "200",
|
||||
"responsebody": `{"code":401,"message":"User is not authenticated"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
// 测试基本配置解析
|
||||
t.Run("basic config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试多个规则配置解析
|
||||
t.Run("multiple rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multipleRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试无效配置 - 缺少 match.statuscode
|
||||
t.Run("invalid config - missing match.statuscode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidConfigMissingStatusCode)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试无效配置 - 缺少 replace.statuscode
|
||||
t.Run("invalid config - missing replace.statuscode", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidConfigMissingReplaceStatusCode)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
|
||||
// 测试空配置解析
|
||||
t.Run("empty config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
// 测试只有规则的配置解析
|
||||
t.Run("rules only config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(rulesOnlyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseHeader(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试状态码匹配 - 没有 x-envoy-upstream-service-time 头
|
||||
t.Run("status code match - no upstream service time header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头,状态码为 403,但没有 x-envoy-upstream-service-time 头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证状态码是否被替换
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "200" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should be replaced to 200")
|
||||
|
||||
// 验证自定义响应头是否被添加
|
||||
customHeaderFound := false
|
||||
contentTypeHeaderFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == "custom-header" && header[1] == "test-value" {
|
||||
customHeaderFound = true
|
||||
}
|
||||
if header[0] == "content-type" && header[1] == "application/json;charset=UTF-8" {
|
||||
contentTypeHeaderFound = true
|
||||
}
|
||||
}
|
||||
require.True(t, customHeaderFound, "Custom header should be added")
|
||||
require.True(t, contentTypeHeaderFound, "Content-Type header should be replaced")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试状态码匹配 - 有 x-envoy-upstream-service-time 头(不生效)
|
||||
t.Run("status code match - with upstream service time header", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头,状态码为 403,且有 x-envoy-upstream-service-time 头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
{"x-envoy-upstream-service-time", "123"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 由于有 x-envoy-upstream-service-time 头,插件不应该生效
|
||||
// 状态码应该保持为 403
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "403" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should remain 403 when upstream service time header exists")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试状态码不匹配
|
||||
t.Run("status code no match", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头,状态码为 404,不匹配规则
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "404"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 状态码应该保持为 404
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "404" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should remain 404 when no rule matches")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多个规则配置
|
||||
t.Run("multiple rules config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multipleRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 测试第一个规则:403 -> 200
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证状态码是否被替换
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "200" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should be replaced to 200 for 403 match")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空配置
|
||||
t.Run("empty config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 设置响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 由于没有规则,状态码应该保持为 403
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "403" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should remain 403 when no rules configured")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpResponseBody(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// 测试响应体匹配和替换
|
||||
t.Run("response body match and replace", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理响应体
|
||||
originalBody := []byte("RBAC: access denied")
|
||||
action = host.CallOnHttpResponseBody(originalBody)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体是否被替换
|
||||
responseBody := host.GetResponseBody()
|
||||
expectedBody := `{"code":401,"message":"User is not authenticated"}`
|
||||
require.Equal(t, expectedBody, string(responseBody), "Response body should be replaced")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试响应体不匹配
|
||||
t.Run("response body no match", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理不匹配的响应体
|
||||
originalBody := []byte("Different error message")
|
||||
action = host.CallOnHttpResponseBody(originalBody)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 响应体应该保持不变
|
||||
responseBody := host.GetResponseBody()
|
||||
require.Equal(t, "Different error message", string(responseBody), "Response body should remain unchanged")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试多个规则的响应体匹配
|
||||
t.Run("multiple rules response body match", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(multipleRulesConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "503"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理响应体
|
||||
originalBody := []byte("no healthy upstream")
|
||||
action = host.CallOnHttpResponseBody(originalBody)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 验证响应体是否被替换
|
||||
responseBody := host.GetResponseBody()
|
||||
expectedBody := `{"code":404,"message":"No Healthy Service"}`
|
||||
require.Equal(t, expectedBody, string(responseBody), "Response body should be replaced for 503 match")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
// 测试空配置的响应体处理
|
||||
t.Run("empty config response body", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(emptyConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 先处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 处理响应体
|
||||
originalBody := []byte("RBAC: access denied")
|
||||
action = host.CallOnHttpResponseBody(originalBody)
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 由于没有规则,响应体应该保持不变
|
||||
responseBody := host.GetResponseBody()
|
||||
require.Equal(t, "RBAC: access denied", string(responseBody), "Response body should remain unchanged when no rules configured")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompleteFlow(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("complete response flow", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// 1. 处理响应头
|
||||
action := host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "403"},
|
||||
{"content-type", "text/plain"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 2. 处理响应体
|
||||
originalBody := []byte("RBAC: access denied")
|
||||
action = host.CallOnHttpResponseBody(originalBody)
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// 3. 验证完整的响应处理结果
|
||||
// 验证状态码
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
statusCodeFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == ":status" && header[1] == "200" {
|
||||
statusCodeFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, statusCodeFound, "Status code should be replaced to 200")
|
||||
|
||||
// 验证响应体
|
||||
responseBody := host.GetResponseBody()
|
||||
expectedBody := `{"code":401,"message":"User is not authenticated"}`
|
||||
require.Equal(t, expectedBody, string(responseBody), "Response body should be replaced")
|
||||
|
||||
// 验证自定义响应头
|
||||
customHeaderFound := false
|
||||
for _, header := range responseHeaders {
|
||||
if header[0] == "custom-header" && header[1] == "test-value" {
|
||||
customHeaderFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, customHeaderFound, "Custom header should be added")
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
43
plugins/wasm-go/extensions/hello-world/main_test.go
Normal file
43
plugins/wasm-go/extensions/hello-world/main_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(nil)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/test"},
|
||||
{":method", "GET"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.Equal(t, uint32(http.StatusOK), localResponse.StatusCode)
|
||||
require.Equal(t, "hello world", string(localResponse.Data))
|
||||
})
|
||||
}
|
||||
@@ -5,14 +5,20 @@ go 1.24.1
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
||||
github.com/higress-group/wasm-go v1.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.0 h1:4Ik5n3FsJ5+r13KLQl2ky+8NuAE8dfWQwoKxXYD2KAw=
|
||||
github.com/higress-group/wasm-go v1.0.0/go.mod h1:ODBV27sjmhIW8Cqv3R74EUcTnbdkE69bmXBQFuRkY1M=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -19,5 +22,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user