@wenathlan/getry 2.0.152 → 2.0.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
1
+ /**
2
+ * authentication — aggressive tests for the 12 auth methods
3
+ * inbound extraction, key resolution from env inline ephemeral,
4
+ * outbound header building for every mode, query param building,
5
+ * round robin rotation and masking
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
9
+
10
+ import {
11
+ appendqueryparams,
12
+ buildauthheaders,
13
+ buildauthqueryparams,
14
+ extractchatid,
15
+ extractrequestid,
16
+ extractsessionid,
17
+ extracttoken,
18
+ getkey,
19
+ getkeycount,
20
+ masktoken,
21
+ resolvekeys,
22
+ validatetoken,
23
+ } from "../auth.js";
24
+ import type { authconfig } from "@wenathlan/getry/types";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // helpers
28
+ // ---------------------------------------------------------------------------
29
+
30
+ function req(headers: Record<string, string>, method = "GET"): Request {
31
+ return new Request("http://x/api", { method, headers });
32
+ }
33
+
34
+ const savedenv: Record<string, string | undefined> = {};
35
+
36
+ beforeEach(() => {
37
+ delete process.env.gatewaytestkeys;
38
+ delete process.env.gatewaytestsingle;
39
+ delete process.env.gatewaytestempty;
40
+ });
41
+
42
+ afterEach(() => {
43
+ for (const [k, v] of Object.entries(savedenv)) {
44
+ if (v === undefined) delete process.env[k];
45
+ else process.env[k] = v;
46
+ }
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // inbound extractors — token from every header position
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe("extracttoken — every inbound position", () => {
54
+ it("x-token header wins first", () => {
55
+ expect(extracttoken(req({ "x-token": "tok-x" }))).toBe("tok-x");
56
+ });
57
+ it("authorization bearer is stripped", () => {
58
+ expect(extracttoken(req({ authorization: "Bearer tok-b" }))).toBe("tok-b");
59
+ expect(extracttoken(req({ authorization: "bearer tok-b" }))).toBe("tok-b");
60
+ expect(extracttoken(req({ authorization: "BEARER tok-b" }))).toBe("tok-b");
61
+ });
62
+ it("api key headers fall through the chain", () => {
63
+ expect(extracttoken(req({ "x-api-key": "k1" }))).toBe("k1");
64
+ expect(extracttoken(req({ "x-goog-api-key": "k2" }))).toBe("k2");
65
+ expect(extracttoken(req({ "api-key": "k3" }))).toBe("k3");
66
+ });
67
+ it("body token fields are read when headers are absent", () => {
68
+ expect(extracttoken(req({}), { token: "bt" })).toBe("bt");
69
+ expect(extracttoken(req({}), { apiKey: "bak" })).toBe("bak");
70
+ });
71
+ it("headers beat body", () => {
72
+ expect(extracttoken(req({ "x-token": "h" }), { token: "b" })).toBe("h");
73
+ });
74
+ it("empty when nothing matches", () => {
75
+ expect(extracttoken(req({}))).toBe("");
76
+ expect(extracttoken(req({}), {})).toBe("");
77
+ });
78
+ it("authorization without bearer prefix passes as-is", () => {
79
+ expect(extracttoken(req({ authorization: "Basic abc" }))).toBe("Basic abc");
80
+ });
81
+ });
82
+
83
+ describe("extractchatid — chat id resolution", () => {
84
+ it("x-chat-id wins", () => {
85
+ expect(extractchatid(req({ "x-chat-id": "c1" }))).toBe("c1");
86
+ });
87
+ it("x-user-id is the alias", () => {
88
+ expect(extractchatid(req({ "x-user-id": "u1" }))).toBe("u1");
89
+ });
90
+ it("body fields fall through — chat_id chatId userId", () => {
91
+ expect(extractchatid(req({}), { chat_id: "b1" })).toBe("b1");
92
+ expect(extractchatid(req({}), { chatId: "b2" })).toBe("b2");
93
+ expect(extractchatid(req({}), { userId: "b3" })).toBe("b3");
94
+ });
95
+ it("generates a chat- id when absent", () => {
96
+ const id = extractchatid(req({}));
97
+ expect(id.startsWith("chat-")).toBe(true);
98
+ });
99
+ });
100
+
101
+ describe("extractsessionid — session id resolution", () => {
102
+ it("header wins over body", () => {
103
+ expect(extractsessionid(req({ "x-session-id": "s-h" }), { session_id: "s-b" })).toBe("s-h");
104
+ });
105
+ it("body session_id and sessionId both work", () => {
106
+ expect(extractsessionid(req({}), { session_id: "a" })).toBe("a");
107
+ expect(extractsessionid(req({}), { sessionId: "b" })).toBe("b");
108
+ });
109
+ it("generates a sess- id when absent", () => {
110
+ expect(extractsessionid(req({})).startsWith("sess-")).toBe(true);
111
+ });
112
+ it("two generated ids differ", () => {
113
+ expect(extractsessionid(req({}))).not.toBe(extractsessionid(req({})));
114
+ });
115
+ });
116
+
117
+ describe("extractrequestid — request id resolution", () => {
118
+ it("header is honored", () => {
119
+ expect(extractrequestid(req({ "x-request-id": "r1" }))).toBe("r1");
120
+ });
121
+ it("generates a req- id when absent", () => {
122
+ expect(extractrequestid(req({})).startsWith("req-")).toBe(true);
123
+ });
124
+ });
125
+
126
+ describe("validatetoken masktoken — token guards", () => {
127
+ it("validation rejects short and empty tokens", () => {
128
+ expect(validatetoken("")).toBe(false);
129
+ expect(validatetoken("short")).toBe(false);
130
+ expect(validatetoken("1234567890")).toBe(true);
131
+ expect(validatetoken("a-much-longer-token")).toBe(true);
132
+ });
133
+ it("masking hides the middle", () => {
134
+ // biome-ignore lint/security/noSecrets: synthetic token fixture — not a credential
135
+ const masked = masktoken("sk-1234567890abcdef");
136
+ expect(masked).toBe("sk-12345...cdef");
137
+ expect(masked).not.toContain("abcdefg");
138
+ });
139
+ it("masking rejects short tokens", () => {
140
+ expect(masktoken("short")).toBe("invalid");
141
+ expect(masktoken("")).toBe("invalid");
142
+ expect(masktoken("12-chars-ok!")).not.toBe("invalid");
143
+ });
144
+ });
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // key resolution — env inline and mixed sources
148
+ // ---------------------------------------------------------------------------
149
+
150
+ describe("resolvekeys — env source", () => {
151
+ it("resolves comma separated keys", async () => {
152
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa, key-bbbbbbbbbb ,key-cccccccccc";
153
+ const keys = await resolvekeys({
154
+ mode: "bearer",
155
+ envvar: "gatewaytestkeys",
156
+ keysources: ["env"],
157
+ });
158
+ expect(keys.map((k) => k.key)).toEqual(["key-aaaaaaaaaa", "key-bbbbbbbbbb", "key-cccccccccc"]);
159
+ expect(keys.every((k) => k.source === "env")).toBe(true);
160
+ });
161
+ it("honors a custom separator", async () => {
162
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa|key-bbbbbbbbbb";
163
+ const keys = await resolvekeys({
164
+ mode: "bearer",
165
+ envvar: "gatewaytestkeys",
166
+ envseparator: "|",
167
+ keysources: ["env"],
168
+ });
169
+ expect(keys).toHaveLength(2);
170
+ });
171
+ it("empty env yields no keys", async () => {
172
+ process.env.gatewaytestempty = "";
173
+ const keys = await resolvekeys({
174
+ mode: "bearer",
175
+ envvar: "gatewaytestempty",
176
+ keysources: ["env"],
177
+ });
178
+ expect(keys).toHaveLength(0);
179
+ });
180
+ it("unset env yields no keys", async () => {
181
+ const keys = await resolvekeys({
182
+ mode: "bearer",
183
+ envvar: "gatewaytestmissing",
184
+ keysources: ["env"],
185
+ });
186
+ expect(keys).toHaveLength(0);
187
+ });
188
+ it("whitespace only entries are filtered", async () => {
189
+ process.env.gatewaytestkeys = " , ,key-aaaaaaaaaa,,";
190
+ const keys = await resolvekeys({
191
+ mode: "bearer",
192
+ envvar: "gatewaytestkeys",
193
+ keysources: ["env"],
194
+ });
195
+ expect(keys).toHaveLength(1);
196
+ });
197
+ it("minkeylength filters short keys", async () => {
198
+ process.env.gatewaytestkeys = "short,key-aaaaaaaaaa";
199
+ const keys = await resolvekeys({
200
+ mode: "bearer",
201
+ envvar: "gatewaytestkeys",
202
+ keysources: ["env"],
203
+ minkeylength: 8,
204
+ });
205
+ expect(keys).toHaveLength(1);
206
+ expect(keys[0].key).toBe("key-aaaaaaaaaa");
207
+ });
208
+ it("key labels use the configured format", async () => {
209
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa,key-bbbbbbbbbb";
210
+ const keys = await resolvekeys({
211
+ mode: "bearer",
212
+ envvar: "gatewaytestkeys",
213
+ keysources: ["env"],
214
+ keylabelformat: "nvapi-key-{idx}",
215
+ });
216
+ expect(keys[0].label).toBe("nvapi-key-0");
217
+ expect(keys[1].label).toBe("nvapi-key-1");
218
+ });
219
+ });
220
+
221
+ describe("resolvekeys — inline source", () => {
222
+ it("resolves inline keys with labels", async () => {
223
+ const keys = await resolvekeys({
224
+ mode: "bearer",
225
+ keysources: ["inline"],
226
+ inlinekeys: ["inline-aaaaaaaaaa", "inline-bbbbbbbbbb"],
227
+ });
228
+ expect(keys).toHaveLength(2);
229
+ expect(keys[0].source).toBe("inline");
230
+ expect(keys[0].key).toBe("inline-aaaaaaaaaa");
231
+ });
232
+ it("empty inline list yields nothing", async () => {
233
+ const keys = await resolvekeys({ mode: "bearer", keysources: ["inline"], inlinekeys: [] });
234
+ expect(keys).toHaveLength(0);
235
+ });
236
+ });
237
+
238
+ describe("resolvekeys — default source is env", () => {
239
+ it("keysources defaults to env", async () => {
240
+ const auth = { mode: "bearer", envvar: "gatewaytestmissing" } as authconfig;
241
+ const keys = await resolvekeys(auth);
242
+ expect(keys).toHaveLength(0);
243
+ });
244
+ });
245
+
246
+ describe("resolvekeys — order and merge", () => {
247
+ it("db source with an unknown model yields nothing without throwing", async () => {
248
+ const keys = await resolvekeys({
249
+ mode: "bearer",
250
+ keysources: ["db"],
251
+ dbmodel: "definitelynotamodel",
252
+ });
253
+ expect(keys).toHaveLength(0);
254
+ });
255
+ it("multiple sources concatenate", async () => {
256
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa";
257
+ const keys = await resolvekeys({
258
+ mode: "bearer",
259
+ keysources: ["inline", "env"],
260
+ inlinekeys: ["inline-bbbbbbbbbb"],
261
+ envvar: "gatewaytestkeys",
262
+ });
263
+ expect(keys).toHaveLength(2);
264
+ });
265
+ it("default minkeylength is 10", async () => {
266
+ const keys = await resolvekeys({
267
+ mode: "bearer",
268
+ keysources: ["inline"],
269
+ inlinekeys: ["123456789", "1234567890"],
270
+ });
271
+ expect(keys).toHaveLength(1);
272
+ });
273
+ });
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // outbound header building — all 12 auth methods
277
+ // ---------------------------------------------------------------------------
278
+
279
+ describe("buildauthheaders — every auth mode", () => {
280
+ it("bearer sets authorization bearer", () => {
281
+ const h = buildauthheaders({ mode: "bearer" }, "tok");
282
+ expect(h["authorization"]).toBe("Bearer tok");
283
+ });
284
+ it("oauth2clientcredentials rides the bearer pattern", () => {
285
+ const h = buildauthheaders({ mode: "oauth2clientcredentials" }, "tok");
286
+ expect(h["authorization"]).toBe("Bearer tok");
287
+ });
288
+ it("jwtsign rides the bearer pattern", () => {
289
+ const h = buildauthheaders({ mode: "jwtsign" }, "tok");
290
+ expect(h["authorization"]).toBe("Bearer tok");
291
+ });
292
+ it("apikeyheader defaults to x-api-key", () => {
293
+ const h = buildauthheaders({ mode: "apikeyheader" }, "tok");
294
+ expect(h["x-api-key"]).toBe("tok");
295
+ expect(h["authorization"]).toBeUndefined();
296
+ });
297
+ it("apikeyheader honors custom header names", () => {
298
+ expect(buildauthheaders({ mode: "apikeyheader", headername: "x-goog-api-key" }, "k")["x-goog-api-key"]).toBe("k");
299
+ expect(buildauthheaders({ mode: "apikeyheader", headername: "api-key" }, "k")["api-key"]).toBe("k");
300
+ });
301
+ it("basic encodes base64 credentials", () => {
302
+ const h = buildauthheaders({ mode: "basic" }, "user:pass");
303
+ expect(h["authorization"]).toBe(`Basic ${Buffer.from("user:pass").toString("base64")}`);
304
+ });
305
+ it("queryparam sets no headers", () => {
306
+ const h = buildauthheaders({ mode: "queryparam" }, "tok");
307
+ expect(Object.keys(h)).toHaveLength(0);
308
+ });
309
+ it("headerless modes produce no headers", () => {
310
+ for (const mode of ["cookie", "mtls", "sigv4", "hmacsign", "keylesssdk", "anonymous", "none"]) {
311
+ const h = buildauthheaders({ mode: mode as authconfig["mode"] }, "tok");
312
+ expect(Object.keys(h)).toHaveLength(0);
313
+ }
314
+ });
315
+ it("extraheaders ride along every mode", () => {
316
+ const h = buildauthheaders({ mode: "bearer", extraheaders: { "http-referer": "https://x", "x-title": "t" } }, "k");
317
+ expect(h["http-referer"]).toBe("https://x");
318
+ expect(h["x-title"]).toBe("t");
319
+ expect(h["authorization"]).toBe("Bearer k");
320
+ });
321
+ it("extraheaders override mode headers", () => {
322
+ const h = buildauthheaders({ mode: "bearer", extraheaders: { authorization: "custom" } }, "k");
323
+ expect(h["authorization"]).toBe("custom");
324
+ });
325
+ });
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // query param building
329
+ // ---------------------------------------------------------------------------
330
+
331
+ describe("buildauthqueryparams — query auth", () => {
332
+ it("queryparam mode injects the key", () => {
333
+ const p = buildauthqueryparams({ mode: "queryparam" }, "secret");
334
+ expect(p).toEqual({ key: "secret" });
335
+ });
336
+ it("custom param name", () => {
337
+ const p = buildauthqueryparams({ mode: "queryparam", queryparamname: "api_key" }, "s");
338
+ expect(p).toEqual({ api_key: "s" });
339
+ });
340
+ it("non queryparam modes produce nothing", () => {
341
+ expect(buildauthqueryparams({ mode: "bearer" }, "k")).toEqual({});
342
+ expect(buildauthqueryparams({ mode: "apikeyheader" }, "k")).toEqual({});
343
+ });
344
+ it("extraparams ride along", () => {
345
+ const p = buildauthqueryparams({ mode: "queryparam", extraparams: { "api-version": "2024-02-01" } }, "k");
346
+ expect(p).toEqual({ key: "k", "api-version": "2024-02-01" });
347
+ });
348
+ });
349
+
350
+ describe("appendqueryparams — url assembly", () => {
351
+ it("empty params return the url untouched", () => {
352
+ expect(appendqueryparams("http://x/y", {})).toBe("http://x/y");
353
+ });
354
+ it("adds the first param with a question mark", () => {
355
+ expect(appendqueryparams("http://x/y", { a: "1" })).toBe("http://x/y?a=1");
356
+ });
357
+ it("joins additional params with ampersand", () => {
358
+ expect(appendqueryparams("http://x/y", { a: "1", b: "2" })).toBe("http://x/y?a=1&b=2");
359
+ });
360
+ it("existing query strings are preserved", () => {
361
+ expect(appendqueryparams("http://x/y?z=9", { a: "1" })).toBe("http://x/y?z=9&a=1");
362
+ });
363
+ it("values are url encoded", () => {
364
+ // biome-ignore lint/security/noSecrets: synthetic query fixture — not a credential
365
+ expect(appendqueryparams("http://x", { q: "a b&c=d" })).toBe("http://x?q=a%20b%26c%3Dd");
366
+ });
367
+ it("keys are url encoded", () => {
368
+ expect(appendqueryparams("http://x", { "a b": "1" })).toBe("http://x?a%20b=1");
369
+ });
370
+ });
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // key helpers — round robin and counts
374
+ // ---------------------------------------------------------------------------
375
+
376
+ describe("getkey getkeycount — key helpers", () => {
377
+ it("returns empty when no keys resolve", async () => {
378
+ expect(await getkey({ mode: "bearer", envvar: "gatewaytestmissing" })).toBe("");
379
+ });
380
+ it("returns the first key by default", async () => {
381
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa,key-bbbbbbbbbb";
382
+ expect(await getkey({ mode: "bearer", envvar: "gatewaytestkeys" })).toBe("key-aaaaaaaaaa");
383
+ });
384
+ it("index wraps modulo the pool size", async () => {
385
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa,key-bbbbbbbbbb";
386
+ const auth = { mode: "bearer", envvar: "gatewaytestkeys" } as authconfig;
387
+ expect(await getkey(auth, 2)).toBe("key-aaaaaaaaaa");
388
+ expect(await getkey(auth, 3)).toBe("key-bbbbbbbbbb");
389
+ });
390
+ it("counts resolvable keys", async () => {
391
+ process.env.gatewaytestkeys = "key-aaaaaaaaaa,key-bbbbbbbbbb,key-cccccccccc";
392
+ expect(await getkeycount({ mode: "bearer", envvar: "gatewaytestkeys" })).toBe(3);
393
+ });
394
+ });