@voxgig/sdkgen 4.23.0 → 4.24.1

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.
Files changed (163) hide show
  1. package/bin/voxgig-sdkgen +18 -2
  2. package/dist/action/doctor.d.ts +1 -1
  3. package/dist/action/doctor.js +12 -10
  4. package/dist/action/doctor.js.map +1 -1
  5. package/dist/action/edition.js +4 -0
  6. package/dist/action/edition.js.map +1 -1
  7. package/dist/action/feature.js +5 -1
  8. package/dist/action/feature.js.map +1 -1
  9. package/dist/action/remove.d.ts +16 -0
  10. package/dist/action/remove.js +345 -0
  11. package/dist/action/remove.js.map +1 -0
  12. package/dist/action/target.js +6 -1
  13. package/dist/action/target.js.map +1 -1
  14. package/dist/cmp/Deploy.js +9 -6
  15. package/dist/cmp/Deploy.js.map +1 -1
  16. package/dist/helpers/semver.js +3 -1
  17. package/dist/helpers/semver.js.map +1 -1
  18. package/dist/tsconfig.tsbuildinfo +1 -1
  19. package/package.json +2 -2
  20. package/project/.sdk/tm/c/core/context.c +3 -1
  21. package/project/.sdk/tm/c/feature/paging.c +37 -3
  22. package/project/.sdk/tm/c/feature/test.c +51 -27
  23. package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
  24. package/project/.sdk/tm/c/utility/transform_request.c +25 -3
  25. package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
  26. package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
  27. package/project/.sdk/tm/cpp/core/types.hpp +1 -1
  28. package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
  29. package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
  30. package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
  31. package/project/.sdk/tm/csharp/core/Context.cs +1 -1
  32. package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
  33. package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
  34. package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
  35. package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
  36. package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
  37. package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
  38. package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
  39. package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
  40. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
  41. package/project/.sdk/tm/go/core/context.go +13 -13
  42. package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
  43. package/project/.sdk/tm/go/feature/test_feature.go +3 -15
  44. package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
  45. package/project/.sdk/tm/go/test/feature_test.go +54 -0
  46. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  47. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  48. package/project/.sdk/tm/go/utility/fetcher.go +41 -20
  49. package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
  50. package/project/.sdk/tm/go/utility/transform_request.go +22 -3
  51. package/project/.sdk/tm/java/core/Context.java +2 -1
  52. package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
  53. package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
  54. package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
  55. package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
  56. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  57. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  58. package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
  59. package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
  60. package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
  61. package/project/.sdk/tm/js/src/Context.js +7 -2
  62. package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
  63. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
  64. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
  65. package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
  66. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  67. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  68. package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
  69. package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
  70. package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
  71. package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
  72. package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
  73. package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
  74. package/project/.sdk/tm/lua/core/context.lua +8 -1
  75. package/project/.sdk/tm/lua/core/control.lua +3 -0
  76. package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
  77. package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
  78. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  79. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  80. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  81. package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
  82. package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
  83. package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
  85. package/project/.sdk/tm/perl/core/context.pm +2 -1
  86. package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
  87. package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
  88. package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
  89. package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
  90. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  91. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  92. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  93. package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
  94. package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
  95. package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
  96. package/project/.sdk/tm/php/core/Context.php +5 -1
  97. package/project/.sdk/tm/php/core/Control.php +5 -0
  98. package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
  99. package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
  100. package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
  101. package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
  102. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  103. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  104. package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
  105. package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
  106. package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
  107. package/project/.sdk/tm/py/pkg/core/context.py +2 -1
  108. package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
  109. package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
  110. package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
  111. package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
  112. package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
  113. package/project/.sdk/tm/py/test/test_feature.py +27 -0
  114. package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
  115. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  116. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  117. package/project/.sdk/tm/rb/core/context.rb +1 -1
  118. package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
  119. package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
  120. package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
  121. package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
  122. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  123. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  124. package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
  125. package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
  126. package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
  127. package/project/.sdk/tm/rust/core/context.rs +2 -6
  128. package/project/.sdk/tm/rust/feature/paging.rs +44 -3
  129. package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
  130. package/project/.sdk/tm/rust/feature/test.rs +66 -33
  131. package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
  132. package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
  133. package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
  134. package/project/.sdk/tm/scala/core/Context.scala +2 -1
  135. package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
  136. package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
  137. package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
  138. package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
  139. package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
  140. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
  141. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
  142. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
  143. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
  144. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
  145. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
  146. package/project/.sdk/tm/ts/src/Context.ts +7 -2
  147. package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
  148. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
  149. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
  150. package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
  151. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  152. package/project/.sdk/tm/zig/core/context.zig +1 -1
  153. package/project/.sdk/tm/zig/core/utility.zig +19 -5
  154. package/project/.sdk/tm/zig/feature/paging.zig +34 -4
  155. package/project/.sdk/tm/zig/feature/test.zig +54 -26
  156. package/project/sdkgen-package.json +1 -1
  157. package/src/action/doctor.ts +12 -9
  158. package/src/action/edition.ts +8 -0
  159. package/src/action/feature.ts +7 -1
  160. package/src/action/remove.ts +492 -0
  161. package/src/action/target.ts +8 -1
  162. package/src/cmp/Deploy.ts +11 -6
  163. package/src/helpers/semver.ts +3 -1
