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:
Sanaei
2026-06-15 15:17:03 +02:00
committed by GitHub
parent b5872af279
commit 7605902324
37 changed files with 2580 additions and 330 deletions
@@ -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)
}
})
}
}
+28
View File
@@ -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)
}
})
}
+201
View File
@@ -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)
}
}
+32 -6
View File
@@ -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)
}
}