@voxgig/sdkgen 4.24.0 → 4.24.2

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 (29) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Deploy.js +9 -6
  3. package/dist/cmp/Deploy.js.map +1 -1
  4. package/dist/helpers/semver.js +3 -1
  5. package/dist/helpers/semver.js.map +1 -1
  6. package/dist/tsconfig.tsbuildinfo +1 -1
  7. package/package.json +4 -4
  8. package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
  9. package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
  10. package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
  11. package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
  12. package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
  13. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
  14. package/project/.sdk/tm/lua/test/omni.lua +8 -1
  15. package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
  16. package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
  17. package/project/.sdk/tm/perl/t/omni.pm +7 -1
  18. package/project/.sdk/tm/perl/t/pipeline.t +124 -27
  19. package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
  20. package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
  21. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
  22. package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
  23. package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
  24. package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
  25. package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
  26. package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
  27. package/project/sdkgen-package.json +1 -1
  28. package/src/cmp/Deploy.ts +11 -6
  29. 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))
@@ -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
 
@@ -533,7 +533,24 @@ class PrimaryUtilityTest < Minitest::Test
533
533
  unwrap(@utility.prepare_auth.call(ctx))
534
534
  end
535
535
 
536
- runsection("prepareAuth", subject)
536
+ # The corpus writes the credential as `headers.authorization`: a
537
+ # PLACEHOLDER each runner points at the container and name this API
538
+ # actually uses.
539
+ cred = auth_credential(auth_client)
540
+ refute_nil cred, "prepare_auth placed no credential in headers or query"
541
+
542
+ # An absent section is runsection's report to make.
543
+ section = @spec.is_a?(Hash) ? @spec["prepareAuth"] : nil
544
+ swap = section.is_a?(Hash) &&
545
+ !("headers" == cred["where"] && "authorization" == cred["name"])
546
+ original = swap ? section["basic"] : nil
547
+ section["basic"] = retarget_auth(original, cred) if swap
548
+
549
+ begin
550
+ runsection("prepareAuth", subject)
551
+ ensure
552
+ section["basic"] = original if swap
553
+ end
537
554
  end
538
555
 
539
556
 
@@ -650,6 +667,47 @@ class PrimaryUtilityTest < Minitest::Test
650
667
  utility.make_context.call(ctxmap, client.get_root_ctx)
651
668
  end
652
669
 
670
+ # Where this SDK's prepare_auth actually puts the credential: the name is
671
+ # the API's OWN scheme name, not always `authorization`, so it is
672
+ # discovered by running prepare_auth once.
673
+ def auth_credential(client)
674
+ utility = client.get_utility
675
+ ctx = make_test_ctx(client, utility, nil)
676
+ ctx.spec = ProjectNameSpec.new({ "headers" => {}, "query" => {}, "step" => "s" })
677
+ _, err = utility.prepare_auth.call(ctx)
678
+ return nil unless err.nil?
679
+ ["headers", "query"].each do |where|
680
+ # A cookie credential rides the header bag, because a cookie IS a header.
681
+ bag = "query" == where ? ctx.spec.query : ctx.spec.headers
682
+ bag.each { |name, _value| return { "where" => where, "name" => name } }
683
+ end
684
+ nil
685
+ end
686
+
687
+ # Rename the corpus's `headers` bag to the real container, and the
688
+ # `authorization` key inside it to the real credential name. Applied only
689
+ # to the prepareAuth section, so real header assertions elsewhere are
690
+ # untouched.
691
+ def retarget_auth(node, cred)
692
+ return node.map { |child| retarget_auth(child, cred) } if node.is_a?(Array)
693
+ return node unless node.is_a?(Hash)
694
+
695
+ out = {}
696
+ node.each do |key, value|
697
+ if "headers" == key
698
+ bag = {}
699
+ (value.is_a?(Hash) ? value : {}).each do |bagkey, bagvalue|
700
+ name = "authorization" == bagkey ? cred["name"] : bagkey
701
+ bag[name] = retarget_auth(bagvalue, cred)
702
+ end
703
+ out[cred["where"]] = bag
704
+ else
705
+ out[key] = retarget_auth(value, cred)
706
+ end
707
+ end
708
+ out
709
+ end
710
+
653
711
  # === Helper: make_test_full_ctx ===
654
712
  def make_test_full_ctx(client, utility)
655
713
  ctx = make_test_ctx(client, utility, nil)
@@ -317,6 +317,11 @@ describe('pipeline:feature order', () => {
317
317
  })
318
318
 
319
319
 