@@ -13,6 +13,8 @@
13
13
  # result_body utility (it has no parse guard), so that ts case is
14
14
  # omitted.
15
15
 
16
+ import re
17
+
16
18
  import pytest
17
19
 
18
20
  from projectname_sdk import ProjectNameSDK
@@ -495,6 +497,10 @@ class TestFeatureOrdering:
495
497
 
496
498
  class TestPrepareAuth:
497
499
 
500
+ # A cookie credential as prepare_auth writes it: `<scheme>=K` for the
501
+ # probe key, with no scheme prefix and nothing else in the bag.
502
+ COOKIE_PAIR = re.compile(r"^[^=;]+=K$")
503
+
498
504
  class _AuthClient:
499
505
  def __init__(self, options):
500
506
  self._options = options
@@ -503,57 +509,128 @@ class TestPrepareAuth:
503
509
  return self._options
504
510
 
505
511
  # Fake client so the exact options.auth / apikey shape is controlled.
506
- def _auth_ctx(self, client, options, headers):
512
+ def _auth_ctx(self, client, options, spec):
507
513
  utility = client._utility
508
514
  ctx = utility.make_context({"opname": "load"}, client.get_root_ctx())
509
515
  ctx.client = self._AuthClient(options)
510
- ctx.spec = None if headers is None else ProjectNameSpec(
511
- {"headers": headers})
516
+ ctx.spec = spec
512
517
  return ctx
513
518
 
519
+ @staticmethod
520
+ def _bags():
521
+ return ProjectNameSpec({"headers": {}, "query": {}})
522
+
523
+ @staticmethod
524
+ def _bag(spec, where):
525
+ # A cookie credential rides the header bag, because a cookie IS a
526
+ # header.
527
+ return spec.query if "query" == where else spec.headers
528
+
529
+ @staticmethod
530
+ def _auth(prefix):
531
+ # `basic: false` is explicit: an HTTP Basic API's generated config
532
+ # carries `auth.basic: true`, and a client that merges it in takes a
533
+ # branch that needs a secret as well. With none supplied that branch
534
+ # deliberately writes nothing, which the probe reads as a public API.
535
+ return {"prefix": prefix, "basic": False}
536
+
537
+ # Run prepare_auth with both containers present and see which one the
538
+ # generated utility writes to, and under what name. None means this SDK
539
+ # places no credential at all - a public API - which is a legitimate
540
+ # shape, and the tests below assert exactly that instead. `pair` is the
541
+ # `<scheme>=` lead-in of a COOKIE credential, which rides the header bag
542
+ # under the key `cookie` instead of taking a header of its own.
543
+ def _probe(self, client, options):
544
+ ctx = self._auth_ctx(client, options, self._bags())
545
+ client._utility.prepare_auth(ctx)
546
+ for where in ("headers", "query"):
547
+ bag = self._bag(ctx.spec, where)
548
+ for name in bag:
549
+ value = bag[name]
550
+ pair = ""
551
+ if ("headers" == where and "cookie" == name
552
+ and isinstance(value, str)
553
+ and self.COOKIE_PAIR.match(value)):
554
+ pair = value[:-1]
555
+ return {"where": where, "name": name, "value": value, "pair": pair}
556
+ return None
557
+
558
+ def _credential(self, client):
559
+ return self._probe(
560
+ client, {"apikey": "K", "auth": self._auth("Bearer")})
561
+
562
+ # Every credential this SDK could possibly place: both credentials and
563
+ # Basic switched on, so whichever branch the API has, something lands
564
+ # unless the API is public.
565
+ def _any_credential(self, client):
566
+ return self._probe(client, {
567
+ "apikey": "K", "secret": "S",
568
+ "auth": {"prefix": "Bearer", "basic": True}})
569
+
570
+ def _placed(self, client, options, seed=None):
571
+ cred = self._credential(client)
572
+ spec = self._bags()
573
+ if cred is not None and seed is not None:
574
+ self._bag(spec, cred["where"])[cred["name"]] = seed
575
+ ctx = self._auth_ctx(client, options, spec)
576
+ client._utility.prepare_auth(ctx)
577
+ if cred is None:
578
+ return None
579
+ return self._bag(ctx.spec, cred["where"]).get(cred["name"])
580
+
514
581
  def test_guards_a_missing_spec(self):
