unitysvc-core 0.2.34__tar.gz → 0.2.35__tar.gz

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 (48) hide show
  1. {unitysvc_core-0.2.34/src/unitysvc_core.egg-info → unitysvc_core-0.2.35}/PKG-INFO +6 -2
  2. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/README.md +5 -1
  3. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/pyproject.toml +1 -1
  4. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/parameter_usage.py +34 -3
  5. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/validator.py +25 -1
  6. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35/src/unitysvc_core.egg-info}/PKG-INFO +6 -2
  7. unitysvc_core-0.2.35/tests/test_parameter_usage.py +185 -0
  8. unitysvc_core-0.2.34/tests/test_parameter_usage.py +0 -121
  9. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/LICENSE +0 -0
  10. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/setup.cfg +0 -0
  11. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/__init__.py +0 -0
  12. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/__init__.py +0 -0
  13. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/base.py +0 -0
  14. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/documents.py +0 -0
  15. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/listing_data.py +0 -0
  16. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/listing_v1.py +0 -0
  17. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/offering_data.py +0 -0
  18. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/offering_v1.py +0 -0
  19. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/pricing.py +0 -0
  20. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/promotion_data.py +0 -0
  21. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/provider_data.py +0 -0
  22. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/provider_v1.py +0 -0
  23. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/service.py +0 -0
  24. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/service_group_data.py +0 -0
  25. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/validators.py +0 -0
  26. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/py.typed +0 -0
  27. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/base.json +0 -0
  28. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/listing_v1.json +0 -0
  29. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/offering_v1.json +0 -0
  30. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  31. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/provider_v1.json +0 -0
  32. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  33. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/utils.py +0 -0
  34. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
  35. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  36. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/requires.txt +0 -0
  37. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  38. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_access_interface_routing.py +0 -0
  39. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_effective_price.py +0 -0
  40. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_hidden_path_skip.py +0 -0
  41. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_offering_data.py +0 -0
  42. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_pricing.py +0 -0
  43. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_provider_account_rate_limits.py +0 -0
  44. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_service_group.py +0 -0
  45. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_service_status.py +0 -0
  46. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_utils.py +0 -0
  47. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_validator.py +0 -0
  48. {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.34
3
+ Version: 0.2.35
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -136,7 +136,11 @@ MIT
136
136
  ### Required enrollment parameter usage
137
137
 
138
138
  The shared validator rejects required user parameters that no channel, interface,
139
- or document consumes. File validation checks a listing against its paired offering
139
+ or document consumes. Only those consumer fields count — the offering's
140
+ `upstream_access_config`, the listing's `user_access_interfaces`, and document
141
+ content (a `.j2` document's template source, which the seller validator reads from
142
+ disk as the SDK uploads it); metadata such as `display_name` or `description` is
143
+ never rendered with `params`. File validation checks a listing against its paired offering
140
144
  when present; backend ingest calls the same `validate_required_parameter_usage`
141
145
  helper with the complete listing and offering. Schema declarations alone are not
142
146
  consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
@@ -101,7 +101,11 @@ MIT
101
101
  ### Required enrollment parameter usage
102
102
 
103
103
  The shared validator rejects required user parameters that no channel, interface,
104
- or document consumes. File validation checks a listing against its paired offering
104
+ or document consumes. Only those consumer fields count — the offering's
105
+ `upstream_access_config`, the listing's `user_access_interfaces`, and document
106
+ content (a `.j2` document's template source, which the seller validator reads from
107
+ disk as the SDK uploads it); metadata such as `display_name` or `description` is
108
+ never rendered with `params`. File validation checks a listing against its paired offering
105
109
  when present; backend ingest calls the same `validate_required_parameter_usage`
106
110
  helper with the complete listing and offering. Schema declarations alone are not
107
111
  consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unitysvc-core"
7
- version = "0.2.34"
7
+ version = "0.2.35"
8
8
  description = "Shared data models and validation helpers for UnitySVC packages"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
@@ -2,6 +2,12 @@
2
2
 
3
3
  This checks that required parameters have consumers. It does not prove that
4
4
  an intended enrollment channel consumes them; interface/document use counts.
5
+
6
+ Only the fields the platform renders with an enrollment's ``params`` are
7
+ consumers: the offering's channels (``upstream_access_config``), the listing's
8
+ interfaces (``user_access_interfaces``) and document content. Metadata such as
9
+ ``display_name`` or ``description`` is never rendered with ``params``, so a
10
+ reference there does not count (unitysvc/unitysvc#2649).
5
11
  """
6
12
 
7
13
  from typing import Any
@@ -66,6 +72,31 @@ def unused_required_params(parameters_schema: dict[str, Any] | None, *sources: A
66
72
  return [name for name in required if name not in used]
67
73
 
68
74
 
75
+ # Keys of a document entry that hold its rendered content. Backend ingest sees
76
+ # a ``.j2`` document's template source inlined as ``file_content``; the seller
77
+ # validator inlines the same file before calling this module.
78
+ _DOCUMENT_CONTENT_KEYS = ("file_content", "content")
79
+
80
+
81
+ def _document_contents(documents: Any) -> list[str]:
82
+ entries = documents.values() if isinstance(documents, dict) else documents if isinstance(documents, list) else ()
83
+ out: list[str] = []
84
+ for doc in entries:
85
+ if isinstance(doc, dict):
86
+ out.extend(doc[key] for key in _DOCUMENT_CONTENT_KEYS if isinstance(doc.get(key), str))
87
+ return out
88
+
89
+
90
+ def consumer_sources(listing: dict[str, Any], offering: dict[str, Any]) -> list[Any]:
91
+ """The parts of a service the platform renders with enrollment ``params``."""
92
+ return [
93
+ offering.get("upstream_access_config"),
94
+ listing.get("user_access_interfaces"),
95
+ _document_contents(listing.get("documents")),
96
+ _document_contents(offering.get("documents")),
97
+ ]
98
+
99
+
69
100
  def validate_required_parameter_usage(listing: dict[str, Any], offering: dict[str, Any]) -> list[str]:
70
101
  """Return hard validation errors, shared by seller validation and backend ingest.
71
102
 
@@ -75,15 +106,15 @@ def validate_required_parameter_usage(listing: dict[str, Any], offering: dict[st
75
106
  if listing.get("status") == "deprecated" or offering.get("status") == "deprecated":
76
107
  return []
77
108
  schema = listing.get("user_parameters_schema") or listing.get("parameters_schema")
78
- content = {k: v for k, v in listing.items() if k not in ("user_parameters_schema", "parameters_schema")}
79
109
  try:
80
- unused = unused_required_params(schema, content, offering)
110
+ unused = unused_required_params(schema, *consumer_sources(listing, offering))
81
111
  except TemplateSyntaxError as exc:
82
112
  return [f"Cannot validate required parameter usage: invalid Jinja template: {exc}"]
83
113
  if not unused:
84
114
  return []
85
115
  return [
86
- f"Required user parameter(s) {', '.join(unused)} are used by no channel, interface or document. "
116
+ f"Required user parameter(s) {', '.join(unused)} are used by no channel (upstream_access_config), "
117
+ "interface (user_access_interfaces) or document content. "
87
118
  "Reference each one as {{ params.NAME }} (for example ${ customer_secrets.{{ params.NAME }} }), "
88
119
  "or drop it from the required list."
89
120
  ]
@@ -1027,9 +1027,33 @@ class DataValidator:
1027
1027
  except Exception as exc:
1028
1028
  return [f"Cannot check required parameter usage: {exc}"]
1029
1029
  if isinstance(offering, dict) and (offering.get("schema") == "offering_v1" or candidate.stem == "offering"):
1030
- return validate_required_parameter_usage(listing, offering)
1030
+ return validate_required_parameter_usage(
1031
+ self._with_template_documents(listing, file_path.parent),
1032
+ self._with_template_documents(offering, candidate.parent),
1033
+ )
1031
1034
  return []
1032
1035
 
1036
+ @staticmethod
1037
+ def _with_template_documents(data: dict[str, Any], base_dir: Path) -> dict[str, Any]:
1038
+ """``data`` with each ``.j2`` document's template source inlined as
1039
+ ``file_content``, as the seller SDK uploads it — so the seller check
1040
+ sees the same document content backend ingest does."""
1041
+ documents = data.get("documents")
1042
+ if not isinstance(documents, dict):
1043
+ return data
1044
+ inlined: dict[str, Any] = {}
1045
+ for title, doc in documents.items():
1046
+ if isinstance(doc, dict) and "file_content" not in doc:
1047
+ rel = doc.get("file_path")
1048
+ if isinstance(rel, str) and rel.endswith(".j2"):
1049
+ path = Path(rel) if Path(rel).is_absolute() else base_dir / rel
1050
+ try:
1051
+ doc = {**doc, "file_content": path.read_text(encoding="utf-8")}
1052
+ except OSError:
1053
+ pass # a missing file is reported by the file-reference checks
1054
+ inlined[title] = doc
1055
+ return {**data, "documents": inlined}
1056
+
1033
1057
  def _resolve_provider_name(self, file_path: Path) -> str | None:
1034
1058
  """Find the provider name (``provider_v1.name``) governing a listing file.
1035
1059
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.34
3
+ Version: 0.2.35
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -136,7 +136,11 @@ MIT
136
136
  ### Required enrollment parameter usage
137
137
 
138
138
  The shared validator rejects required user parameters that no channel, interface,
139
- or document consumes. File validation checks a listing against its paired offering
139
+ or document consumes. Only those consumer fields count — the offering's
140
+ `upstream_access_config`, the listing's `user_access_interfaces`, and document
141
+ content (a `.j2` document's template source, which the seller validator reads from
142
+ disk as the SDK uploads it); metadata such as `display_name` or `description` is
143
+ never rendered with `params`. File validation checks a listing against its paired offering
140
144
  when present; backend ingest calls the same `validate_required_parameter_usage`
141
145
  helper with the complete listing and offering. Schema declarations alone are not
142
146
  consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
@@ -0,0 +1,185 @@
1
+ """Required enrollment parameters must have consumers across service data."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from unitysvc_core.parameter_usage import referenced_param_names, validate_required_parameter_usage
9
+ from unitysvc_core.validator import DataValidator
10
+
11
+
12
+ @pytest.mark.parametrize("expression", ["params.name", "params['name']", 'params.get("name")'])
13
+ def test_named_reads(expression):
14
+ assert referenced_param_names("{{ " + expression + " }}") == {"name"}
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ "expression",
19
+ [
20
+ "params",
21
+ "params | tojson",
22
+ "params.items()",
23
+ "params.keys()",
24
+ "params.values()",
25
+ "params.get(key)",
26
+ "params[key]",
27
+ ],
28
+ )
29
+ def test_dynamic_and_whole_map_reads(expression):
30
+ assert referenced_param_names("{{ " + expression + " }}") is None
31
+
32
+
33
+ def test_strings_comments_and_non_template_text_are_not_consumers():
34
+ assert referenced_param_names(["params.name", "{# params.name #}", '{{ "params.name" }}']) == set()
35
+
36
+
37
+ def listing(**changes):
38
+ return {"status": "ready", "user_parameters_schema": {"required": ["host", "token"]}, **changes}
39
+
40
+
41
+ def channel(**fields):
42
+ """An offering whose one channel carries ``fields``."""
43
+ return {"upstream_access_config": {"plus": fields}}
44
+
45
+
46
+ def test_required_names_in_the_schema_do_not_count_as_use():
47
+ errors = validate_required_parameter_usage(
48
+ listing(), {"upstream_access_config": {"free": {"api_key": "${ customer_secrets.TOKEN }"}}}
49
+ )
50
+ assert len(errors) == 1
51
+ assert "host, token are used by no channel" in errors[0]
52
+
53
+
54
+ @pytest.mark.parametrize(
55
+ ("listing_changes", "offering"),
56
+ [
57
+ ({"display_name": "{{ params.host }} {{ params.token }}"}, {}),
58
+ ({"description": "{{ params.host }}", "summary": "{{ params.token }}"}, {}),
59
+ ({}, {"description": "{{ params.host }} {{ params.token }}", "name": "{{ params.host }}"}),
60
+ # a document's metadata is not its content
61
+ ({"documents": {"Doc": {"description": "{{ params.host }} {{ params.token }}"}}}, {}),
62
+ ({"service_options": {"note": "{{ params.host }} {{ params.token }}"}}, {}),
63
+ ],
64
+ )
65
+ def test_metadata_is_not_a_consumer(listing_changes, offering):
66
+ """Only channels, interfaces and document content are rendered with
67
+ params; a reference anywhere else must not satisfy the rule (#2649)."""
68
+ errors = validate_required_parameter_usage(listing(**listing_changes), offering)
69
+ assert len(errors) == 1
70
+ assert "host, token are used by no channel" in errors[0]
71
+
72
+
73
+ def test_dynamic_use_in_metadata_does_not_disable_the_rule():
74
+ errors = validate_required_parameter_usage(listing(description="{{ params | tojson }}"), {})
75
+ assert "host, token" in errors[0]
76
+
77
+
78
+ def test_partial_use_is_rejected():
79
+ errors = validate_required_parameter_usage(listing(), channel(base_url="{{ params.host }}"))
80
+ assert "token are used by no channel" in errors[0]
81
+
82
+
83
+ def test_channel_interface_and_document_reads_count_without_proving_attribution():
84
+ data = listing(documents={"example": {"file_content": "{{ params.token }}"}})
85
+ assert validate_required_parameter_usage(data, channel(base_url="{{ params.host }}")) == []
86
+ assert (
87
+ validate_required_parameter_usage(
88
+ listing(
89
+ user_access_interfaces={"canonical": {"base_url": "{{ params.host }}"}},
90
+ documents={"Example": {"file_content": "{{ params.token }}"}},
91
+ ),
92
+ {},
93
+ )
94
+ == []
95
+ )
96
+ # offering-level document content counts too
97
+ assert (
98
+ validate_required_parameter_usage(
99
+ listing(),
100
+ {**channel(base_url="{{ params.host }}"), "documents": {"Doc": {"file_content": "{{ params.token }}"}}},
101
+ )
102
+ == []
103
+ )
104
+
105
+
106
+ def test_required_parameter_can_have_punctuation_in_its_name():
107
+ assert (
108
+ validate_required_parameter_usage(
109
+ listing(user_parameters_schema={"required": ["host-name"]}), channel(host="{{ params['host-name'] }}")
110
+ )
111
+ == []
112
+ )
113
+
114
+
115
+ def test_deprecated_content_can_be_retired():
116
+ assert validate_required_parameter_usage(listing(status="deprecated"), {}) == []
117
+ assert validate_required_parameter_usage(listing(), {"status": "deprecated"}) == []
118
+
119
+
120
+ def test_no_required_parameters():
121
+ assert validate_required_parameter_usage({}, {}) == []
122
+
123
+
124
+ @pytest.mark.parametrize("suffix", ["json", "toml"])
125
+ def test_file_validator_checks_paired_offering(tmp_path, suffix):
126
+ schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
127
+ validator = DataValidator(tmp_path, schema_dir)
128
+ data = listing()
129
+ (tmp_path / "listing.json").write_text(json.dumps(data))
130
+ path = tmp_path / f"offering.{suffix}"
131
+ path.write_text("{}" if suffix == "json" else "")
132
+ errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
133
+ assert "host, token" in errors[0]
134
+ path.write_text(
135
+ json.dumps(channel(host="{{ params.host }}", token="{{ params.token }}"))
136
+ if suffix == "json"
137
+ else '[upstream_access_config.plus]\nhost = "{{ params.host }}"\ntoken = "{{ params.token }}"\n'
138
+ )
139
+ assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
140
+
141
+
142
+ def test_file_validator_reads_template_documents_like_ingest(tmp_path):
143
+ """The seller SDK uploads a .j2 document's source as file_content, so the
144
+ file validator must count a param used only there, as ingest does."""
145
+ schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
146
+ validator = DataValidator(tmp_path, schema_dir)
147
+ (tmp_path / "offering.json").write_text(json.dumps(channel(base_url="{{ params.host }}")))
148
+ (tmp_path / "connectivity.sh.j2").write_text('curl "$URL" -H "X-Token: {{ params.token }}"\n')
149
+ data = listing(documents={"Connectivity": {"file_path": "connectivity.sh.j2", "category": "connectivity_test"}})
150
+ (tmp_path / "listing.json").write_text(json.dumps(data))
151
+ assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
152
+ # a non-template document is uploaded as-is, never rendered: not a consumer
153
+ (tmp_path / "usage.md").write_text("{{ params.token }}")
154
+ data = listing(documents={"Usage": {"file_path": "usage.md"}})
155
+ errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
156
+ assert "token are used by no channel" in errors[0]
157
+
158
+
159
+ def test_validation_pipeline_invokes_the_shared_rule(tmp_path, monkeypatch):
160
+ schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
161
+ validator = DataValidator(tmp_path, schema_dir)
162
+ path = tmp_path / "listing.json"
163
+ path.write_text(json.dumps({"name": "labs/test"}))
164
+ calls = []
165
+
166
+ def check(data, file_path):
167
+ calls.append(file_path)
168
+ return ["unused-param-sentinel"]
169
+
170
+ monkeypatch.setattr(validator, "validate_required_parameter_usage", check)
171
+ valid, errors = validator.validate_data_file(path, "listing_v1")
172
+ assert not valid
173
+ assert "unused-param-sentinel" in errors
174
+ assert calls == [path]
175
+
176
+
177
+ def test_invalid_jinja_does_not_bypass_the_rule():
178
+ errors = validate_required_parameter_usage(listing(), channel(base_url="{{ params.host"))
179
+ assert "invalid Jinja template" in errors[0]
180
+
181
+
182
+ def test_invalid_jinja_in_metadata_is_ignored():
183
+ """Metadata is never rendered with params, so it is not parsed either."""
184
+ errors = validate_required_parameter_usage(listing(description="Go: {{ .Name }}"), {})
185
+ assert "host, token are used by no channel" in errors[0]
@@ -1,121 +0,0 @@
1
- """Required enrollment parameters must have consumers across service data."""
2
-
3
- import json
4
- from pathlib import Path
5
-
6
- import pytest
7
-
8
- from unitysvc_core.parameter_usage import referenced_param_names, validate_required_parameter_usage
9
- from unitysvc_core.validator import DataValidator
10
-
11
-
12
- @pytest.mark.parametrize("expression", ["params.name", "params['name']", 'params.get("name")'])
13
- def test_named_reads(expression):
14
- assert referenced_param_names("{{ " + expression + " }}") == {"name"}
15
-
16
-
17
- @pytest.mark.parametrize(
18
- "expression",
19
- [
20
- "params",
21
- "params | tojson",
22
- "params.items()",
23
- "params.keys()",
24
- "params.values()",
25
- "params.get(key)",
26
- "params[key]",
27
- ],
28
- )
29
- def test_dynamic_and_whole_map_reads(expression):
30
- assert referenced_param_names("{{ " + expression + " }}") is None
31
-
32
-
33
- def test_strings_comments_and_non_template_text_are_not_consumers():
34
- assert referenced_param_names(["params.name", "{# params.name #}", '{{ "params.name" }}']) == set()
35
-
36
-
37
- def listing(**changes):
38
- return {"status": "ready", "user_parameters_schema": {"required": ["host", "token"]}, **changes}
39
-
40
-
41
- def test_required_names_in_the_schema_do_not_count_as_use():
42
- errors = validate_required_parameter_usage(
43
- listing(), {"upstream_access_config": {"free": {"api_key": "${ customer_secrets.TOKEN }"}}}
44
- )
45
- assert len(errors) == 1
46
- assert "host, token are used by no channel" in errors[0]
47
-
48
-
49
- def test_partial_use_is_rejected():
50
- errors = validate_required_parameter_usage(listing(), {"host": "{{ params.host }}"})
51
- assert "token are used by no channel" in errors[0]
52
-
53
-
54
- def test_channel_interface_and_document_reads_count_without_proving_attribution():
55
- data = listing(documents={"example": {"body": "{{ params.token }}"}})
56
- assert validate_required_parameter_usage(data, {"host": "{{ params.host }}"}) == []
57
- assert (
58
- validate_required_parameter_usage(
59
- listing(user_access_interfaces={"url": "{{ params.host }}"}, documents={"body": "{{ params.token }}"}), {}
60
- )
61
- == []
62
- )
63
-
64
-
65
- def test_required_parameter_can_have_punctuation_in_its_name():
66
- assert (
67
- validate_required_parameter_usage(
68
- listing(user_parameters_schema={"required": ["host-name"]}), {"host": "{{ params['host-name'] }}"}
69
- )
70
- == []
71
- )
72
-
73
-
74
- def test_deprecated_content_can_be_retired():
75
- assert validate_required_parameter_usage(listing(status="deprecated"), {}) == []
76
- assert validate_required_parameter_usage(listing(), {"status": "deprecated"}) == []
77
-
78
-
79
- def test_no_required_parameters():
80
- assert validate_required_parameter_usage({}, {}) == []
81
-
82
-
83
- @pytest.mark.parametrize("suffix", ["json", "toml"])
84
- def test_file_validator_checks_paired_offering(tmp_path, suffix):
85
- schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
86
- validator = DataValidator(tmp_path, schema_dir)
87
- data = listing()
88
- (tmp_path / "listing.json").write_text(json.dumps(data))
89
- path = tmp_path / f"offering.{suffix}"
90
- path.write_text("{}" if suffix == "json" else "")
91
- errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
92
- assert "host, token" in errors[0]
93
- path.write_text(
94
- json.dumps({"host": "{{ params.host }}", "token": "{{ params.token }}"})
95
- if suffix == "json"
96
- else 'host = "{{ params.host }}"\ntoken = "{{ params.token }}"\n'
97
- )
98
- assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
99
-
100
-
101
- def test_validation_pipeline_invokes_the_shared_rule(tmp_path, monkeypatch):
102
- schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
103
- validator = DataValidator(tmp_path, schema_dir)
104
- path = tmp_path / "listing.json"
105
- path.write_text(json.dumps({"name": "labs/test"}))
106
- calls = []
107
-
108
- def check(data, file_path):
109
- calls.append(file_path)
110
- return ["unused-param-sentinel"]
111
-
112
- monkeypatch.setattr(validator, "validate_required_parameter_usage", check)
113
- valid, errors = validator.validate_data_file(path, "listing_v1")
114
- assert not valid
115
- assert "unused-param-sentinel" in errors
116
- assert calls == [path]
117
-
118
-
119
- def test_invalid_jinja_does_not_bypass_the_rule():
120
- errors = validate_required_parameter_usage(listing(), {"host": "{{ params.host"})
121
- assert "invalid Jinja template" in errors[0]
File without changes
File without changes