@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/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 ledgerId = api.guessLedgerId(request);
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 ledgerId = api.guessLedgerId(request);
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 ledgerId = api.guessLedgerId(request);
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 ledgerId = api.guessLedgerId(request);
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 ledgerId = api.guessLedgerId(request);
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 ledgerId = api.guessLedgerId(request);
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