515
582
  client = _client()
516
583
  ctx = self._auth_ctx(client,
517
- {"auth": {"prefix": ""}, "apikey": "K"}, None)
584
+ {"auth": self._auth(""), "apikey": "K"}, None)
518
585
  _, err = client._utility.prepare_auth(ctx)
519
586
  assert err.code == "auth_no_spec"
520
587
 
521
- def test_an_apikey_with_a_prefix_is_space_joined(self):
522
- client = _client()
523
- ctx = self._auth_ctx(client,
524
- {"apikey": "K", "auth": {"prefix": "Bearer"}}, {})
525
- _, err = client._utility.prepare_auth(ctx)
526
- assert err is None
527
- assert ctx.spec.headers["authorization"] == "Bearer K"
588
+ # Without this the cases below cannot fail for an SDK whose credential the
589
+ # probe misses: every one of them takes the public-API path instead.
590
+ def test_the_probe_finds_the_credential_this_sdk_places(self):
591
+ client = _client()
592
+ assert (self._credential(client) is None) == (
593
+ self._any_credential(client) is None)
594
+
595
+ def test_the_apikey_is_placed_where_this_api_puts_it(self):
596
+ client = _client()
597
+ cred = self._credential(client)
598
+ if cred is None:
599
+ # A public API places nothing, and that is the whole assertion.
600
+ assert self._placed(
601
+ client, {"apikey": "K", "auth": self._auth("Bearer")}) is None
602
+ return
603
+ assert cred["where"] in ("headers", "query")
604
+ if "" != cred["pair"]:
605
+ # A cookie credential is a `<scheme>=<key>` pair, and the scheme
606
+ # name leaves no room for the option's prefix.
607
+ assert self.COOKIE_PAIR.match(cred["value"]), cred["value"]
608
+ return
609
+ # A header credential is prefix-joined; a query credential is the raw
610
+ # key, because a query parameter has nowhere to put a scheme name.
611
+ assert cred["value"] == ("K" if "query" == cred["where"] else "Bearer K")
528
612
 
529
613
  def test_a_raw_apikey_goes_in_as_is(self):
530
614
  client = _client()
531
- ctx = self._auth_ctx(client,
532
- {"apikey": "K", "auth": {"prefix": ""}}, {})
533
- client._utility.prepare_auth(ctx)
534
- assert ctx.spec.headers["authorization"] == "K"
615
+ cred = self._credential(client)
616
+ expected = None if cred is None else cred["pair"] + "K"
617
+ assert self._placed(
618
+ client, {"apikey": "K", "auth": self._auth("")}) == expected
535
619
 
536
- def test_an_empty_apikey_drops_the_header(self):
620
+ def test_an_empty_apikey_drops_the_credential(self):
537
621
  client = _client()
538
- ctx = self._auth_ctx(client,
539
- {"apikey": "", "auth": {"prefix": "Bearer"}},
540
- {"authorization": "stale"})
541
- client._utility.prepare_auth(ctx)
542
- assert ctx.spec.headers.get("authorization") is None
622
+ assert self._placed(
623
+ client, {"apikey": "", "auth": self._auth("Bearer")},
624
+ "stale") is None
543
625
 
544
- def test_a_public_api_with_no_auth_block_drops_the_header(self):
626
+ def test_a_public_api_with_no_auth_block_drops_the_credential(self):
545
627
  client = _client()
546
- ctx = self._auth_ctx(client, {"apikey": "K"},
547
- {"authorization": "stale"})
548
- client._utility.prepare_auth(ctx)
549
- assert ctx.spec.headers.get("authorization") is None
628
+ assert self._placed(client, {"apikey": "K"}, "stale") is None
550
629
 
551
- def test_a_missing_apikey_option_drops_the_header(self):
630
+ def test_a_missing_apikey_option_drops_the_credential(self):
552
631
  client = _client()
553
- ctx = self._auth_ctx(client, {"auth": {"prefix": "Bearer"}},
554
- {"authorization": "stale"})
555
- client._utility.prepare_auth(ctx)
556
- assert ctx.spec.headers.get("authorization") is None
632
+ assert self._placed(
633
+ client, {"auth": self._auth("Bearer")}, "stale") is None
557
634
 
558
635
 
559
636
  class TestResultHelpers:
@@ -47,6 +47,63 @@ client = _run['client']['sdk']
47
47
  utility = client._utility
