@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.
- package/bin/voxgig-sdkgen +18 -2
- package/dist/action/doctor.d.ts +1 -1
- package/dist/action/doctor.js +12 -10
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +4 -0
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +5 -1
- package/dist/action/feature.js.map +1 -1
- package/dist/action/remove.d.ts +16 -0
- package/dist/action/remove.js +345 -0
- package/dist/action/remove.js.map +1 -0
- package/dist/action/target.js +6 -1
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Deploy.js +9 -6
- package/dist/cmp/Deploy.js.map +1 -1
- package/dist/helpers/semver.js +3 -1
- package/dist/helpers/semver.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/project/.sdk/tm/c/core/context.c +3 -1
- package/project/.sdk/tm/c/feature/paging.c +37 -3
- package/project/.sdk/tm/c/feature/test.c +51 -27
- package/project/.sdk/tm/c/utility/prepare_query.c +2 -1
- package/project/.sdk/tm/c/utility/transform_request.c +25 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +21 -7
- package/project/.sdk/tm/clojure/src/sdk/features.clj +68 -18
- package/project/.sdk/tm/cpp/core/types.hpp +1 -1
- package/project/.sdk/tm/cpp/feature/paging.hpp +36 -3
- package/project/.sdk/tm/cpp/feature/test.hpp +41 -13
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +16 -4
- package/project/.sdk/tm/csharp/core/Context.cs +1 -1
- package/project/.sdk/tm/csharp/feature/PagingFeature.cs +58 -4
- package/project/.sdk/tm/csharp/feature/TestFeature.cs +13 -21
- package/project/.sdk/tm/csharp/utility/Fetcher.cs +19 -12
- package/project/.sdk/tm/csharp/utility/PrepareQuery.cs +1 -1
- package/project/.sdk/tm/csharp/utility/TransformRequest.cs +23 -3
- package/project/.sdk/tm/elixir/lib/projectname/context.ex +1 -1
- package/project/.sdk/tm/elixir/lib/projectname/feature/paging.ex +52 -10
- package/project/.sdk/tm/elixir/lib/projectname/feature/test.ex +47 -17
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +30 -9
- package/project/.sdk/tm/go/core/context.go +13 -13
- package/project/.sdk/tm/go/feature/paging_feature.go +40 -2
- package/project/.sdk/tm/go/feature/test_feature.go +3 -15
- package/project/.sdk/tm/go/test/feature_corpus_test.go +1 -0
- package/project/.sdk/tm/go/test/feature_test.go +54 -0
- package/project/.sdk/tm/go/test/pipeline_test.go +163 -57
- package/project/.sdk/tm/go/test/primary_utility_test.go +53 -0
- package/project/.sdk/tm/go/utility/fetcher.go +41 -20
- package/project/.sdk/tm/go/utility/prepare_query.go +1 -1
- package/project/.sdk/tm/go/utility/transform_request.go +22 -3
- package/project/.sdk/tm/java/core/Context.java +2 -1
- package/project/.sdk/tm/java/feature/PagingFeature.java +46 -4
- package/project/.sdk/tm/java/feature/TestFeature.java +10 -13
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +3 -0
- package/project/.sdk/tm/java/test/FeatureTest.java +42 -0
- package/project/.sdk/tm/java/test/PipelineTest.java +141 -48
- package/project/.sdk/tm/java/test/PrimaryUtilityTest.java +67 -6
- package/project/.sdk/tm/java/utility/Fetcher.java +36 -28
- package/project/.sdk/tm/java/utility/PrepareQuery.java +1 -1
- package/project/.sdk/tm/java/utility/TransformRequest.java +18 -3
- package/project/.sdk/tm/js/src/Context.js +7 -2
- package/project/.sdk/tm/js/src/feature/paging/PagingFeature.js +24 -6
- package/project/.sdk/tm/js/src/feature/test/TestFeature.js +7 -5
- package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +1 -1
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +4 -0
- package/project/.sdk/tm/js/test/pipeline.test.js +88 -22
- package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +47 -1
- package/project/.sdk/tm/kotlin/core/Context.kt +1 -1
- package/project/.sdk/tm/kotlin/feature/PagingFeature.kt +36 -4
- package/project/.sdk/tm/kotlin/feature/TestFeature.kt +11 -14
- package/project/.sdk/tm/kotlin/utility/Fetcher.kt +33 -24
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +1 -1
- package/project/.sdk/tm/kotlin/utility/ResultTransform.kt +18 -3
- package/project/.sdk/tm/lua/core/context.lua +8 -1
- package/project/.sdk/tm/lua/core/control.lua +3 -0
- package/project/.sdk/tm/lua/feature/paging_feature.lua +44 -5
- package/project/.sdk/tm/lua/feature/test_feature.lua +32 -14
- package/project/.sdk/tm/lua/test/omni.lua +8 -1
- package/project/.sdk/tm/lua/test/pipeline_test.lua +107 -28
- package/project/.sdk/tm/lua/test/primary_utility_test.lua +83 -3
- package/project/.sdk/tm/lua/utility/prepare_query.lua +2 -1
- package/project/.sdk/tm/lua/utility/transform_request.lua +19 -3
- package/project/.sdk/tm/ocaml/sdk_features.ml +72 -21
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +23 -8
- package/project/.sdk/tm/perl/core/context.pm +2 -1
- package/project/.sdk/tm/perl/feature/paging_feature.pm +31 -5
- package/project/.sdk/tm/perl/feature/test_feature.pm +46 -16
- package/project/.sdk/tm/perl/feature/timeout_feature.pm +17 -13
- package/project/.sdk/tm/perl/t/feature_corpus.t +14 -1
- package/project/.sdk/tm/perl/t/omni.pm +7 -1
- package/project/.sdk/tm/perl/t/pipeline.t +124 -27
- package/project/.sdk/tm/perl/t/primary_utility.t +73 -0
- package/project/.sdk/tm/perl/utility/fetcher.pm +15 -1
- package/project/.sdk/tm/perl/utility/prepare_query.pm +1 -0
- package/project/.sdk/tm/perl/utility/transform_request.pm +13 -3
- package/project/.sdk/tm/php/core/Context.php +5 -1
- package/project/.sdk/tm/php/core/Control.php +5 -0
- package/project/.sdk/tm/php/feature/PagingFeature.php +26 -5
- package/project/.sdk/tm/php/feature/TestFeature.php +27 -42
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +17 -1
- package/project/.sdk/tm/php/test/FeatureTest.php +44 -0
- package/project/.sdk/tm/php/test/PipelineTest.php +143 -26
- package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +80 -2
- package/project/.sdk/tm/php/utility/Fetcher.php +17 -3
- package/project/.sdk/tm/php/utility/PrepareQuery.php +1 -1
- package/project/.sdk/tm/php/utility/TransformRequest.php +15 -3
- package/project/.sdk/tm/py/pkg/core/context.py +2 -1
- package/project/.sdk/tm/py/pkg/feature/paging_feature.py +29 -4
- package/project/.sdk/tm/py/pkg/feature/test_feature.py +29 -12
- package/project/.sdk/tm/py/pkg/utility/fetcher.py +63 -65
- package/project/.sdk/tm/py/pkg/utility/prepare_query.py +2 -1
- package/project/.sdk/tm/py/pkg/utility/transform_request.py +11 -3
- package/project/.sdk/tm/py/test/test_feature.py +27 -0
- package/project/.sdk/tm/py/test/test_feature_corpus.py +11 -1
- package/project/.sdk/tm/py/test/test_pipeline.py +108 -31
- package/project/.sdk/tm/py/test/test_primary_utility.py +76 -1
- package/project/.sdk/tm/rb/core/context.rb +1 -1
- package/project/.sdk/tm/rb/feature/paging_feature.rb +27 -5
- package/project/.sdk/tm/rb/feature/test_feature.rb +33 -7
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +12 -1
- package/project/.sdk/tm/rb/test/feature_test.rb +38 -0
- package/project/.sdk/tm/rb/test/pipeline_test.rb +100 -24
- package/project/.sdk/tm/rb/test/primary_utility_test.rb +59 -1
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -3
- package/project/.sdk/tm/rb/utility/prepare_query.rb +1 -1
- package/project/.sdk/tm/rb/utility/transform_request.rb +10 -3
- package/project/.sdk/tm/rust/core/context.rs +2 -6
- package/project/.sdk/tm/rust/feature/paging.rs +44 -3
- package/project/.sdk/tm/rust/feature/secrets.rs +1 -1
- package/project/.sdk/tm/rust/feature/test.rs +66 -33
- package/project/.sdk/tm/rust/utility/fetcher.rs +41 -14
- package/project/.sdk/tm/rust/utility/prepare_query.rs +1 -1
- package/project/.sdk/tm/rust/utility/transform_request.rs +31 -5
- package/project/.sdk/tm/scala/core/Context.scala +2 -1
- package/project/.sdk/tm/scala/feature/PagingFeature.scala +41 -4
- package/project/.sdk/tm/scala/feature/TestFeature.scala +11 -12
- package/project/.sdk/tm/scala/utility/Fetcher.scala +34 -13
- package/project/.sdk/tm/scala/utility/Prepare.scala +1 -1
- package/project/.sdk/tm/scala/utility/Transform.scala +19 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/core/Context.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/PagingFeature.swift +31 -4
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/feature/TestFeature.swift +12 -9
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Fetcher.swift +22 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +1 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/ResultUtil.swift +14 -3
- package/project/.sdk/tm/ts/src/Context.ts +7 -2
- package/project/.sdk/tm/ts/src/feature/paging/PagingFeature.ts +19 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +3 -0
- package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +1 -1
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +1 -0
- package/project/.sdk/tm/ts/test/pipeline.test.ts +52 -11
- package/project/.sdk/tm/zig/core/context.zig +1 -1
- package/project/.sdk/tm/zig/core/utility.zig +19 -5
- package/project/.sdk/tm/zig/feature/paging.zig +34 -4
- package/project/.sdk/tm/zig/feature/test.zig +54 -26
- package/project/sdkgen-package.json +1 -1
- package/src/action/doctor.ts +12 -9
- package/src/action/edition.ts +8 -0
- package/src/action/feature.ts +7 -1
- package/src/action/remove.ts +492 -0
- package/src/action/target.ts +8 -1
- package/src/cmp/Deploy.ts +11 -6
- 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,
|
|
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 =
|
|
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":
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
620
|
+
def test_an_empty_apikey_drops_the_credential(self):
|
|
537
621
|
client = _client()
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
626
|
+
def test_a_public_api_with_no_auth_block_drops_the_credential(self):
|
|
545
627
|
client = _client()
|
|
546
|
-
|
|
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
|
|
630
|
+
def test_a_missing_apikey_option_drops_the_credential(self):
|
|
552
631
|
client = _client()
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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" =>
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
assert_equal "K",
|
|
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
|
|
546
|
-
|
|
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
|
|
553
|
-
|
|
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
|
|
559
|
-
|
|
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
|
|