mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +08:00
feat: Add traffic-editor plugin (#2825)
This commit is contained in:
515
plugins/wasm-go/extensions/traffic-editor/pkg/command.go
Normal file
515
plugins/wasm-go/extensions/traffic-editor/pkg/command.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
commandTypeSet = "set"
|
||||
commandTypeConcat = "concat"
|
||||
commandTypeCopy = "copy"
|
||||
commandTypeDelete = "delete"
|
||||
commandTypeRename = "rename"
|
||||
)
|
||||
|
||||
var (
|
||||
commandFactories = map[string]func(gjson.Result) (Command, error){
|
||||
"set": newSetCommand,
|
||||
"concat": newConcatCommand,
|
||||
"copy": newCopyCommand,
|
||||
"delete": newDeleteCommand,
|
||||
"rename": newRenameCommand,
|
||||
}
|
||||
)
|
||||
|
||||
type CommandSet struct {
|
||||
DisableReroute bool `json:"disableReroute"`
|
||||
Commands []Command `json:"commands,omitempty"`
|
||||
RelatedStages map[Stage]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *CommandSet) FromJson(json gjson.Result) error {
|
||||
relatedStages := map[Stage]bool{}
|
||||
if commandsJson := json.Get("commands"); commandsJson.Exists() && commandsJson.IsArray() {
|
||||
for _, item := range commandsJson.Array() {
|
||||
if command, err := NewCommand(item); err != nil {
|
||||
return fmt.Errorf("failed to create command from json: %v\n %v", err, item)
|
||||
} else {
|
||||
s.Commands = append(s.Commands, command)
|
||||
for _, ref := range command.GetRefs() {
|
||||
relatedStages[ref.GetStage()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.RelatedStages = relatedStages
|
||||
if disableReroute := json.Get("disableReroute"); disableReroute.Exists() {
|
||||
s.DisableReroute = disableReroute.Bool()
|
||||
} else {
|
||||
s.DisableReroute = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommandSet) CreatExecutors() []Executor {
|
||||
executors := make([]Executor, 0, len(s.Commands))
|
||||
for _, command := range s.Commands {
|
||||
executor := command.CreateExecutor()
|
||||
executors = append(executors, executor)
|
||||
}
|
||||
return executors
|
||||
}
|
||||
|
||||
type ConditionalCommandSet struct {
|
||||
ConditionSet
|
||||
CommandSet
|
||||
}
|
||||
|
||||
func (s *ConditionalCommandSet) FromJson(json gjson.Result) error {
|
||||
if err := s.ConditionSet.FromJson(json); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.CommandSet.FromJson(json); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Command interface {
|
||||
GetType() string
|
||||
GetRefs() []*Ref
|
||||
CreateExecutor() Executor
|
||||
}
|
||||
|
||||
type Executor interface {
|
||||
GetCommand() Command
|
||||
Run(editorContext EditorContext, stage Stage) error
|
||||
}
|
||||
|
||||
func NewCommand(json gjson.Result) (Command, error) {
|
||||
t := json.Get("type").String()
|
||||
if t == "" {
|
||||
return nil, errors.New("command type is required")
|
||||
}
|
||||
if constructor, ok := commandFactories[t]; ok && constructor != nil {
|
||||
return constructor(json)
|
||||
} else {
|
||||
return nil, errors.New("unknown command type: " + t)
|
||||
}
|
||||
}
|
||||
|
||||
type baseExecutor struct {
|
||||
finished bool
|
||||
}
|
||||
|
||||
// setCommand
|
||||
func newSetCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("setCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
var value string
|
||||
if v := json.Get("value"); !v.Exists() {
|
||||
return nil, errors.New("setCommand: value field is required")
|
||||
} else {
|
||||
value = v.String()
|
||||
if value == "" {
|
||||
return nil, errors.New("setCommand: value cannot be empty")
|
||||
}
|
||||
}
|
||||
return &setCommand{
|
||||
targetRef: targetRef,
|
||||
value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type setCommand struct {
|
||||
targetRef *Ref
|
||||
value string
|
||||
}
|
||||
|
||||
func (c *setCommand) GetType() string {
|
||||
return commandTypeSet
|
||||
}
|
||||
|
||||
func (c *setCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *setCommand) CreateExecutor() Executor {
|
||||
return &setExecutor{command: c}
|
||||
}
|
||||
|
||||
type setExecutor struct {
|
||||
baseExecutor
|
||||
command *setCommand
|
||||
}
|
||||
|
||||
func (e *setExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *setExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("setCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
if command.targetRef.GetStage() == stage {
|
||||
log.Debugf("setCommand: set %s to %s", command.targetRef, command.value)
|
||||
editorContext.SetRefValue(command.targetRef, command.value)
|
||||
e.finished = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// concatCommand
|
||||
func newConcatCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("concatCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("concatCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
valuesJson := json.Get("values")
|
||||
if !valuesJson.Exists() || !valuesJson.IsArray() {
|
||||
return nil, errors.New("concatCommand: values field is required and must be an array")
|
||||
}
|
||||
|
||||
values := make([]interface{}, 0, len(valuesJson.Array()))
|
||||
for _, item := range valuesJson.Array() {
|
||||
var value interface{}
|
||||
if item.IsObject() {
|
||||
valueRef, err := NewRef(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("concatCommand: failed to create ref from values field: %v\n %v", err, item.Raw)
|
||||
}
|
||||
if valueRef.GetStage() > targetRef.GetStage() {
|
||||
return nil, fmt.Errorf("concatCommand: the processing stage of value [%s] cannot be after the stage of target [%s]", Stage2String[valueRef.GetStage()], Stage2String[targetRef.GetStage()])
|
||||
}
|
||||
value = valueRef
|
||||
} else {
|
||||
value = item.String()
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return &concatCommand{
|
||||
targetRef: targetRef,
|
||||
values: values,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type concatCommand struct {
|
||||
targetRef *Ref
|
||||
values []interface{}
|
||||
}
|
||||
|
||||
func (c *concatCommand) GetType() string {
|
||||
return commandTypeConcat
|
||||
}
|
||||
|
||||
func (c *concatCommand) GetRefs() []*Ref {
|
||||
refs := []*Ref{c.targetRef}
|
||||
if c.values != nil && len(c.values) != 0 {
|
||||
for _, value := range c.values {
|
||||
if ref, ok := value.(*Ref); ok {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
func (c *concatCommand) CreateExecutor() Executor {
|
||||
return &concatExecutor{command: c}
|
||||
}
|
||||
|
||||
type concatExecutor struct {
|
||||
baseExecutor
|
||||
command *concatCommand
|
||||
values []string
|
||||
}
|
||||
|
||||
func (e *concatExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *concatExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
|
||||
if e.values == nil {
|
||||
e.values = make([]string, len(command.values))
|
||||
}
|
||||
|
||||
for i, value := range command.values {
|
||||
if value == nil || e.values[i] != "" {
|
||||
continue
|
||||
}
|
||||
v := ""
|
||||
if s, ok := value.(string); ok {
|
||||
v = s
|
||||
} else if ref, ok := value.(*Ref); ok && ref.GetStage() == stage {
|
||||
v = editorContext.GetRefValue(ref)
|
||||
}
|
||||
e.values[i] = v
|
||||
}
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
result := ""
|
||||
for _, v := range e.values {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
result += v
|
||||
}
|
||||
log.Debugf("concatCommand: set %s to %s", command.targetRef, result)
|
||||
editorContext.SetRefValue(command.targetRef, result)
|
||||
e.finished = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyCommand
|
||||
func newCopyCommand(json gjson.Result) (Command, error) {
|
||||
var sourceRef *Ref
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("source"); !t.Exists() {
|
||||
return nil, errors.New("copyCommand: source field is required")
|
||||
} else {
|
||||
sourceRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copyCommand: failed to create ref from source field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("copyCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copyCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
if sourceRef.GetStage() > targetRef.GetStage() {
|
||||
return nil, fmt.Errorf("copyCommand: the processing stage of source [%s] cannot be after the stage of target [%s]", Stage2String[sourceRef.GetStage()], Stage2String[targetRef.GetStage()])
|
||||
}
|
||||
return ©Command{
|
||||
sourceRef: sourceRef,
|
||||
targetRef: targetRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type copyCommand struct {
|
||||
sourceRef *Ref
|
||||
targetRef *Ref
|
||||
}
|
||||
|
||||
func (c *copyCommand) GetType() string {
|
||||
return commandTypeCopy
|
||||
}
|
||||
|
||||
func (c *copyCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.sourceRef, c.targetRef}
|
||||
}
|
||||
|
||||
func (c *copyCommand) CreateExecutor() Executor {
|
||||
return ©Executor{command: c}
|
||||
}
|
||||
|
||||
type copyExecutor struct {
|
||||
baseExecutor
|
||||
command *copyCommand
|
||||
valueToCopy string
|
||||
}
|
||||
|
||||
func (e *copyExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *copyExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
|
||||
if command.sourceRef.GetStage() == stage {
|
||||
e.valueToCopy = editorContext.GetRefValue(command.sourceRef)
|
||||
log.Debugf("copyCommand: valueToCopy=%s", e.valueToCopy)
|
||||
}
|
||||
|
||||
if e.valueToCopy == "" {
|
||||
log.Debug("copyCommand: valueToCopy is empty. skip.")
|
||||
e.finished = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
editorContext.SetRefValue(command.targetRef, e.valueToCopy)
|
||||
log.Debugf("copyCommand: set %s to %s", e.valueToCopy, command.targetRef)
|
||||
e.finished = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteCommand
|
||||
func newDeleteCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("deleteCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deleteCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
return &deleteCommand{
|
||||
targetRef: targetRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type deleteCommand struct {
|
||||
targetRef *Ref
|
||||
}
|
||||
|
||||
func (c *deleteCommand) GetType() string {
|
||||
return commandTypeDelete
|
||||
}
|
||||
|
||||
func (c *deleteCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *deleteCommand) CreateExecutor() Executor {
|
||||
return &deleteExecutor{command: c}
|
||||
}
|
||||
|
||||
type deleteExecutor struct {
|
||||
baseExecutor
|
||||
command *deleteCommand
|
||||
}
|
||||
|
||||
func (e *deleteExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *deleteExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("deleteCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
log.Debugf("deleteCommand: delete %s", command.targetRef)
|
||||
editorContext.DeleteRefValues(command.targetRef)
|
||||
e.finished = true
|
||||
log.Debugf("deleteCommand: finished deleting %s", command.targetRef)
|
||||
} else {
|
||||
log.Debugf("deleteCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renameCommand
|
||||
func newRenameCommand(json gjson.Result) (Command, error) {
|
||||
var targetRef *Ref
|
||||
var err error
|
||||
if t := json.Get("target"); !t.Exists() {
|
||||
return nil, errors.New("renameCommand: target field is required")
|
||||
} else {
|
||||
targetRef, err = NewRef(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("renameCommand: failed to create ref from target field: %v\n %v", err, t.Raw)
|
||||
}
|
||||
}
|
||||
newName := json.Get("newName").String()
|
||||
if newName == "" {
|
||||
return nil, errors.New("renameCommand: newName field is required")
|
||||
}
|
||||
return &renameCommand{
|
||||
targetRef: targetRef,
|
||||
newName: newName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type renameCommand struct {
|
||||
targetRef *Ref
|
||||
newName string
|
||||
}
|
||||
|
||||
func (c *renameCommand) GetType() string {
|
||||
return commandTypeRename
|
||||
}
|
||||
|
||||
func (c *renameCommand) GetRefs() []*Ref {
|
||||
return []*Ref{c.targetRef}
|
||||
}
|
||||
|
||||
func (c *renameCommand) CreateExecutor() Executor {
|
||||
return &renameExecutor{command: c}
|
||||
}
|
||||
|
||||
type renameExecutor struct {
|
||||
baseExecutor
|
||||
command *renameCommand
|
||||
}
|
||||
|
||||
func (e *renameExecutor) GetCommand() Command {
|
||||
return e.command
|
||||
}
|
||||
|
||||
func (e *renameExecutor) Run(editorContext EditorContext, stage Stage) error {
|
||||
if e.finished {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := e.command
|
||||
log.Debugf("renameCommand: checking stage %s for target %s", Stage2String[stage], command.targetRef)
|
||||
|
||||
if command.targetRef.GetStage() == stage {
|
||||
if command.newName == command.targetRef.Name {
|
||||
log.Debugf("renameCommand: skip renaming %s to itself", command.targetRef)
|
||||
} else {
|
||||
values := editorContext.GetRefValues(command.targetRef)
|
||||
log.Debugf("renameCommand: rename %s to %s value=%v", command.targetRef, command.newName, values)
|
||||
editorContext.SetRefValues(&Ref{
|
||||
Type: command.targetRef.Type,
|
||||
Name: command.newName,
|
||||
}, values)
|
||||
editorContext.DeleteRefValues(command.targetRef)
|
||||
log.Debugf("renameCommand: finished renaming %s to %s", command.targetRef, command.newName)
|
||||
}
|
||||
e.finished = true
|
||||
} else {
|
||||
log.Debugf("renameCommand: stage %s does not match targetRef stage %s, skipping.", Stage2String[stage], Stage2String[command.targetRef.GetStage()])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
309
plugins/wasm-go/extensions/traffic-editor/pkg/command_test.go
Normal file
309
plugins/wasm-go/extensions/traffic-editor/pkg/command_test.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestNewSetCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"},"value":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newSetCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "set" {
|
||||
t.Errorf("expected type 'set', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSetCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"set","value":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newSetCommand(json)
|
||||
if err == nil || err.Error() != "setCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSetCommand_MissingValue(t *testing.T) {
|
||||
jsonStr := `{"type":"set","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newSetCommand(json)
|
||||
if err == nil || err.Error() != "setCommand: value field is required" {
|
||||
t.Errorf("expected value field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"},"values":["a","b"]}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newConcatCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "concat" {
|
||||
t.Errorf("expected type 'concat', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) < 1 {
|
||||
t.Errorf("expected at least 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","values":["a","b"]}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newConcatCommand(json)
|
||||
if err == nil || err.Error() != "concatCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConcatCommand_MissingValues(t *testing.T) {
|
||||
jsonStr := `{"type":"concat","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newConcatCommand(json)
|
||||
if err == nil || err.Error() != "concatCommand: values field is required and must be an array" {
|
||||
t.Errorf("expected values field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newCopyCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "copy" {
|
||||
t.Errorf("expected type 'copy', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 2 {
|
||||
t.Errorf("expected 2 refs, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_MissingSource(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: source field is required" {
|
||||
t.Errorf("expected source field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCopyCommand_SourceStageAfterTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"copy","source":{"type":"response_header","name":"foo"},"target":{"type":"request_header","name":"bar"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newCopyCommand(json)
|
||||
if err == nil || err.Error() != "copyCommand: the processing stage of source [response_headers] cannot be after the stage of target [request_headers]" {
|
||||
t.Errorf("expected source stage field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDeleteCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"delete","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newDeleteCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "delete" {
|
||||
t.Errorf("expected type 'delete', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDeleteCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"delete"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newDeleteCommand(json)
|
||||
if err == nil || err.Error() != "deleteCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_Success(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"},"newName":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
cmd, err := newRenameCommand(json)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if cmd.GetType() != "rename" {
|
||||
t.Errorf("expected type 'rename', got %s", cmd.GetType())
|
||||
}
|
||||
refs := cmd.GetRefs()
|
||||
if len(refs) != 1 {
|
||||
t.Errorf("expected 1 ref, got %d", len(refs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_MissingTarget(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","newName":"bar"}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newRenameCommand(json)
|
||||
if err == nil || err.Error() != "renameCommand: target field is required" {
|
||||
t.Errorf("expected target field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRenameCommand_MissingNewName(t *testing.T) {
|
||||
jsonStr := `{"type":"rename","target":{"type":"request_header","name":"foo"}}`
|
||||
json := gjson.Parse(jsonStr)
|
||||
_, err := newRenameCommand(json)
|
||||
if err == nil || err.Error() != "renameCommand: newName field is required" {
|
||||
t.Errorf("expected newName field error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExecutor_Run_SingleStage(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
cmd := &setCommand{targetRef: ref, value: "bar"}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "bar" {
|
||||
t.Errorf("expected value 'bar', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcatExecutor_Run_SingleStage(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
|
||||
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(srcRef, "-")
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "a-b" {
|
||||
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcatExecutor_Run_MultiStages(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeResponseHeader, Name: "foo"}
|
||||
srcRef := &Ref{Type: RefTypeRequestHeader, Name: "test"}
|
||||
cmd := &concatCommand{targetRef: ref, values: []interface{}{"a", srcRef, "b"}}
|
||||
executor := cmd.CreateExecutor()
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(srcRef, "-")
|
||||
|
||||
err := executor.Run(ctx, StageRequestHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
err = executor.Run(ctx, StageResponseHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "a-b" {
|
||||
t.Errorf("expected value 'a-b', got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyExecutor_Run_SingleStage(t *testing.T) {
|
||||
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
target := &Ref{Type: RefTypeRequestHeader, Name: "bar"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(source, "baz")
|
||||
cmd := ©Command{sourceRef: source, targetRef: target}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(target) != "baz" {
|
||||
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyExecutor_Run_MultiStages(t *testing.T) {
|
||||
source := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
target := &Ref{Type: RefTypeResponseHeader, Name: "bar"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(source, "baz")
|
||||
cmd := ©Command{sourceRef: source, targetRef: target}
|
||||
executor := cmd.CreateExecutor()
|
||||
|
||||
err := executor.Run(ctx, StageRequestHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
err = executor.Run(ctx, StageResponseHeaders)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(target) != "baz" {
|
||||
t.Errorf("expected value 'baz' for target, got %s", ctx.GetRefValue(target))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteExecutor_Run(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(ref, "bar")
|
||||
cmd := &deleteCommand{targetRef: ref}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "" {
|
||||
t.Errorf("expected value to be deleted, got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameExecutor_Run(t *testing.T) {
|
||||
ref := &Ref{Type: RefTypeRequestHeader, Name: "foo"}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRefValue(ref, "bar")
|
||||
cmd := &renameCommand{targetRef: ref, newName: "baz"}
|
||||
executor := cmd.CreateExecutor()
|
||||
stage := StageRequestHeaders
|
||||
|
||||
err := executor.Run(ctx, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
newRef := &Ref{Type: ref.Type, Name: "baz"}
|
||||
if ctx.GetRefValue(newRef) != "bar" {
|
||||
t.Errorf("expected value 'bar' for new name, got %s", ctx.GetRefValue(newRef))
|
||||
}
|
||||
if ctx.GetRefValue(ref) != "" {
|
||||
t.Errorf("expected old name to be deleted, got %s", ctx.GetRefValue(ref))
|
||||
}
|
||||
}
|
||||
325
plugins/wasm-go/extensions/traffic-editor/pkg/condition.go
Normal file
325
plugins/wasm-go/extensions/traffic-editor/pkg/condition.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
conditionTypeEquals = "equals"
|
||||
conditionTypePrefix = "prefix"
|
||||
conditionTypeSuffix = "suffix"
|
||||
conditionTypeContains = "contains"
|
||||
conditionTypeRegex = "regex"
|
||||
)
|
||||
|
||||
var (
|
||||
conditionFactories = map[string]func(gjson.Result) (Condition, error){
|
||||
conditionTypeEquals: newEqualsCondition,
|
||||
conditionTypePrefix: newPrefixCondition,
|
||||
conditionTypeSuffix: newSuffixCondition,
|
||||
conditionTypeContains: newContainsCondition,
|
||||
conditionTypeRegex: newRegexCondition,
|
||||
}
|
||||
)
|
||||
|
||||
type ConditionSet struct {
|
||||
Conditions []Condition `json:"conditions,omitempty"`
|
||||
RelatedStages map[Stage]bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *ConditionSet) FromJson(json gjson.Result) error {
|
||||
relatedStages := map[Stage]bool{}
|
||||
s.Conditions = nil
|
||||
if conditionsJson := json.Get("conditions"); conditionsJson.Exists() && conditionsJson.IsArray() {
|
||||
for _, item := range conditionsJson.Array() {
|
||||
if condition, err := CreateCondition(item); err != nil {
|
||||
return fmt.Errorf("failed to create condition from json: %v\n %v", err, item)
|
||||
} else {
|
||||
s.Conditions = append(s.Conditions, condition)
|
||||
for _, ref := range condition.GetRefs() {
|
||||
relatedStages[ref.GetStage()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.RelatedStages = relatedStages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConditionSet) Matches(editorContext EditorContext) bool {
|
||||
if len(s.Conditions) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, condition := range s.Conditions {
|
||||
if !condition.Evaluate(editorContext) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Condition interface {
|
||||
GetType() string
|
||||
GetRefs() []*Ref
|
||||
Evaluate(ctx EditorContext) bool
|
||||
}
|
||||
|
||||
func CreateCondition(json gjson.Result) (Condition, error) {
|
||||
t := json.Get("type").String()
|
||||
if t == "" {
|
||||
return nil, errors.New("condition type is required")
|
||||
}
|
||||
if constructor, ok := conditionFactories[t]; !ok || constructor == nil {
|
||||
return nil, errors.New("unknown condition type: " + t)
|
||||
} else if condition, err := constructor(json); err != nil {
|
||||
return nil, fmt.Errorf("failed to create condition with type %s: %v", t, err)
|
||||
} else {
|
||||
for _, ref := range condition.GetRefs() {
|
||||
if ref.GetStage() >= StageResponseHeaders {
|
||||
return nil, fmt.Errorf("condition only supports request refs")
|
||||
}
|
||||
}
|
||||
return condition, nil
|
||||
}
|
||||
}
|
||||
|
||||
// equalsCondition
|
||||
func newEqualsCondition(json gjson.Result) (Condition, error) {
|
||||
value1 := json.Get("value1")
|
||||
if value1.Type != gjson.JSON {
|
||||
return nil, errors.New("equalsCondition: value1 field type must be JSON object")
|
||||
}
|
||||
value1Ref, err := NewRef(value1)
|
||||
if err != nil {
|
||||
return nil, errors.New("equalsCondition: failed to create value1 ref: " + err.Error())
|
||||
}
|
||||
value2 := json.Get("value2").String()
|
||||
return &equalsCondition{
|
||||
value1Ref: value1Ref,
|
||||
value2: value2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type equalsCondition struct {
|
||||
value1Ref *Ref
|
||||
value2 string
|
||||
}
|
||||
|
||||
func (c *equalsCondition) GetType() string {
|
||||
return conditionTypeEquals
|
||||
}
|
||||
|
||||
func (c *equalsCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.value1Ref}
|
||||
}
|
||||
|
||||
func (c *equalsCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating equals condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
|
||||
ref1Values := ctx.GetRefValues(c.value1Ref)
|
||||
if len(ref1Values) == 0 {
|
||||
log.Debugf("No values found for ref1: %v", c.value1Ref)
|
||||
return false
|
||||
}
|
||||
for _, value1 := range ref1Values {
|
||||
if value1 == c.value2 {
|
||||
log.Debugf("Condition matched: %s == %s", value1, c.value2)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: value1Ref=%v, value2=%s", c.value1Ref, c.value2)
|
||||
return false
|
||||
}
|
||||
|
||||
// prefixCondition
|
||||
func newPrefixCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("prefixCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("prefixCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
prefix := json.Get("prefix").String()
|
||||
return &prefixCondition{
|
||||
valueRef: valueRef,
|
||||
prefix: prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type prefixCondition struct {
|
||||
valueRef *Ref
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *prefixCondition) GetType() string {
|
||||
return conditionTypePrefix
|
||||
}
|
||||
|
||||
func (c *prefixCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
|
||||
func (c *prefixCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating prefix condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.HasPrefix(value, c.prefix) {
|
||||
log.Debugf("Condition matched: %s starts with %s", value, c.prefix)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.prefix)
|
||||
return false
|
||||
}
|
||||
|
||||
// suffixCondition
|
||||
func newSuffixCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("suffixCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("suffixCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
suffix := json.Get("suffix").String()
|
||||
return &suffixCondition{
|
||||
valueRef: valueRef,
|
||||
suffix: suffix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type suffixCondition struct {
|
||||
valueRef *Ref
|
||||
suffix string
|
||||
}
|
||||
|
||||
func (c *suffixCondition) GetType() string {
|
||||
return conditionTypeSuffix
|
||||
}
|
||||
|
||||
func (c *suffixCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
func (c *suffixCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating suffix condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.HasSuffix(value, c.suffix) {
|
||||
log.Debugf("Condition matched: %s ends with %s", value, c.suffix)
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, prefix=%s", c.valueRef, c.suffix)
|
||||
return false
|
||||
}
|
||||
|
||||
// containsCondition
|
||||
func newContainsCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("containsCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("containsCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
part := json.Get("part").String()
|
||||
return &containsCondition{
|
||||
valueRef: valueRef,
|
||||
part: part,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type containsCondition struct {
|
||||
valueRef *Ref
|
||||
part string
|
||||
}
|
||||
|
||||
func (c *containsCondition) GetType() string {
|
||||
return conditionTypeContains
|
||||
}
|
||||
|
||||
func (c *containsCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
|
||||
func (c *containsCondition) Evaluate(ctx EditorContext) bool {
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if strings.Contains(value, c.part) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// regexCondition
|
||||
func newRegexCondition(json gjson.Result) (Condition, error) {
|
||||
value := json.Get("value")
|
||||
if value.Type != gjson.JSON {
|
||||
return nil, errors.New("regexCondition: value field type must be JSON object")
|
||||
}
|
||||
valueRef, err := NewRef(value)
|
||||
if err != nil {
|
||||
return nil, errors.New("regexCondition: failed to create value ref: " + err.Error())
|
||||
}
|
||||
patternStr := json.Get("pattern").String()
|
||||
pattern, err := regexp.Compile(patternStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("regexCondition: failed to compile pattern: " + err.Error())
|
||||
}
|
||||
return ®exCondition{
|
||||
valueRef: valueRef,
|
||||
pattern: pattern,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type regexCondition struct {
|
||||
valueRef *Ref
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
func (c *regexCondition) GetType() string {
|
||||
return conditionTypeRegex
|
||||
}
|
||||
|
||||
func (c *regexCondition) Evaluate(ctx EditorContext) bool {
|
||||
log.Debugf("Evaluating regex condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
|
||||
refValues := ctx.GetRefValues(c.valueRef)
|
||||
if len(refValues) == 0 {
|
||||
log.Debugf("No values found for ref: %v", c.valueRef)
|
||||
return false
|
||||
}
|
||||
for _, value := range refValues {
|
||||
if c.pattern.MatchString(value) {
|
||||
log.Debugf("Condition matched: %s matches %s", value, c.pattern.String())
|
||||
return true
|
||||
}
|
||||
}
|
||||
log.Debugf("No matches found for condition: valueRef=%v, pattern=%s", c.valueRef, c.pattern.String())
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *regexCondition) GetRefs() []*Ref {
|
||||
return []*Ref{c.valueRef}
|
||||
}
|
||||
217
plugins/wasm-go/extensions/traffic-editor/pkg/condition_test.go
Normal file
217
plugins/wasm-go/extensions/traffic-editor/pkg/condition_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// --- equalsCondition tests ---
|
||||
func TestEqualsCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("equalsCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualsCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"def"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("equalsCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- prefixCondition tests ---
|
||||
func TestPrefixCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("prefixCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bazbar"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("prefixCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- suffixCondition tests ---
|
||||
func TestSuffixCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-end": {"123xyz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("suffixCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"suffix","value":{"type":"request_header","name":"x-end"},"suffix":"xyz"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-end": {"xyz123"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("suffixCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- containsCondition tests ---
|
||||
func TestContainsCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("containsCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"contains","value":{"type":"request_query","name":"foo"},"part":"baz"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("containsCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- regexCondition tests ---
|
||||
func TestRegexCondition_Match(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"abcdef"}})
|
||||
if !cond.Evaluate(ctx) {
|
||||
t.Error("regexCondition should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCondition_NoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"regex","value":{"type":"request_header","name":"x-reg"},"pattern":"^abc.*"}`)
|
||||
cond, _ := CreateCondition(json)
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-reg": {"defabc"}})
|
||||
if cond.Evaluate(ctx) {
|
||||
t.Error("regexCondition should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- CreateCondition error cases ---
|
||||
func TestCreateCondition_UnknownType(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"unknown","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_MissingType(t *testing.T) {
|
||||
json := gjson.Parse(`{"value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for missing type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_InvalidRefType(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"invalid_type","name":"x-test"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for invalid ref type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCondition_MissingRefName(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header"},"value2":"abc"}`)
|
||||
_, err := CreateCondition(json)
|
||||
if err == nil {
|
||||
t.Error("CreateCondition should fail for missing ref name")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ConditionSet tests ---
|
||||
func TestConditionSet_Matches_AllMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"barbaz"}})
|
||||
if !set.Matches(ctx) {
|
||||
t.Error("ConditionSet should match when all conditions match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionSet_Matches_OneNoMatch(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"},{"type":"prefix","value":{"type":"request_query","name":"foo"},"prefix":"bar"}]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
ctx.SetRequestHeaders(map[string][]string{"x-test": {"abc"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"baz"}})
|
||||
if set.Matches(ctx) {
|
||||
t.Error("ConditionSet should not match if one condition does not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionSet_Matches_Empty(t *testing.T) {
|
||||
json := gjson.Parse(`{"conditions":[]}`)
|
||||
var set ConditionSet
|
||||
if err := set.FromJson(json); err != nil {
|
||||
t.Fatalf("FromJson failed: %v", err)
|
||||
}
|
||||
ctx := NewEditorContext()
|
||||
if !set.Matches(ctx) {
|
||||
t.Error("ConditionSet with no conditions should always match")
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetType/GetRefs coverage ---
|
||||
func TestCondition_GetTypeAndRefs(t *testing.T) {
|
||||
json := gjson.Parse(`{"type":"equals","value1":{"type":"request_header","name":"x-test"},"value2":"abc"}`)
|
||||
cond, err := CreateCondition(json)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCondition failed: %v", err)
|
||||
}
|
||||
if cond.GetType() != "equals" {
|
||||
t.Error("GetType should return 'equals'")
|
||||
}
|
||||
refs := cond.GetRefs()
|
||||
if len(refs) != 1 || refs[0].Type != "request_header" || refs[0].Name != "x-test" {
|
||||
t.Error("GetRefs should return correct ref")
|
||||
}
|
||||
}
|
||||
310
plugins/wasm-go/extensions/traffic-editor/pkg/context.go
Normal file
310
plugins/wasm-go/extensions/traffic-editor/pkg/context.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
type Stage int
|
||||
|
||||
const (
|
||||
StageInvalid Stage = iota
|
||||
StageRequestHeaders
|
||||
StageRequestBody
|
||||
StageResponseHeaders
|
||||
StageResponseBody
|
||||
|
||||
pathHeader = ":path"
|
||||
)
|
||||
|
||||
var (
|
||||
OrderedStages = []Stage{
|
||||
StageRequestHeaders,
|
||||
StageRequestBody,
|
||||
StageResponseHeaders,
|
||||
StageResponseBody,
|
||||
}
|
||||
Stage2String = map[Stage]string{
|
||||
StageRequestHeaders: "request_headers",
|
||||
StageRequestBody: "request_body",
|
||||
StageResponseHeaders: "response_headers",
|
||||
StageResponseBody: "response_body",
|
||||
}
|
||||
)
|
||||
|
||||
type EditorContext interface {
|
||||
GetEffectiveCommandSet() *CommandSet
|
||||
SetEffectiveCommandSet(cmdSet *CommandSet)
|
||||
GetCommandExecutors() []Executor
|
||||
SetCommandExecutors(executors []Executor)
|
||||
GetCurrentStage() Stage
|
||||
SetCurrentStage(stage Stage)
|
||||
|
||||
GetRequestPath() string
|
||||
SetRequestPath(path string)
|
||||
GetRequestHeader(key string) []string
|
||||
GetRequestHeaders() map[string][]string
|
||||
SetRequestHeaders(map[string][]string)
|
||||
GetRequestQuery(key string) []string
|
||||
GetRequestQueries() map[string][]string
|
||||
SetRequestQueries(map[string][]string)
|
||||
GetResponseHeader(key string) []string
|
||||
GetResponseHeaders() map[string][]string
|
||||
SetResponseHeaders(map[string][]string)
|
||||
|
||||
GetRefValue(ref *Ref) string
|
||||
GetRefValues(ref *Ref) []string
|
||||
SetRefValue(ref *Ref, value string)
|
||||
SetRefValues(ref *Ref, values []string)
|
||||
DeleteRefValues(ref *Ref)
|
||||
|
||||
IsRequestHeadersDirty() bool
|
||||
IsResponseHeadersDirty() bool
|
||||
ResetDirtyFlags()
|
||||
}
|
||||
|
||||
func NewEditorContext() EditorContext {
|
||||
return &editorContext{}
|
||||
}
|
||||
|
||||
type editorContext struct {
|
||||
effectiveCommandSet *CommandSet
|
||||
commandExecutors []Executor
|
||||
|
||||
currentStage Stage
|
||||
|
||||
requestPath string
|
||||
requestHeaders map[string][]string
|
||||
requestQueries map[string][]string
|
||||
responseHeaders map[string][]string
|
||||
|
||||
requestHeadersDirty bool
|
||||
responseHeadersDirty bool
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetEffectiveCommandSet() *CommandSet {
|
||||
return ctx.effectiveCommandSet
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetEffectiveCommandSet(cmdSet *CommandSet) {
|
||||
ctx.effectiveCommandSet = cmdSet
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetCommandExecutors() []Executor {
|
||||
return ctx.commandExecutors
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetCommandExecutors(executors []Executor) {
|
||||
ctx.commandExecutors = executors
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetCurrentStage() Stage {
|
||||
return ctx.currentStage
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetCurrentStage(stage Stage) {
|
||||
ctx.currentStage = stage
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestPath() string {
|
||||
return ctx.requestPath
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestPath(path string) {
|
||||
ctx.requestPath = path
|
||||
ctx.savePathToHeader()
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestHeader(key string) []string {
|
||||
if ctx.requestHeaders == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.requestHeaders[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestHeaders() map[string][]string {
|
||||
return maps.Clone(ctx.requestHeaders)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestHeaders(headers map[string][]string) {
|
||||
ctx.requestHeaders = headers
|
||||
ctx.loadPathFromHeader()
|
||||
ctx.requestHeadersDirty = true
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestQuery(key string) []string {
|
||||
if ctx.requestQueries == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.requestQueries[key]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRequestQueries() map[string][]string {
|
||||
return maps.Clone(ctx.requestQueries)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRequestQueries(queries map[string][]string) {
|
||||
ctx.requestQueries = queries
|
||||
ctx.savePathToHeader()
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetResponseHeader(key string) []string {
|
||||
if ctx.responseHeaders == nil {
|
||||
return nil
|
||||
}
|
||||
return ctx.responseHeaders[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetResponseHeaders() map[string][]string {
|
||||
return maps.Clone(ctx.responseHeaders)
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetResponseHeaders(headers map[string][]string) {
|
||||
ctx.responseHeaders = headers
|
||||
ctx.responseHeadersDirty = true
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRefValue(ref *Ref) string {
|
||||
values := ctx.GetRefValues(ref)
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func (ctx *editorContext) GetRefValues(ref *Ref) []string {
|
||||
if ref == nil {
|
||||
return nil
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
return ctx.GetRequestHeader(strings.ToLower(ref.Name))
|
||||
case RefTypeRequestQuery:
|
||||
return ctx.GetRequestQuery(ref.Name)
|
||||
case RefTypeResponseHeader:
|
||||
return ctx.GetResponseHeader(strings.ToLower(ref.Name))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRefValue(ref *Ref, value string) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
ctx.SetRefValues(ref, []string{value})
|
||||
}
|
||||
|
||||
func (ctx *editorContext) SetRefValues(ref *Ref, values []string) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
if ctx.requestHeaders == nil {
|
||||
ctx.requestHeaders = make(map[string][]string)
|
||||
}
|
||||
loweredRefName := strings.ToLower(ref.Name)
|
||||
ctx.requestHeaders[loweredRefName] = values
|
||||
ctx.requestHeadersDirty = true
|
||||
if loweredRefName == pathHeader {
|
||||
ctx.loadPathFromHeader()
|
||||
}
|
||||
break
|
||||
case RefTypeRequestQuery:
|
||||
if ctx.requestQueries == nil {
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
}
|
||||
ctx.requestQueries[ref.Name] = values
|
||||
ctx.savePathToHeader()
|
||||
break
|
||||
case RefTypeResponseHeader:
|
||||
if ctx.responseHeaders == nil {
|
||||
ctx.responseHeaders = make(map[string][]string)
|
||||
}
|
||||
ctx.responseHeaders[strings.ToLower(ref.Name)] = values
|
||||
ctx.responseHeadersDirty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) DeleteRefValues(ref *Ref) {
|
||||
if ref == nil {
|
||||
return
|
||||
}
|
||||
switch ref.Type {
|
||||
case RefTypeRequestHeader:
|
||||
delete(ctx.requestHeaders, strings.ToLower(ref.Name))
|
||||
ctx.requestHeadersDirty = true
|
||||
break
|
||||
case RefTypeRequestQuery:
|
||||
delete(ctx.requestQueries, ref.Name)
|
||||
ctx.savePathToHeader()
|
||||
break
|
||||
case RefTypeResponseHeader:
|
||||
delete(ctx.responseHeaders, strings.ToLower(ref.Name))
|
||||
ctx.responseHeadersDirty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *editorContext) IsRequestHeadersDirty() bool {
|
||||
return ctx.requestHeadersDirty
|
||||
}
|
||||
|
||||
func (ctx *editorContext) IsResponseHeadersDirty() bool {
|
||||
return ctx.responseHeadersDirty
|
||||
}
|
||||
|
||||
func (ctx *editorContext) ResetDirtyFlags() {
|
||||
ctx.requestHeadersDirty = false
|
||||
ctx.responseHeadersDirty = false
|
||||
}
|
||||
|
||||
func (ctx *editorContext) savePathToHeader() {
|
||||
u, err := url.Parse(ctx.requestPath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to build the new path with query strings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
for k, vs := range ctx.requestQueries {
|
||||
for _, v := range vs {
|
||||
query.Add(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
ctx.SetRefValue(&Ref{Type: RefTypeRequestHeader, Name: pathHeader}, u.String())
|
||||
}
|
||||
|
||||
func (ctx *editorContext) loadPathFromHeader() {
|
||||
paths := ctx.GetRequestHeader(pathHeader)
|
||||
|
||||
if len(paths) == 0 || paths[0] == "" {
|
||||
log.Warn("the request has an empty path")
|
||||
ctx.requestPath = ""
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
return
|
||||
}
|
||||
|
||||
path := paths[0]
|
||||
queries := make(map[string][]string)
|
||||
|
||||
u, err := url.Parse(path)
|
||||
if err != nil {
|
||||
log.Warnf("unable to parse the request path: %s", path)
|
||||
ctx.requestPath = ""
|
||||
ctx.requestQueries = make(map[string][]string)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.requestPath = u.Path
|
||||
for k, vs := range u.Query() {
|
||||
queries[k] = vs
|
||||
}
|
||||
ctx.requestQueries = queries
|
||||
}
|
||||
218
plugins/wasm-go/extensions/traffic-editor/pkg/context_test.go
Normal file
218
plugins/wasm-go/extensions/traffic-editor/pkg/context_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newTestRef(t, name string) *Ref {
|
||||
return &Ref{Type: t, Name: name}
|
||||
}
|
||||
|
||||
func TestEditorContext_CommandSetAndExecutors(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
cmdSet := &CommandSet{}
|
||||
ctx.SetEffectiveCommandSet(cmdSet)
|
||||
if ctx.GetEffectiveCommandSet() != cmdSet {
|
||||
t.Errorf("EffectiveCommandSet not set/get correctly")
|
||||
}
|
||||
|
||||
executors := []Executor{nil, nil}
|
||||
ctx.SetCommandExecutors(executors)
|
||||
if !reflect.DeepEqual(ctx.GetCommandExecutors(), executors) {
|
||||
t.Errorf("CommandExecutors not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_Stage(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetCurrentStage(StageRequestHeaders)
|
||||
if ctx.GetCurrentStage() != StageRequestHeaders {
|
||||
t.Errorf("CurrentStage not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestPath(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetRequestPath("/foo/bar")
|
||||
if ctx.GetRequestPath() != "/foo/bar" {
|
||||
t.Errorf("RequestPath not set/get correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetRequestHeaders(headers)
|
||||
if !reflect.DeepEqual(ctx.GetRequestHeaders(), headers) {
|
||||
t.Errorf("RequestHeaders not set/get correctly")
|
||||
}
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetRequestHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetRequestHeader failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RequestQueries(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
queries := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetRequestQueries(queries)
|
||||
if !reflect.DeepEqual(ctx.GetRequestQueries(), queries) {
|
||||
t.Errorf("RequestQueries not set/get correctly")
|
||||
}
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetRequestQuery("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetRequestQuery failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_ResponseHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
headers := map[string][]string{"foo": {"bar"}, "baz": {"qux"}}
|
||||
ctx.SetResponseHeaders(headers)
|
||||
if !reflect.DeepEqual(ctx.GetResponseHeaders(), headers) {
|
||||
t.Errorf("ResponseHeaders not set/get correctly")
|
||||
}
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty not set correctly")
|
||||
}
|
||||
if got := ctx.GetResponseHeader("foo"); !reflect.DeepEqual(got, []string{"bar"}) {
|
||||
t.Errorf("GetResponseHeader failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_RefValueAndValues(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
rh := newTestRef(RefTypeRequestHeader, "foo")
|
||||
rq := newTestRef(RefTypeRequestQuery, "bar")
|
||||
rh2 := newTestRef(RefTypeResponseHeader, "baz")
|
||||
|
||||
ctx.SetRefValue(rh, "v1")
|
||||
ctx.SetRefValues(rq, []string{"v2", "v3"})
|
||||
ctx.SetRefValues(rh2, []string{"v4"})
|
||||
|
||||
if v := ctx.GetRefValue(rh); v != "v1" {
|
||||
t.Errorf("GetRefValue(RequestHeader) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rq); !reflect.DeepEqual(v, []string{"v2", "v3"}) {
|
||||
t.Errorf("GetRefValues(RequestQuery) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rh2); !reflect.DeepEqual(v, []string{"v4"}) {
|
||||
t.Errorf("GetRefValues(ResponseHeader) failed: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_DeleteRefValues(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
rh := newTestRef(RefTypeRequestHeader, "foo")
|
||||
rq := newTestRef(RefTypeRequestQuery, "bar")
|
||||
rh2 := newTestRef(RefTypeResponseHeader, "baz")
|
||||
|
||||
ctx.SetRefValue(rh, "v1")
|
||||
ctx.SetRefValues(rq, []string{"v2", "v3"})
|
||||
ctx.SetRefValues(rh2, []string{"v4"})
|
||||
|
||||
ctx.DeleteRefValues(rh)
|
||||
ctx.DeleteRefValues(rq)
|
||||
ctx.DeleteRefValues(rh2)
|
||||
|
||||
if v := ctx.GetRefValues(rh); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(RequestHeader) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rq); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(RequestQuery) failed: %v", v)
|
||||
}
|
||||
if v := ctx.GetRefValues(rh2); len(v) != 0 {
|
||||
t.Errorf("DeleteRefValues(ResponseHeader) failed: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_ResetDirtyFlags(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() || ctx.IsRequestHeadersDirty() || ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResetDirtyFlags failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsRequestHeadersDirty_SetHeaders(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be false initially")
|
||||
}
|
||||
ctx.SetRequestHeaders(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after SetRequestHeaders")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeRequestHeader, "foo")
|
||||
ctx.SetRefValue(ref, "baz")
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after SetRefValue")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestHeadersDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsRequestHeadersDirty_SetQueries(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be false initially")
|
||||
}
|
||||
ctx.SetRequestQueries(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after SetRequestQueries")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeRequestQuery, "foo")
|
||||
ctx.SetRefValues(ref, []string{"baz"})
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after SetRefValues")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsRequestHeadersDirty() {
|
||||
t.Errorf("RequestQueriesDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditorContext_IsResponseHeadersDirty(t *testing.T) {
|
||||
ctx := NewEditorContext().(*editorContext)
|
||||
if ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be false initially")
|
||||
}
|
||||
ctx.SetResponseHeaders(map[string][]string{"foo": {"bar"}})
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after SetResponseHeaders")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
if ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be false after ResetDirtyFlags")
|
||||
}
|
||||
ref := newTestRef(RefTypeResponseHeader, "foo")
|
||||
ctx.SetRefValues(ref, []string{"baz"})
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after SetRefValues")
|
||||
}
|
||||
ctx.ResetDirtyFlags()
|
||||
ctx.DeleteRefValues(ref)
|
||||
if !ctx.IsResponseHeadersDirty() {
|
||||
t.Errorf("ResponseHeadersDirty should be true after DeleteRefValues")
|
||||
}
|
||||
}
|
||||
26
plugins/wasm-go/extensions/traffic-editor/pkg/mock_test.go
Normal file
26
plugins/wasm-go/extensions/traffic-editor/pkg/mock_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"github.com/higress-group/wasm-go/pkg/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize mock logger for testing
|
||||
log.SetPluginLog(&mockLogger{})
|
||||
}
|
||||
|
||||
type mockLogger struct{}
|
||||
|
||||
func (m *mockLogger) Trace(msg string) {}
|
||||
func (m *mockLogger) Tracef(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Debug(msg string) {}
|
||||
func (m *mockLogger) Debugf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Info(msg string) {}
|
||||
func (m *mockLogger) Infof(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Warn(msg string) {}
|
||||
func (m *mockLogger) Warnf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Error(msg string) {}
|
||||
func (m *mockLogger) Errorf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) Critical(msg string) {}
|
||||
func (m *mockLogger) Criticalf(format string, args ...interface{}) {}
|
||||
func (m *mockLogger) ResetID(pluginID string) {}
|
||||
64
plugins/wasm-go/extensions/traffic-editor/pkg/ref.go
Normal file
64
plugins/wasm-go/extensions/traffic-editor/pkg/ref.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
RefTypeRequestHeader = "request_header"
|
||||
RefTypeRequestQuery = "request_query"
|
||||
RefTypeResponseHeader = "response_header"
|
||||
)
|
||||
|
||||
var (
|
||||
refType2Stage = map[string]Stage{
|
||||
RefTypeRequestHeader: StageRequestHeaders,
|
||||
RefTypeRequestQuery: StageRequestHeaders,
|
||||
RefTypeResponseHeader: StageResponseHeaders,
|
||||
}
|
||||
)
|
||||
|
||||
type Ref struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
stage Stage
|
||||
}
|
||||
|
||||
func NewRef(json gjson.Result) (*Ref, error) {
|
||||
ref := &Ref{}
|
||||
|
||||
if t := json.Get("type").String(); t != "" {
|
||||
ref.Type = t
|
||||
} else {
|
||||
return nil, errors.New("missing type field")
|
||||
}
|
||||
|
||||
if _, ok := refType2Stage[ref.Type]; !ok {
|
||||
return nil, fmt.Errorf("unknown ref type: %s", ref.Type)
|
||||
}
|
||||
|
||||
if name := json.Get("name").String(); name != "" {
|
||||
ref.Name = name
|
||||
} else {
|
||||
return nil, errors.New("missing name field")
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func (r *Ref) GetStage() Stage {
|
||||
if r.stage == 0 {
|
||||
if stage, ok := refType2Stage[r.Type]; ok {
|
||||
r.stage = stage
|
||||
}
|
||||
}
|
||||
return r.stage
|
||||
}
|
||||
|
||||
func (r *Ref) String() string {
|
||||
return fmt.Sprintf("%s/%s", r.Type, r.Name)
|
||||
}
|
||||
Reference in New Issue
Block a user