@usageflow/core 0.4.1 → 0.4.3
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.
- package/dist/base.d.ts +34 -1
- package/dist/base.js +301 -7
- package/dist/base.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/base.ts +325 -7
- package/src/types.ts +8 -0
- package/test/base.test.ts +741 -24
- package/test/src/types.d.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/test/base.test.ts
CHANGED
|
@@ -162,9 +162,18 @@ describe('UsageFlowAPI', () => {
|
|
|
162
162
|
method: 'GET',
|
|
163
163
|
url: '/test',
|
|
164
164
|
path: '/test',
|
|
165
|
+
app: {
|
|
166
|
+
_router: {
|
|
167
|
+
stack: []
|
|
168
|
+
}
|
|
169
|
+
} as any,
|
|
165
170
|
route: { path: '/test/:id' },
|
|
166
171
|
headers: {},
|
|
167
|
-
app: {
|
|
172
|
+
app: {
|
|
173
|
+
_router: {
|
|
174
|
+
stack: []
|
|
175
|
+
}
|
|
176
|
+
} as any
|
|
168
177
|
};
|
|
169
178
|
const pattern = api.getRoutePattern(request);
|
|
170
179
|
assert.strictEqual(pattern, '/test/:id');
|
|
@@ -175,7 +184,8 @@ describe('UsageFlowAPI', () => {
|
|
|
175
184
|
url: '/users/:id',
|
|
176
185
|
method: 'GET',
|
|
177
186
|
identityFieldName: 'id',
|
|
178
|
-
identityFieldLocation: 'path_params'
|
|
187
|
+
identityFieldLocation: 'path_params',
|
|
188
|
+
hasRateLimit: false
|
|
179
189
|
}];
|
|
180
190
|
|
|
181
191
|
const request: UsageFlowRequest = {
|
|
@@ -183,11 +193,17 @@ describe('UsageFlowAPI', () => {
|
|
|
183
193
|
url: '/users',
|
|
184
194
|
path: '/users/123',
|
|
185
195
|
params: { id: '123' },
|
|
186
|
-
headers: {}
|
|
196
|
+
headers: {},
|
|
197
|
+
app: {
|
|
198
|
+
_router: {
|
|
199
|
+
stack: []
|
|
200
|
+
}
|
|
201
|
+
} as any
|
|
187
202
|
};
|
|
188
203
|
|
|
189
|
-
const
|
|
190
|
-
assert.strictEqual(ledgerId, 'GET /users/123 123');
|
|
204
|
+
const result = api.guessLedgerId(request);
|
|
205
|
+
assert.strictEqual(result.ledgerId, 'GET /users/123 123');
|
|
206
|
+
assert.strictEqual(result.hasLimit, false);
|
|
191
207
|
});
|
|
192
208
|
|
|
193
209
|
test('should guess ledger ID from query params', () => {
|
|
@@ -195,18 +211,25 @@ describe('UsageFlowAPI', () => {
|
|
|
195
211
|
url: '/users',
|
|
196
212
|
method: 'GET',
|
|
197
213
|
identityFieldName: 'userId',
|
|
198
|
-
identityFieldLocation: 'query_params'
|
|
214
|
+
identityFieldLocation: 'query_params',
|
|
215
|
+
hasRateLimit: true
|
|
199
216
|
}];
|
|
200
217
|
|
|
201
218
|
const request: UsageFlowRequest = {
|
|
202
219
|
method: 'GET',
|
|
203
220
|
url: '/users',
|
|
204
221
|
query: { userId: '456' },
|
|
205
|
-
headers: {}
|
|
222
|
+
headers: {},
|
|
223
|
+
app: {
|
|
224
|
+
_router: {
|
|
225
|
+
stack: []
|
|
226
|
+
}
|
|
227
|
+
} as any
|
|
206
228
|
};
|
|
207
229
|
|
|
208
|
-
const
|
|
209
|
-
assert.strictEqual(ledgerId, 'GET /users 456');
|
|
230
|
+
const result = api.guessLedgerId(request);
|
|
231
|
+
assert.strictEqual(result.ledgerId, 'GET /users 456');
|
|
232
|
+
assert.strictEqual(result.hasLimit, true);
|
|
210
233
|
});
|
|
211
234
|
|
|
212
235
|
test('should guess ledger ID from body', () => {
|
|
@@ -214,18 +237,25 @@ describe('UsageFlowAPI', () => {
|
|
|
214
237
|
url: '/users',
|
|
215
238
|
method: 'POST',
|
|
216
239
|
identityFieldName: 'userId',
|
|
217
|
-
identityFieldLocation: 'body'
|
|
240
|
+
identityFieldLocation: 'body',
|
|
241
|
+
hasRateLimit: false
|
|
218
242
|
}];
|
|
219
243
|
|
|
220
244
|
const request: UsageFlowRequest = {
|
|
221
245
|
method: 'POST',
|
|
222
246
|
url: '/users',
|
|
223
247
|
body: { userId: '789' },
|
|
224
|
-
headers: {}
|
|
248
|
+
headers: {},
|
|
249
|
+
app: {
|
|
250
|
+
_router: {
|
|
251
|
+
stack: []
|
|
252
|
+
}
|
|
253
|
+
} as any
|
|
225
254
|
};
|
|
226
255
|
|
|
227
|
-
const
|
|
228
|
-
assert.strictEqual(ledgerId, 'POST /users 789');
|
|
256
|
+
const result = api.guessLedgerId(request);
|
|
257
|
+
assert.strictEqual(result.ledgerId, 'POST /users 789');
|
|
258
|
+
assert.strictEqual(result.hasLimit, false);
|
|
229
259
|
});
|
|
230
260
|
|
|
231
261
|
test('should guess ledger ID from bearer token', () => {
|
|
@@ -233,7 +263,8 @@ describe('UsageFlowAPI', () => {
|
|
|
233
263
|
url: '/users',
|
|
234
264
|
method: 'GET',
|
|
235
265
|
identityFieldName: 'userId',
|
|
236
|
-
identityFieldLocation: 'bearer_token'
|
|
266
|
+
identityFieldLocation: 'bearer_token',
|
|
267
|
+
hasRateLimit: true
|
|
237
268
|
}];
|
|
238
269
|
|
|
239
270
|
const header = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64');
|
|
@@ -243,11 +274,17 @@ describe('UsageFlowAPI', () => {
|
|
|
243
274
|
const request: UsageFlowRequest = {
|
|
244
275
|
method: 'GET',
|
|
245
276
|
url: '/users',
|
|
246
|
-
headers: { authorization: `Bearer ${token}` }
|
|
277
|
+
headers: { authorization: `Bearer ${token}` },
|
|
278
|
+
app: {
|
|
279
|
+
_router: {
|
|
280
|
+
stack: []
|
|
281
|
+
}
|
|
282
|
+
} as any
|
|
247
283
|
};
|
|
248
284
|
|
|
249
|
-
const
|
|
250
|
-
assert.strictEqual(ledgerId, 'GET /users token-user');
|
|
285
|
+
const result = api.guessLedgerId(request);
|
|
286
|
+
assert.strictEqual(result.ledgerId, 'GET /users token-user');
|
|
287
|
+
assert.strictEqual(result.hasLimit, true);
|
|
251
288
|
});
|
|
252
289
|
|
|
253
290
|
test('should guess ledger ID from headers', () => {
|
|
@@ -255,17 +292,24 @@ describe('UsageFlowAPI', () => {
|
|
|
255
292
|
url: '/users',
|
|
256
293
|
method: 'GET',
|
|
257
294
|
identityFieldName: 'x-user-id',
|
|
258
|
-
identityFieldLocation: 'headers'
|
|
295
|
+
identityFieldLocation: 'headers',
|
|
296
|
+
hasRateLimit: false
|
|
259
297
|
}];
|
|
260
298
|
|
|
261
299
|
const request: UsageFlowRequest = {
|
|
262
300
|
method: 'GET',
|
|
263
301
|
url: '/users',
|
|
264
|
-
headers: { 'x-user-id': 'header-user' }
|
|
302
|
+
headers: { 'x-user-id': 'header-user' },
|
|
303
|
+
app: {
|
|
304
|
+
_router: {
|
|
305
|
+
stack: []
|
|
306
|
+
}
|
|
307
|
+
} as any
|
|
265
308
|
};
|
|
266
309
|
|
|
267
|
-
const
|
|
268
|
-
assert.strictEqual(ledgerId, 'GET /users header-user');
|
|
310
|
+
const result = api.guessLedgerId(request);
|
|
311
|
+
assert.strictEqual(result.ledgerId, 'GET /users header-user');
|
|
312
|
+
assert.strictEqual(result.hasLimit, false);
|
|
269
313
|
});
|
|
270
314
|
|
|
271
315
|
test('should return default ledger ID when no config matches', () => {
|
|
@@ -274,11 +318,526 @@ describe('UsageFlowAPI', () => {
|
|
|
274
318
|
const request: UsageFlowRequest = {
|
|
275
319
|
method: 'GET',
|
|
276
320
|
url: '/users',
|
|
277
|
-
headers: {}
|
|
321
|
+
headers: {},
|
|
322
|
+
app: {
|
|
323
|
+
_router: {
|
|
324
|
+
stack: []
|
|
325
|
+
}
|
|
326
|
+
} as any
|
|
278
327
|
};
|
|
279
328
|
|
|
280
|
-
const
|
|
281
|
-
assert.strictEqual(ledgerId, 'GET /users');
|
|
329
|
+
const result = api.guessLedgerId(request);
|
|
330
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
331
|
+
assert.strictEqual(result.hasLimit, false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('guessLedgerId - Cookie scenarios', () => {
|
|
335
|
+
test('should guess ledger ID from standard cookie with cookie. prefix', () => {
|
|
336
|
+
(api as any).apiConfigs = [{
|
|
337
|
+
url: '/users',
|
|
338
|
+
method: 'GET',
|
|
339
|
+
identityFieldName: 'cookie.sessionId',
|
|
340
|
+
identityFieldLocation: 'cookie',
|
|
341
|
+
hasRateLimit: true
|
|
342
|
+
}];
|
|
343
|
+
|
|
344
|
+
const request: UsageFlowRequest = {
|
|
345
|
+
method: 'GET',
|
|
346
|
+
url: '/users',
|
|
347
|
+
headers: { cookie: 'sessionId=abc123; other=value' },
|
|
348
|
+
app: {
|
|
349
|
+
_router: {
|
|
350
|
+
stack: []
|
|
351
|
+
}
|
|
352
|
+
} as any
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = api.guessLedgerId(request);
|
|
356
|
+
assert.strictEqual(result.ledgerId, 'GET /users abc123');
|
|
357
|
+
assert.strictEqual(result.hasLimit, true);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('should guess ledger ID from standard cookie without prefix (dot notation)', () => {
|
|
361
|
+
(api as any).apiConfigs = [{
|
|
362
|
+
url: '/users',
|
|
363
|
+
method: 'GET',
|
|
364
|
+
identityFieldName: 'sessionId',
|
|
365
|
+
identityFieldLocation: 'cookie',
|
|
366
|
+
hasRateLimit: false
|
|
367
|
+
}];
|
|
368
|
+
|
|
369
|
+
const request: UsageFlowRequest = {
|
|
370
|
+
method: 'GET',
|
|
371
|
+
url: '/users',
|
|
372
|
+
headers: { cookie: 'sessionId=xyz789' },
|
|
373
|
+
app: {
|
|
374
|
+
_router: {
|
|
375
|
+
stack: []
|
|
376
|
+
}
|
|
377
|
+
} as any
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const result = api.guessLedgerId(request);
|
|
381
|
+
assert.strictEqual(result.ledgerId, 'GET /users xyz789');
|
|
382
|
+
assert.strictEqual(result.hasLimit, false);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('should guess ledger ID from JWT cookie with technique and pick', () => {
|
|
386
|
+
(api as any).apiConfigs = [{
|
|
387
|
+
url: '/users',
|
|
388
|
+
method: 'GET',
|
|
389
|
+
identityFieldName: '[technique=jwt]sess[pick=sub]',
|
|
390
|
+
identityFieldLocation: 'cookie',
|
|
391
|
+
hasRateLimit: true
|
|
392
|
+
}];
|
|
393
|
+
|
|
394
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64');
|
|
395
|
+
const payload = Buffer.from(JSON.stringify({ sub: 'jwt-user-123', name: 'Test User' })).toString('base64');
|
|
396
|
+
const jwtToken = `${header}.${payload}.signature`;
|
|
397
|
+
|
|
398
|
+
const request: UsageFlowRequest = {
|
|
399
|
+
method: 'GET',
|
|
400
|
+
url: '/users',
|
|
401
|
+
headers: { cookie: `sess=${jwtToken}` },
|
|
402
|
+
app: {
|
|
403
|
+
_router: {
|
|
404
|
+
stack: []
|
|
405
|
+
}
|
|
406
|
+
} as any
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const result = api.guessLedgerId(request);
|
|
410
|
+
assert.strictEqual(result.ledgerId, 'GET /users jwt-user-123');
|
|
411
|
+
assert.strictEqual(result.hasLimit, true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('should handle case-insensitive cookie header (Cookie vs cookie)', () => {
|
|
415
|
+
(api as any).apiConfigs = [{
|
|
416
|
+
url: '/users',
|
|
417
|
+
method: 'GET',
|
|
418
|
+
identityFieldName: 'cookie.sessionId',
|
|
419
|
+
identityFieldLocation: 'cookie',
|
|
420
|
+
hasRateLimit: false
|
|
421
|
+
}];
|
|
422
|
+
|
|
423
|
+
const request: UsageFlowRequest = {
|
|
424
|
+
method: 'GET',
|
|
425
|
+
url: '/users',
|
|
426
|
+
headers: { Cookie: 'sessionId=case-test-123' },
|
|
427
|
+
app: {
|
|
428
|
+
_router: {
|
|
429
|
+
stack: []
|
|
430
|
+
}
|
|
431
|
+
} as any
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const result = api.guessLedgerId(request);
|
|
435
|
+
assert.strictEqual(result.ledgerId, 'GET /users case-test-123');
|
|
436
|
+
assert.strictEqual(result.hasLimit, false);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('should return default when cookie not found', () => {
|
|
440
|
+
(api as any).apiConfigs = [{
|
|
441
|
+
url: '/users',
|
|
442
|
+
method: 'GET',
|
|
443
|
+
identityFieldName: 'cookie.missing',
|
|
444
|
+
identityFieldLocation: 'cookie',
|
|
445
|
+
hasRateLimit: false
|
|
446
|
+
}];
|
|
447
|
+
|
|
448
|
+
const request: UsageFlowRequest = {
|
|
449
|
+
method: 'GET',
|
|
450
|
+
url: '/users',
|
|
451
|
+
headers: { cookie: 'other=value' },
|
|
452
|
+
app: {
|
|
453
|
+
_router: {
|
|
454
|
+
stack: []
|
|
455
|
+
}
|
|
456
|
+
} as any
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const result = api.guessLedgerId(request);
|
|
460
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
461
|
+
assert.strictEqual(result.hasLimit, false);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('should handle JWT cookie with missing claim', () => {
|
|
465
|
+
(api as any).apiConfigs = [{
|
|
466
|
+
url: '/users',
|
|
467
|
+
method: 'GET',
|
|
468
|
+
identityFieldName: '[technique=jwt]sess[pick=missingClaim]',
|
|
469
|
+
identityFieldLocation: 'cookie',
|
|
470
|
+
hasRateLimit: false
|
|
471
|
+
}];
|
|
472
|
+
|
|
473
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64');
|
|
474
|
+
const payload = Buffer.from(JSON.stringify({ sub: 'user-123' })).toString('base64');
|
|
475
|
+
const jwtToken = `${header}.${payload}.signature`;
|
|
476
|
+
|
|
477
|
+
const request: UsageFlowRequest = {
|
|
478
|
+
method: 'GET',
|
|
479
|
+
url: '/users',
|
|
480
|
+
headers: { cookie: `sess=${jwtToken}` },
|
|
481
|
+
app: {
|
|
482
|
+
_router: {
|
|
483
|
+
stack: []
|
|
484
|
+
}
|
|
485
|
+
} as any
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const result = api.guessLedgerId(request);
|
|
489
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
490
|
+
assert.strictEqual(result.hasLimit, false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('should handle JWT cookie with invalid token', () => {
|
|
494
|
+
(api as any).apiConfigs = [{
|
|
495
|
+
url: '/users',
|
|
496
|
+
method: 'GET',
|
|
497
|
+
identityFieldName: '[technique=jwt]sess[pick=sub]',
|
|
498
|
+
identityFieldLocation: 'cookie',
|
|
499
|
+
hasRateLimit: false
|
|
500
|
+
}];
|
|
501
|
+
|
|
502
|
+
const request: UsageFlowRequest = {
|
|
503
|
+
method: 'GET',
|
|
504
|
+
url: '/users',
|
|
505
|
+
headers: { cookie: 'sess=invalid-token' },
|
|
506
|
+
app: {
|
|
507
|
+
_router: {
|
|
508
|
+
stack: []
|
|
509
|
+
}
|
|
510
|
+
} as any
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const result = api.guessLedgerId(request);
|
|
514
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
515
|
+
assert.strictEqual(result.hasLimit, false);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('guessLedgerId - Edge cases', () => {
|
|
520
|
+
test('should use first matching config when multiple configs exist', () => {
|
|
521
|
+
(api as any).apiConfigs = [
|
|
522
|
+
{
|
|
523
|
+
url: '/users',
|
|
524
|
+
method: 'GET',
|
|
525
|
+
identityFieldName: 'userId',
|
|
526
|
+
identityFieldLocation: 'query_params',
|
|
527
|
+
hasRateLimit: true
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
url: '/users',
|
|
531
|
+
method: 'GET',
|
|
532
|
+
identityFieldName: 'id',
|
|
533
|
+
identityFieldLocation: 'path_params',
|
|
534
|
+
hasRateLimit: false
|
|
535
|
+
}
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
const request: UsageFlowRequest = {
|
|
539
|
+
method: 'GET',
|
|
540
|
+
url: '/users',
|
|
541
|
+
query: { userId: 'first-match' },
|
|
542
|
+
params: { id: 'second-match' },
|
|
543
|
+
headers: {},
|
|
544
|
+
app: {
|
|
545
|
+
_router: {
|
|
546
|
+
stack: []
|
|
547
|
+
}
|
|
548
|
+
} as any
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const result = api.guessLedgerId(request);
|
|
552
|
+
assert.strictEqual(result.ledgerId, 'GET /users first-match');
|
|
553
|
+
assert.strictEqual(result.hasLimit, true);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('should not match when method does not match', () => {
|
|
557
|
+
(api as any).apiConfigs = [{
|
|
558
|
+
url: '/users',
|
|
559
|
+
method: 'POST',
|
|
560
|
+
identityFieldName: 'userId',
|
|
561
|
+
identityFieldLocation: 'query_params',
|
|
562
|
+
hasRateLimit: false
|
|
563
|
+
}];
|
|
564
|
+
|
|
565
|
+
const request: UsageFlowRequest = {
|
|
566
|
+
method: 'GET',
|
|
567
|
+
url: '/users',
|
|
568
|
+
query: { userId: '123' },
|
|
569
|
+
headers: {},
|
|
570
|
+
app: {
|
|
571
|
+
_router: {
|
|
572
|
+
stack: []
|
|
573
|
+
}
|
|
574
|
+
} as any
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const result = api.guessLedgerId(request);
|
|
578
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
579
|
+
assert.strictEqual(result.hasLimit, false);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('should not match when URL does not match', () => {
|
|
583
|
+
(api as any).apiConfigs = [{
|
|
584
|
+
url: '/posts',
|
|
585
|
+
method: 'GET',
|
|
586
|
+
identityFieldName: 'userId',
|
|
587
|
+
identityFieldLocation: 'query_params',
|
|
588
|
+
hasRateLimit: false
|
|
589
|
+
}];
|
|
590
|
+
|
|
591
|
+
const request: UsageFlowRequest = {
|
|
592
|
+
method: 'GET',
|
|
593
|
+
url: '/users',
|
|
594
|
+
query: { userId: '123' },
|
|
595
|
+
headers: {},
|
|
596
|
+
app: {
|
|
597
|
+
_router: {
|
|
598
|
+
stack: []
|
|
599
|
+
}
|
|
600
|
+
} as any
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const result = api.guessLedgerId(request);
|
|
604
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
605
|
+
assert.strictEqual(result.hasLimit, false);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test('should handle missing identity field in path params', () => {
|
|
609
|
+
(api as any).apiConfigs = [{
|
|
610
|
+
url: '/users/:id',
|
|
611
|
+
method: 'GET',
|
|
612
|
+
identityFieldName: 'id',
|
|
613
|
+
identityFieldLocation: 'path_params',
|
|
614
|
+
hasRateLimit: false
|
|
615
|
+
}];
|
|
616
|
+
|
|
617
|
+
const request: UsageFlowRequest = {
|
|
618
|
+
method: 'GET',
|
|
619
|
+
url: '/users',
|
|
620
|
+
params: { otherParam: '123' },
|
|
621
|
+
headers: {},
|
|
622
|
+
app: {
|
|
623
|
+
_router: {
|
|
624
|
+
stack: []
|
|
625
|
+
}
|
|
626
|
+
} as any
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const result = api.guessLedgerId(request);
|
|
630
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
631
|
+
assert.strictEqual(result.hasLimit, false);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('should handle missing identity field in query params', () => {
|
|
635
|
+
(api as any).apiConfigs = [{
|
|
636
|
+
url: '/users',
|
|
637
|
+
method: 'GET',
|
|
638
|
+
identityFieldName: 'userId',
|
|
639
|
+
identityFieldLocation: 'query_params',
|
|
640
|
+
hasRateLimit: false
|
|
641
|
+
}];
|
|
642
|
+
|
|
643
|
+
const request: UsageFlowRequest = {
|
|
644
|
+
method: 'GET',
|
|
645
|
+
url: '/users',
|
|
646
|
+
query: { otherParam: '123' },
|
|
647
|
+
headers: {},
|
|
648
|
+
app: {
|
|
649
|
+
_router: {
|
|
650
|
+
stack: []
|
|
651
|
+
}
|
|
652
|
+
} as any
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const result = api.guessLedgerId(request);
|
|
656
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
657
|
+
assert.strictEqual(result.hasLimit, false);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('should handle missing identity field in body', () => {
|
|
661
|
+
(api as any).apiConfigs = [{
|
|
662
|
+
url: '/users',
|
|
663
|
+
method: 'POST',
|
|
664
|
+
identityFieldName: 'userId',
|
|
665
|
+
identityFieldLocation: 'body',
|
|
666
|
+
hasRateLimit: false
|
|
667
|
+
}];
|
|
668
|
+
|
|
669
|
+
const request: UsageFlowRequest = {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
url: '/users',
|
|
672
|
+
body: { otherField: '123' },
|
|
673
|
+
headers: {},
|
|
674
|
+
app: {
|
|
675
|
+
_router: {
|
|
676
|
+
stack: []
|
|
677
|
+
}
|
|
678
|
+
} as any
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const result = api.guessLedgerId(request);
|
|
682
|
+
assert.strictEqual(result.ledgerId, 'POST /users');
|
|
683
|
+
assert.strictEqual(result.hasLimit, false);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test('should handle bearer token without authorization header', () => {
|
|
687
|
+
(api as any).apiConfigs = [{
|
|
688
|
+
url: '/users',
|
|
689
|
+
method: 'GET',
|
|
690
|
+
identityFieldName: 'userId',
|
|
691
|
+
identityFieldLocation: 'bearer_token',
|
|
692
|
+
hasRateLimit: false
|
|
693
|
+
}];
|
|
694
|
+
|
|
695
|
+
const request: UsageFlowRequest = {
|
|
696
|
+
method: 'GET',
|
|
697
|
+
url: '/users',
|
|
698
|
+
headers: {},
|
|
699
|
+
app: {
|
|
700
|
+
_router: {
|
|
701
|
+
stack: []
|
|
702
|
+
}
|
|
703
|
+
} as any
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const result = api.guessLedgerId(request);
|
|
707
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
708
|
+
assert.strictEqual(result.hasLimit, false);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test('should handle bearer token with invalid format', () => {
|
|
712
|
+
(api as any).apiConfigs = [{
|
|
713
|
+
url: '/users',
|
|
714
|
+
method: 'GET',
|
|
715
|
+
identityFieldName: 'userId',
|
|
716
|
+
identityFieldLocation: 'bearer_token',
|
|
717
|
+
hasRateLimit: false
|
|
718
|
+
}];
|
|
719
|
+
|
|
720
|
+
const request: UsageFlowRequest = {
|
|
721
|
+
method: 'GET',
|
|
722
|
+
url: '/users',
|
|
723
|
+
headers: { authorization: 'Invalid token' },
|
|
724
|
+
app: {
|
|
725
|
+
_router: {
|
|
726
|
+
stack: []
|
|
727
|
+
}
|
|
728
|
+
} as any
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const result = api.guessLedgerId(request);
|
|
732
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
733
|
+
assert.strictEqual(result.hasLimit, false);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('should handle bearer token with missing claim', () => {
|
|
737
|
+
(api as any).apiConfigs = [{
|
|
738
|
+
url: '/users',
|
|
739
|
+
method: 'GET',
|
|
740
|
+
identityFieldName: 'userId',
|
|
741
|
+
identityFieldLocation: 'bearer_token',
|
|
742
|
+
hasRateLimit: false
|
|
743
|
+
}];
|
|
744
|
+
|
|
745
|
+
const header = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64');
|
|
746
|
+
const payload = Buffer.from(JSON.stringify({ otherClaim: 'value' })).toString('base64');
|
|
747
|
+
const token = `${header}.${payload}.signature`;
|
|
748
|
+
|
|
749
|
+
const request: UsageFlowRequest = {
|
|
750
|
+
method: 'GET',
|
|
751
|
+
url: '/users',
|
|
752
|
+
headers: { authorization: `Bearer ${token}` },
|
|
753
|
+
app: {
|
|
754
|
+
_router: {
|
|
755
|
+
stack: []
|
|
756
|
+
}
|
|
757
|
+
} as any
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const result = api.guessLedgerId(request);
|
|
761
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
762
|
+
assert.strictEqual(result.hasLimit, false);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('should handle missing header field', () => {
|
|
766
|
+
(api as any).apiConfigs = [{
|
|
767
|
+
url: '/users',
|
|
768
|
+
method: 'GET',
|
|
769
|
+
identityFieldName: 'x-user-id',
|
|
770
|
+
identityFieldLocation: 'headers',
|
|
771
|
+
hasRateLimit: false
|
|
772
|
+
}];
|
|
773
|
+
|
|
774
|
+
const request: UsageFlowRequest = {
|
|
775
|
+
method: 'GET',
|
|
776
|
+
url: '/users',
|
|
777
|
+
headers: { 'other-header': 'value' },
|
|
778
|
+
app: {
|
|
779
|
+
_router: {
|
|
780
|
+
stack: []
|
|
781
|
+
}
|
|
782
|
+
} as any
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const result = api.guessLedgerId(request);
|
|
786
|
+
assert.strictEqual(result.ledgerId, 'GET /users');
|
|
787
|
+
assert.strictEqual(result.hasLimit, false);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test('should use overrideUrl when provided', () => {
|
|
791
|
+
(api as any).apiConfigs = [{
|
|
792
|
+
url: '/custom/path',
|
|
793
|
+
method: 'GET',
|
|
794
|
+
identityFieldName: 'userId',
|
|
795
|
+
identityFieldLocation: 'query_params',
|
|
796
|
+
hasRateLimit: true
|
|
797
|
+
}];
|
|
798
|
+
|
|
799
|
+
const request: UsageFlowRequest = {
|
|
800
|
+
method: 'GET',
|
|
801
|
+
url: '/users',
|
|
802
|
+
query: { userId: 'override-test' },
|
|
803
|
+
headers: {},
|
|
804
|
+
app: {
|
|
805
|
+
_router: {
|
|
806
|
+
stack: []
|
|
807
|
+
}
|
|
808
|
+
} as any
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const result = api.guessLedgerId(request, '/custom/path');
|
|
812
|
+
assert.strictEqual(result.ledgerId, 'GET /custom/path override-test');
|
|
813
|
+
assert.strictEqual(result.hasLimit, true);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test('should handle hasRateLimit as undefined (defaults to false)', () => {
|
|
817
|
+
(api as any).apiConfigs = [{
|
|
818
|
+
url: '/users',
|
|
819
|
+
method: 'GET',
|
|
820
|
+
identityFieldName: 'userId',
|
|
821
|
+
identityFieldLocation: 'query_params'
|
|
822
|
+
// hasRateLimit is undefined
|
|
823
|
+
}];
|
|
824
|
+
|
|
825
|
+
const request: UsageFlowRequest = {
|
|
826
|
+
method: 'GET',
|
|
827
|
+
url: '/users',
|
|
828
|
+
query: { userId: '123' },
|
|
829
|
+
headers: {},
|
|
830
|
+
app: {
|
|
831
|
+
_router: {
|
|
832
|
+
stack: []
|
|
833
|
+
}
|
|
834
|
+
} as any
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const result = api.guessLedgerId(request);
|
|
838
|
+
assert.strictEqual(result.ledgerId, 'GET /users 123');
|
|
839
|
+
assert.strictEqual(result.hasLimit, false);
|
|
840
|
+
});
|
|
282
841
|
});
|
|
283
842
|
|
|
284
843
|
test('should destroy and clean up resources', () => {
|
|
@@ -286,5 +845,163 @@ describe('UsageFlowAPI', () => {
|
|
|
286
845
|
// Should not throw
|
|
287
846
|
assert.doesNotThrow(() => api.destroy());
|
|
288
847
|
});
|
|
848
|
+
|
|
849
|
+
describe('getValueByPath', () => {
|
|
850
|
+
test('should extract simple property', () => {
|
|
851
|
+
const obj = { amount: 100 };
|
|
852
|
+
const result = api.getValueByPath(obj, 'amount');
|
|
853
|
+
assert.strictEqual(result, 100);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test('should extract nested property', () => {
|
|
857
|
+
const obj = { data: { amount: 200 } };
|
|
858
|
+
const result = api.getValueByPath(obj, 'data.amount');
|
|
859
|
+
assert.strictEqual(result, 200);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test('should extract deeply nested property', () => {
|
|
863
|
+
const obj = { response: { data: { result: { amount: 300 } } } };
|
|
864
|
+
const result = api.getValueByPath(obj, 'response.data.result.amount');
|
|
865
|
+
assert.strictEqual(result, 300);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test('should extract array element by index', () => {
|
|
869
|
+
const obj = { items: [{ id: 1 }, { id: 2 }, { id: 3 }] };
|
|
870
|
+
const result = api.getValueByPath(obj, 'items[0].id');
|
|
871
|
+
assert.strictEqual(result, 1);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test('should extract from array with wildcard notation', () => {
|
|
875
|
+
const obj = { items: [{ id: 1 }, { id: 2 }, { id: 3 }] };
|
|
876
|
+
const result = api.getValueByPath(obj, 'items[*].id');
|
|
877
|
+
assert.strictEqual(result, 1); // Should return first found
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test('should extract from nested array with wildcard', () => {
|
|
881
|
+
const obj = {
|
|
882
|
+
data: [
|
|
883
|
+
{ users: [{ id: 10 }, { id: 20 }] },
|
|
884
|
+
{ users: [{ id: 30 }] }
|
|
885
|
+
]
|
|
886
|
+
};
|
|
887
|
+
const result = api.getValueByPath(obj, 'data[*].users[*].id');
|
|
888
|
+
assert.strictEqual(result, 10); // Should return first found
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test('should extract amount from response body structure', () => {
|
|
892
|
+
const responseBody = {
|
|
893
|
+
success: true,
|
|
894
|
+
data: {
|
|
895
|
+
transaction: {
|
|
896
|
+
amount: 1500
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const result = api.getValueByPath(responseBody, 'data.transaction.amount');
|
|
901
|
+
assert.strictEqual(result, 1500);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test('should extract amount from array response', () => {
|
|
905
|
+
const responseBody = {
|
|
906
|
+
results: [
|
|
907
|
+
{ id: 1, amount: 100 },
|
|
908
|
+
{ id: 2, amount: 200 },
|
|
909
|
+
{ id: 3, amount: 300 }
|
|
910
|
+
]
|
|
911
|
+
};
|
|
912
|
+
const result = api.getValueByPath(responseBody, 'results[*].amount');
|
|
913
|
+
assert.strictEqual(result, 100); // First found
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test('should return undefined for non-existent path', () => {
|
|
917
|
+
const obj = { data: { amount: 100 } };
|
|
918
|
+
const result = api.getValueByPath(obj, 'data.missing');
|
|
919
|
+
assert.strictEqual(result, undefined);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test('should return undefined for null object', () => {
|
|
923
|
+
const result = api.getValueByPath(null, 'amount');
|
|
924
|
+
assert.strictEqual(result, undefined);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test('should return undefined for undefined object', () => {
|
|
928
|
+
const result = api.getValueByPath(undefined, 'amount');
|
|
929
|
+
assert.strictEqual(result, undefined);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test('should return undefined for empty path', () => {
|
|
933
|
+
const obj = { amount: 100 };
|
|
934
|
+
const result = api.getValueByPath(obj, '');
|
|
935
|
+
assert.strictEqual(result, undefined);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test('should handle array with wildcard at root level', () => {
|
|
939
|
+
const obj = [{ id: 1, amount: 50 }, { id: 2, amount: 75 }];
|
|
940
|
+
const result = api.getValueByPath(obj, '[*].amount');
|
|
941
|
+
assert.strictEqual(result, 50); // First element's amount
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test('should handle wildcard when array is empty', () => {
|
|
945
|
+
const obj = { items: [] };
|
|
946
|
+
const result = api.getValueByPath(obj, 'items[*].id');
|
|
947
|
+
assert.strictEqual(result, undefined);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test('should handle wildcard when property does not exist in array items', () => {
|
|
951
|
+
const obj = { items: [{ name: 'test' }, { name: 'test2' }] };
|
|
952
|
+
const result = api.getValueByPath(obj, 'items[*].missing');
|
|
953
|
+
assert.strictEqual(result, undefined);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test('should handle complex nested structure with multiple wildcards', () => {
|
|
957
|
+
const obj = {
|
|
958
|
+
orders: [
|
|
959
|
+
{
|
|
960
|
+
items: [
|
|
961
|
+
{ product: { price: 10 } },
|
|
962
|
+
{ product: { price: 20 } }
|
|
963
|
+
]
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
items: [
|
|
967
|
+
{ product: { price: 30 } }
|
|
968
|
+
]
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
};
|
|
972
|
+
const result = api.getValueByPath(obj, 'orders[*].items[*].product.price');
|
|
973
|
+
assert.strictEqual(result, 10); // First found
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test('should handle numeric string indices', () => {
|
|
977
|
+
const obj = { items: ['first', 'second', 'third'] };
|
|
978
|
+
const result = api.getValueByPath(obj, 'items[1]');
|
|
979
|
+
assert.strictEqual(result, 'second');
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test('should return first element when wildcard is last segment', () => {
|
|
983
|
+
const obj = { items: [{ id: 1 }, { id: 2 }] };
|
|
984
|
+
const result = api.getValueByPath(obj, 'items[*]');
|
|
985
|
+
assert.deepStrictEqual(result, { id: 1 }); // First element
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
test('should handle null values in path', () => {
|
|
989
|
+
const obj = { data: null };
|
|
990
|
+
const result = api.getValueByPath(obj, 'data.amount');
|
|
991
|
+
assert.strictEqual(result, undefined);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test('should extract from response body with amount field', () => {
|
|
995
|
+
const responseBody = {
|
|
996
|
+
status: 'success',
|
|
997
|
+
amount: 5000,
|
|
998
|
+
metadata: {
|
|
999
|
+
timestamp: '2024-01-01'
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const result = api.getValueByPath(responseBody, 'amount');
|
|
1003
|
+
assert.strictEqual(result, 5000);
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
289
1006
|
});
|
|
290
1007
|
|