320
+ // A cookie credential as prepareAuth writes it: `<scheme>=K` for the probe
321
+ // key, with no scheme prefix and nothing else in the bag.
322
+ const COOKIE_PAIR = /^[^=;]+=K$/
323
+
324
+
320
325
  describe('pipeline:prepareAuth', () => {
321
326
 
322
327
  function authCtx(options: any, spec: any) {
@@ -327,19 +332,42 @@ describe('pipeline:prepareAuth', () => {
327
332
  return { headers: {} as any, query: {} as any }
328
333
  }
329
334
 
335
+ // `basic: false` is explicit: an HTTP Basic API's generated config carries
336
+ // `auth.basic: true`, and a client that merges it in takes a branch needing
337
+ // a secret as well. With none supplied that branch writes nothing, which
338
+ // the probe below would then read as a public API.
339
+ function auth(prefix: string): any {
340
+ return { prefix, basic: false }
341
+ }
342
+
330
343
  // Run prepareAuth with both containers present and see which one the
331
344
  // generated utility writes to, and under what name. Null means this SDK
332
- // places no credential at all — a public API — which is a legitimate
333
- // shape, and the tests below assert exactly that instead.
334
- const CRED = (() => {
335
- const ctx = authCtx({ apikey: 'K', auth: { prefix: 'Bearer' } }, bags())
345
+ // places no credential at all — a public API — which the cases below then
346
+ // assert instead.
347
+
348
+ // `pair` is the `<scheme>=` lead-in of a COOKIE credential, which rides the
349
+ // header bag under the key `cookie` rather than taking a header of its own.
350
+ function probe(options: any) {
351
+ const ctx = authCtx(options, bags())
336
352
  ;(stdutil as any).prepareAuth(ctx)
337
353
  for (const where of ['headers', 'query'] as const) {
338
354
  const name = Object.keys(ctx.spec[where] as any)[0]
339
- if (null != name) return { where, name, value: ctx.spec[where][name] }
355
+ if (null == name) continue
356
+ const value = ctx.spec[where][name]
357
+ const pair = COOKIE_PAIR.test(String(value)) && 'cookie' === name && 'headers' === where
358
+ ? String(value).slice(0, -1)
359
+ : ''
360
+ return { where, name, value, pair }
340
361
  }
341
362
  return null
342
- })()
363
+ }
364
+
365
+ const CRED = probe({ apikey: 'K', auth: auth('Bearer') })
366
+
367
+ // Every credential this SDK could possibly place: both credentials and
368
+ // Basic switched on, so whichever branch the API has, something lands
369
+ // unless the API is public.
370
+ const ANY = probe({ apikey: 'K', secret: 'S', auth: { prefix: 'Bearer', basic: true } })
343
371
 
344
372
  function placed(options: any, seed?: any) {
345
373
  const spec: any = bags()
@@ -350,27 +378,40 @@ describe('pipeline:prepareAuth', () => {
350
378
  }
351
379
 
352
380
  test('guards a missing spec', () => {
353
- strictEqual((stdutil as any).prepareAuth(authCtx({ auth: { prefix: '' }, apikey: 'K' }, null)).code, 'auth_no_spec')
381
+ strictEqual((stdutil as any).prepareAuth(authCtx({ auth: auth(''), apikey: 'K' }, null)).code, 'auth_no_spec')
382
+ })
383
+
384
+ // Without this the cases below cannot fail for an SDK whose credential the
385
+ // probe misses: every one of them takes the public-API path instead.
386
+ test('the probe finds the credential this SDK places', () => {
387
+ strictEqual(null != CRED, null != ANY)
354
388
  })
355
389
 
356
390
  test('the apikey is placed where this API puts it', () => {
357
391
  if (null == CRED) {
358
392
  // A public API places nothing, and that is the whole assertion.
359
- strictEqual(placed({ apikey: 'K', auth: { prefix: 'Bearer' } }), undefined)
393
+ strictEqual(placed({ apikey: 'K', auth: auth('Bearer') }), undefined)
360
394
  return
361
395
  }
362
396
  ok('headers' === CRED.where || 'query' === CRED.where)
397
+ if ('' !== CRED.pair) {
398
+ // A cookie credential is a `<scheme>=<key>` pair, and the scheme name
399
+ // leaves no room for the option's prefix.
400
+ ok(COOKIE_PAIR.test(String(CRED.value)), String(CRED.value))
401
+ return
402
+ }
363
403
  // A header credential is prefix-joined; a query credential is the raw
364
404
  // key, because a query parameter has nowhere to put a scheme name.
365
405
  strictEqual(CRED.value, 'headers' === CRED.where ? 'Bearer K' : 'K')
366
406
  })
367
407
 
368
408
  test('a raw apikey (empty prefix) goes in as-is', () => {
369
- strictEqual(placed({ apikey: 'K', auth: { prefix: '' } }), null == CRED ? undefined : 'K')
409
+ const raw = null == CRED ? undefined : CRED.pair + 'K'
410
+ strictEqual(placed({ apikey: 'K', auth: auth('') }), raw)
370
411
  })
371
412
 
372
413
  test('an empty apikey drops the credential', () => {
373
- strictEqual(placed({ apikey: '', auth: { prefix: 'Bearer' } }, 'stale'), undefined)
414
+ strictEqual(placed({ apikey: '', auth: auth('Bearer') }, 'stale'), undefined)
374
415
  })
375
416
 
376
417
  test('a public API (no auth block) drops the credential', () => {
@@ -378,7 +419,7 @@ describe('pipeline:prepareAuth', () => {
378
419
  })
379
420
 
380
421
  test('a missing apikey option drops the credential', () => {
381
- strictEqual(placed({ auth: { prefix: 'Bearer' } }, 'stale'), undefined)
422
+ strictEqual(placed({ auth: auth('Bearer') }, 'stale'), undefined)
382
423
  })
383
424
  })
384
425
 
@@ -3,7 +3,7 @@
3
3
  "package": 1
4
4
  },
5
5
  "name": "@voxgig/sdkgen",
6
- "version": "4.24.0",
6
+ "version": "4.24.2",
7
7
  "provides": {
8
8
  "target": [
9
9
  "c",