mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-19 08:37:14 +00:00
Test-quality audit: fix 2 prod bugs, strengthen weak tests, add mutation/fuzz/CI tooling (#5345)
* test(audit): add gremlins/rapid/coverage tooling + AUDIT.md scaffold * test(audit): hygiene sweep (race-clean except logger global; Finding #2) + smell inventory * test(audit): cover untested error/edge branches (TLS proxy+pin, migration tag cleanup=Finding #1) * test(audit): strengthen internal/sub link tests (dedup key, TLS/Reality mapping, clash well-formedness) * test(audit): property (rapid) + fuzz tests for joinHostPort/userinfo/pin/ParseLink * test(audit): tighten frontend subSortIndex rejection assertions + wire coverage * ci(audit): add shuffle gate + non-blocking race job (Finding #2) + fuzz-smoke; document mutation policy * chore(audit): gitignore frontend coverage output * test(audit): exhaustive whole-repo pass — strengthen 5 weak/fake tests (netproxy, CSP, modal per-protocol loops, schema coercions) * docs(contributing): add Testing section (conventions, race/shuffle, fuzz, mutation policy); drop AUDIT.md ledger * fix(logger,migration): guard logBuffer with mutex; execute legacy tag cleanup (tx.Exec); make CI race gate blocking * ci(mutation): add nightly scoped gremlins workflow (informational artifacts) * test(audit): strengthen runtime tests — baseURL scheme/port bounds, isNonEmptySlice, trafficReset * test(audit): strengthen clash tests — reality field mapping + tcp-header validation * test(audit): runtime — egress-proxy + content-type tests; drop redundant bp=='' branch * test(audit): strengthen link parser/helper tests (defaultPort, splitComma, base64, canonicalQuery, tls/reality/transport mapping) * test(audit): strengthen sub/xray/common/netsafe/mtproto/config/middleware tests (kill surviving mutants) * test(audit): raise timeout on protocol-iteration modal tests (heavy re-renders, slow on CI) * fix(logger): GetLogs returns at most c entries (off-by-one fix; addresses PR review) * perf(logger): snapshot logBuffer under lock so GetLogs doesn't block logging; clarify fuzz-seed docs (addresses PR review)
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestFormatTraffic_UnitBoundaries pins the exact switch point in the loop
|
||||
// condition `size >= 1024`: a unit must roll over at exactly 1024 (not 1023,
|
||||
// not 1025), and a value one byte short must stay in the lower unit. This kills
|
||||
// CONDITIONALS_BOUNDARY (>= -> >) and ARITHMETIC_BASE on the 1024 comparison.
|
||||
func TestFormatTraffic_UnitBoundaries(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
bytes int64
|
||||
want string
|
||||
}{
|
||||
// Just below the first boundary: must NOT roll over to KB.
|
||||
{"one_below_kb", 1023, "1023.00B"},
|
||||
// Exactly at the boundary: must roll over to KB.
|
||||
{"exactly_kb", 1024, "1.00KB"},
|
||||
// Just above: stays in KB.
|
||||
{"one_above_kb", 1025, "1.00KB"},
|
||||
// Just below the MB boundary: stays in KB (proves division divisor 1024).
|
||||
{"one_below_mb", 1024*1024 - 1, "1024.00KB"},
|
||||
// Exactly at the MB boundary: rolls over to MB.
|
||||
{"exactly_mb", 1024 * 1024, "1.00MB"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := FormatTraffic(c.bytes); got != c.want {
|
||||
t.Fatalf("FormatTraffic(%d) = %q, want %q", c.bytes, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFormatTraffic_ClampsAtPB pins the upper bound guard
|
||||
// `unitIndex < len(units)-1`: huge values must clamp at PB instead of indexing
|
||||
// past the units slice. A mutated bound (< -> <= via CONDITIONALS_BOUNDARY, or
|
||||
// len(units)-1 -> len(units)+1 via INVERT_NEGATIVES/ARITHMETIC_BASE) would run
|
||||
// one extra iteration and panic with index-out-of-range, so the assertion that
|
||||
// these return a normal "PB" string kills those mutants.
|
||||
func TestFormatTraffic_ClampsAtPB(t *testing.T) {
|
||||
const pb = int64(1024 * 1024 * 1024 * 1024 * 1024)
|
||||
cases := []struct {
|
||||
name string
|
||||
bytes int64
|
||||
want string
|
||||
}{
|
||||
// Stays at PB even though size is still >= 1024 at the PB level.
|
||||
{"1024_pb", 1024 * pb, "1024.00PB"},
|
||||
// Max int64 must not overflow the units slice.
|
||||
{"max_int64", 9223372036854775807, "8192.00PB"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := FormatTraffic(c.bytes); got != c.want {
|
||||
t.Fatalf("FormatTraffic(%d) = %q, want %q", c.bytes, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package link
|
||||
|
||||
import "testing"
|
||||
|
||||
// FuzzParseLink asserts the parser never panics and upholds its (result, error) contract
|
||||
// — exactly one non-nil. It base64-decodes and type-asserts attacker-controllable JSON,
|
||||
// the classic panic source.
|
||||
func FuzzParseLink(f *testing.F) {
|
||||
seeds := []string{
|
||||
"",
|
||||
"not-a-link",
|
||||
"vmess://eyJ2IjoiMiIsInBzIjoidCIsImFkZCI6ImEuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwibmV0IjoidGNwIn0=",
|
||||
"vless://11111111-2222-4333-8444-555555555555@a.com:443?type=tcp&security=none#x",
|
||||
"trojan://pass@a.com:443?security=tls#x",
|
||||
"ss://YWVzLTI1Ni1nY206cGFzcw==@a.com:8388#x",
|
||||
"hysteria2://pass@a.com:443?sni=a.com#x",
|
||||
"wireguard://cGsdkey@a.com:51820?publickey=pub#x",
|
||||
}
|
||||
for _, s := range seeds {
|
||||
f.Add(s)
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, s string) {
|
||||
res, err := ParseLink(s)
|
||||
if (res == nil) == (err == nil) {
|
||||
t.Fatalf("ParseLink(%q): exactly one of (result, error) must be non-nil; got res=%v err=%v", s, res, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultPort(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
def int
|
||||
want int
|
||||
}{
|
||||
{"", 443, 443},
|
||||
{"8080", 443, 8080},
|
||||
{"0", 443, 443}, // non-positive falls back
|
||||
{"-1", 443, 443}, // negative falls back
|
||||
{"abc", 443, 443}, // unparseable falls back
|
||||
{"65535", 443, 65535},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := defaultPort(c.in, c.def); got != c.want {
|
||||
t.Errorf("defaultPort(%q,%d) = %d, want %d", c.in, c.def, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstNonEmptyAndParam(t *testing.T) {
|
||||
if got := firstNonEmpty("a", "b"); got != "a" {
|
||||
t.Errorf("firstNonEmpty(a,b) = %q, want a", got)
|
||||
}
|
||||
if got := firstNonEmpty("", "b"); got != "b" {
|
||||
t.Errorf("firstNonEmpty(,b) = %q, want b", got)
|
||||
}
|
||||
p := url.Values{"x": {""}, "y": {"hit"}, "z": {"z"}}
|
||||
if got := firstParam(p, "x", "y", "z"); got != "hit" {
|
||||
t.Errorf("firstParam = %q, want hit (first non-empty)", got)
|
||||
}
|
||||
if got := firstParam(p, "x"); got != "" {
|
||||
t.Errorf("firstParam(only empty) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitComma(t *testing.T) {
|
||||
if got := splitComma(""); got != nil {
|
||||
t.Errorf("splitComma(empty) = %v, want nil", got)
|
||||
}
|
||||
if got := splitComma("a, ,b ,, c"); !reflect.DeepEqual(got, []string{"a", "b", "c"}) {
|
||||
t.Errorf("splitComma trim/skip = %v, want [a b c]", got)
|
||||
}
|
||||
if got := splitCommaOrDefault("", []string{"d"}); !reflect.DeepEqual(got, []string{"d"}) {
|
||||
t.Errorf("splitCommaOrDefault(empty) = %v, want [d]", got)
|
||||
}
|
||||
if got := splitCommaOrDefault("x,y", []string{"d"}); !reflect.DeepEqual(got, []string{"x", "y"}) {
|
||||
t.Errorf("splitCommaOrDefault(x,y) = %v, want [x y]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPadAndBase64DecodeFlexible(t *testing.T) {
|
||||
if got := padBase64("abc"); got != "abc=" {
|
||||
t.Errorf("padBase64(abc) = %q, want abc=", got)
|
||||
}
|
||||
if got := padBase64("abcd"); got != "abcd" {
|
||||
t.Errorf("padBase64(abcd) = %q, want unchanged", got)
|
||||
}
|
||||
std := base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:secret"))
|
||||
if got, err := base64DecodeFlexible(std); err != nil || got != "aes-256-gcm:secret" {
|
||||
t.Errorf("base64DecodeFlexible(std) = (%q,%v), want (aes-256-gcm:secret,nil)", got, err)
|
||||
}
|
||||
rawURL := base64.RawURLEncoding.EncodeToString([]byte("m:p"))
|
||||
if got, err := base64DecodeFlexible(rawURL); err != nil || got != "m:p" {
|
||||
t.Errorf("base64DecodeFlexible(rawurl) = (%q,%v), want (m:p,nil)", got, err)
|
||||
}
|
||||
if _, err := base64DecodeFlexible("!!!not!!!"); err == nil {
|
||||
t.Error("base64DecodeFlexible(garbage) should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeHash(t *testing.T) {
|
||||
if got := decodeHash(""); got != "" {
|
||||
t.Errorf("decodeHash(empty) = %q, want empty", got)
|
||||
}
|
||||
if got := decodeHash("a%20b"); got != "a b" {
|
||||
t.Errorf("decodeHash(a%%20b) = %q, want 'a b'", got)
|
||||
}
|
||||
if got := decodeHash("plain"); got != "plain" {
|
||||
t.Errorf("decodeHash(plain) = %q, want plain", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalQuery_SortsKeys(t *testing.T) {
|
||||
// unsorted input must come out key-sorted for a stable identity
|
||||
got := canonicalQuery(url.Values{"c": {"3"}, "a": {"1"}, "b": {"2"}})
|
||||
if got != "a=1&b=2&c=3" {
|
||||
t.Fatalf("canonicalQuery = %q, want a=1&b=2&c=3", got)
|
||||
}
|
||||
}
|
||||
|
||||
// stream navigates res.Outbound["streamSettings"][key] as a map.
|
||||
func streamSub(t *testing.T, res *ParseResult, key string) map[string]any {
|
||||
t.Helper()
|
||||
ss, _ := res.Outbound["streamSettings"].(map[string]any)
|
||||
m, ok := ss[key].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("streamSettings.%s missing/not a map: %#v", key, ss)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestParse_RealitySecurityMapped(t *testing.T) {
|
||||
res, err := ParseLink("vless://uuid@h.com:443?type=tcp&security=reality&pbk=PBK&sid=SID&sni=SNI&fp=firefox&spx=%2Fspx&pqv=PQV")
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
re := streamSub(t, res, "realitySettings")
|
||||
for k, want := range map[string]string{"publicKey": "PBK", "shortId": "SID", "serverName": "SNI", "fingerprint": "firefox", "spiderX": "/spx", "mldsa65Verify": "PQV"} {
|
||||
if re[k] != want {
|
||||
t.Errorf("realitySettings[%q] = %v, want %q", k, re[k], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_TLSSecurityMapped(t *testing.T) {
|
||||
res, err := ParseLink("trojan://pw@h.com:443?type=tcp&security=tls&sni=SNI&fp=chrome&alpn=h2,http/1.1&ech=ECH&pcs=PCS")
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
tls := streamSub(t, res, "tlsSettings")
|
||||
if tls["serverName"] != "SNI" || tls["fingerprint"] != "chrome" || tls["echConfigList"] != "ECH" || tls["pinnedPeerCertSha256"] != "PCS" {
|
||||
t.Errorf("tlsSettings fields = %#v", tls)
|
||||
}
|
||||
if alpn, _ := tls["alpn"].([]string); !reflect.DeepEqual(alpn, []string{"h2", "http/1.1"}) {
|
||||
t.Errorf("alpn = %#v, want [h2 http/1.1]", tls["alpn"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_WSAndGRPCTransport(t *testing.T) {
|
||||
ws, err := ParseLink("vless://uuid@h.com:443?type=ws&host=H&path=%2Fwspath")
|
||||
if err != nil {
|
||||
t.Fatalf("parse ws: %v", err)
|
||||
}
|
||||
wss := streamSub(t, ws, "wsSettings")
|
||||
if wss["host"] != "H" || wss["path"] != "/wspath" {
|
||||
t.Errorf("wsSettings = %#v, want host=H path=/wspath", wss)
|
||||
}
|
||||
|
||||
grpc, err := ParseLink("vless://uuid@h.com:443?type=grpc&serviceName=svc&authority=auth&mode=multi")
|
||||
if err != nil {
|
||||
t.Fatalf("parse grpc: %v", err)
|
||||
}
|
||||
gs := streamSub(t, grpc, "grpcSettings")
|
||||
if gs["serviceName"] != "svc" || gs["authority"] != "auth" || gs["multiMode"] != true {
|
||||
t.Errorf("grpcSettings = %#v, want serviceName=svc authority=auth multiMode=true", gs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_TCPHTTPHeader(t *testing.T) {
|
||||
res, err := ParseLink("vless://uuid@h.com:443?type=tcp&headerType=http&host=ex.com&path=%2F")
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
tcp := streamSub(t, res, "tcpSettings")
|
||||
header, _ := tcp["header"].(map[string]any)
|
||||
if header["type"] != "http" {
|
||||
t.Errorf("tcp header type = %v, want http", header["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVless_CoreFields(t *testing.T) {
|
||||
res, err := ParseLink("vless://the-uuid@9.9.9.9:8443?type=tcp&security=none&flow=xtls-rprx-vision#tag1")
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
st, _ := res.Outbound["settings"].(map[string]any)
|
||||
if st["address"] != "9.9.9.9" || st["port"] != 8443 || st["id"] != "the-uuid" || st["flow"] != "xtls-rprx-vision" {
|
||||
t.Errorf("vless settings = %#v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTrojanAndSS_CoreFields(t *testing.T) {
|
||||
tr, err := ParseLink("trojan://secret@t.com:443?type=tcp&security=tls#tj")
|
||||
if err != nil {
|
||||
t.Fatalf("parse trojan: %v", err)
|
||||
}
|
||||
srv := tr.Outbound["settings"].(map[string]any)["servers"].([]any)[0].(map[string]any)
|
||||
if srv["address"] != "t.com" || srv["port"] != 443 || srv["password"] != "secret" {
|
||||
t.Errorf("trojan server = %#v", srv)
|
||||
}
|
||||
|
||||
ssLink := "ss://" + base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:sspass")) + "@s.com:8388#ss1"
|
||||
ss, err := ParseLink(ssLink)
|
||||
if err != nil {
|
||||
t.Fatalf("parse ss: %v", err)
|
||||
}
|
||||
ssrv := ss.Outbound["settings"].(map[string]any)["servers"].([]any)[0].(map[string]any)
|
||||
if ssrv["address"] != "s.com" || ssrv["port"] != 8388 || ssrv["password"] != "sspass" || ssrv["method"] != "aes-256-gcm" {
|
||||
t.Errorf("ss server = %#v", ssrv)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package netproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -22,6 +24,10 @@ func TestNewHTTPClient(t *testing.T) {
|
||||
{name: "unsupported scheme errors", proxyURL: "ftp://127.0.0.1:21", wantErr: true},
|
||||
}
|
||||
|
||||
// baseTransport clones http.DefaultTransport, whose Proxy and DialContext are already
|
||||
// non-nil — so "!= nil" can't prove our proxy/dialer was applied. Check the real values.
|
||||
defaultDialPtr := reflect.ValueOf(http.DefaultTransport.(*http.Transport).DialContext).Pointer()
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, err := NewHTTPClient(tc.proxyURL, 5*time.Second)
|
||||
@@ -37,16 +43,36 @@ func TestNewHTTPClient(t *testing.T) {
|
||||
if client.Timeout != 5*time.Second {
|
||||
t.Errorf("timeout = %v, want 5s", client.Timeout)
|
||||
}
|
||||
// Empty proxyURL → a plain direct client with no custom transport.
|
||||
if tc.proxyURL == "" {
|
||||
if client.Transport != nil {
|
||||
t.Errorf("empty proxy must yield a direct client (nil Transport), got %T", client.Transport)
|
||||
}
|
||||
return
|
||||
}
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatalf("transport is %T, want *http.Transport", client.Transport)
|
||||
}
|
||||
if tc.wantProxy {
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok || transport.Proxy == nil {
|
||||
t.Errorf("expected transport with Proxy set for %q", tc.proxyURL)
|
||||
// Prove the CONFIGURED proxy is applied: transport.Proxy(req) must
|
||||
// return our URL, not the cloned default's ProxyFromEnvironment.
|
||||
req := httptest.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
u, perr := transport.Proxy(req)
|
||||
if perr != nil {
|
||||
t.Fatalf("transport.Proxy returned error: %v", perr)
|
||||
}
|
||||
if u == nil || u.String() != tc.proxyURL {
|
||||
t.Errorf("transport.Proxy(req) = %v, want %q (configured proxy not applied)", u, tc.proxyURL)
|
||||
}
|
||||
}
|
||||
if tc.wantDial {
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok || transport.DialContext == nil {
|
||||
t.Errorf("expected transport with DialContext set for %q", tc.proxyURL)
|
||||
if transport.DialContext == nil {
|
||||
t.Fatal("DialContext is nil")
|
||||
}
|
||||
// Must be the socks5 dialer, not the cloned default DialContext.
|
||||
if reflect.ValueOf(transport.DialContext).Pointer() == defaultDialPtr {
|
||||
t.Error("DialContext is still the default; socks5 dialer was not applied")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package netsafe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSSRFGuardedDialContext_LiteralIPSkipsResolver pins the netsafe.go:37
|
||||
// decision (`if ip := net.ParseIP(host); ip != nil`). The string "fe80::1%eth0"
|
||||
// is rejected by net.ParseIP (returns nil) but accepted by the resolver, which
|
||||
// yields the link-local address fe80::1. With the branch intact, ParseIP returns
|
||||
// nil so the host falls through to LookupIPAddr, resolves to fe80::1, and is
|
||||
// blocked by IsBlockedIP -> the error mentions the resolved blocked address.
|
||||
// If the condition is flipped to `ip == nil`, the nil-IP literal path is taken
|
||||
// instead: ips = [{IP: nil}], IsBlockedIP(nil) is false, the guard never fires
|
||||
// and the error would never say "blocked private/internal address fe80::1".
|
||||
func TestSSRFGuardedDialContext_LiteralIPSkipsResolver(t *testing.T) {
|
||||
_, err := SSRFGuardedDialContext(context.Background(), "tcp", "[fe80::1%eth0]:80")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for link-local host with zone suffix")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "blocked private/internal address fe80::1") {
|
||||
t.Fatalf("expected guard to block resolved link-local fe80::1, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSRFGuardedDialContext_LiteralPrivateIPv6Blocked complements the above by
|
||||
// confirming that a valid IP literal (parsed by the line 37 branch) is still run
|
||||
// through IsBlockedIP and rejected with the literal in the message.
|
||||
func TestSSRFGuardedDialContext_LiteralPrivateIPv6Blocked(t *testing.T) {
|
||||
_, err := SSRFGuardedDialContext(context.Background(), "tcp", "[::1]:80")
|
||||
if err == nil {
|
||||
t.Fatal("expected dial to ::1 to be blocked")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "blocked private/internal address ::1") {
|
||||
t.Fatalf("expected '::1' literal in blocked error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeHost_LengthBoundary pins the netsafe.go:76 length check
|
||||
// (`len(addr) > 253`). A valid-pattern hostname of exactly 253 chars must be
|
||||
// accepted (kills `>` -> `>=` / off-by-one mutations of the bound), while the
|
||||
// same hostname at 254 chars must be rejected.
|
||||
func TestNormalizeHost_LengthBoundary(t *testing.T) {
|
||||
label := strings.Repeat("a", 61)
|
||||
base := label + "." + label + "." + label + "." // 186 chars, valid pattern
|
||||
h253 := base + strings.Repeat("a", 253-len(base))
|
||||
if len(h253) != 253 {
|
||||
t.Fatalf("test setup: expected 253-char host, got %d", len(h253))
|
||||
}
|
||||
h254 := h253 + "a"
|
||||
|
||||
got, err := NormalizeHost(h253)
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeHost(253-char valid host) returned error: %v", err)
|
||||
}
|
||||
if got != h253 {
|
||||
t.Fatalf("NormalizeHost(253-char host) = %q, want unchanged input", got)
|
||||
}
|
||||
|
||||
if _, err := NormalizeHost(h254); err == nil {
|
||||
t.Fatal("NormalizeHost(254-char host) expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeHost_PatternClauseIndependentOfLength pins the OR in line 76:
|
||||
// a short hostname (well under the 253 limit) that violates the pattern must
|
||||
// still be rejected. If `||` were mutated to `&&`, this short-but-invalid host
|
||||
// would slip through because the length clause is false.
|
||||
func TestNormalizeHost_PatternClauseIndependentOfLength(t *testing.T) {
|
||||
cases := []string{
|
||||
"under_score.example.com",
|
||||
"bad host",
|
||||
"exa$mple.com",
|
||||
"-leadingdash.com",
|
||||
}
|
||||
for _, in := range cases {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
if len(in) > 253 {
|
||||
t.Fatalf("test setup: %q should be short to isolate the pattern clause", in)
|
||||
}
|
||||
if _, err := NormalizeHost(in); err == nil {
|
||||
t.Fatalf("NormalizeHost(%q) expected error for invalid pattern, got nil", in)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeHost_ValidShortHostAccepted ensures a short valid-pattern host is
|
||||
// accepted, so a mutation dropping the `!` on the pattern match (rejecting valid
|
||||
// hosts) is caught alongside the rejection cases above.
|
||||
func TestNormalizeHost_ValidShortHostAccepted(t *testing.T) {
|
||||
const in = "node-1.example.com"
|
||||
got, err := NormalizeHost(in)
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeHost(%q) returned error: %v", in, err)
|
||||
}
|
||||
if got != in {
|
||||
t.Fatalf("NormalizeHost(%q) = %q, want %q", in, got, in)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user