Files
xk-hy-forward-go/internal/forward/apilog_test.go

220 lines
6.1 KiB
Go
Raw Normal View History

2026-05-28 17:04:29 +08:00
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)
}