48
48
 
49
49
 
50
+ def _auth_bag(spec_obj, where):
51
+ # A cookie credential rides the header bag, because a cookie IS a header.
52
+ return spec_obj.query if "query" == where else spec_obj.headers
53
+
54
+
55
+ class _AuthProbeClient:
56
+ """Minimal stand-in so the probe below controls the apikey: the suite's
57
+ own client carries whatever options a project configured, and an empty
58
+ apikey places no credential to find."""
59
+
60
+ def options_map(self):
61
+ # `basic: False`: a Basic API's config defaults it true, and the branch
62
+ # it selects needs a secret, so the probe would find nothing.
63
+ return {"apikey": "PROBE", "auth": {"prefix": "", "basic": False}}
64
+
65
+
66
+ def _auth_credential():
67
+ """Which container the generated prepare_auth writes the credential
68
+ into, and under what name. The name is the API's OWN scheme name, so it
69
+ is discovered by running prepare_auth once rather than assumed."""
70
+ ctx = _make_test_ctx(client, utility)
71
+ ctx.client = _AuthProbeClient()
72
+ ctx.spec = ProjectNameSpec({"headers": {}, "query": {}})
73
+ try:
74
+ utility.prepare_auth(ctx)
75
+ except Exception:
76
+ return None
77
+ for where in ("headers", "query"):
78
+ bag = _auth_bag(ctx.spec, where)
79
+ for name in bag:
80
+ return {"where": where, "name": name}
81
+ return None
82
+
83
+
84
+ def _retarget_auth(node, cred):
85
+ """Rename the corpus's `headers` bag to the real container, and the
86
+ `authorization` key inside it to the real credential name. Applied only
87
+ to the prepareAuth section, so real header assertions elsewhere are
88
+ untouched."""
89
+ if isinstance(node, list):
90
+ return [_retarget_auth(n, cred) for n in node]
91
+ if not isinstance(node, dict):
92
+ return node
93
+ out = {}
94
+ for key in node:
95
+ if "headers" == key:
96
+ bag = {}
97
+ inner = node[key] if isinstance(node[key], dict) else {}
98
+ for bagkey in inner:
99
+ name = cred["name"] if "authorization" == bagkey else bagkey
100
+ bag[name] = _retarget_auth(inner[bagkey], cred)
101
+ out[cred["where"]] = bag
102
+ else:
103
+ out[key] = _retarget_auth(node[key], cred)
104
+ return out
105
+
106
+
50
107
  # Sections deliberately left empty in the shared corpus
51
108
  # (.sdk/test/primary/<name>.aon carries a PENDING header). Everything else
52
109
  # MUST contribute cases.
@@ -538,7 +595,25 @@ class TestPrimaryUtility:
538
595
  ctx.client = auth_client
539
596
  return _unwrap(utility.prepare_auth(ctx))
540
597
 
541
- runsection('prepareAuth', subject)
598
+ # The corpus writes the credential as `headers.authorization`: a
599
+ # PLACEHOLDER each runner points at the container and name this API
600
+ # actually uses.
601
+ cred = _auth_credential()
602
+ assert cred is not None, \
603
+ 'prepare_auth placed no credential in headers or query'
604
+
605
+ # An absent section is runsection's report to make.
606
+ section = spec.get('prepareAuth') if isinstance(spec, dict) else None
607
+ swap = isinstance(section, dict) and (
608
+ 'headers' != cred['where'] or 'authorization' != cred['name'])
609
+ original = section['basic'] if swap else None
610
+ if swap:
611
+ section['basic'] = _retarget_auth(original, cred)
612
+ try:
613
+ runsection('prepareAuth', subject)
614
+ finally:
615
+ if swap:
616
+ section['basic'] = original
542
617
 
543
618
  def test_prepare_body_basic(self):
544
619
  runsection('prepareBody', lambda ctx: utility.prepare_body(ctx))
@@ -30,7 +30,7 @@ class ProjectNameContext
30
30
  @ctrl.explain = ctrl_raw["explain"] if ctrl_raw["explain"].is_a?(Hash)
31
31
  @ctrl.actor = ctrl_raw["actor"] if ctrl_raw.key?("actor")
32
32
  @ctrl.paging = ctrl_raw["paging"] if ctrl_raw["paging"].is_a?(Hash)
33
- elsif basectx&.ctrl
33
+ elsif basectx&.ctrl && ProjectNameHelpers.get_ctx_prop(ctxmap, "opname").nil?
34
34
  @ctrl = basectx.ctrl
35
35
  end
36
36
 
@@ -4,8 +4,8 @@
4
4
  # stamps page/limit (or a cursor) into the request query; on the way back
