@usageflow/core 0.4.2 → 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 +20 -1
- package/dist/base.js +215 -9
- package/dist/base.js.map +1 -1
- package/dist/socket.js +1 -0
- package/dist/socket.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 +222 -9
- package/src/socket.ts +1 -0
- package/src/types.ts +8 -0
- package/test/base.test.ts +695 -17
- package/test/src/types.d.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/test/base.test.ts
CHANGED
|
@@ -184,7 +184,8 @@ describe('UsageFlowAPI', () => {
|
|
|
184
184
|
url: '/users/:id',
|
|
185
185
|
method: 'GET',
|
|
186
186
|
identityFieldName: 'id',
|
|
187
|
-
identityFieldLocation: 'path_params'
|
|
187
|
+
identityFieldLocation: 'path_params',
|
|
188
|
+
hasRateLimit: false
|
|
188
189
|
}];
|
|
189
190
|
|
|
190
191
|
const request: UsageFlowRequest = {
|
|
@@ -200,8 +201,9 @@ describe('UsageFlowAPI', () => {
|
|
|
200
201
|
} as any
|
|
201
202
|
};
|
|
202
203
|
|
|
203
|
-
const
|
|
204
|
-
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);
|
|
205
207
|
});
|
|
206
208
|
|
|
207
209
|
test('should guess ledger ID from query params', () => {
|
|
@@ -209,7 +211,8 @@ describe('UsageFlowAPI', () => {
|
|
|
209
211
|
url: '/users',
|
|
210
212
|
method: 'GET',
|
|
211
213
|
identityFieldName: 'userId',
|
|
212
|
-
identityFieldLocation: 'query_params'
|
|
214
|
+
identityFieldLocation: 'query_params',
|
|
215
|
+
hasRateLimit: true
|
|
213
216
|
}];
|
|
214
217
|
|
|
215
218
|
const request: UsageFlowRequest = {
|
|
@@ -224,8 +227,9 @@ describe('UsageFlowAPI', () => {
|
|
|
224
227
|
} as any
|
|
225
228
|
};
|
|
226
229
|
|
|
227
|
-
const
|
|
228
|
-
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);
|
|
229
233
|
});
|
|
230
234
|
|
|
231
235
|
test('should guess ledger ID from body', () => {
|
|
@@ -233,7 +237,8 @@ describe('UsageFlowAPI', () => {
|
|
|
233
237
|
url: '/users',
|
|
234
238
|
method: 'POST',
|
|
235
239
|
identityFieldName: 'userId',
|
|
236
|
-
identityFieldLocation: 'body'
|
|
240
|
+
identityFieldLocation: 'body',
|
|
241
|
+
hasRateLimit: false
|
|
237
242
|
}];
|
|
238
243
|
|
|
239
244
|
const request: UsageFlowRequest = {
|
|
@@ -248,8 +253,9 @@ describe('UsageFlowAPI', () => {
|
|
|
248
253
|
} as any
|
|
249
254
|
};
|
|
250
255
|
|
|
251
|
-
const
|
|
252
|
-
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);
|
|
253
259
|
});
|
|
254
260
|
|
|
255
261
|
test('should guess ledger ID from bearer token', () => {
|
|
@@ -257,7 +263,8 @@ describe('UsageFlowAPI', () => {
|
|
|
257
263
|
url: '/users',
|
|
258
264
|
method: 'GET',
|
|
259
265
|
identityFieldName: 'userId',
|
|
260
|
-
identityFieldLocation: 'bearer_token'
|
|
266
|
+
identityFieldLocation: 'bearer_token',
|
|
267
|
+
hasRateLimit: true
|
|
261
268
|
}];
|
|
262
269
|
|
|
263
270
|
const header = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64');
|
|
@@ -275,8 +282,9 @@ describe('UsageFlowAPI', () => {
|
|
|
275
282
|
} as any
|
|
276
283
|
};
|
|
277
284
|
|
|
278
|
-
const
|
|
279
|
-
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);
|
|
280
288
|
});
|
|
281
289
|
|
|
282
290
|
test('should guess ledger ID from headers', () => {
|
|
@@ -284,7 +292,8 @@ describe('UsageFlowAPI', () => {
|
|
|
284
292
|
url: '/users',
|
|
285
293
|
method: 'GET',
|
|
286
294
|
identityFieldName: 'x-user-id',
|
|
287
|
-
identityFieldLocation: 'headers'
|
|
295
|
+
identityFieldLocation: 'headers',
|
|
296
|
+
hasRateLimit: false
|
|
288
297
|
}];
|
|
289
298
|
|
|
290
299
|
const request: UsageFlowRequest = {
|
|
@@ -298,8 +307,9 @@ describe('UsageFlowAPI', () => {
|
|
|
298
307
|
} as any
|
|
299
308
|
};
|
|
300
309
|
|
|
301
|
-
const
|
|
302
|
-
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);
|
|
303
313
|
});
|
|
304
314
|
|
|
305
315
|
test('should return default ledger ID when no config matches', () => {
|
|
@@ -316,8 +326,518 @@ describe('UsageFlowAPI', () => {
|
|
|
316
326
|
} as any
|
|
317
327
|
};
|
|
318
328
|
|
|
319
|
-
const
|
|
320
|
-
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
|
+
});
|
|
321
841
|
});
|
|
322
842
|
|
|
323
843
|
test('should destroy and clean up resources', () => {
|
|
@@ -325,5 +845,163 @@ describe('UsageFlowAPI', () => {
|
|
|
325
845
|
// Should not throw
|
|
326
846
|
assert.doesNotThrow(() => api.destroy());
|
|
327
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
|
+
});
|
|
328
1006
|
});
|
|
329
1007
|
|