Files
higress/plugins/wasm-go/extensions/request-validation/main_extra_test.go

227 lines
8.0 KiB
Go

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