feat(rules): add post-request rules system with response manipulation (#160)

* Add comprehensive post-request rules support for response phase
* Enable response body, status, and header manipulation via set commands
* Refactor command handlers to support both request and response phases
* Implement response modifier system for post-request template execution
* Support response-based rule matching with status and header checks
* Add comprehensive benchmarks for matcher performance
* Refactor authentication and proxying commands for unified error handling
* Support negated conditions with !
* Enhance error handling, error formatting and validation
* Routes: add `rule_file` field with rule preset support
* Environment variable substitution: now supports variables without `GODOXY_` prefix

* new conditions:
  * `on resp_header <key> [<value>]`
  * `on status <status>`
* new commands:
  * `require_auth`
  * `set resp_header <key> <template>`
  * `set resp_body <template>`
  * `set status <code>`
  * `log <level> <path> <template>`
  * `notify <level> <provider> <title_template> <body_template>`
This commit is contained in:
Yuzerion
2025-10-14 23:53:06 +08:00
committed by GitHub
parent 19968834d2
commit 53f3397b7a
41 changed files with 4425 additions and 528 deletions

View File

@@ -7,6 +7,86 @@ import (
expect "github.com/yusing/goutils/testing"
)
func TestSplitPipe(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{
name: "empty",
input: "",
want: []string{},
},
{
name: "single",
input: "rule",
want: []string{"rule"},
},
{
name: "simple_pipe",
input: "rule1 | rule2",
want: []string{"rule1", "rule2"},
},
{
name: "multiple_pipes",
input: "rule1 | rule2 | rule3",
want: []string{"rule1", "rule2", "rule3"},
},
{
name: "pipe_in_quotes",
input: `path regex("^(_next/static|_next/image|favicon.ico).*$")`,
want: []string{`path regex("^(_next/static|_next/image|favicon.ico).*$")`},
},
{
name: "pipe_in_single_quotes",
input: `path regex('^(_next/static|_next/image|favicon.ico).*$')`,
want: []string{`path regex('^(_next/static|_next/image|favicon.ico).*$')`},
},
{
name: "pipe_in_backticks",
input: "path regex(`^(_next/static|_next/image|favicon.ico).*$`)",
want: []string{"path regex(`^(_next/static|_next/image|favicon.ico).*$`)"},
},
{
name: "pipe_in_brackets",
input: "path regex(^(_next/static|_next/image|favicon.ico).*$)",
want: []string{"path regex(^(_next/static|_next/image|favicon.ico).*$)"},
},
{
name: "escaped_pipe",
input: `path regex("^(_next/static\|_next/image\|favicon.ico).*$")`,
want: []string{`path regex("^(_next/static\|_next/image\|favicon.ico).*$")`},
},
{
name: "mixed_quotes_and_pipes",
input: `rule1 | path regex("^(_next/static|_next/image|favicon.ico).*$") | rule3`,
want: []string{"rule1", `path regex("^(_next/static|_next/image|favicon.ico).*$")`, "rule3"},
},
{
name: "nested_brackets",
input: "path regex(^(foo|bar(baz|qux)).*$)",
want: []string{"path regex(^(foo|bar(baz|qux)).*$)"},
},
{
name: "spaces_around",
input: " rule1 | rule2 | rule3 ",
want: []string{"rule1", "rule2", "rule3"},
},
{
name: "empty_segments",
input: "rule1 || rule2 | | rule3",
want: []string{"rule1", "", "rule2", "", "rule3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := splitPipe(tt.input)
expect.Equal(t, got, tt.want)
})
}
}
func TestSplitAnd(t *testing.T) {
tests := []struct {
name string
@@ -179,6 +259,27 @@ func TestParseOn(t *testing.T) {
input: "route example1 example2",
wantErr: ErrExpectOneArg,
},
// pipe splitting tests
{
name: "pipe_simple",
input: "method GET | method POST",
wantErr: nil,
},
{
name: "pipe_in_quotes",
input: `path regex("^(_next/static|_next/image|favicon.ico).*$")`,
wantErr: nil,
},
{
name: "pipe_in_brackets",
input: "path regex(^(_next/static|_next/image|favicon.ico).*$)",
wantErr: nil,
},
{
name: "pipe_mixed",
input: `method GET | path regex("^(_next/static|_next/image|favicon.ico).*$") | header Authorization`,
wantErr: nil,
},
}
for _, tt := range tests {