Files
godoxy-yusing/internal/route/rules/do_log_test.go
2026-01-10 18:40:06 +08:00

296 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"
gperr "github.com/yusing/goutils/errs"
)
// 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) gperr.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)
}