5
5
  # (PreResult) it reads the server's pagination signals - a Link rel="next"
6
6
  # header, X-Page/X-Next-Page/X-Total-Count headers, or next/cursor/
7
- # nextCursor/hasMore fields in the body - and records them on
8
- # result.paging. A per-call ctrl paging value (page or cursor) takes
7
+ # nextCursor/hasMore fields in the body (snake_case forms too) - and records
8
+ # them on result.paging and the caller's ctrl paging. A per-call ctrl paging value (page or cursor) takes
9
9
  # priority. Parameter names ("pageParam", "limitParam", "cursorParam"),
10
10
  # "startPage" (default 1) and page size ("limit") are configurable.
11
11
 
@@ -56,8 +56,10 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
56
56
  if !paging["cursor"].nil?
57
57
  spec.query[cursor_param] = paging["cursor"]
58
58
  elsif spec.query[page_param].nil?
59
- spec.query[page_param] =
60
- paging["page"].nil? ? (@options["startPage"] || 1) : paging["page"]
59
+ # A record written back by PreResult holds the page just fetched as
60
+ # "page" and the one to fetch as "nextPage", so nextPage wins.
61
+ page = paging["nextPage"].nil? ? paging["page"] : paging["nextPage"]
62
+ spec.query[page_param] = page.nil? ? (@options["startPage"] || 1) : page
61
63
  end
62
64
 
63
65
  if !@options["limit"].nil? && spec.query[limit_param].nil?
@@ -158,11 +160,20 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
158
160
  paging["next"] = m[1] if m
159
161
  end
160
162
 
161
- # Body-level cursors.
163
+ # Body-level signals; snake_case forms are read first so camelCase wins.
162
164
  if body.is_a?(Hash)
163
165
  paging["next"] = paging["next"] || body["next"] unless body["next"].nil?
166
+ paging["cursor"] = body["next_cursor"] unless body["next_cursor"].nil?
164
167
  paging["cursor"] = body["cursor"] unless body["cursor"].nil?
165
168
  paging["cursor"] = body["nextCursor"] unless body["nextCursor"].nil?
169
+ if paging["nextPage"].nil?
170
+ np = body["nextPage"].nil? ? body["next_page"] : body["nextPage"]
171
+ paging["nextPage"] = np if np.is_a?(Numeric) || np.is_a?(String)
172
+ end
173
+ if body["has_more"] == true || body["has_more"] == false
174
+ paging["hasMore"] = body["has_more"]
175
+ explicit_more = true
176
+ end
166
177
  if body["hasMore"] == true || body["hasMore"] == false
167
178
  paging["hasMore"] = body["hasMore"]
168
179
  explicit_more = true
@@ -181,6 +192,17 @@ class ProjectNamePagingFeature < ProjectNameBaseFeature
181
192
 
182
193
  result.paging = paging
183
194
 
195
+ if ctx.ctrl && ctx.ctrl.respond_to?(:paging=)
196
+ # The Context shares the caller's paging hash by reference, so refill
197
+ # it in place; a fresh hash on ctrl never reaches the caller.
198
+ if ctx.ctrl.paging.is_a?(Hash)
199
+ ctx.ctrl.paging.clear
200
+ ctx.ctrl.paging.merge!(paging)
201
+ else
202
+ ctx.ctrl.paging = paging
203
+ end
204
+ end
205
+
184
206
  @client.instance_variable_set(:@_paging, { "last" => paging })
185
207
  end
186
208
 
@@ -127,12 +127,10 @@ class ProjectNameTestFeature < ProjectNameBaseFeature
127
127
  args = test_self.build_args(fctx, op, update_match)
128
128
  found = VoxgigStruct.select(entmap, args)
129
129
  ent = VoxgigStruct.getelem(found, 0)
130
- if ent.nil? && entmap.is_a?(Hash) && !entmap.empty?
131
- ent = entmap.values.find { |e| e.is_a?(Hash) }
132
- end
130
+ # update miss: 404, never another record
133
131
  return respond.call(404, nil, { "statusText" => "Not found" }) unless ent
134
- if ent.is_a?(Hash) && fctx.reqdata
135
- fctx.reqdata.each { |k, v| ent[k] = v }
132
+ if ent.is_a?(Hash) && fctx.reqdata.is_a?(Hash)
133
+ VoxgigStruct.merge([ent, fctx.reqdata])
136
134
  end
137
135
  VoxgigStruct.delprop(ent, "$KEY")
138
136
  out = VoxgigStruct.clone(ent)
@@ -237,14 +235,42 @@ class ProjectNameTestFeature < ProjectNameBaseFeature
237
235
  }
238
236
  end
239
237
 
