mirror of
https://github.com/alibaba/higress.git
synced 2026-06-26 02:35:02 +08:00
Signed-off-by: jingze <daijingze.djz@alibaba-inc.com> Co-authored-by: woody <yaodiwu618@gmail.com>
227 lines
8.0 KiB
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))
|
|
})
|
|
}
|