// 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" ) // 测试配置:启用头部验证,使用Draft7 var headerValidationConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "header_schema": `{ "type": "object", "properties": { "content-type": {"type": "string"}, "authorization": {"type": "string"} }, "required": ["content-type"] }`, "enable_oas3": true, "rejected_code": 400, "rejected_msg": "Invalid headers", }) return data }() // 测试配置:启用体部验证,使用Draft4 var bodyValidationConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "body_schema": `{ "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0} }, "required": ["name"] }`, "enable_swagger": true, "rejected_code": 422, "rejected_msg": "Invalid request body", }) return data }() // 测试配置:同时启用头部和体部验证 var bothValidationConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "header_schema": `{ "type": "object", "properties": { "content-type": {"type": "string"} }, "required": ["content-type"] }`, "body_schema": `{ "type": "object", "properties": { "id": {"type": "integer"} } }`, "enable_oas3": true, "rejected_code": 400, "rejected_msg": "Validation failed", }) return data }() // 测试配置:禁用所有验证 var noValidationConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "rejected_code": 403, "rejected_msg": "Access denied", }) return data }() // 测试配置:无效的JSON Schema var invalidSchemaConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "header_schema": `{ "type": "invalid_type", "properties": {} }`, "enable_oas3": true, }) return data }() // 测试配置:同时启用swagger和oas3(应该失败) var conflictingDraftConfig = func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "header_schema": `{"type": "object"}`, "enable_swagger": true, "enable_oas3": true, }) return data }() func TestParseConfig(t *testing.T) { test.RunGoTest(t, func(t *testing.T) { // 测试头部验证配置 t.Run("header validation config", func(t *testing.T) { host, status := test.NewTestHost(headerValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.True(t, validationConfig.enableHeaderSchema) require.False(t, validationConfig.enableBodySchema) require.Equal(t, uint32(400), validationConfig.rejectedCode) require.Equal(t, "Invalid headers", validationConfig.rejectedMsg) require.NotNil(t, validationConfig.compiler) }) // 测试体部验证配置 t.Run("body validation config", func(t *testing.T) { host, status := test.NewTestHost(bodyValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.False(t, validationConfig.enableHeaderSchema) require.True(t, validationConfig.enableBodySchema) require.Equal(t, uint32(422), validationConfig.rejectedCode) require.Equal(t, "Invalid request body", validationConfig.rejectedMsg) require.NotNil(t, validationConfig.compiler) }) // 测试同时启用头部和体部验证 t.Run("both validation config", func(t *testing.T) { host, status := test.NewTestHost(bothValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.True(t, validationConfig.enableHeaderSchema) require.True(t, validationConfig.enableBodySchema) require.Equal(t, uint32(400), validationConfig.rejectedCode) require.Equal(t, "Validation failed", validationConfig.rejectedMsg) require.NotNil(t, validationConfig.compiler) }) // 测试禁用所有验证 t.Run("no validation config", func(t *testing.T) { host, status := test.NewTestHost(noValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.False(t, validationConfig.enableHeaderSchema) require.False(t, validationConfig.enableBodySchema) require.Equal(t, uint32(403), validationConfig.rejectedCode) require.Equal(t, "Access denied", validationConfig.rejectedMsg) require.NotNil(t, validationConfig.compiler) }) // 测试无效的JSON Schema t.Run("invalid schema config", func(t *testing.T) { host, status := test.NewTestHost(invalidSchemaConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.True(t, validationConfig.enableHeaderSchema) require.False(t, validationConfig.enableBodySchema) }) // 测试冲突的draft版本配置 t.Run("conflicting draft config", func(t *testing.T) { host, status := test.NewTestHost(conflictingDraftConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusFailed, status) }) }) } func TestOnHttpRequestHeaders(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试有效的请求头 t.Run("valid headers", func(t *testing.T) { host, status := test.NewTestHost(headerValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, {"content-type", "application/json"}, {"authorization", "Bearer token123"}, }) require.Equal(t, types.ActionContinue, action) require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.Nil(t, localResponse, "Valid headers should not be rejected") host.CompleteHttp() }) // 测试无效的请求头(缺少必需的content-type) t.Run("invalid headers - missing required", func(t *testing.T) { host, status := test.NewTestHost(headerValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, {"authorization", "Bearer token123"}, // 缺少 content-type }) require.Equal(t, types.ActionPause, action) require.Equal(t, types.ActionPause, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.NotNil(t, localResponse) require.Equal(t, uint32(400), localResponse.StatusCode) require.Equal(t, "Invalid headers", string(localResponse.Data)) host.CompleteHttp() }) // 测试禁用头部验证 t.Run("header validation disabled", func(t *testing.T) { host, status := test.NewTestHost(noValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "GET"}, // 没有验证规则,应该继续 }) require.Equal(t, types.ActionContinue, action) require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.Nil(t, localResponse) host.CompleteHttp() }) }) } func TestOnHttpRequestBody(t *testing.T) { test.RunTest(t, func(t *testing.T) { // 测试有效的请求体 t.Run("valid body", func(t *testing.T) { host, status := test.NewTestHost(bodyValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先调用头部处理 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, }) require.Equal(t, types.ActionContinue, action) // 测试有效的请求体 validBody := `{"name": "John Doe", "age": 30}` action = host.CallOnHttpRequestBody([]byte(validBody)) require.Equal(t, types.ActionContinue, action) require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.Nil(t, localResponse, "Valid body should not be rejected") host.CompleteHttp() }) // 测试无效的请求体(缺少必需的name字段) t.Run("invalid body - missing required", func(t *testing.T) { host, status := test.NewTestHost(bodyValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先调用头部处理 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, }) require.Equal(t, types.ActionContinue, action) // 测试无效的请求体 invalidBody := `{"age": 30}` action = host.CallOnHttpRequestBody([]byte(invalidBody)) require.Equal(t, types.ActionPause, action) require.Equal(t, types.ActionPause, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.NotNil(t, localResponse) require.Equal(t, uint32(422), localResponse.StatusCode) require.Equal(t, "Invalid request body", string(localResponse.Data)) host.CompleteHttp() }) // 测试无效的请求体(age为负数) t.Run("invalid body - invalid value", func(t *testing.T) { host, status := test.NewTestHost(bodyValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先调用头部处理 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, }) require.Equal(t, types.ActionContinue, action) // 测试无效的请求体 invalidBody := `{"name": "John Doe", "age": -5}` action = host.CallOnHttpRequestBody([]byte(invalidBody)) require.Equal(t, types.ActionPause, action) require.Equal(t, types.ActionPause, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.NotNil(t, localResponse) require.Equal(t, uint32(422), localResponse.StatusCode) require.Equal(t, "Invalid request body", string(localResponse.Data)) host.CompleteHttp() }) // 测试禁用体部验证 t.Run("body validation disabled", func(t *testing.T) { host, status := test.NewTestHost(noValidationConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) // 先调用头部处理 action := host.CallOnHttpRequestHeaders([][2]string{ {":authority", "test.com"}, {":path", "/api/test"}, {":method", "POST"}, }) require.Equal(t, types.ActionContinue, action) // 测试任意请求体 anyBody := `{"invalid": "data"}` action = host.CallOnHttpRequestBody([]byte(anyBody)) require.Equal(t, types.ActionContinue, action) require.Equal(t, types.ActionContinue, host.GetHttpStreamAction()) localResponse := host.GetLocalResponse() require.Nil(t, localResponse) host.CompleteHttp() }) }) } func TestDraftVersions(t *testing.T) { test.RunGoTest(t, func(t *testing.T) { // 测试Draft4 (Swagger) t.Run("draft4 swagger", func(t *testing.T) { swaggerConfig := func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "header_schema": `{ "type": "object", "properties": { "x-api-key": {"type": "string"} } }`, "enable_swagger": true, "rejected_code": 401, "rejected_msg": "Missing API key", }) return data }() host, status := test.NewTestHost(swaggerConfig) defer host.Reset() require.Equal(t, types.OnPluginStartStatusOK, status) config, err := host.GetMatchConfig() require.NoError(t, err) require.NotNil(t, config) validationConfig := config.(*Config) require.True(t, validationConfig.enableHeaderSchema) require.Equal(t, uint32(401), validationConfig.rejectedCode) }) // 测试Draft7 (OAS3) t.Run("draft7 oas3", func(t *testing.T) { oas3Config := func() json.RawMessage { data, _ := json.Marshal(map[string]interface{}{ "body_schema": `{ "type": "object", "properties": { "email": {"type": "string", "format": "email"} } }`, "enable_oas3": true, "rejected_code": 400, "rejected_msg": "Invalid email format", }) return data }() 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) validationConfig := config.(*Config) require.True(t, validationConfig.enableBodySchema) require.Equal(t, uint32(400), validationConfig.rejectedCode) }) }) }