238
+ # The entity's own endpoint: a terminal `{param}` marks a record route,
239
+ # and among equals the shallower path wins (the same rule as make_point).
240
+ def pick_point(points)
241
+ return nil unless points.is_a?(Array) && !points.empty?
242
+ terminal = ->(p) {
243
+ parts = VoxgigStruct.getprop(p, "parts")
244
+ parts.is_a?(Array) && !parts.empty? && parts[-1].is_a?(String) && parts[-1].start_with?("{")
245
+ }
246
+ depth = ->(p) {
247
+ parts = VoxgigStruct.getprop(p, "parts")
248
+ parts.is_a?(Array) ? parts.length : 0
249
+ }
250
+ point = points[0]
251
+ points[1..].each do |cand|
252
+ if terminal.call(cand) != terminal.call(point)
253
+ point = cand if terminal.call(cand)
254
+ elsif depth.call(cand) < depth.call(point)
255
+ point = cand
256
+ end
257
+ end
258
+ point
259
+ end
260
+
240
261
  def build_args(ctx, op, args)
241
262
  opname = op.name
242
263
  points = VoxgigStruct.getpath(ctx.config, "entity.#{ctx.entity.get_name}.op.#{opname}.points")
243
- point = VoxgigStruct.getelem(points, -1)
264
+ point = pick_point(points)
244
265
 
266
+ # Path AND query: a path-only read misses a query-addressed record
267
+ # (e.g. GET /result?trace_id=), which has no path param at all.
245
268
  params_path = VoxgigStruct.getpath(point, "args.params")
246
269
  reqd_params = VoxgigStruct.select(params_path, { "reqd" => true })
247
- reqd = VoxgigStruct.transform(reqd_params, ["`$EACH`", "", "`$KEY.name`"])
270
+ query_path = VoxgigStruct.getpath(point, "args.query")
271
+ reqd_query = VoxgigStruct.select(query_path, { "reqd" => true })
272
+ reqd = (VoxgigStruct.transform(reqd_params, ["`$EACH`", "", "`$KEY.name`"]) || []) +
273
+ (VoxgigStruct.transform(reqd_query, ["`$EACH`", "", "`$KEY.name`"]) || [])
248
274
 
249
275
  qand = []
250
276
  q = { "`$AND`" => qand }
@@ -200,6 +200,13 @@ class FeatureCorpusTest < Minitest::Test
200
200
  assert_equal expect, actual, path
201
201
  end
202
202
 
203
+ # Whether the SDK built the feature at all. The activity record is the
204
+ # wrong probe: most features create theirs on first use, so an idle
205
+ # client has none and every section but the eager ones read as inert.
206
+ def present?(client, name)
207
+ (client.features || []).any? { |f| f.respond_to?(:name) && f.name == name }
208
+ end
209
+
203
210
  def record(client, name)
204
211
  # A corpus key is any manifest item name, so it may carry characters
205
212
  # (foo-bar, foo.2) that cannot form a Ruby instance-variable name. The
@@ -246,7 +253,11 @@ class FeatureCorpusTest < Minitest::Test
246
253
  # Probed by ACTIVATING it: the feature defaults to inactive, so an idle
247
254
  # client never builds it and its absence says nothing.
248
255
  probe = build_client({ "feature" => [{ "name" => name, "active" => true }] })
249
- next if record(probe, name).nil?
256
+ unless present?(probe, name)
257
+ # The one line every runner prints for an inert section.
258
+ puts "feature.#{name}: inert (this SDK does not generate the feature)"
259
+ next
260
+ end
250
261
 
251
262
  ops = usable_ops(2)
252
263
  by_key = ops.each_with_object({}) { |o, h| h[o["key"]] = o }
@@ -911,6 +911,44 @@ class FeatureTest < Minitest::Test
911
911
  assert_equal true, res["result"].paging["hasMore"]
912
912
  end
913
913
 
