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>
282 lines
10 KiB
Go
282 lines
10 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"
|
|
"net/http"
|
|
"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 =============================================================
|
|
|
|
// mustConfig marshals m to JSON and fails the test on error.
|
|
func mustConfig(t *testing.T, m map[string]interface{}) json.RawMessage {
|
|
t.Helper()
|
|
b, err := json.Marshal(m)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
// routeRuleConfig produces a config with two consumers and a single _rules_ entry
|
|
// scoped to "route-a". globalAuth=nil leaves global_auth unset; allow controls
|
|
// who is permitted on route-a.
|
|
func routeRuleConfig(t *testing.T, globalAuth interface{}, allow []string) json.RawMessage {
|
|
t.Helper()
|
|
cfg := map[string]interface{}{
|
|
"consumers": []map[string]interface{}{
|
|
{"name": "consumer1", "credential": "token1"},
|
|
{"name": "consumer2", "credential": "token2"},
|
|
},
|
|
"keys": []string{"x-api-key"},
|
|
"in_header": true,
|
|
"_rules_": []map[string]interface{}{
|
|
{
|
|
"_match_route_": []string{"route-a"},
|
|
"allow": allow,
|
|
},
|
|
},
|
|
}
|
|
if globalAuth != nil {
|
|
cfg["global_auth"] = globalAuth
|
|
}
|
|
return mustConfig(t, cfg)
|
|
}
|
|
|
|
// === Module A — contains helper =========================================
|
|
//
|
|
// `contains` ships at 0% in the baseline; it is reachable only through the
|
|
// allow-list branches of onHttpRequestHeaders, which Module B drives. A direct
|
|
// unit test pins behavior independently in case the wasm dispatch path
|
|
// regresses or the helper is reused elsewhere (e.g. a future plugin pulling
|
|
// in the same util pattern, mirroring basic-auth's contains).
|
|
|
|
func TestContains_Hit(t *testing.T) {
|
|
require.True(t, contains([]string{"a", "b", "c"}, "b"))
|
|
}
|
|
|
|
func TestContains_Miss(t *testing.T) {
|
|
require.False(t, contains([]string{"a", "b", "c"}, "z"))
|
|
}
|
|
|
|
func TestContains_EmptySlice(t *testing.T) {
|
|
require.False(t, contains([]string{}, "x"))
|
|
}
|
|
|
|
func TestContains_NilSlice(t *testing.T) {
|
|
require.False(t, contains(nil, "x"))
|
|
}
|
|
|
|
// === Module B — onHttpRequestHeaders allow-list branches ================
|
|
//
|
|
// Baseline onHttpRequestHeaders is 68.9%. Existing main_test.go only drives
|
|
// the early-exit and "no allow list" paths plus token-extraction failures.
|
|
// The four allow-list dispatches at main.go:344-363 — global_auth=true vs
|
|
// global_auth=false, consumer in vs not in allow — are uncovered. Each of
|
|
// these reaches `contains`, so this module also exercises the helper through
|
|
// its production caller.
|
|
|
|
// global_auth=true + route-scoped allow + consumer in allow → authenticated
|
|
// + X-Mse-Consumer header injected (main.go:344-351 success branch).
|
|
func TestOnHttpRequestHeaders_GlobalAuthTrue_RouteAllow_ConsumerAllowed(t *testing.T) {
|
|
test.RunTest(t, func(t *testing.T) {
|
|
host, status := test.NewTestHost(routeRuleConfig(t, true, []string{"consumer1"}))
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusOK, status)
|
|
require.NoError(t, host.SetRouteName("route-a"))
|
|
|
|
action := host.CallOnHttpRequestHeaders([][2]string{
|
|
{":authority", "example.com"},
|
|
{":path", "/api/test"},
|
|
{":method", "GET"},
|
|
{"x-api-key", "token1"},
|
|
})
|
|
require.Equal(t, types.ActionContinue, action)
|
|
require.Nil(t, host.GetLocalResponse())
|
|
require.True(t, test.HasHeaderWithValue(host.GetRequestHeaders(), "X-Mse-Consumer", "consumer1"))
|
|
})
|
|
}
|
|
|
|
// global_auth=true + route-scoped allow + consumer not in allow → 403 via
|
|
// deniedUnauthorizedConsumer (main.go:344-348). The credential decodes and
|
|
// authenticates against credential2Name — `consumer2` exists but is not
|
|
// permitted on route-a — distinct from the "credential not configured"
|
|
// variant already covered by main_test.go's "invalid api key" case.
|
|
func TestOnHttpRequestHeaders_GlobalAuthTrue_RouteAllow_ConsumerNotAllowed(t *testing.T) {
|
|
test.RunTest(t, func(t *testing.T) {
|
|
host, status := test.NewTestHost(routeRuleConfig(t, true, []string{"consumer1"}))
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusOK, status)
|
|
require.NoError(t, host.SetRouteName("route-a"))
|
|
|
|
action := host.CallOnHttpRequestHeaders([][2]string{
|
|
{":authority", "example.com"},
|
|
{":path", "/api/test"},
|
|
{":method", "GET"},
|
|
{"x-api-key", "token2"},
|
|
})
|
|
require.Equal(t, types.ActionContinue, action)
|
|
|
|
resp := host.GetLocalResponse()
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, uint32(http.StatusForbidden), resp.StatusCode)
|
|
require.True(t, test.HasHeader(resp.Headers, "WWW-Authenticate"))
|
|
})
|
|
}
|
|
|
|
// global_auth=false + route-scoped allow + consumer in allow → authenticated
|
|
// path through the case-3 branch (main.go:354-362 success). Verifies
|
|
// X-Mse-Consumer is still injected when auth is enabled per-route only.
|
|
func TestOnHttpRequestHeaders_GlobalAuthFalse_RouteAllow_ConsumerAllowed(t *testing.T) {
|
|
test.RunTest(t, func(t *testing.T) {
|
|
host, status := test.NewTestHost(routeRuleConfig(t, false, []string{"consumer1"}))
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusOK, status)
|
|
require.NoError(t, host.SetRouteName("route-a"))
|
|
|
|
action := host.CallOnHttpRequestHeaders([][2]string{
|
|
{":authority", "example.com"},
|
|
{":path", "/api/test"},
|
|
{":method", "GET"},
|
|
{"x-api-key", "token1"},
|
|
})
|
|
require.Equal(t, types.ActionContinue, action)
|
|
require.Nil(t, host.GetLocalResponse())
|
|
require.True(t, test.HasHeaderWithValue(host.GetRequestHeaders(), "X-Mse-Consumer", "consumer1"))
|
|
})
|
|
}
|
|
|
|
// global_auth=false + route-scoped allow + consumer not in allow → 403 via
|
|
// deniedUnauthorizedConsumer (main.go:354-359 reject). Mirror of the
|
|
// global_auth=true rejection but exercises the case-3 entry condition
|
|
// `(globalAuthSetFalse || (globalAuthNoSet && ruleSet)) && !noAllow`.
|
|
func TestOnHttpRequestHeaders_GlobalAuthFalse_RouteAllow_ConsumerNotAllowed(t *testing.T) {
|
|
test.RunTest(t, func(t *testing.T) {
|
|
host, status := test.NewTestHost(routeRuleConfig(t, false, []string{"consumer1"}))
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusOK, status)
|
|
require.NoError(t, host.SetRouteName("route-a"))
|
|
|
|
action := host.CallOnHttpRequestHeaders([][2]string{
|
|
{":authority", "example.com"},
|
|
{":path", "/api/test"},
|
|
{":method", "GET"},
|
|
{"x-api-key", "token2"},
|
|
})
|
|
require.Equal(t, types.ActionContinue, action)
|
|
|
|
resp := host.GetLocalResponse()
|
|
require.NotNil(t, resp)
|
|
require.Equal(t, uint32(http.StatusForbidden), resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// global_auth unset + at least one route configured + current route NOT
|
|
// configured → noAllow short-circuit through `(globalAuthNoSet && ruleSet)`
|
|
// at main.go:288-293. Existing tests use global_auth=false for this branch;
|
|
// this drives the unset path so the boolean expression's other operand is
|
|
// covered.
|
|
func TestOnHttpRequestHeaders_GlobalAuthUnset_RuleSet_OtherRoute_PassThrough(t *testing.T) {
|
|
test.RunTest(t, func(t *testing.T) {
|
|
// global_auth omitted; _rules_ entry on route-a only.
|
|
host, status := test.NewTestHost(routeRuleConfig(t, nil, []string{"consumer1"}))
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusOK, status)
|
|
// Drive a request on a different route — current rule context has no
|
|
// allow list, so auth must be skipped entirely.
|
|
require.NoError(t, host.SetRouteName("route-b"))
|
|
|
|
action := host.CallOnHttpRequestHeaders([][2]string{
|
|
{":authority", "example.com"},
|
|
{":path", "/api/test"},
|
|
{":method", "GET"},
|
|
})
|
|
require.Equal(t, types.ActionContinue, action)
|
|
require.Nil(t, host.GetLocalResponse())
|
|
})
|
|
}
|
|
|
|
// === Module C — parse-time edge rejects =================================
|
|
//
|
|
// parseGlobalConfig is 93.2% and parseOverrideRuleConfig is 90.9%. The
|
|
// missing branches are the empty-string credential rejects (singular and
|
|
// inside the credentials array) and the "allow key absent entirely" branch
|
|
// at parseOverrideRuleConfig:251 which existing tests don't reach because
|
|
// every route-rule fixture supplies `allow` (possibly empty).
|
|
|
|
// credential: "" must be rejected at parseGlobalConfig:206-208 — distinct
|
|
// from "credential field absent" already covered by mixed-credential
|
|
// fixtures in main_test.go.
|
|
func TestParseGlobalConfig_EmptyCredentialString(t *testing.T) {
|
|
test.RunGoTest(t, func(t *testing.T) {
|
|
cfg := mustConfig(t, map[string]interface{}{
|
|
"consumers": []map[string]interface{}{
|
|
{"name": "consumer1", "credential": ""},
|
|
},
|
|
"keys": []string{"x-api-key"},
|
|
"in_header": true,
|
|
})
|
|
host, status := test.NewTestHost(cfg)
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
|
})
|
|
}
|
|
|
|
// An empty string inside the credentials array must be rejected at
|
|
// parseGlobalConfig:215-217 — separate branch from the "credentials array
|
|
// empty" reject already covered by invalidEmptyPluralCredentialsConfig.
|
|
func TestParseGlobalConfig_EmptyCredentialInArray(t *testing.T) {
|
|
test.RunGoTest(t, func(t *testing.T) {
|
|
cfg := mustConfig(t, map[string]interface{}{
|
|
"consumers": []map[string]interface{}{
|
|
{"name": "consumer1", "credentials": []string{"token1", ""}},
|
|
},
|
|
"keys": []string{"x-api-key"},
|
|
"in_header": true,
|
|
})
|
|
host, status := test.NewTestHost(cfg)
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
|
})
|
|
}
|
|
|
|
// _rules_ entry without an `allow` key at all — distinct from
|
|
// invalidRuleConfig which supplies `allow: []`. Hits the
|
|
// `if !allow.Exists()` branch at parseOverrideRuleConfig:251.
|
|
func TestParseOverrideRuleConfig_AllowMissing(t *testing.T) {
|
|
test.RunGoTest(t, func(t *testing.T) {
|
|
cfg := mustConfig(t, map[string]interface{}{
|
|
"consumers": []map[string]interface{}{
|
|
{"name": "consumer1", "credential": "token1"},
|
|
},
|
|
"keys": []string{"x-api-key"},
|
|
"in_header": true,
|
|
"_rules_": []map[string]interface{}{
|
|
{
|
|
"_match_route_": []string{"route-a"},
|
|
// no "allow" key at all
|
|
},
|
|
},
|
|
})
|
|
host, status := test.NewTestHost(cfg)
|
|
defer host.Reset()
|
|
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
|
})
|
|
}
|