onesvm-browser-server/server/internal/httpx/httpx_test.go
chii eb972dfa93 feat: 落地 browser-server 控制面并打通 mgr1 海外订阅
单二进制三角色 + Dock 适配器 + Swarm stack 达到可部署态;mgr1 实测订阅经 central-proxy bootstrap,探活 alive=41/52。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 15:05:12 +08:00

137 lines
4.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.

// httpx_test.go:守卫 hook / 类型白名单 / 错误归类(环回测试,无外网)。
package httpx
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// fakeGuard 测试用守卫(可编程拒绝)。
type fakeGuard struct{ deny map[string]bool }
func (f fakeGuard) RedirectCheck(_ context.Context, url string) error {
if f.deny[url] {
return &DeniedError{Reason: "test deny"}
}
return nil
}
// TestContentTypeWhitelist 类型白名单判定。
func TestContentTypeWhitelist(t *testing.T) {
cases := map[string]bool{
"text/html; charset=utf-8": true,
"application/json": true,
"application/pdf": true,
"text/xml": true,
"text/csv": true, // text/* 放行
"application/octet-stream": false,
"image/png": false,
"video/mp4": false,
"": true, // 空 Content-Type 保守放行
}
for ct, want := range cases {
if got := allowedType(ct); got != want {
t.Errorf("allowedType(%q)=%v want %v", ct, got, want)
}
}
}
// TestMaxBytesGuard 5MB 上限守卫。
func TestMaxBytesGuard(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = io.Copy(w, strings.NewReader(strings.Repeat("a", 4096)))
}))
defer srv.Close()
c := New(nil, 1024) // 上限 1KB,响应 4KB → blocked
resp, err := c.Get(context.Background(), srv.URL)
if err == nil {
t.Fatalf("超限应拒绝: %+v", resp)
}
if _, ok := err.(*BlockedError); !ok {
t.Fatalf("应 BlockedError: %v", err)
}
}
// TestRedirectGuard 重定向每跳重验。
func TestRedirectGuard(t *testing.T) {
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if calls == 1 {
http.Redirect(w, r, "/step2", http.StatusFound)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte("ok"))
}))
defer srv.Close()
// 守卫放行
c := New(fakeGuard{deny: map[string]bool{}}, 0)
resp, err := c.Get(context.Background(), srv.URL)
if err != nil {
t.Fatalf("放行路径应成功: %v", err)
}
if resp.StatusCode != 200 || resp.FinalURL == "" {
t.Fatalf("响应不符: %+v", resp)
}
// 守卫拒绝目标跳:CheckRedirect 对相对 Location 以 req.URL.String() 传入,
// Go 会先解析为绝对 URL,但错误信息显示传入形态含相对路径;deny 表同时覆盖两种形态。
calls = 0 // 重置:让 c2 的首跳重新 302 到 /step2
denyTarget := srv.URL + "/step2"
c2 := New(fakeGuard{deny: map[string]bool{denyTarget: true, "/step2": true}}, 0)
_, err = c2.Get(context.Background(), srv.URL)
if err == nil {
t.Fatal("守卫拒绝重定向目标应报错")
}
if !strings.Contains(err.Error(), "denied") {
t.Fatalf("拒绝错误应含 denied: %v", err)
}
}
// TestGuardNil 允许无守卫构造(环回测试场景)。
func TestGuardNil(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
c := New(nil, 0)
resp, err := c.Get(context.Background(), srv.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 || !strings.Contains(string(resp.Body), "ok") {
t.Fatalf("响应不符: %+v", resp)
}
}
// TestErrorCodeMapping 错误归类。
func TestErrorCodeMapping(t *testing.T) {
if got := ErrorCode(&BlockedError{Reason: "x"}); got != "blocked" {
t.Fatalf("blocked: %s", got)
}
if got := ErrorCode(&DeniedError{Reason: "x"}); got != "denied" {
t.Fatalf("denied: %s", got)
}
if got := ErrorCode(&TimeoutError{Reason: "x"}); got != "timeout" {
t.Fatalf("timeout: %s", got)
}
if got := ErrorCode(errors.New("net fail")); got != "upstream" {
t.Fatalf("upstream: %s", got)
}
}
// TestUserAgent UA 与 bench 一致。
func TestUserAgent(t *testing.T) {
if !strings.Contains(UA, "Chrome/151") {
t.Fatalf("UA 应为 Chrome 151: %s", UA)
}
}
var _ = io.Discard // 锚点:io 依赖保留