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:
Jingze
2026-06-15 20:33:10 +08:00
committed by GitHub
parent 3065d4e071
commit 45fc5a31bc
11 changed files with 2270 additions and 0 deletions

View File

@@ -0,0 +1,342 @@
// 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 (
"bytes"
"encoding/json"
"mime/multipart"
"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 mustConfigBytes(t *testing.T, m map[string]interface{}) json.RawMessage {
t.Helper()
b, err := json.Marshal(m)
require.NoError(t, err)
return b
}
// === Module A — onHttpRequestHeaders edges ==============================
//
// onHttpRequestHeaders is 87.5% in baseline. Existing main_test.go drives
// the bare-suffix match and miss paths but never the query-string strip
// (main.go:126-128) or the explicit `*` wildcard short-circuit
// (main.go:132). Both are part of the documented suffix-matching contract.
// Path with `?...` query string must be stripped before suffix matching.
// Without the strip, `/v1/chat/completions?stream=true` would be compared
// against `/v1/chat/completions` and miss — pin the strip.
func TestOnHttpRequestHeaders_PathWithQueryStripped(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions?stream=true&debug=1"},
{":method", "POST"},
{"content-type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
})
}
// `*` is the explicit catch-all suffix — must enable for any path. Pins the
// `suffix == "*"` short-circuit so a future refactor that switched to pure
// HasSuffix matching (where `"*"` would only match a literal `*` ending)
// would fail this test.
func TestOnHttpRequestHeaders_WildcardSuffixEnablesAnyPath(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
cfg := mustConfigBytes(t, map[string]interface{}{
"modelKey": "model",
"addProviderHeader": "x-provider",
"modelToHeader": "x-model",
"enableOnPathSuffix": []string{"*"},
})
host, status := test.NewTestHost(cfg)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/anything/at/all"},
{":method", "POST"},
{"content-type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
})
}
// === Module B — onHttpRequestBody dispatch fall-through =================
//
// onHttpRequestBody is 75.0% in baseline. The `else` fall-through at
// main.go:161-163 — content-type matched the path-suffix gate but is
// neither application/json nor multipart/form-data — is uncovered. The
// existing "do not process for unsupported content-type" test only checks
// the headers phase; nothing exercises the body-side neutral exit.
func TestOnHttpRequestBody_UnsupportedContentType_PassThrough(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "text/plain"},
})
action := host.CallOnHttpRequestBody([]byte("hello"))
require.Equal(t, types.ActionContinue, action)
// Neither header was injected.
_, found := getHeader(host.GetRequestHeaders(), "x-model")
require.False(t, found)
_, found = getHeader(host.GetRequestHeaders(), "x-provider")
require.False(t, found)
})
}
// === Module C — handleJsonBody edges ====================================
//
// handleJsonBody is 83.3%. Two reachable uncovered branches:
// - main.go:204-207 — invalid JSON → log + ActionContinue (fail-open)
// - main.go:264-266 — modelValue contains no `/` while addProviderHeader
// is configured → SplitN returns 1 element, provider rewrite skipped
// Malformed JSON body must NOT block the request. The plugin's contract is
// that a bad payload is the upstream's problem, not this filter's.
func TestHandleJsonBody_InvalidJson_PassThrough(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "application/json"},
})
action := host.CallOnHttpRequestBody([]byte("{not json"))
require.Equal(t, types.ActionContinue, action)
// Crucially: no header injection on bad body — provider rewrite
// must never fire on a body that wasn't validated.
_, found := getHeader(host.GetRequestHeaders(), "x-provider")
require.False(t, found)
_, found = getHeader(host.GetRequestHeaders(), "x-model")
require.False(t, found)
})
}
// `model: "plain-model"` (no `/` separator) with addProviderHeader set ⇒
// modelToHeader still fires (separate concern), but the provider-split
// block enters and exits via the `else` log branch without rewriting body
// or setting addProviderHeader. Pins the asymmetry between the two header
// configs in the no-slash case.
func TestHandleJsonBody_ModelWithoutSlash_AddProviderConfigured(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "application/json"},
})
action := host.CallOnHttpRequestBody([]byte(`{"model":"plain-model","messages":[]}`))
require.Equal(t, types.ActionContinue, action)
// modelToHeader fires unconditionally (line 246-248).
hv, found := getHeader(host.GetRequestHeaders(), "x-model")
require.True(t, found)
require.Equal(t, "plain-model", hv)
// addProviderHeader path skipped — no x-provider.
_, found = getHeader(host.GetRequestHeaders(), "x-provider")
require.False(t, found)
})
}
// === Module D — handleMultipartBody edges ===============================
//
// handleMultipartBody is 73.8% — the largest single gap in main.go. Four
// reachable uncovered branches addressed below; the writer.CreatePart and
// io.ReadAll error paths require host-injected i/o failures and are not
// covered.
// content-type is structurally invalid (`boundary` param with no `=value`)
// ⇒ mime.ParseMediaType returns "invalid media parameter" at main.go:273-277
// → log + ActionContinue. Distinct from the NoBoundary case below: this
// fails parsing entirely; that one parses but the param is absent.
func TestHandleMultipartBody_BadContentType(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "multipart/form-data; boundary"}, // missing `=value`
})
action := host.CallOnHttpRequestBody([]byte("ignored"))
require.Equal(t, types.ActionContinue, action)
_, found := getHeader(host.GetRequestHeaders(), "x-model")
require.False(t, found)
})
}
// Body advances past the boundary delimiter into a malformed MIME header
// (no colon) ⇒ NextPart returns a non-EOF error at main.go:296-299 →
// log + ActionContinue. Existing test only sees clean parts followed by
// EOF; the inner-loop error path was unreached.
func TestHandleMultipartBody_NextPartError(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "multipart/form-data; boundary=xxx"},
})
// Boundary delimiter is correct, but the part header that follows
// has no colon — NextPart fails with "malformed MIME header".
body := []byte("--xxx\r\nbroken header here\r\n\r\nbody\r\n--xxx--\r\n")
action := host.CallOnHttpRequestBody(body)
require.Equal(t, types.ActionContinue, action)
})
}
// content-type contains the literal `multipart/form-data` (so the dispatch
// at main.go:159 picks the multipart handler) but no `boundary` parameter
// ⇒ params["boundary"] miss at main.go:278-282 → log + ActionContinue.
func TestHandleMultipartBody_NoBoundary(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "multipart/form-data"}, // no `boundary=` param
})
action := host.CallOnHttpRequestBody([]byte("ignored"))
require.Equal(t, types.ActionContinue, action)
// No header rewrites — handler exited before the parts loop.
_, found := getHeader(host.GetRequestHeaders(), "x-model")
require.False(t, found)
})
}
// model field present but value has no `/` ⇒ provider-split block enters
// the `else` log branch (main.go:343) and falls through to the bottom
// original-write path; the model field is round-tripped unchanged. modified
// stays false so the body is not replaced. modelToHeader still fires
// (mirrors the JSON-side asymmetry above).
func TestHandleMultipartBody_ModelWithoutSlash(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
modelW, err := writer.CreateFormField("model")
require.NoError(t, err)
_, err = modelW.Write([]byte("plain-model"))
require.NoError(t, err)
promptW, err := writer.CreateFormField("prompt")
require.NoError(t, err)
_, err = promptW.Write([]byte("hi"))
require.NoError(t, err)
require.NoError(t, writer.Close())
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "multipart/form-data; boundary=" + writer.Boundary()},
})
action := host.CallOnHttpRequestBody(buf.Bytes())
require.Equal(t, types.ActionContinue, action)
// modelToHeader fires before the split logic.
hv, found := getHeader(host.GetRequestHeaders(), "x-model")
require.True(t, found)
require.Equal(t, "plain-model", hv)
// No provider — split path skipped via the else branch.
_, found = getHeader(host.GetRequestHeaders(), "x-provider")
require.False(t, found)
})
}
// addProviderHeader empty (only modelToHeader configured) ⇒ the entire
// provider-split block at main.go:316-345 is skipped; the model field is
// written through the bottom "original part" branch unchanged even with
// `provider/model` form. Pins the `addProviderHeader == ""` short-circuit.
func TestHandleMultipartBody_NoAddProviderHeader(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
cfg := mustConfigBytes(t, map[string]interface{}{
"modelKey": "model",
"modelToHeader": "x-model",
"enableOnPathSuffix": []string{
"/v1/chat/completions",
},
})
host, status := test.NewTestHost(cfg)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
modelW, err := writer.CreateFormField("model")
require.NoError(t, err)
_, err = modelW.Write([]byte("openai/gpt-4o"))
require.NoError(t, err)
require.NoError(t, writer.Close())
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"content-type", "multipart/form-data; boundary=" + writer.Boundary()},
})
action := host.CallOnHttpRequestBody(buf.Bytes())
require.Equal(t, types.ActionContinue, action)
// Header carries the FULL value — no split happened.
hv, found := getHeader(host.GetRequestHeaders(), "x-model")
require.True(t, found)
require.Equal(t, "openai/gpt-4o", hv)
// No provider header was ever requested.
_, found = getHeader(host.GetRequestHeaders(), "x-provider")
require.False(t, found)
})
}