package checker import ( "context" "errors" "syscall" "testing" "github.com/stretchr/testify/assert" ) func TestHintFor(t *testing.T) { t.Run("nil_err_returns_empty", func(t *testing.T) { assert.Equal(t, "", hintFor("tcp", nil)) assert.Equal(t, "", hintFor("anything", nil)) }) t.Run("context_canceled_uniform", func(t *testing.T) { // Cancellation is always reported as «Проверка отменена.» across // all testIDs. for _, id := range []string{"tcp", "greet", "auth", "connect", "udp", "voice-quality", "api", "unknown"} { assert.Equal(t, "Проверка отменена.", hintFor(id, context.Canceled), "id=%s", id) assert.Equal(t, "Проверка отменена.", hintFor(id, context.DeadlineExceeded), "id=%s", id) } }) cases := []struct { name string testID string err error substring string }{ {"tcp_timeout", "tcp", &timeoutOnlyError{}, "таймаут"}, {"greet_bad_version_mentions_socks5", "greet", ErrSocks5BadVersion, "SOCKS5"}, {"greet_bad_version_mentions_negation", "greet", ErrSocks5BadVersion, "не"}, {"greet_rejected_all_auth_mentions_auth_or_methods", "greet", ErrSocks5RejectedAllAuth, "авторизаци"}, {"auth_login", "auth", ErrAuthRejected, "Логин"}, {"auth_password", "auth", ErrAuthRejected, "паро"}, {"connect_refused_rep05", "connect", ErrSocks5Reply{Code: 0x05}, "REP=05"}, {"connect_refused_text", "connect", ErrSocks5Reply{Code: 0x05}, "connection refused"}, {"connect_unsupported_rep07", "connect", ErrSocks5Reply{Code: 0x07}, "REP=07"}, {"udp_unsupported_mentions_udp", "udp", ErrSocks5Reply{Code: 0x07}, "UDP"}, {"udp_unsupported_mentions_unsupported", "udp", ErrSocks5Reply{Code: 0x07}, "не поддерж"}, {"udp_atyp_ipv6", "udp", ErrUnsupportedRelayATYP, "IPv6"}, {"voice_quality_no_mapped_xor", "voice-quality", ErrSTUNNoMappedAddress, "XOR-MAPPED"}, {"voice_quality_timeout_mentions_stun", "voice-quality", &timeoutOnlyError{}, "STUN"}, {"api_timeout_mentions_api_or_timeout", "api", &timeoutOnlyError{}, "таймаут"}, {"unknown_test_fallback_id", "unknown_test", errors.New("oops"), "unknown_test"}, {"unknown_test_fallback_err", "unknown_test", errors.New("oops"), "oops"}, {"tcp_fallback_friendly_name", "tcp", errors.New("weird"), "TCP"}, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { got := hintFor(c.testID, c.err) assert.Contains(t, got, c.substring, "got=%q", got) }) } } func TestHintFor_AllSocks5ReplyCodesCovered(t *testing.T) { // Every documented REP code (0x01..0x08) should produce a non-empty // hint when surfaced via "connect" or "udp". for code := byte(0x01); code <= 0x08; code++ { err := ErrSocks5Reply{Code: code} assert.NotEmpty(t, hintFor("connect", err), "connect code=%02X", code) assert.NotEmpty(t, hintFor("udp", err), "udp code=%02X", code) } // Unknown REP code (0xFE) still gets a sensible fallback rather than // an empty string. err := ErrSocks5Reply{Code: 0xFE} assert.NotEmpty(t, hintFor("connect", err)) assert.NotEmpty(t, hintFor("udp", err)) } func TestHintFor_PerStepBranches(t *testing.T) { cases := []struct { name string testID string err error substring string }{ // tcp: ECONNREFUSED + generic fallback {"tcp_econnrefused", "tcp", syscall.ECONNREFUSED, "отклонил"}, {"tcp_generic", "tcp", errors.New("dial fail"), "TCP"}, // greet: short reply, timeout, fallback {"greet_short_reply", "greet", ErrShortReply, "укороченный"}, {"greet_timeout", "greet", &timeoutOnlyError{}, "вовремя"}, {"greet_fallback", "greet", errors.New("weird"), "приветствие"}, // auth: credential too long, short reply, timeout, fallback {"auth_cred_too_long", "auth", ErrCredentialTooLong, "255"}, {"auth_short_reply", "auth", ErrShortReply, "укороченный"}, {"auth_timeout", "auth", &timeoutOnlyError{}, "вовремя"}, {"auth_fallback", "auth", errors.New("weird"), "авторизация"}, // connect: host too long, short reply, timeout, generic REP, fallback {"connect_host_too_long", "connect", ErrHostTooLong, "255"}, {"connect_short_reply", "connect", ErrShortReply, "укороченный"}, {"connect_timeout", "connect", &timeoutOnlyError{}, "вовремя"}, {"connect_generic_rep", "connect", ErrSocks5Reply{Code: 0x03}, "REP=03"}, {"connect_unknown_rep", "connect", ErrSocks5Reply{Code: 0xFE}, "REP=FE"}, {"connect_fallback", "connect", errors.New("weird"), "TCP-туннель"}, // udp: short reply, timeout, fallback, non-7 REP {"udp_short_reply", "udp", ErrShortReply, "укороченный"}, {"udp_timeout", "udp", &timeoutOnlyError{}, "вовремя"}, {"udp_other_rep", "udp", ErrSocks5Reply{Code: 0x05}, "REP=05"}, {"udp_unknown_rep", "udp", ErrSocks5Reply{Code: 0xFE}, "REP=FE"}, {"udp_fallback", "udp", errors.New("weird"), "UDP ASSOCIATE"}, // voice-quality: every sentinel branch (collapsed in 2026-05-01 // rewrite into a single user-visible message rather than // per-error "магник cookie" / "семейство адресов" exposition) {"voice_quality_too_short", "voice-quality", ErrSTUNTooShort, "мусор"}, {"voice_quality_bad_magic", "voice-quality", ErrSTUNBadMagicCookie, "мусор"}, {"voice_quality_not_success", "voice-quality", ErrSTUNNotSuccess, "мусор"}, {"voice_quality_txid_mismatch", "voice-quality", ErrSTUNTxIDMismatch, "мусор"}, {"voice_quality_unsupported_family", "voice-quality", ErrSTUNUnsupportedFamily, "мусор"}, {"voice_quality_fallback", "voice-quality", errors.New("weird"), "качество"}, // api: timeout vs generic {"api_timeout", "api", &timeoutOnlyError{}, "таймаут"}, {"api_generic", "api", errors.New("tls boom"), "TLS"}, // socks5ReplyHint via uncategorised step (default branch) // — we can't reach it via hintFor with current testIDs, but the // default formatter still needs to be exercised. } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { got := hintFor(c.testID, c.err) assert.Contains(t, got, c.substring, "got=%q", got) }) } } func TestSocks5ReplyHint_DefaultStep(t *testing.T) { // socks5ReplyHint("", code) hits the final fallback formatter. got := socks5ReplyHint("", 0x03) assert.Contains(t, got, "REP=03") got = socks5ReplyHint("", 0xFE) assert.Contains(t, got, "REP=FE") } func TestTcpFriendlyName(t *testing.T) { cases := map[string]string{ "tcp": "TCP", "greet": "приветствие SOCKS5", "auth": "авторизация SOCKS5", "connect": "TCP-туннель к Discord", "udp": "UDP ASSOCIATE", "voice-quality": "качество UDP-канала", "api": "Discord API", "weirdo": "weirdo", } for in, want := range cases { t.Run(in, func(t *testing.T) { assert.Equal(t, want, tcpFriendlyName(in)) }) } }