Files
xk-hy-forward-go/internal/forward/apilog_test.go
2026-05-28 17:04:29 +08:00

220 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package forward
import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLogHTTP_writesThreeBlockLog(t *testing.T) {
tmp := t.TempDir()
setAPILogRootForTest(tmp)
t.Cleanup(func() { setAPILogRootForTest("") })
const (
reqBody = `{"hello":"world"}`
respBody = `{"code":200,"msg":"ok"}`
)
mux := http.NewServeMux()
mux.HandleFunc("/province/supervise/data", func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("X-Test-Token"); got != "secret-value-12345" {
t.Errorf("header=%q", got)
}
b, _ := io.ReadAll(r.Body)
if string(b) != reqBody {
t.Errorf("body=%q", b)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(respBody))
})
srv := httptest.NewServer(logHTTP(mux))
defer srv.Close()
req, err := http.NewRequest(http.MethodPost, srv.URL+"/province/supervise/data", strings.NewReader(reqBody))
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-Test-Token", "secret-value-12345")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d", resp.StatusCode)
}
content := readOnlyAPILogFile(t, tmp)
checks := []string{
"=== META ===",
"Kind: supervise",
"=== 1. 入站请求transit → forward===",
"X-Test-Token: secret-value-12345",
reqBody,
"=== 2. 出站转发forward → 政务云)===",
"(未发起出站转发)",
"=== 3. 回复(政务云 → forward → transit===",
respBody,
`"code": 200`,
"BodyJSON:",
}
for _, c := range checks {
if !strings.Contains(content, c) {
t.Errorf("log missing %q\n---\n%s", c, content)
}
}
}
func TestLogHTTP_forwardProxyThreeBlocks(t *testing.T) {
tmp := t.TempDir()
setAPILogRootForTest(tmp)
t.Cleanup(func() { setAPILogRootForTest("") })
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/mng/file/auth/upload" {
t.Errorf("path=%s", r.URL.Path)
}
if r.Header.Get("X-Authorization") != "tok" {
t.Errorf("X-Authorization=%q", r.Header.Get("X-Authorization"))
}
if r.Header.Get("X-Forward-Token") != "" {
t.Error("X-Forward-Token must not reach upstream")
}
b, _ := io.ReadAll(r.Body)
if string(b) != "pdf-bytes" {
t.Errorf("body=%q", b)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"success":true,"record":{"fileId":"abc123"}}`))
}))
defer upstream.Close()
fileTarget := strings.TrimRight(upstream.URL, "/") + "/mng/file/auth/upload"
proxy, err := newReverseProxy(fileTarget, "file")
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/mng/file/auth/upload", func(w http.ResponseWriter, r *http.Request) {
handleForward(w, r, "file", proxy, fileTarget, "", "", true)
})
srv := httptest.NewServer(logHTTP(mux))
defer srv.Close()
req, err := http.NewRequest(http.MethodPost, srv.URL+"/mng/file/auth/upload", strings.NewReader("pdf-bytes"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "multipart/form-data; boundary=abc")
req.Header.Set("X-Authorization", "tok")
req.Header.Set("X-Forward-Token", "should-strip")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status=%d", resp.StatusCode)
}
content := readOnlyAPILogFile(t, tmp)
checks := []string{
"Kind: file",
"=== 1. 入站请求transit → forward===",
"X-Forward-Token: should-strip",
"body_len=9",
"=== 2. 出站转发forward → 政务云)===",
"TargetURL: " + fileTarget,
"X-Authorization: tok",
"=== 3. 回复(政务云 → forward → transit===",
"UpstreamStatus: 200",
`"fileId": "abc123"`,
"BodyJSON:",
}
for _, c := range checks {
if !strings.Contains(content, c) {
t.Errorf("log missing %q\n---\n%s", c, content)
}
}
if strings.Contains(content, "X-Forward-Token:") && strings.Contains(content, "=== 2. 出站转发") {
// X-Forward-Token must not appear in outbound headers block
outboundSection := content[strings.Index(content, "=== 2. 出站转发"):strings.Index(content, "=== 3. 回复")]
if strings.Contains(outboundSection, "X-Forward-Token") {
t.Errorf("outbound block leaked X-Forward-Token:\n%s", outboundSection)
}
}
}
func TestLogHTTP_forbiddenHasOutboundAndClientResponse(t *testing.T) {
tmp := t.TempDir()
setAPILogRootForTest(tmp)
t.Cleanup(func() { setAPILogRootForTest("") })
mux := http.NewServeMux()
mux.HandleFunc("/province/supervise/data", func(w http.ResponseWriter, r *http.Request) {
handleForward(w, r, "supervise", nil, "http://example.com/up", "10.0.0.1", "secret", true)
})
srv := httptest.NewServer(logHTTP(mux))
defer srv.Close()
req, err := http.NewRequest(http.MethodPost, srv.URL+"/province/supervise/data", strings.NewReader(`{}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Forward-Token", "wrong")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("status=%d", resp.StatusCode)
}
content := readOnlyAPILogFile(t, tmp)
checks := []string{
"=== 2. 出站转发forward → 政务云)===",
"TargetURL: http://example.com/up",
"=== 3. 回复(政务云 → forward → transit===",
"forbidden",
}
for _, c := range checks {
if !strings.Contains(content, c) {
t.Errorf("log missing %q\n---\n%s", c, content)
}
}
}
func readOnlyAPILogFile(t *testing.T, root string) string {
t.Helper()
hourDirs, err := os.ReadDir(root)
if err != nil || len(hourDirs) == 0 {
t.Fatalf("no hour dir under %s: %v", root, err)
}
files, err := os.ReadDir(filepath.Join(root, hourDirs[0].Name()))
if err != nil || len(files) != 1 {
t.Fatalf("want 1 log file, got %d err=%v", len(files), err)
}
raw, err := os.ReadFile(filepath.Join(root, hourDirs[0].Name(), files[0].Name()))
if err != nil {
t.Fatal(err)
}
return string(raw)
}