Files
higress/plugins/wasm-go/extensions/gw-error-format/main_test.go

528 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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()
})
})
}