feat(wasm-go): add wasm go plugin unit test and ci workflow (#2809)

This commit is contained in:
Jingze
2025-08-28 20:02:03 +08:00
committed by GitHub
parent 3e0a5f02a7
commit a00b810be5
138 changed files with 27695 additions and 313 deletions

View File

@@ -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, 并在本地对插件功能完成测试验证。

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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=

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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
)

View File

@@ -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=

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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)
})
})
}

View File

@@ -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

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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 (

View File

@@ -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=

View 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")
})
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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)
})
}

View File

@@ -16,4 +16,3 @@
!*/
/out
/test

View File

@@ -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

View File

@@ -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=

View File

@@ -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)
}

View 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")
})
})
}

View 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")
})
})
}

View File

File diff suppressed because it is too large Load Diff

View 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")
})
})
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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
)

View File

@@ -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=

View 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&quota=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)
})
}
}

View File

@@ -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]})

View 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)
})
}

View File

@@ -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
)

View File

@@ -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=

View 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)
})
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
)

View File

@@ -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=

View 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)
})
}

View File

@@ -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
)

View File

@@ -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=

View 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))
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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()
})
})
}

View File

@@ -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
)

View File

@@ -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=

View 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))
})
}

View File

@@ -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
)

View File

@@ -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