mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-17 14:09:44 +02:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
295 lines
7.9 KiB
Go
295 lines
7.9 KiB
Go
package rules
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yusing/godoxy/internal/serialization"
|
|
)
|
|
|
|
// mockUpstream creates a simple upstream handler for testing
|
|
func mockUpstream(status int, body string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(body))
|
|
}
|
|
}
|
|
|
|
// mockUpstreamWithHeaders creates an upstream that returns specific headers
|
|
func mockUpstreamWithHeaders(status int, body string, headers http.Header) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
maps.Copy(w.Header(), headers)
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(body))
|
|
}
|
|
}
|
|
|
|
func parseRules(data string, target *Rules) error {
|
|
_, err := serialization.ConvertString(data, reflect.ValueOf(target))
|
|
return err
|
|
}
|
|
|
|
func TestLogCommand_TemporaryFile(t *testing.T) {
|
|
upstream := mockUpstreamWithHeaders(200, "success response", http.Header{
|
|
"Content-Type": []string{"application/json"},
|
|
})
|
|
|
|
logFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
- name: log-request-response
|
|
do: |
|
|
log info %q '$req_method $req_url $status_code $resp_header(Content-Type)'
|
|
`, logFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest("POST", "/api/users", nil)
|
|
req.Header.Set("User-Agent", "test-agent")
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
assert.Equal(t, "success response", w.Body.String())
|
|
|
|
// Read and verify log content
|
|
content := TestFileContent(logFile)
|
|
logContent := string(content)
|
|
|
|
assert.Equal(t, "POST /api/users 200 application/json\n", logContent)
|
|
}
|
|
|
|
func TestLogCommand_StdoutAndStderr(t *testing.T) {
|
|
upstream := mockUpstream(200, "success")
|
|
|
|
var rules Rules
|
|
err := parseRules(`
|
|
- name: log-stdout
|
|
do: |
|
|
log info /dev/stdout "stdout: $req_method $status_code"
|
|
- name: log-stderr
|
|
do: |
|
|
log error /dev/stderr "stderr: $req_path $status_code"
|
|
`, &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, 200, w.Code)
|
|
// Note: We can't easily capture stdout/stderr in unit tests,
|
|
// but we can verify no errors occurred and the handler completed
|
|
}
|
|
|
|
func TestLogCommand_DifferentLogLevels(t *testing.T) {
|
|
upstream := mockUpstream(404, "not found")
|
|
|
|
infoFile := TestRandomFileName()
|
|
warnFile := TestRandomFileName()
|
|
errorFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
- name: log-info
|
|
do: |
|
|
log info %s "INFO: $req_method $status_code"
|
|
- name: log-warn
|
|
do: |
|
|
log warn %s "WARN: $req_path $status_code"
|
|
- name: log-error
|
|
do: |
|
|
log error %s "ERROR: $req_method $req_path $status_code"
|
|
`, infoFile, warnFile, errorFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest("DELETE", "/api/resource/123", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, 404, w.Code)
|
|
|
|
// Verify each log file
|
|
infoContent := TestFileContent(infoFile)
|
|
assert.Equal(t, "INFO: DELETE 404", strings.TrimSpace(string(infoContent)))
|
|
|
|
warnContent := TestFileContent(warnFile)
|
|
assert.Equal(t, "WARN: /api/resource/123 404", strings.TrimSpace(string(warnContent)))
|
|
|
|
errorContent := TestFileContent(errorFile)
|
|
assert.Equal(t, "ERROR: DELETE /api/resource/123 404", strings.TrimSpace(string(errorContent)))
|
|
}
|
|
|
|
func TestLogCommand_TemplateVariables(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Custom-Header", "custom-value")
|
|
w.Header().Set("Content-Length", "42")
|
|
w.WriteHeader(201)
|
|
w.Write([]byte("created"))
|
|
})
|
|
|
|
tempFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
- name: log-with-templates
|
|
do: |
|
|
log info %s 'Request: $req_method $req_url Host: $req_host User-Agent: $header(User-Agent) Response: $status_code Custom-Header: $resp_header(X-Custom-Header) Content-Length: $resp_header(Content-Length)'
|
|
`, tempFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
req := httptest.NewRequest("PUT", "/api/resource", nil)
|
|
req.Header.Set("User-Agent", "test-client/1.0")
|
|
req.Host = "example.com"
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, 201, w.Code)
|
|
|
|
// Verify log content
|
|
content := TestFileContent(tempFile)
|
|
logContent := strings.TrimSpace(string(content))
|
|
|
|
assert.Equal(t, "Request: PUT /api/resource Host: example.com User-Agent: test-client/1.0 Response: 201 Custom-Header: custom-value Content-Length: 42", logContent)
|
|
}
|
|
|
|
func TestLogCommand_ConditionalLogging(t *testing.T) {
|
|
upstream := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/error":
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("internal server error"))
|
|
case "/notfound":
|
|
w.WriteHeader(404)
|
|
w.Write([]byte("not found"))
|
|
default:
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("success"))
|
|
}
|
|
})
|
|
|
|
successFile := TestRandomFileName()
|
|
errorFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
- name: log-success
|
|
on: status 2xx
|
|
do: |
|
|
log info %q "SUCCESS: $req_method $req_path $status_code"
|
|
- name: log-error
|
|
on: status 4xx | status 5xx
|
|
do: |
|
|
log error %q "ERROR: $req_method $req_path $status_code"
|
|
`, successFile, errorFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Test success request
|
|
req1 := httptest.NewRequest("GET", "/success", nil)
|
|
w1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w1, req1)
|
|
assert.Equal(t, 200, w1.Code)
|
|
|
|
// Test not found request
|
|
req2 := httptest.NewRequest("GET", "/notfound", nil)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
assert.Equal(t, 404, w2.Code)
|
|
|
|
// Test server error request
|
|
req3 := httptest.NewRequest("POST", "/error", nil)
|
|
w3 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w3, req3)
|
|
assert.Equal(t, 500, w3.Code)
|
|
|
|
// Verify success log
|
|
successContent := TestFileContent(successFile)
|
|
successLines := strings.Split(strings.TrimSpace(string(successContent)), "\n")
|
|
assert.Len(t, successLines, 1)
|
|
assert.Equal(t, "SUCCESS: GET /success 200", successLines[0])
|
|
|
|
// Verify error log
|
|
errorContent := TestFileContent(errorFile)
|
|
errorLines := strings.Split(strings.TrimSpace(string(errorContent)), "\n")
|
|
require.Len(t, errorLines, 2)
|
|
assert.Equal(t, "ERROR: GET /notfound 404", errorLines[0])
|
|
assert.Equal(t, "ERROR: POST /error 500", errorLines[1])
|
|
}
|
|
|
|
func TestLogCommand_MultipleLogEntries(t *testing.T) {
|
|
upstream := mockUpstream(200, "response")
|
|
|
|
tempFile := TestRandomFileName()
|
|
|
|
var rules Rules
|
|
err := parseRules(fmt.Sprintf(`
|
|
- name: log-multiple
|
|
do: |
|
|
log info %q "$req_method $req_path $status_code"`, tempFile), &rules)
|
|
require.NoError(t, err)
|
|
|
|
handler := rules.BuildHandler(upstream)
|
|
|
|
// Make multiple requests
|
|
requests := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{"GET", "/users"},
|
|
{"POST", "/users"},
|
|
{"PUT", "/users/1"},
|
|
{"DELETE", "/users/1"},
|
|
}
|
|
|
|
for _, reqInfo := range requests {
|
|
req := httptest.NewRequest(reqInfo.method, reqInfo.path, nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, 200, w.Code)
|
|
}
|
|
|
|
// Verify all requests were logged
|
|
content := TestFileContent(tempFile)
|
|
logContent := strings.TrimSpace(string(content))
|
|
lines := strings.Split(logContent, "\n")
|
|
|
|
assert.Len(t, lines, len(requests))
|
|
|
|
for i, reqInfo := range requests {
|
|
expectedLog := reqInfo.method + " " + reqInfo.path + " 200"
|
|
assert.Equal(t, expectedLog, lines[i])
|
|
}
|
|
}
|
|
|
|
func TestLogCommand_InvalidTemplate(t *testing.T) {
|
|
var rules Rules
|
|
|
|
// Test with invalid template syntax
|
|
err := parseRules(`
|
|
- name: log-invalid
|
|
do: |
|
|
log info /dev/stdout "$invalid_var"`, &rules)
|
|
assert.ErrorIs(t, err, ErrUnexpectedVar)
|
|
}
|