914
+ def test_paging_snake_case_signals_and_ctrl_write_back
915
+ skip_unless_feature("paging")
916
+ server, calls = H.recording_server { |n, _fd|
917
+ body = 1 == n ? { "has_more" => true, "next_cursor" => "c2" } : { "has_more" => false }
918
+ [H.make_response(200, body), nil]
919
+ }
920
+ h = harness([fspec("paging")], server: server)
921
+ pg = {}
922
+ ctrl = { "paging" => pg }
923
+ res = h.op(opname: "list", path: "/w", ctrl: ctrl)
924
+ assert_equal true, res["result"].paging["hasMore"]
925
+ assert_equal "c2", res["result"].paging["cursor"]
926
+ assert_equal "c2", pg["cursor"], "record written back into ctrl"
927
+ assert_equal true, pg["hasMore"]
928
+ h.op(opname: "list", path: "/w", ctrl: ctrl)
929
+ assert_match(/[?&]cursor=c2(&|\z)/, calls[1]["url"])
930
+ assert_equal false, pg["hasMore"]
931
+ assert_nil pg["cursor"]
932
+ end
933
+
934
+ def test_paging_continues_from_written_back_next_page
935
+ skip_unless_feature("paging")
936
+ server, calls = H.recording_server { |n, _fd|
937
+ body = 1 == n ? { "next_page" => 2 } : {}
938
+ [H.make_response(200, body, "x-page" => n.to_s), nil]
939
+ }
940
+ h = harness([fspec("paging")], server: server)
941
+ pg = {}
942
+ ctrl = { "paging" => pg }
943
+ h.op(opname: "list", path: "/w", ctrl: ctrl)
944
+ assert_equal 1, pg["page"]
945
+ assert_equal 2, pg["nextPage"]
946
+ assert_equal true, pg["hasMore"]
947
+ h.op(opname: "list", path: "/w", ctrl: ctrl)
948
+ assert_match(/[?&]page=2(&|\z)/, calls[1]["url"])
949
+ assert_equal false, pg["hasMore"]
950
+ end
951
+
914
952
  def test_paging_non_list_op_is_not_paged
915
953
  skip_unless_feature("paging")
916
954
  server, calls = H.recording_server
@@ -513,52 +513,128 @@ class PipelineTest < Minitest::Test
513
513
 
514
514
  # === prepare_auth ===
515
515
 
516
- def auth_ctx(options, headers)
516
+ # A cookie credential as prepare_auth writes it: `<scheme>=K` for the probe
517
+ # key, with no scheme prefix and nothing else in the bag.
518
+ COOKIE_PAIR = /\A[^=;]+=K\z/
519
+
520
+ def auth_ctx(options, spec)
517
521
  ctx = ProjectNameContext.new({
518
522
  "client" => OptClient.new(options),
519
523
  "utility" => @utility,
520
524
  "opname" => "load",
521
525
  }, nil)
522
- ctx.spec = headers.nil? ? nil : ProjectNameSpec.new({ "headers" => headers, "step" => "s" })
526
+ ctx.spec = spec
523
527
  ctx
524
528
  end
525
529
 
530
+ def auth_bags
531
+ ProjectNameSpec.new({ "headers" => {}, "query" => {}, "step" => "s" })
532
+ end
533
+
534
+ # A cookie credential rides the header bag, because a cookie IS a header.
535
+ def auth_bag(spec, where)
536
+ "query" == where ? spec.query : spec.headers
537
+ end
538
+
539
+ # `basic: false` is explicit: an HTTP Basic API's generated config carries
540
+ # `auth.basic: true`, and a client that merges it in takes a branch that
541
+ # needs a secret as well. With none supplied that branch deliberately writes
542
+ # nothing, which the probe would read as a public API.
543
+ def auth_block(prefix)
544
+ { "prefix" => prefix, "basic" => false }
545
+ end
546
+
547
+ # Run prepare_auth with both containers present and see which one the
548
+ # generated utility writes to, and under what name. nil means this SDK
549
+ # places no credential at all - a public API - which is a legitimate shape,
550
+ # and the tests below assert exactly that instead. `pair` is the `<scheme>=`
551
+ # lead-in of a COOKIE credential, which rides the header bag under the key
552
+ # `cookie` instead of taking a header of its own.
553
+ def auth_probe(options)
554
+ ctx = auth_ctx(options, auth_bags)
555
+ @utility.prepare_auth.call(ctx)
556
+ ["headers", "query"].each do |where|
557
+ bag = auth_bag(ctx.spec, where)
558
+ bag.each do |name, value|
559
+ pair = ""
560
+ if "headers" == where && "cookie" == name && value.is_a?(String) &&
561
+ COOKIE_PAIR.match?(value)
562
+ pair = value[0..-2]
563
+ end
564
+ return { "where" => where, "name" => name, "value" => value, "pair" => pair }
565
+ end
566
+ end
567
+ nil
568
+ end
569
+
570
+ def auth_credential
571
+ auth_probe({ "apikey" => "K", "auth" => auth_block("Bearer") })
572
+ end
573
+
574
+ # Every credential this SDK could possibly place: both credentials and Basic
575
+ # switched on, so whichever branch the API has, something lands unless the
576
+ # API is public.
577
+ def auth_any_credential
578
+ auth_probe({ "apikey" => "K", "secret" => "S",
579
+ "auth" => { "prefix" => "Bearer", "basic" => true } })
580
+ end
581
+
582
+ def auth_placed(options, seed = nil)
583
+ cred = auth_credential
584
+ spec = auth_bags
585
+ auth_bag(spec, cred["where"])[cred["name"]] = seed if !cred.nil? && !seed.nil?
586
+ ctx = auth_ctx(options, spec)
587
+ @utility.prepare_auth.call(ctx)
588
+ cred.nil? ? nil : auth_bag(ctx.spec, cred["where"])[cred["name"]]
589
+ end
590
+
526
591
  def test_prepare_auth_guards_a_missing_spec
