Files
higress/plugins/wasm-go/pkg/matcher/rule_matcher_test.go
2024-07-24 19:23:55 +08:00

439 lines
10 KiB
Go

// Copyright (c) 2022 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 matcher
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
type customConfig struct {
name string
age int64
}
func parseConfig(json gjson.Result, config *customConfig) error {
config.name = json.Get("name").String()
config.age = json.Get("age").Int()
return nil
}
func TestHostMatch(t *testing.T) {
cases := []struct {
name string
config RuleConfig[customConfig]
host string
result bool
}{
{
name: "prefix",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Prefix,
host: "www.",
},
},
},
host: "www.test.com",
result: true,
},
{
name: "prefix failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Prefix,
host: "www.",
},
},
},
host: "test.com",
result: false,
},
{
name: "suffix",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
},
},
host: "www.example.com",
result: true,
},
{
name: "suffix failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
},
},
host: "example.com",
result: false,
},
{
name: "exact",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "www.example.com",
result: true,
},
{
name: "exact failed",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "example.com",
result: false,
},
{
name: "exact port",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "www.example.com:8080",
result: true,
},
{
name: "any",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Suffix,
host: "",
},
},
},
host: "www.example.com",
result: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var m RuleMatcher[customConfig]
assert.Equal(t, c.result, m.hostMatch(c.config, c.host))
})
}
}
func TestServiceMatch(t *testing.T) {
cases := []struct {
name string
config RuleConfig[customConfig]
service string
result bool
}{
{
name: "fqdn",
config: RuleConfig[customConfig]{
services: map[string]struct{}{
"qwen.dns": {},
},
},
service: "outbound|443||qwen.dns",
result: true,
},
{
name: "fqdn with port",
config: RuleConfig[customConfig]{
services: map[string]struct{}{
"qwen.dns:443": {},
},
},
service: "outbound|443||qwen.dns",
result: true,
},
{
name: "not match",
config: RuleConfig[customConfig]{
services: map[string]struct{}{
"moonshot.dns:443": {},
},
},
service: "outbound|443||qwen.dns",
result: false,
},
{
name: "error config format",
config: RuleConfig[customConfig]{
services: map[string]struct{}{
"qwen.dns:": {},
},
},
service: "outbound|443||qwen.dns",
result: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var m RuleMatcher[customConfig]
assert.Equal(t, c.result, m.serviceMatch(c.config, c.service))
})
}
}
func TestParseRuleConfig(t *testing.T) {
cases := []struct {
name string
config string
errMsg string
expected RuleMatcher[customConfig]
}{
{
name: "global config",
config: `{"name":"john", "age":18}`,
expected: RuleMatcher[customConfig]{
globalConfig: customConfig{
name: "john",
age: 18,
},
hasGlobalConfig: true,
},
},
{
name: "rules config",
config: `{"_rules_":[{"_match_domain_":["*.example.com","www.*","*","www.abc.com"],"name":"john", "age":18},{"_match_route_":["test1","test2"],"name":"ann", "age":16},{"_match_service_":["test1.dns","test2.static:8080"],"name":"ann", "age":16},{"_match_route_prefix_":["api1","api2"],"name":"ann", "age":16}]}`,
expected: RuleMatcher[customConfig]{
ruleConfig: []RuleConfig[customConfig]{
{
category: Host,
hosts: []HostMatcher{
{
matchType: Suffix,
host: ".example.com",
},
{
matchType: Prefix,
host: "www.",
},
{
matchType: Suffix,
host: "",
},
{
matchType: Exact,
host: "www.abc.com",
},
},
routes: map[string]struct{}{},
services: map[string]struct{}{},
routePrefixs: map[string]struct{}{},
config: customConfig{
name: "john",
age: 18,
},
},
{
category: Route,
routes: map[string]struct{}{
"test1": {},
"test2": {},
},
services: map[string]struct{}{},
routePrefixs: map[string]struct{}{},
config: customConfig{
name: "ann",
age: 16,
},
},
{
category: Service,
routes: map[string]struct{}{},
services: map[string]struct{}{
"test1.dns": {},
"test2.static:8080": {},
},
routePrefixs: map[string]struct{}{},
config: customConfig{
name: "ann",
age: 16,
},
},
{
category: RoutePrefix,
routes: map[string]struct{}{},
services: map[string]struct{}{},
routePrefixs: map[string]struct{}{
"api1": {},
"api2": {},
},
config: customConfig{
name: "ann",
age: 16,
},
},
},
},
},
{
name: "no rule",
config: `{"_rules_":[]}`,
errMsg: "parse config failed, no valid rules; global config parse error:<nil>",
},
{
name: "invalid rule",
config: `{"_rules_":[{"_match_domain_":["*"],"_match_route_":["test"]}]}`,
errMsg: "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.",
},
{
name: "invalid rule",
config: `{"_rules_":[{"_match_domain_":["*"],"_match_service_":["test.dns"]}]}`,
errMsg: "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.",
},
{
name: "invalid rule",
config: `{"_rules_":[{"age":16}]}`,
errMsg: "there is only one of '_match_route_', '_match_domain_', '_match_service_' and '_match_route_prefix_' can present in configuration.",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var actual RuleMatcher[customConfig]
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseConfig, nil)
if err != nil {
if c.errMsg == "" {
t.Errorf("parse failed: %v", err)
}
if err.Error() != c.errMsg {
t.Errorf("expect err: %s, actual err: %s", c.errMsg,
err.Error())
}
return
}
assert.Equal(t, c.expected, actual)
})
}
}
type completeConfig struct {
// global config
consumers []string
// rule config
allow []string
}
func parseGlobalConfig(json gjson.Result, global *completeConfig) error {
if json.Get("consumers").Exists() && json.Get("allow").Exists() {
return errors.New("consumers and allow should not be configured at the same level")
}
for _, item := range json.Get("consumers").Array() {
global.consumers = append(global.consumers, item.String())
}
return nil
}
func parseOverrideRuleConfig(json gjson.Result, global completeConfig, config *completeConfig) error {
if json.Get("consumers").Exists() && json.Get("allow").Exists() {
return errors.New("consumers and allow should not be configured at the same level")
}
// override config via global
*config = global
for _, item := range json.Get("allow").Array() {
config.allow = append(config.allow, item.String())
}
return nil
}
func TestParseOverrideConfig(t *testing.T) {
cases := []struct {
name string
config string
errMsg string
expected RuleMatcher[completeConfig]
}{
{
name: "override rule config",
config: `{"consumers":["c1","c2","c3"],"_rules_":[{"_match_route_":["r1","r2"],"allow":["c1","c3"]}]}`,
expected: RuleMatcher[completeConfig]{
ruleConfig: []RuleConfig[completeConfig]{
{
category: Route,
routes: map[string]struct{}{
"r1": {},
"r2": {},
},
services: map[string]struct{}{},
routePrefixs: map[string]struct{}{},
config: completeConfig{
consumers: []string{"c1", "c2", "c3"},
allow: []string{"c1", "c3"},
},
},
},
globalConfig: completeConfig{
consumers: []string{"c1", "c2", "c3"},
},
hasGlobalConfig: true,
},
},
{
name: "invalid config",
config: `{"consumers":["c1","c2","c3"],"allow":["c1"]}`,
errMsg: "parse config failed, no valid rules; global config parse error:consumers and allow should not be configured at the same level",
},
{
name: "invalid config",
config: `{"_rules_":[{"_match_route_":["r1","r2"],"consumers":["c1","c2"],"allow":["c1"]}]}`,
errMsg: "consumers and allow should not be configured at the same level",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var actual RuleMatcher[completeConfig]
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseGlobalConfig, parseOverrideRuleConfig)
if err != nil {
if c.errMsg == "" {
t.Errorf("parse failed: %v", err)
}
if err.Error() != c.errMsg {
t.Errorf("expect err: %s, actual err: %s", c.errMsg, err.Error())
}
return
}
assert.Equal(t, c.expected, actual)
})
}
}