forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherror-utils.test.ts
More file actions
250 lines (196 loc) · 8.83 KB
/
error-utils.test.ts
File metadata and controls
250 lines (196 loc) · 8.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
// src/lib/__tests__/error-utils.test.ts
import { describe, expect, it } from "vitest"
import { classifyFetchError, isDecryptionError, sanitizeNetworkError } from "@/lib/error-utils"
// ---------------------------------------------------------------------------
// isDecryptionError
// ---------------------------------------------------------------------------
describe("isDecryptionError", () => {
// Positive cases — messages that indicate AES-GCM authentication failure
it("returns true for 'Unsupported state or unable to authenticate data'", () => {
expect(isDecryptionError(new Error("Unsupported state or unable to authenticate data"))).toBe(
true
)
})
it("returns true for 'bad decrypt'", () => {
expect(isDecryptionError(new Error("bad decrypt"))).toBe(true)
})
it("returns true for 'bad decrypt' with mixed casing", () => {
expect(isDecryptionError(new Error("Bad Decrypt"))).toBe(true)
})
it("returns true for 'Invalid key length'", () => {
expect(isDecryptionError(new Error("Invalid key length"))).toBe(true)
})
it("returns true for messages containing 'EVP_' (OpenSSL error codes)", () => {
expect(
isDecryptionError(
new Error("error:1e000065:Cipher functions:OPENSSL_internal:EVP_DecryptFinal_ex")
)
).toBe(true)
})
it("returns true for messages containing 'decrypt' anywhere", () => {
expect(isDecryptionError(new Error("Failed to decrypt cipher text"))).toBe(true)
})
it("returns true for 'authenticate data' partial match", () => {
expect(isDecryptionError(new Error("unable to authenticate data"))).toBe(true)
})
it("is case-insensitive for 'DECRYPT'", () => {
expect(isDecryptionError(new Error("DECRYPT failed"))).toBe(true)
})
it("is case-insensitive for 'INVALID KEY'", () => {
expect(isDecryptionError(new Error("INVALID KEY supplied"))).toBe(true)
})
// Negative cases — errors unrelated to decryption
it("returns false for 'Connection refused'", () => {
expect(isDecryptionError(new Error("Connection refused"))).toBe(false)
})
it("returns false for 'Timeout'", () => {
expect(isDecryptionError(new Error("Timeout"))).toBe(false)
})
it("returns false for 'ECONNREFUSED'", () => {
expect(isDecryptionError(new Error("ECONNREFUSED"))).toBe(false)
})
it("returns false for 'Not found'", () => {
expect(isDecryptionError(new Error("Not found"))).toBe(false)
})
it("returns false for a generic empty error message", () => {
expect(isDecryptionError(new Error(""))).toBe(false)
})
// Non-Error inputs
it("returns false for a plain string (not an Error instance)", () => {
expect(isDecryptionError("bad decrypt")).toBe(false)
})
it("returns false for null", () => {
expect(isDecryptionError(null)).toBe(false)
})
it("returns false for undefined", () => {
expect(isDecryptionError(undefined)).toBe(false)
})
it("returns false for a number", () => {
expect(isDecryptionError(42)).toBe(false)
})
it("returns false for a plain object", () => {
expect(isDecryptionError({ message: "bad decrypt" })).toBe(false)
})
it("returns false for an array", () => {
expect(isDecryptionError(["bad decrypt"])).toBe(false)
})
})
// ---------------------------------------------------------------------------
// sanitizeNetworkError
// ---------------------------------------------------------------------------
describe("sanitizeNetworkError", () => {
it("maps 'timed out' to 'Request timed out'", () => {
expect(sanitizeNetworkError("Request timed out after 15s")).toBe("Request timed out")
})
it("maps 'timeout' variant to 'Request timed out'", () => {
expect(sanitizeNetworkError("Connection timeout")).toBe("Request timed out")
})
it("maps 'ECONNREFUSED' to 'Connection refused'", () => {
expect(sanitizeNetworkError("ECONNREFUSED 127.0.0.1:8080")).toBe("Connection refused")
})
it("maps 'ENOTFOUND' to 'Host not found'", () => {
expect(sanitizeNetworkError("getaddrinfo ENOTFOUND example.invalid")).toBe("Host not found")
})
it("maps 'EHOSTUNREACH' to 'Host unreachable'", () => {
expect(sanitizeNetworkError("EHOSTUNREACH")).toBe("Host unreachable")
})
it("maps 'ECONNRESET' to 'Connection reset'", () => {
expect(sanitizeNetworkError("ECONNRESET")).toBe("Connection reset")
})
it("maps 'ip ban' to IP ban message", () => {
expect(sanitizeNetworkError("ip ban detected")).toBe("IP temporarily banned by tracker")
})
it("maps 'rate-limit' to IP ban message", () => {
expect(sanitizeNetworkError("rate-limit exceeded")).toBe("IP temporarily banned by tracker")
})
it("maps '401' to 'Authentication failed'", () => {
expect(sanitizeNetworkError("HTTP 401 Unauthorized")).toBe("Authentication failed")
})
it("maps 'Unauthorized' to 'Authentication failed'", () => {
expect(sanitizeNetworkError("Unauthorized")).toBe("Authentication failed")
})
it("maps 'Session expired' to 'Session expired'", () => {
expect(sanitizeNetworkError("Session expired")).toBe("Session expired")
})
it("maps 'proxy' to 'Proxy connection failed'", () => {
expect(sanitizeNetworkError("Could not connect via proxy")).toBe("Proxy connection failed")
})
it("extracts status code from 'Tracker API error: 404'", () => {
expect(sanitizeNetworkError("Tracker API error: 404")).toBe("API returned 404")
})
it("extracts status code from 'Tracker API error: 500'", () => {
expect(sanitizeNetworkError("Tracker API error: 500")).toBe("API returned 500")
})
it("returns the default fallback for an unrecognized message", () => {
expect(sanitizeNetworkError("Something completely unexpected happened")).toBe(
"Connection failed"
)
})
it("uses a custom fallback when provided", () => {
expect(sanitizeNetworkError("Unknown error", "Polling failed")).toBe("Polling failed")
})
})
// classifyFetchError
// ---------------------------------------------------------------------------
describe("classifyFetchError", () => {
const host = "avistaz.to"
it("unwraps TypeError wrapping ECONNREFUSED (Node.js native fetch)", () => {
const cause = Object.assign(new Error("connect ECONNREFUSED 104.21.0.1:443"), {
code: "ECONNREFUSED",
})
const outer = new TypeError("fetch failed", { cause })
const result = classifyFetchError(outer, host)
expect(result.message).toBe("Failed to connect to avistaz.to: ECONNREFUSED")
})
it("unwraps TypeError wrapping ENOTFOUND", () => {
const cause = Object.assign(new Error("getaddrinfo ENOTFOUND avistaz.to"), {
code: "ENOTFOUND",
})
const outer = new TypeError("fetch failed", { cause })
const result = classifyFetchError(outer, host)
expect(result.message).toBe("Failed to connect to avistaz.to: ENOTFOUND")
})
it("unwraps TypeError wrapping a timeout DOMException", () => {
const cause = new DOMException("The operation was timed out.", "TimeoutError")
const outer = new TypeError("fetch failed", { cause })
const result = classifyFetchError(outer, host)
expect(result.message).toBe("Request to avistaz.to timed out")
})
it("handles direct DOMException TimeoutError (non-wrapped)", () => {
const err = new DOMException("timeout", "TimeoutError")
const result = classifyFetchError(err, host)
expect(result.message).toBe("Request to avistaz.to timed out")
})
it("handles direct DOMException AbortError", () => {
const err = new DOMException("aborted", "AbortError")
const result = classifyFetchError(err, host)
expect(result.message).toBe("Request to avistaz.to timed out")
})
it("handles direct ECONNREFUSED error (no TypeError wrapper)", () => {
const err = Object.assign(new Error("connect ECONNREFUSED"), { code: "ECONNREFUSED" })
const result = classifyFetchError(err, host)
expect(result.message).toBe("Failed to connect to avistaz.to: ECONNREFUSED")
})
it("uses inner message when cause has no code", () => {
const cause = new Error("some TLS error")
const outer = new TypeError("fetch failed", { cause })
const result = classifyFetchError(outer, host)
expect(result.message).toBe("Failed to connect to avistaz.to: some TLS error")
})
it("uses err.message for bare TypeError with no cause", () => {
const err = new TypeError("fetch failed")
const result = classifyFetchError(err, host)
expect(result.message).toBe("Failed to connect to avistaz.to: fetch failed")
})
it("unwraps string cause from TypeError", () => {
const err = new TypeError("fetch failed", { cause: "connect ECONNREFUSED 10.0.0.1:443" })
const result = classifyFetchError(err, host)
expect(result.message).toBe(
"Failed to connect to avistaz.to: connect ECONNREFUSED 10.0.0.1:443"
)
})
it("returns Unknown for non-Error values", () => {
const result = classifyFetchError("string error", host)
expect(result.message).toBe("Failed to connect to avistaz.to: Unknown")
})
})