mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-22 09:09:08 +01:00
- add block syntax parser/scanner with nested @blocks and elif/else support - restructure rule execution into explicit pre/post phases with phase flags - classify commands by phase and termination behavior - enforce flow semantics (default rule handling, dead-rule detection) - expand HTTP flow coverage with block + YAML parity tests and benches - refresh rules README/spec and update playground/docs integration
1329 lines
36 KiB
Go
1329 lines
36 KiB
Go
package rules_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yusing/godoxy/internal/route/routes"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
. "github.com/yusing/godoxy/internal/route/rules"
|
|
)
|
|
|
|
func TestHTTPFlow_BasicPreRules(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Custom-Header", r.Header.Get("X-Custom-Header"))
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("upstream response"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
- name: add-header
|
|
on: path /
|
|
do: set header X-Custom-Header test-value
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "upstream response", w.Body.String())
|
|
assert.Equal(t, "test-value", w.Header().Get("X-Custom-Header"))
|
|
}
|
|
|
|
func TestHTTPFlow_TerminatingCommand(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "should not be called")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path /error {
|
|
error 403 Forbidden
|
|
}
|
|
path /error {
|
|
set header X-Header ignored
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/error", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
assert.Equal(t, "Forbidden\n", w.Body.String())
|
|
assert.Empty(t, w.Header().Get("X-Header"))
|
|
}
|
|
|
|
func TestHTTPFlow_RedirectFlow(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "should not be called")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path /old-path {
|
|
redirect /new-path
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/old-path", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
|
|
assert.Equal(t, "/new-path", w.Header().Get("Location"))
|
|
}
|
|
|
|
func TestHTTPFlow_RewriteFlow(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("path: " + r.URL.Path))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path glob(/api/*) {
|
|
rewrite /api/ /v1/
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/users", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "path: /v1/users", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_MultiplePreRules(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("upstream: " + r.Header.Get("X-Request-Id")))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path / {
|
|
set header X-Request-Id req-123
|
|
}
|
|
path / {
|
|
set header X-Auth-Token token-456
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "upstream: req-123", w.Body.String())
|
|
assert.Equal(t, "token-456", req.Header.Get("X-Auth-Token"))
|
|
}
|
|
|
|
func TestHTTPFlow_PostResponseRule(t *testing.T) {
|
|
upstream := mockUpstreamWithHeaders(http.StatusOK, "success", http.Header{
|
|
"X-Upstream": []string{"upstream-value"},
|
|
})
|
|
|
|
tempFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
path /test {
|
|
log info %s "$req_method $status_code"
|
|
}
|
|
`, tempFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "success", w.Body.String())
|
|
assert.Equal(t, "upstream-value", w.Header().Get("X-Upstream"))
|
|
|
|
// Check log file
|
|
content := TestFileContent(tempFile)
|
|
assert.Equal(t, "GET 200\n", string(content))
|
|
}
|
|
|
|
func TestHTTPFlow_ResponseRuleWithStatusCondition(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/success" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("success"))
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte("not found"))
|
|
}
|
|
})
|
|
|
|
var rules Rules
|
|
|
|
// Create a temporary file for logging
|
|
tempFile := TestRandomFileName()
|
|
|
|
err := parseRules(fmt.Sprintf(`
|
|
status 4xx {
|
|
log error %s "$req_url returned $status_code"
|
|
}
|
|
`, tempFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test successful request (should not log)
|
|
req1 := httptest.NewRequest(http.MethodGet, "/success", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
|
|
// Test error request (should log)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/notfound", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w2.Code)
|
|
|
|
// Check log file
|
|
content := TestFileContent(tempFile)
|
|
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
|
|
require.Len(t, lines, 1, "only 4xx requests should be logged")
|
|
assert.Equal(t, "/notfound returned 404", lines[0])
|
|
}
|
|
|
|
func TestHTTPFlow_ConditionalRules(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("hello " + r.Header.Get("X-Username")))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
header Authorization {
|
|
set header X-Username authenticated-user
|
|
set resp_header X-Username authenticated-user
|
|
}
|
|
default {
|
|
set header X-Username anonymous
|
|
set resp_header X-Username anonymous
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test with Authorization header
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1.Header.Set("Authorization", "Bearer token")
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "hello authenticated-user", w1.Body.String())
|
|
assert.Equal(t, "authenticated-user", w1.Header().Get("X-Username"))
|
|
|
|
// Test without Authorization header
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "hello anonymous", w2.Body.String())
|
|
assert.Equal(t, "anonymous", w2.Header().Get("X-Username"))
|
|
}
|
|
|
|
func TestHTTPFlow_ComplexFlowWithPreAndPostRules(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Simulate different responses based on path
|
|
if r.URL.Path == "/protected" {
|
|
if r.Header.Get("X-Auth") != "valid" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprint(w, "unauthorized")
|
|
return
|
|
}
|
|
}
|
|
w.Header().Set("X-Response-Time", "100ms")
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "success")
|
|
})
|
|
|
|
// Create temporary files for logging
|
|
logFile := TestRandomFileName()
|
|
errorLogFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
{
|
|
set resp_header X-Correlation-Id random_uuid
|
|
}
|
|
path /protected {
|
|
require_basic_auth "Protected Area"
|
|
}
|
|
{
|
|
log info %q "$req_method $req_url -> $status_code"
|
|
}
|
|
status 4xx {
|
|
log error %q "ERROR: $req_method $req_url $status_code"
|
|
}
|
|
`, logFile, errorLogFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test successful request
|
|
req1 := httptest.NewRequest(http.MethodGet, "/public", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "success", w1.Body.String())
|
|
assert.Equal(t, "random_uuid", w1.Header().Get("X-Correlation-Id"))
|
|
assert.Equal(t, "100ms", w1.Header().Get("X-Response-Time"))
|
|
|
|
// Test unauthorized protected request
|
|
req2 := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w2.Code)
|
|
assert.Equal(t, w2.Body.String(), "Unauthorized\n")
|
|
|
|
// Test authorized protected request
|
|
req3 := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
|
req3.SetBasicAuth("user", "pass")
|
|
w3 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w3, req3)
|
|
|
|
// This should fail because our simple upstream expects X-Auth: valid header
|
|
// but the basic auth requirement should add the appropriate header
|
|
assert.Equal(t, http.StatusUnauthorized, w3.Code)
|
|
|
|
// Check log files
|
|
logContent := TestFileContent(logFile)
|
|
lines := strings.Split(strings.TrimSpace(string(logContent)), "\n")
|
|
require.Len(t, lines, 3, "all requests should be logged")
|
|
assert.Equal(t, "GET /public -> 200", lines[0])
|
|
assert.Equal(t, "GET /protected -> 401", lines[1])
|
|
assert.Equal(t, "GET /protected -> 401", lines[2])
|
|
|
|
errorLogContent := TestFileContent(errorLogFile)
|
|
// Should have at least one 401 error logged
|
|
lines = strings.Split(strings.TrimSpace(string(errorLogContent)), "\n")
|
|
require.Len(t, lines, 2, "all errors should be logged")
|
|
assert.Equal(t, "ERROR: GET /protected 401", lines[0])
|
|
assert.Equal(t, "ERROR: GET /protected 401", lines[1])
|
|
}
|
|
|
|
func TestHTTPFlow_DefaultRule(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "upstream response")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
default {
|
|
set resp_header X-Default-Applied true
|
|
}
|
|
path /special {
|
|
set resp_header X-Special-Handled true
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test default rule
|
|
req1 := httptest.NewRequest(http.MethodGet, "/regular", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "true", w1.Header().Get("X-Default-Applied"))
|
|
assert.Empty(t, w1.Header().Get("X-Special-Handled"))
|
|
|
|
// Test special rule + default rule
|
|
req2 := httptest.NewRequest(http.MethodGet, "/special", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "true", w2.Header().Get("X-Default-Applied"))
|
|
assert.Equal(t, "true", w2.Header().Get("X-Special-Handled"))
|
|
}
|
|
|
|
func TestHTTPFlow_HeaderManipulation(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Echo back a header
|
|
headerValue := r.Header.Get("X-Test-Header")
|
|
w.Header().Set("X-Echoed-Header", headerValue)
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("header echoed"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
default {
|
|
remove resp_header X-Secret
|
|
add resp_header X-Custom-Header custom-value
|
|
}
|
|
header X-Test-Header {
|
|
set header X-Test-Header modified-value
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set("X-Secret", "secret-value")
|
|
req.Header.Set("X-Test-Header", "original-value")
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "modified-value", w.Header().Get("X-Echoed-Header"))
|
|
assert.Equal(t, "custom-value", w.Header().Get("X-Custom-Header"))
|
|
// Ensure the secret header was removed and not passed to upstream
|
|
// (we can't directly test this, but the upstream shouldn't see it)
|
|
}
|
|
|
|
func TestHTTPFlow_NestedBlocks_RemoteOverride(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Remote-Type", r.Header.Get("X-Remote-Type"))
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
header X-Test-Header {
|
|
set header X-Remote-Type public
|
|
@remote 127.0.0.1 | remote 192.168.0.0/16 {
|
|
set header X-Remote-Type private
|
|
}
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Localhost => private
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1.Header.Set("X-Test-Header", "1")
|
|
req1.RemoteAddr = "127.0.0.1:12345"
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "private", w1.Header().Get("X-Remote-Type"))
|
|
|
|
// Public IP => public
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req2.Header.Set("X-Test-Header", "1")
|
|
req2.RemoteAddr = "10.0.0.1:12345"
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "public", w2.Header().Get("X-Remote-Type"))
|
|
}
|
|
|
|
func TestHTTPFlow_NestedBlocks_ElifElse(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Mode", r.Header.Get("X-Mode"))
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("ok"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
header X-Test-Header {
|
|
@method GET {
|
|
set header X-Mode get
|
|
} elif method POST {
|
|
set header X-Mode post
|
|
} else {
|
|
set header X-Mode other
|
|
}
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// GET => get
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1.Header.Set("X-Test-Header", "1")
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "get", w1.Header().Get("X-Mode"))
|
|
|
|
// POST => post
|
|
req2 := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
req2.Header.Set("X-Test-Header", "1")
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "post", w2.Header().Get("X-Mode"))
|
|
|
|
// other methods => else branch
|
|
req3 := httptest.NewRequest(http.MethodPut, "/", nil)
|
|
req3.Header.Set("X-Test-Header", "1")
|
|
w3 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w3, req3)
|
|
assert.Equal(t, http.StatusOK, w3.Code)
|
|
assert.Equal(t, "other", w3.Header().Get("X-Mode"))
|
|
|
|
// no match
|
|
req4 := httptest.NewRequest(http.MethodDelete, "/", nil)
|
|
w4 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w4, req4)
|
|
assert.Equal(t, http.StatusOK, w4.Code)
|
|
assert.Equal(t, "", w4.Header().Get("X-Mode"))
|
|
}
|
|
|
|
func TestHTTPFlow_NestedBlocks_TerminatingActionStopsFlow(t *testing.T) {
|
|
called := false
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("upstream"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path / {
|
|
set header X-Pre pre
|
|
@header X-Block {
|
|
error 403 "blocked"
|
|
}
|
|
set resp_header X-After should-not-run
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Without X-Block => should reach upstream and execute non-terminating commands
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.True(t, called)
|
|
assert.Equal(t, "should-not-run", w1.Header().Get("X-After"))
|
|
assert.Equal(t, "pre", req1.Header.Get("X-Pre"))
|
|
|
|
// With X-Block => nested terminating action should stop processing before upstream
|
|
called = false
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req2.Header.Set("X-Block", "1")
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
assert.Equal(t, 403, w2.Code)
|
|
assert.Equal(t, "blocked\n", w2.Body.String())
|
|
assert.False(t, called, "nested error should terminate before calling upstream")
|
|
assert.Empty(t, w2.Header().Get("X-After"), "commands after the nested block should not run")
|
|
}
|
|
|
|
func TestHTTPFlow_NestedBlocks_InResponseRule_ModifiesResponseByRequestMethod(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("upstream"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
{
|
|
set header X-Method "should-be-overridden"
|
|
@method POST {
|
|
set header X-Method "post"
|
|
} elif method GET {
|
|
set header X-Method "get"
|
|
} else {
|
|
set header X-Method "other"
|
|
}
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
t.Run(http.MethodGet, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "get", req.Header.Get("X-Method"))
|
|
})
|
|
|
|
t.Run(http.MethodPost, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "post", req.Header.Get("X-Method"))
|
|
})
|
|
|
|
t.Run("other", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPut, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "other", req.Header.Get("X-Method"))
|
|
})
|
|
}
|
|
|
|
func TestHTTPFlow_QueryParameterHandling(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query()
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("query: " + query.Get("param")))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
query param {
|
|
set query param added-value
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/path?param=original", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
// The set command should have modified the query parameter
|
|
assert.Equal(t, "query: added-value", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_ServeCommand(t *testing.T) {
|
|
// Create a temporary directory with test files
|
|
tempDir, err := os.MkdirTemp("", "test-serve-*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
// Create test files directly in the temp directory
|
|
testFile := filepath.Join(tempDir, "index.html")
|
|
err = os.WriteFile(testFile, []byte("<h1>Test Page</h1>"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
var rules Rules
|
|
err = parseRules(fmt.Sprintf(`
|
|
path glob(/files/*) {
|
|
serve %s
|
|
}
|
|
`, tempDir), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(mockUpstream(http.StatusOK, "should not be called"))
|
|
|
|
// Test serving a file - serve command serves files relative to the root directory
|
|
// The path /files/index.html gets mapped to tempDir + "/files/index.html"
|
|
// We need to create the file at the expected path
|
|
filesDir := filepath.Join(tempDir, "files")
|
|
err = os.Mkdir(filesDir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
filesIndexFile := filepath.Join(filesDir, "index.html")
|
|
err = os.WriteFile(filesIndexFile, []byte("<h1>Test Page</h1>"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
req1 := httptest.NewRequest(http.MethodGet, "/files/index.html", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
// The serve command should work, but might redirect
|
|
// Let's just verify it doesn't call the upstream
|
|
assert.NotEqual(t, "should not be called", w1.Body.String())
|
|
|
|
// Test file not found
|
|
req2 := httptest.NewRequest(http.MethodGet, "/files/nonexistent.html", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w2.Code)
|
|
}
|
|
|
|
func TestHTTPFlow_ProxyCommand(t *testing.T) {
|
|
// Create a mock upstream server
|
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Upstream-Header", "upstream-value")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("upstream response"))
|
|
}))
|
|
defer upstreamServer.Close()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
path glob(/api/*) {
|
|
proxy %s
|
|
}
|
|
`, upstreamServer.URL), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(mockUpstream(http.StatusOK, "should not be called"))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
// The proxy command should forward the request to the upstream server
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "upstream response", w.Body.String())
|
|
assert.Equal(t, "upstream-value", w.Header().Get("X-Upstream-Header"))
|
|
}
|
|
|
|
func TestHTTPFlow_NotifyCommand(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "ok")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path /notify {
|
|
notify info test-provider "title $req_method" "body $req_url $status_code"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/notify", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "ok", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_FormConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("form processed"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
form username {
|
|
set resp_header X-Username "$form(username)"
|
|
}
|
|
postform email {
|
|
set resp_header X-Email "$postform(email)"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test form condition
|
|
formData := url.Values{"username": {"john_doe"}}
|
|
req1 := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(formData.Encode()))
|
|
req1.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "john_doe", w1.Header().Get("X-Username"))
|
|
|
|
// Test postform condition
|
|
postFormData := url.Values{"email": {"john@example.com"}}
|
|
req2 := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(postFormData.Encode()))
|
|
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "john@example.com", w2.Header().Get("X-Email"))
|
|
}
|
|
|
|
func TestHTTPFlow_RemoteConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("remote processed"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
remote 127.0.0.1 {
|
|
set resp_header X-Access "local"
|
|
}
|
|
remote 192.168.0.0/16 {
|
|
error 403 "Private network blocked"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test localhost condition
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1.RemoteAddr = "127.0.0.1:12345"
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "local", w1.Header().Get("X-Access"))
|
|
|
|
// Test private network block
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req2.RemoteAddr = "192.168.1.100:12345"
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, 403, w2.Code)
|
|
assert.Equal(t, "Private network blocked\n", w2.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_BasicAuthConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("auth processed"))
|
|
})
|
|
|
|
// Generate bcrypt hashes for passwords
|
|
adminHash, err := bcrypt.GenerateFromPassword([]byte("adminpass"), bcrypt.DefaultCost)
|
|
require.NoError(t, err)
|
|
guestHash, err := bcrypt.GenerateFromPassword([]byte("guestpass"), bcrypt.DefaultCost)
|
|
require.NoError(t, err)
|
|
|
|
var rules Rules
|
|
err = parseRules(fmt.Sprintf(`
|
|
basic_auth admin %q {
|
|
set resp_header X-Auth-Status "admin"
|
|
}
|
|
basic_auth guest %q {
|
|
set resp_header X-Auth-Status "guest"
|
|
}
|
|
`, string(adminHash), string(guestHash)), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test admin user
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1.SetBasicAuth("admin", "adminpass")
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "admin", w1.Header().Get("X-Auth-Status"))
|
|
|
|
// Test guest user
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req2.SetBasicAuth("guest", "guestpass")
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "guest", w2.Header().Get("X-Auth-Status"))
|
|
}
|
|
|
|
func TestHTTPFlow_RouteConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("route processed"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
route backend {
|
|
set resp_header X-Route "backend"
|
|
}
|
|
route frontend {
|
|
set resp_header X-Route "frontend"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test API route
|
|
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req1 = routes.WithRouteContext(req1, mockRoute("backend"))
|
|
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "backend", w1.Header().Get("X-Route"))
|
|
|
|
// Test admin route
|
|
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req2 = routes.WithRouteContext(req2, mockRoute("frontend"))
|
|
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "frontend", w2.Header().Get("X-Route"))
|
|
}
|
|
|
|
func TestHTTPFlow_ResponseStatusConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
fmt.Fprint(w, "method not allowed")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
status 405 {
|
|
error 405 'error'
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
|
assert.Equal(t, "error\n", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_ResponseHeaderConditions(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Response-Header", "response header")
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "processed")
|
|
})
|
|
|
|
t.Run("any_value", func(t *testing.T) {
|
|
var rules Rules
|
|
err := parseRules(`
|
|
resp_header X-Response-Header {
|
|
error 405 "error"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
|
assert.Equal(t, "error\n", w.Body.String())
|
|
})
|
|
t.Run("with_value", func(t *testing.T) {
|
|
var rules Rules
|
|
err := parseRules(`
|
|
resp_header X-Response-Header "response header" {
|
|
error 405 "error"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
|
assert.Equal(t, "error\n", w.Body.String())
|
|
})
|
|
|
|
t.Run("with_value_not_matched", func(t *testing.T) {
|
|
var rules Rules
|
|
err := parseRules(`
|
|
resp_header X-Response-Header "not-matched" {
|
|
error 405 "error"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "processed", w.Body.String())
|
|
})
|
|
}
|
|
|
|
func TestHTTPFlow_ComplexRuleCombinations(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "complex processed")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path glob(/api/admin/*) &
|
|
header Authorization &
|
|
method POST {
|
|
set resp_header X-Access-Level "admin"
|
|
set resp_header X-API-Version "v1"
|
|
}
|
|
path glob(/api/users/*) & method GET {
|
|
set resp_header X-Access-Level "user"
|
|
set resp_header X-API-Version "v1"
|
|
}
|
|
path glob(/api/public/*) & method GET {
|
|
set resp_header X-Access-Level "public"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test admin API (should match first rule)
|
|
req1 := httptest.NewRequest(http.MethodPost, "/api/admin/users", nil)
|
|
req1.Header.Set("Authorization", "Bearer token")
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
|
|
assert.Equal(t, http.StatusOK, w1.Code)
|
|
assert.Equal(t, "admin", w1.Header().Get("X-Access-Level"))
|
|
assert.Equal(t, "v1", w1.Header()["X-API-Version"][0])
|
|
|
|
// Test user API (should match second rule)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/api/users/profile", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
assert.Equal(t, "user", w2.Header().Get("X-Access-Level"))
|
|
assert.Equal(t, "v1", w2.Header()["X-API-Version"][0])
|
|
|
|
// Test public API (should match third rule)
|
|
req3 := httptest.NewRequest(http.MethodGet, "/api/public/info", nil)
|
|
w3 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w3, req3)
|
|
|
|
assert.Equal(t, http.StatusOK, w3.Code)
|
|
assert.Equal(t, "public", w3.Header().Get("X-Access-Level"))
|
|
assert.Empty(t, w3.Header()["X-API-Version"])
|
|
}
|
|
|
|
func TestHTTPFlow_ResponseModifier(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "original response")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`{
|
|
set resp_header X-Modified "true"
|
|
set resp_body "Modified: $req_method $req_path"
|
|
}`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "true", w.Header().Get("X-Modified"))
|
|
assert.Equal(t, "Modified: GET /test\n", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_RequireBasicAuth_Challenge(t *testing.T) {
|
|
called := false
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "upstream")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path /protected {
|
|
require_basic_auth "My Realm"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.False(t, called, "require_basic_auth should terminate before calling upstream")
|
|
assert.Equal(t, 401, w.Code)
|
|
assert.Contains(t, w.Header().Get("WWW-Authenticate"), "Basic")
|
|
assert.Contains(t, w.Header().Get("WWW-Authenticate"), "My Realm")
|
|
}
|
|
|
|
func TestHTTPFlow_NegationMatcher(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "ok")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
!path glob("/public/*") {
|
|
set resp_header X-Scope private
|
|
}
|
|
path glob("/public/*") {
|
|
set resp_header X-Scope public
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
t.Run("public", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/public/index.html", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "public", w.Header().Get("X-Scope"))
|
|
})
|
|
|
|
t.Run("private", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "private", w.Header().Get("X-Scope"))
|
|
})
|
|
}
|
|
|
|
func TestHTTPFlow_BlockSyntaxCommentsAreIgnored(t *testing.T) {
|
|
upstream := mockUpstream(http.StatusOK, "ok")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path /comment {
|
|
// comment with braces { } should be ignored
|
|
set resp_header X-Commented ok # trailing comment should be ignored too
|
|
set resp_header X-Literal "//not-a-comment" // but this one is a real comment
|
|
/* block comment
|
|
spanning multiple lines { } */
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/comment", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "ok", w.Header().Get("X-Commented"))
|
|
assert.Equal(t, "//not-a-comment", w.Header().Get("X-Literal"))
|
|
}
|
|
|
|
func TestHTTPFlow_RemoveResponseHeader_RemovesUpstreamHeader(t *testing.T) {
|
|
upstream := mockUpstreamWithHeaders(http.StatusOK, "ok", http.Header{
|
|
"X-Secret": []string{"top-secret"},
|
|
"X-Keep": []string{"keep"},
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
{
|
|
remove resp_header X-Secret
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "keep", w.Header().Get("X-Keep"))
|
|
assert.Empty(t, w.Result().Header.Get("X-Secret"))
|
|
}
|
|
|
|
func TestHTTPFlow_RemoveRequestHeader_BeforeUpstream(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Seen-Secret", r.Header.Get("X-Secret"))
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "ok")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
{
|
|
remove header X-Secret
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.Header.Set("X-Secret", "should-not-reach-upstream")
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Empty(t, w.Header().Get("X-Seen-Secret"), "X-Secret should be removed before reaching upstream")
|
|
}
|
|
|
|
func TestHTTPFlow_RewritePreservesQueryString(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, "path=%s foo=%s bar=%s", r.URL.Path, r.URL.Query().Get("foo"), r.URL.Query().Get("bar"))
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path glob("/api/*") {
|
|
rewrite /api/ /v1/
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/users?foo=1&bar=2", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "path=/v1/users foo=1 bar=2", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_ResponseModifier_PreservesUpstreamStatus(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusCreated)
|
|
fmt.Fprint(w, "created")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
{
|
|
set resp_body "overridden"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/create", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
assert.Equal(t, "overridden\n", w.Body.String())
|
|
}
|
|
|
|
func TestHTTPFlow_PreTermination_SkipsLaterPreCommands_ButRunsPostOnlyAndPostMatchers(t *testing.T) {
|
|
upstreamCalled := false
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
upstreamCalled = true
|
|
fmt.Fprint(w, "upstream")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
path / {
|
|
error 403 blocked
|
|
}
|
|
path / {
|
|
set resp_header X-Late should-not-run
|
|
}
|
|
status 4xx {
|
|
set resp_header X-Post true
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.False(t, upstreamCalled)
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
assert.Equal(t, "blocked\n", w.Body.String())
|
|
assert.Equal(t, "should-not-run", w.Header().Get("X-Late"))
|
|
assert.Equal(t, "true", w.Header().Get("X-Post"))
|
|
}
|
|
|
|
func TestHTTPFlow_PostRuleTermination_StopsRemainingCommandsInRule(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "ok")
|
|
})
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
status 200 {
|
|
error 500 failed
|
|
set resp_header X-After should-not-run
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
assert.Equal(t, "failed\n", w.Body.String())
|
|
assert.Empty(t, w.Header().Get("X-After"))
|
|
}
|
|
|
|
func TestHTTPFlow_EnvVarExpansionInDoBody(t *testing.T) {
|
|
t.Setenv("GODOXY_TEST_ENV", "env-value")
|
|
|
|
upstream := mockUpstream(http.StatusOK, "ok")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
{
|
|
set resp_header X-From-Env "${GODOXY_TEST_ENV}"
|
|
}
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "env-value", w.Header().Get("X-From-Env"))
|
|
}
|