mirror of
https://github.com/alibaba/higress.git
synced 2026-06-26 02:35:02 +08:00
test(wasm-plugins): lift unit-test coverage to ≥90% across 9 plugins (#3879)
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com> Co-authored-by: woody <yaodiwu618@gmail.com>
This commit is contained in:
226
plugins/wasm-go/extensions/request-validation/main_extra_test.go
Normal file
226
plugins/wasm-go/extensions/request-validation/main_extra_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// Copyright (c) 2025 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"
|
||||
)
|
||||
|
||||
// === helpers =============================================================
|
||||
|
||||
func mustConfig(t *testing.T, m map[string]interface{}) json.RawMessage {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
// invalidHeaderSchemaCfg supplies a header_schema whose JSON syntax is valid
|
||||
// (so AddResource at parseConfig time succeeds) but whose schema keywords are
|
||||
// not (so Compile at request time fails). Used to drive the
|
||||
// "compile schema failed" log+continue branch shared by both
|
||||
// onHttpRequestHeaders and onHttpRequestBody.
|
||||
func invalidHeaderSchemaCfg(t *testing.T) json.RawMessage {
|
||||
return mustConfig(t, map[string]interface{}{
|
||||
"header_schema": `{"type": "invalid_type", "properties": {}}`,
|
||||
"enable_oas3": true,
|
||||
})
|
||||
}
|
||||
|
||||
func invalidBodySchemaCfg(t *testing.T) json.RawMessage {
|
||||
return mustConfig(t, map[string]interface{}{
|
||||
"body_schema": `{"type": "invalid_type", "properties": {}}`,
|
||||
"enable_oas3": true,
|
||||
})
|
||||
}
|
||||
|
||||
// === Module A — parseConfig rejected_code edge defaults ==================
|
||||
//
|
||||
// parseConfig is 94.1% in baseline. The `else` branch at main.go:117-119
|
||||
// (defaultRejectedCode = 403) is unreached because every existing fixture
|
||||
// supplies an in-range rejected_code (400 / 422 / 403). The two failure
|
||||
// modes — code unset (== 0) and code out of valid HTTP range — share the
|
||||
// same default, but only the second is a real config error worth pinning;
|
||||
// the first protects users who omit rejected_code entirely.
|
||||
|
||||
func TestParseConfig_RejectedCodeOmitted_DefaultsTo403(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
cfg := mustConfig(t, map[string]interface{}{
|
||||
"header_schema": `{"type":"object","required":["x-required"]}`,
|
||||
"enable_oas3": true,
|
||||
// rejected_code intentionally omitted — defaultRejectedCode = 403
|
||||
})
|
||||
host, status := test.NewTestHost(cfg)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
conf, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conf)
|
||||
require.Equal(t, uint32(403), conf.(*Config).rejectedCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseConfig_RejectedCodeOutOfRange_DefaultsTo403(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
cfg := mustConfig(t, map[string]interface{}{
|
||||
"header_schema": `{"type":"object","required":["x-required"]}`,
|
||||
"enable_oas3": true,
|
||||
"rejected_code": 999, // > 600 → falls to default
|
||||
})
|
||||
host, status := test.NewTestHost(cfg)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
conf, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conf)
|
||||
require.Equal(t, uint32(403), conf.(*Config).rejectedCode)
|
||||
})
|
||||
}
|
||||
|
||||
// === Module B — onHttpRequestHeaders compile-error pass-through ==========
|
||||
//
|
||||
// The compile-error branch at main.go:152-156 is uncovered: every existing
|
||||
// fixture provides a header_schema that both AddResource and Compile accept.
|
||||
// invalidSchemaConfig in main_test.go uses it only to assert parse success,
|
||||
// not to drive a request through it. Behavior contract: log error and
|
||||
// ActionContinue (fail-open) — distinct from validate-failure which is 4xx.
|
||||
|
||||
func TestOnHttpRequestHeaders_CompileFailure_PassThrough(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidHeaderSchemaCfg(t))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
})
|
||||
}
|
||||
|
||||
// === Module C — onHttpRequestBody parse / compile failure paths ==========
|
||||
//
|
||||
// onHttpRequestBody is 72.7%. Two uncovered branches:
|
||||
// - main.go:174-177 — json.Unmarshal failure (body is not JSON) → continue
|
||||
// - main.go:189-192 — Compile failure (bad schema keywords) → continue
|
||||
// Both are fail-open (request not blocked) — encoding the contract that a
|
||||
// malformed payload or operator schema bug must not turn into a 500.
|
||||
|
||||
func TestOnHttpRequestBody_InvalidJson_PassThrough(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
// Body schema fixture — valid schema, then we feed a non-JSON body.
|
||||
cfg := mustConfig(t, map[string]interface{}{
|
||||
"body_schema": `{"type":"object","required":["name"]}`,
|
||||
"enable_oas3": true,
|
||||
})
|
||||
host, status := test.NewTestHost(cfg)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// Headers must run first to advance lifecycle.
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
// Body is not JSON — Unmarshal fails and the plugin must
|
||||
// fail-open rather than 4xx (would otherwise turn arbitrary
|
||||
// upstream content into a hard reject).
|
||||
action = host.CallOnHttpRequestBody([]byte("not a json {{{"))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnHttpRequestBody_CompileFailure_PassThrough(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(invalidBodySchemaCfg(t))
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
action = host.CallOnHttpRequestBody([]byte(`{"name":"ok"}`))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
})
|
||||
}
|
||||
|
||||
// === Module D — both-schema interaction ==================================
|
||||
//
|
||||
// Coverage for `bothValidationConfig` only exercises parseConfig; nothing
|
||||
// drives a request through both header + body schema in sequence. Pin the
|
||||
// happy interaction so future refactors don't accidentally short-circuit
|
||||
// body validation when header validation passes.
|
||||
|
||||
func TestOnHttpRequestBody_AfterHeaderValidationPass_BodyValidates(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
cfg := mustConfig(t, map[string]interface{}{
|
||||
"header_schema": `{
|
||||
"type":"object",
|
||||
"properties":{"content-type":{"type":"string"}},
|
||||
"required":["content-type"]
|
||||
}`,
|
||||
"body_schema": `{
|
||||
"type":"object",
|
||||
"properties":{"id":{"type":"integer"}},
|
||||
"required":["id"]
|
||||
}`,
|
||||
"enable_oas3": true,
|
||||
"rejected_code": 400,
|
||||
"rejected_msg": "validation failed",
|
||||
})
|
||||
host, status := test.NewTestHost(cfg)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
// Headers pass.
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "POST"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
|
||||
// Body fails schema (missing required `id`).
|
||||
action = host.CallOnHttpRequestBody([]byte(`{"name":"x"}`))
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
resp := host.GetLocalResponse()
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, uint32(400), resp.StatusCode)
|
||||
require.Equal(t, "validation failed", string(resp.Data))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user