Files
higress/plugins/wasm-go/extensions/key-auth/main_extra_test.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)
})
}