527
- ctx = auth_ctx({ "auth" => { "prefix" => "" }, "apikey" => "K" }, nil)
592
+ ctx = auth_ctx({ "auth" => auth_block(""), "apikey" => "K" }, nil)
528
593
  _, err = @utility.prepare_auth.call(ctx)
529
594
  assert_equal "auth_no_spec", err.code
530
595
  end
531
596
 
532
- def test_prepare_auth_an_apikey_with_a_prefix_is_space_joined
533
- ctx = auth_ctx({ "apikey" => "K", "auth" => { "prefix" => "Bearer" } }, {})
534
- _, err = @utility.prepare_auth.call(ctx)
535
- assert_nil err
536
- assert_equal "Bearer K", ctx.spec.headers["authorization"]
597
+ # Without this the cases below cannot fail for an SDK whose credential the
598
+ # probe misses: every one of them takes the public-API path instead.
599
+ def test_prepare_auth_probe_finds_the_credential_this_sdk_places
600
+ assert_equal auth_credential.nil?, auth_any_credential.nil?
601
+ end
602
+
603
+ def test_prepare_auth_places_the_apikey_where_this_api_puts_it
604
+ cred = auth_credential
605
+ if cred.nil?
606
+ # A public API places nothing, and that is the whole assertion.
607
+ assert_nil auth_placed({ "apikey" => "K", "auth" => auth_block("Bearer") })
608
+ return
609
+ end
610
+ assert_includes ["headers", "query"], cred["where"]
611
+ if "" != cred["pair"]
612
+ # A cookie credential is a `<scheme>=<key>` pair, and the scheme name
613
+ # leaves no room for the option's prefix.
614
+ assert_match COOKIE_PAIR, cred["value"]
615
+ return
616
+ end
617
+ # A header credential is prefix-joined; a query credential is the raw
618
+ # key, because a query parameter has nowhere to put a scheme name.
619
+ assert_equal("query" == cred["where"] ? "K" : "Bearer K", cred["value"])
537
620
  end
538
621
 
539
622
  def test_prepare_auth_a_raw_apikey_goes_in_as_is
540
- ctx = auth_ctx({ "apikey" => "K", "auth" => { "prefix" => "" } }, {})
541
- @utility.prepare_auth.call(ctx)
542
- assert_equal "K", ctx.spec.headers["authorization"]
623
+ cred = auth_credential
624
+ expected = cred.nil? ? nil : cred["pair"] + "K"
625
+ assert_equal expected, auth_placed({ "apikey" => "K", "auth" => auth_block("") })
543
626
  end
544
627
 
545
- def test_prepare_auth_an_empty_apikey_drops_the_header
546
- ctx = auth_ctx({ "apikey" => "", "auth" => { "prefix" => "Bearer" } },
547
- { "authorization" => "stale" })
548
- @utility.prepare_auth.call(ctx)
549
- assert_nil ctx.spec.headers["authorization"]
628
+ def test_prepare_auth_an_empty_apikey_drops_the_credential
629
+ assert_nil auth_placed({ "apikey" => "", "auth" => auth_block("Bearer") }, "stale")
550
630
  end
551
631
 
552
- def test_prepare_auth_a_public_api_drops_the_header
553
- ctx = auth_ctx({ "apikey" => "K" }, { "authorization" => "stale" })
554
- @utility.prepare_auth.call(ctx)
555
- assert_nil ctx.spec.headers["authorization"]
632
+ def test_prepare_auth_a_public_api_drops_the_credential
633
+ assert_nil auth_placed({ "apikey" => "K" }, "stale")
556
634
  end
557
635
 
558
- def test_prepare_auth_a_missing_apikey_option_drops_the_header
559
- ctx = auth_ctx({ "auth" => { "prefix" => "Bearer" } }, { "authorization" => "stale" })
560
- @utility.prepare_auth.call(ctx)
561
- assert_nil ctx.spec.headers["authorization"]
636
+ def test_prepare_auth_a_missing_apikey_option_drops_the_credential
637
+ assert_nil auth_placed({ "auth" => auth_block("Bearer") }, "stale")
562
638
  end
563
639
 
564
640