Merge ~woutervb/various/+git/snapstore-proxy-charm:registration_bundle into ~woutervb/various/+git/snapstore-proxy-charm:main
- Git
- lp:~woutervb/various/+git/snapstore-proxy-charm
- registration_bundle
- Merge into main
Proposed by
Wouter van Bommel
Status: | Rejected |
---|---|
Rejected by: | Wouter van Bommel |
Proposed branch: | ~woutervb/various/+git/snapstore-proxy-charm:registration_bundle |
Merge into: | ~woutervb/various/+git/snapstore-proxy-charm:main |
Diff against target: |
575 lines (+328/-21) 10 files modified
README.md (+11/-9) config.yaml (+6/-1) juju/overlay.yaml.example (+1/-2) juju/test-bundle.yaml (+0/-2) src/charm.py (+61/-2) src/helpers.py (+18/-3) src/optionvalidation.py (+26/-0) tests/test_charm.py (+142/-1) tests/test_helpers.py (+34/-0) tests/test_optionvalidation.py (+29/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Wouter van Bommel | Pending | ||
Review via email:
|
Commit message
Add the option to provide the charm with a registration bundle
This bundle, created with snapstore-client, is preferred, as it will allow registration with an ubuntu one account that has 2fa enabled.
Description of the change
To post a comment you must log in.
Unmerged commits
- 647db14... by Wouter van Bommel
-
Support a registration bundle
When using a registration bundle, it is no longer needed to add an
username or password to the config.
Updated the README to reflex the change
Updated the overlay.yaml.example file to reflex this change
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/README.md b/README.md |
2 | index cd1ef35..817c9d7 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -19,9 +19,7 @@ Below is a minimal bundle example that can be used to deploy the charm using juj |
6 | snapstore-proxy: |
7 | charm: snapstore-proxy |
8 | options: |
9 | - domain: http://something.not.local |
10 | - username: some.ubuntu@one.account |
11 | - password: UserPassword |
12 | + registration_bundle: <content of registration file base64 encoded> |
13 | |
14 | relations: |
15 | - - postgresql:db-admin |
16 | @@ -37,7 +35,7 @@ The charm supports the following actions: |
17 | |
18 | * **status** - Get the status result of the proxy, use like; |
19 | |
20 | - `juju run-action snapstore-proxy/leader status --wait` |
21 | + juju run-action snapstore-proxy/leader status --wait |
22 | |
23 | # Advanced options |
24 | |
25 | @@ -45,9 +43,15 @@ The charm supports the following actions: |
26 | |
27 | There are some mandatory config options that have to be configured for the charm to work, these are: |
28 | |
29 | -* **domain** - The domain on which the proxy will listen. This is needed for the registration, and the proxy itself needs to be able to resolve it. Ip addresses don't work. It has to be prepended with the protocol (http). |
30 | +* **registration_bundle** - A bundle created with a snap called `snapstore-client`. This tool supports accounts with 2 Factor authentication for Ubuntu One accounts, and contains everything to reproduce the proxy. This is the recommended way to configure the proxy. It is also a required way, if the machine on which the proxy is installed does not have an internet connection to the store. Easiest way to provide this option on the cli is the following: |
31 | + |
32 | + juju config snapstore-proxy registration_bundle=$(cat <path to file created with snapstore-client> | base64) |
33 | + |
34 | |
35 | -* **username** - This is the username of the ubuntu one account that will be used to register the proxy |
36 | +Or (deprecated) |
37 | + |
38 | +* **domain** - The domain on which the proxy will listen. This is needed for the registration, and the proxy itself needs to be able to resolve it. Ip addresses don't work. It has to be prepended with the protocol (http). |
39 | +* **username** - This is the username of the Ubuntu One account that will be used to register the proxy |
40 | * **password** - The password of the above username |
41 | |
42 | ## Juju resource usage |
43 | @@ -65,9 +69,7 @@ An example bundle to do such a deployment will look like the following. |
44 | snapstore-proxy: |
45 | charm: snapstore-proxy |
46 | options: |
47 | - domain: http://something.not.local |
48 | - username: some.ubuntu@one.account |
49 | - password: UserPassword |
50 | + registration_bundle: <content of registration file base64 encoded> |
51 | resources: |
52 | snap-store-proxy: ./snap-store-proxy.snap |
53 | core: ./core20.snap |
54 | diff --git a/config.yaml b/config.yaml |
55 | index eaaa7df..1c131a6 100644 |
56 | --- a/config.yaml |
57 | +++ b/config.yaml |
58 | @@ -5,11 +5,16 @@ |
59 | # always possible |
60 | |
61 | options: |
62 | + registration_bundle: |
63 | + default: null |
64 | + desciption: | |
65 | + A bundle created via `snapstore-proxy_registration_bundle` added as a base64 |
66 | + encoded string. ie "$(cat <file> | base64)" |
67 | + type: string |
68 | domain: |
69 | default: http://localhost.localdomain |
70 | description: Domain name for the Snap Store Proxy. |
71 | type: string |
72 | - |
73 | username: |
74 | default: null |
75 | description: | |
76 | diff --git a/juju/overlay.yaml.example b/juju/overlay.yaml.example |
77 | index fc9b38d..ddd4f49 100644 |
78 | --- a/juju/overlay.yaml.example |
79 | +++ b/juju/overlay.yaml.example |
80 | @@ -1,5 +1,4 @@ |
81 | applications: |
82 | snapstore-proxy: |
83 | options: |
84 | - password: my super password |
85 | - username: my ubuntu one email |
86 | \ No newline at end of file |
87 | + registration_bundle: `cat <registation file> | base64` |
88 | \ No newline at end of file |
89 | diff --git a/juju/test-bundle.yaml b/juju/test-bundle.yaml |
90 | index 39c1f08..a2be469 100644 |
91 | --- a/juju/test-bundle.yaml |
92 | +++ b/juju/test-bundle.yaml |
93 | @@ -13,8 +13,6 @@ applications: |
94 | snapstore-proxy: |
95 | charm: ../snapstore-proxy_ubuntu-20.04-amd64.charm |
96 | num_units: 1 |
97 | - options: |
98 | - domain: http://something.not.local |
99 | |
100 | |
101 | relations: |
102 | diff --git a/src/charm.py b/src/charm.py |
103 | index 591965f..002cebb 100755 |
104 | --- a/src/charm.py |
105 | +++ b/src/charm.py |
106 | @@ -5,7 +5,9 @@ |
107 | # Learn more at: https://juju.is/docs/sdk |
108 | # |
109 | |
110 | +import base64 |
111 | import hashlib |
112 | +import json |
113 | import logging |
114 | from urllib.parse import urlparse |
115 | |
116 | @@ -121,8 +123,9 @@ class SnapstoreProxyCharm(CharmBase): |
117 | # Then configure it, and only then we can attempt to configure it |
118 | self.handle_config() |
119 | |
120 | - self.unit.status = MaintenanceStatus("Registering proxy") |
121 | - self.handle_registration(event) |
122 | + if not getattr(self._stored, "registered", False): |
123 | + self.unit.status = MaintenanceStatus("Registering proxy") |
124 | + self.handle_registration(event) |
125 | |
126 | self.evaluate_status() |
127 | |
128 | @@ -207,6 +210,11 @@ class SnapstoreProxyCharm(CharmBase): |
129 | A different method, as it will contain mappings between items and the |
130 | actual snap entries""" |
131 | |
132 | + has_registration_bundle = False |
133 | + for item in updates: |
134 | + has_registration_bundle |= item["name"] == "registration_bundle" |
135 | + |
136 | + # If the has_registration_bundle is true, we omit changes to the domain |
137 | for item in updates: |
138 | if item["name"] == "domain": |
139 | logger.info("Configuring the domain") |
140 | @@ -222,6 +230,57 @@ class SnapstoreProxyCharm(CharmBase): |
141 | setattr(self._stored, item["name"], item["value"]) |
142 | setattr(self._stored, f"{item['name']}_md5", item["value"]) |
143 | |
144 | + if has_registration_bundle: |
145 | + # If this config option is set, we basically ignore the username and |
146 | + # password and domain settings. And we take all from this field. |
147 | + for item in updates: |
148 | + if item["name"] == "registration_bundle": |
149 | + break |
150 | + if item["value"]: |
151 | + # A bit harsh, but this way we ensure that only this bundle is used |
152 | + self._stored.registered = True |
153 | + else: |
154 | + # The value is removed / reset, so act accordingly |
155 | + self._stored.registered = False |
156 | + return |
157 | + |
158 | + data = json.loads(base64.b64decode(item["value"])) |
159 | + for entries in [ |
160 | + "domain", |
161 | + "private_key", |
162 | + "public_key", |
163 | + "store_id", |
164 | + # "store_assertion", needed once airgap mode is implemented |
165 | + ]: |
166 | + # see if any of these keys are missing |
167 | + if entries not in data.keys(): |
168 | + self.errors.append( |
169 | + f"Missing key {entries} in the registration bundle" |
170 | + ) |
171 | + |
172 | + if self.errors: |
173 | + # Incomplete bundle(?) so stop doing anything |
174 | + return |
175 | + |
176 | + # Now make 2 lists, option names and values where the indexes |
177 | + # correspond with eachother |
178 | + options = [] |
179 | + values = [] |
180 | + for entry in data.keys(): |
181 | + if entry == "domain": |
182 | + options.append("proxy.domain") |
183 | + values.append(data[entry]) |
184 | + elif entry == "public_key": |
185 | + options.append("proxy.key.public") |
186 | + values.append(data[entry]) |
187 | + elif entry == "private_key": |
188 | + options.append("proxy.key.private") |
189 | + values.append(data[entry]) |
190 | + elif entry == "store_id": |
191 | + options.append("internal.store.id") |
192 | + values.append(data[entry]) |
193 | + configure_proxy(options, values, force=True) |
194 | + |
195 | def handle_registration(self, event): |
196 | """Try to register the store. |
197 | |
198 | diff --git a/src/helpers.py b/src/helpers.py |
199 | index 0575279..07cf19c 100644 |
200 | --- a/src/helpers.py |
201 | +++ b/src/helpers.py |
202 | @@ -10,10 +10,23 @@ import yaml |
203 | logger = logging.getLogger(__name__) |
204 | |
205 | |
206 | -def configure_proxy(option, value): |
207 | +def configure_proxy(option, value, force=False): |
208 | """Simple wrapper around calling snap settings""" |
209 | - logger.debug(f"Running config for option {option} with value {value}") |
210 | - run(["snap-store-proxy", "config", f"{option}={value}"]) |
211 | + |
212 | + if isinstance(option, list): |
213 | + # Make sure that we set multiple values in one go |
214 | + combined = [] |
215 | + for count, entry in enumerate(option): |
216 | + combined.append(f'{entry}="{value[count]}"') |
217 | + command = ["snap-store-proxy", "config"] + combined |
218 | + else: |
219 | + logger.debug(f"Running config for option {option} with value {value}") |
220 | + command = ["snap-store-proxy", "config", f"{option}={value}"] |
221 | + |
222 | + if force: |
223 | + command.append("--force") |
224 | + |
225 | + run(command) |
226 | |
227 | |
228 | def config_options(): |
229 | @@ -36,6 +49,8 @@ def config_options(): |
230 | type_dict.update({item: "url"}) |
231 | elif item == "username": |
232 | type_dict.update({item: "email"}) |
233 | + elif item == "registration_bundle": |
234 | + type_dict.update({item: "base64+json"}) |
235 | else: |
236 | type_dict.update({item: config["options"][item]["type"]}) |
237 | |
238 | diff --git a/src/optionvalidation.py b/src/optionvalidation.py |
239 | index 0ef81d7..2e8efae 100644 |
240 | --- a/src/optionvalidation.py |
241 | +++ b/src/optionvalidation.py |
242 | @@ -2,10 +2,15 @@ |
243 | # See LICENSE file for licensing details. |
244 | # |
245 | |
246 | +import base64 |
247 | +import json |
248 | import re |
249 | +from logging import Logger |
250 | |
251 | from exceptions import InvalidTypeException, UnknownTypeException |
252 | |
253 | +logger = Logger(__name__) |
254 | + |
255 | |
256 | class OptionValidation: |
257 | @staticmethod |
258 | @@ -16,12 +21,15 @@ class OptionValidation: |
259 | return OptionValidationURL() |
260 | if test_type == "email": |
261 | return OptionValidationEmail() |
262 | + if test_type == "base64+json": |
263 | + return OptionValidationBase64Json() |
264 | raise UnknownTypeException(test_type) |
265 | |
266 | |
267 | class OptionValidationString(OptionValidation): |
268 | @staticmethod |
269 | def validate(config_name, value): |
270 | + logger.debug("Validating the type string for {config_name}") |
271 | if not isinstance(value, str): |
272 | raise InvalidTypeException(config_name, "string", value) |
273 | |
274 | @@ -29,6 +37,7 @@ class OptionValidationString(OptionValidation): |
275 | class OptionValidationURL(OptionValidation): |
276 | @staticmethod |
277 | def validate(config_name, value): |
278 | + logger.debug("Validating the type url for {config_name}") |
279 | regex = re.compile( |
280 | r"^(?:http)://" # http:// or https:// |
281 | r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|" |
282 | @@ -45,6 +54,23 @@ class OptionValidationURL(OptionValidation): |
283 | class OptionValidationEmail(OptionValidation): |
284 | @staticmethod |
285 | def validate(config_name, value): |
286 | + logger.debug("Validating the type email for {config_name}") |
287 | regex = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" |
288 | if (not isinstance(value, str)) or (not re.fullmatch(regex, value)): |
289 | raise InvalidTypeException(config_name, "email", value) |
290 | + |
291 | + |
292 | +class OptionValidationBase64Json(OptionValidation): |
293 | + @staticmethod |
294 | + def validate(config_name, value): |
295 | + logger.debug("Validating the type base64+json for {config_name}") |
296 | + try: |
297 | + result = base64.b64decode(value) |
298 | + print(result) |
299 | + except Exception: |
300 | + raise InvalidTypeException(config_name, "base64", value) |
301 | + |
302 | + try: |
303 | + json.loads(result) |
304 | + except Exception: |
305 | + raise InvalidTypeException(config_name, "json", value) |
306 | diff --git a/tests/test_charm.py b/tests/test_charm.py |
307 | index a224ba7..6dc62f1 100644 |
308 | --- a/tests/test_charm.py |
309 | +++ b/tests/test_charm.py |
310 | @@ -4,6 +4,7 @@ |
311 | # Learn more about testing at: https://juju.is/docs/sdk/testing |
312 | # |
313 | |
314 | +import base64 |
315 | import logging |
316 | import subprocess |
317 | from pathlib import Path |
318 | @@ -197,6 +198,146 @@ class TestConfig(CharmTest): |
319 | patched_proxy.assert_called_once_with("postgresql://fake/database") |
320 | |
321 | |
322 | +class TestRegistrationBundle(CharmTest): |
323 | + # Split off to ensure that tests are specific for what they do |
324 | + # data that is passed to do_snap_updates has alrady been validated |
325 | + # via the optionvalidation functions, so no need to test invalid data |
326 | + def test_no_bundle(self): |
327 | + assert not hasattr(self.harness.charm._stored, "registered") |
328 | + |
329 | + self.harness.charm.do_snap_updates({}) |
330 | + |
331 | + # If we start without the registered key, and provide no bundle, no |
332 | + # key will be added |
333 | + assert not hasattr(self.harness.charm._stored, "registered") |
334 | + |
335 | + def test_registration_removed(self): |
336 | + setattr(self.harness.charm._stored, "registered", True) |
337 | + |
338 | + self.harness.charm.do_snap_updates( |
339 | + [{"name": "registration_bundle", "value": None}] |
340 | + ) |
341 | + |
342 | + # We started with a key, so it will stay with us |
343 | + assert not getattr(self.harness.charm._stored, "registered") |
344 | + |
345 | + def test_has_incomplete_bundle_missing_all(self): |
346 | + to_update = [ |
347 | + { |
348 | + "name": "registration_bundle", |
349 | + "value": base64.b64encode("{}".encode("UTF-8")), |
350 | + } |
351 | + ] |
352 | + |
353 | + self.harness.charm.do_snap_updates(to_update) |
354 | + assert len(self.harness.charm.errors) == 4 |
355 | + assert ( |
356 | + self.harness.charm.errors[0] |
357 | + == "Missing key domain in the registration bundle" |
358 | + ) |
359 | + |
360 | + def test_has_incomplete_bundle_added_domain(self): |
361 | + to_update = [ |
362 | + { |
363 | + "name": "registration_bundle", |
364 | + "value": base64.b64encode('{"domain": "sdfsd"}'.encode("UTF-8")), |
365 | + } |
366 | + ] |
367 | + |
368 | + self.harness.charm.do_snap_updates(to_update) |
369 | + assert len(self.harness.charm.errors) == 3 |
370 | + assert ( |
371 | + self.harness.charm.errors[0] |
372 | + == "Missing key private_key in the registration bundle" |
373 | + ) |
374 | + |
375 | + def test_has_incomplete_bundle_added_private_key(self): |
376 | + to_update = [ |
377 | + { |
378 | + "name": "registration_bundle", |
379 | + "value": base64.b64encode( |
380 | + '{"domain": "sdfsd",' '"private_key": "12"}'.encode("UTF-8") |
381 | + ), |
382 | + } |
383 | + ] |
384 | + |
385 | + self.harness.charm.do_snap_updates(to_update) |
386 | + assert len(self.harness.charm.errors) == 2 |
387 | + assert ( |
388 | + self.harness.charm.errors[0] |
389 | + == "Missing key public_key in the registration bundle" |
390 | + ) |
391 | + |
392 | + def test_has_incomplete_bundle_added_public_key(self): |
393 | + to_update = [ |
394 | + { |
395 | + "name": "registration_bundle", |
396 | + "value": base64.b64encode( |
397 | + '{"domain": "sdfsd",' |
398 | + '"public_key": "12",' |
399 | + '"private_key": "12"}'.encode("UTF-8") |
400 | + ), |
401 | + } |
402 | + ] |
403 | + |
404 | + self.harness.charm.do_snap_updates(to_update) |
405 | + assert len(self.harness.charm.errors) == 1 |
406 | + assert ( |
407 | + self.harness.charm.errors[0] |
408 | + == "Missing key store_id in the registration bundle" |
409 | + ) |
410 | + |
411 | + # Possibly needed once airgap mode has been implemented |
412 | + # def test_has_incomplete_bundle_added_store_id(self): |
413 | + # to_update = [ |
414 | + # { |
415 | + # "name": "registration_bundle", |
416 | + # "value": base64.b64encode( |
417 | + # '{"domain": "sdfsd",' |
418 | + # '"public_key": "12",' |
419 | + # '"store_id": "12",' |
420 | + # '"private_key": "12"}'.encode("UTF-8") |
421 | + # ), |
422 | + # } |
423 | + # ] |
424 | + |
425 | + # self.harness.charm.do_snap_updates(to_update) |
426 | + # assert len(self.harness.charm.errors) == 1 |
427 | + # assert ( |
428 | + # self.harness.charm.errors[0] |
429 | + # == "Missing key store_assertion in the registration bundle" |
430 | + # ) |
431 | + |
432 | + def test_has_complete_bundle(self): |
433 | + to_update = [ |
434 | + { |
435 | + "name": "registration_bundle", |
436 | + "value": base64.b64encode( |
437 | + '{"domain": "sdfsd",' |
438 | + '"public_key": "10",' |
439 | + '"store_id": "11",' |
440 | + '"store_assertion": "0",' |
441 | + '"private_key": "12"}'.encode("UTF-8") |
442 | + ), |
443 | + } |
444 | + ] |
445 | + with patch("charm.configure_proxy") as patched_configure: |
446 | + self.harness.charm.do_snap_updates(to_update) |
447 | + |
448 | + patched_configure.assert_called_once_with( |
449 | + [ |
450 | + "proxy.domain", |
451 | + "proxy.key.public", |
452 | + "internal.store.id", |
453 | + "proxy.key.private", |
454 | + ], |
455 | + ["sdfsd", "10", "11", "12"], |
456 | + force=True, |
457 | + ) |
458 | + |
459 | + assert getattr(self.harness.charm._stored, "registered", None) |
460 | + |
461 | + |
462 | class TestStatus(CharmTest): |
463 | def test_status_missing_db_uri(self): |
464 | self.harness.charm._stored.db_uri = None |
465 | @@ -367,7 +508,7 @@ class TestEvents(CharmTest): |
466 | |
467 | patched_get_status.assert_called_once_with() |
468 | event.fail.assert_called_once_with( |
469 | - "Failed to get status, result String, errorcode 1" |
470 | + "Failed to get status, errorcode 1\n, result String" |
471 | ) |
472 | |
473 | |
474 | diff --git a/tests/test_helpers.py b/tests/test_helpers.py |
475 | index 730f683..1e61b15 100644 |
476 | --- a/tests/test_helpers.py |
477 | +++ b/tests/test_helpers.py |
478 | @@ -13,6 +13,40 @@ def test_configure_proxy(): |
479 | ) |
480 | |
481 | |
482 | +def test_configure_proxy_list(): |
483 | + with mock.patch("helpers.run") as mocked_run: |
484 | + options = ["one", "two"] |
485 | + values = ["value_one", "value_two"] |
486 | + helpers.configure_proxy(options, values) |
487 | + |
488 | + assert mocked_run.called |
489 | + mocked_run.assert_called_once_with( |
490 | + ["snap-store-proxy", "config", 'one="value_one"', 'two="value_two"'] |
491 | + ) |
492 | + |
493 | + |
494 | +def test_configure_proxy_forced(): |
495 | + with mock.patch("helpers.run") as mocked_run: |
496 | + helpers.configure_proxy("myvalue", "myoption", True) |
497 | + |
498 | + assert mocked_run.called |
499 | + mocked_run.assert_called_once_with( |
500 | + ["snap-store-proxy", "config", "myvalue=myoption", "--force"] |
501 | + ) |
502 | + |
503 | + |
504 | +def test_configure_proxy_list_forced(): |
505 | + with mock.patch("helpers.run") as mocked_run: |
506 | + options = ["one", "two"] |
507 | + values = ["value_one", "value_two"] |
508 | + helpers.configure_proxy(options, values, True) |
509 | + |
510 | + assert mocked_run.called |
511 | + mocked_run.assert_called_once_with( |
512 | + ["snap-store-proxy", "config", 'one="value_one"', 'two="value_two"', "--force"] |
513 | + ) |
514 | + |
515 | + |
516 | def test_config_options(): |
517 | # Not sure how to do a clean test here, as this function is made |
518 | # in an attempt to prevent double registration of config items |
519 | diff --git a/tests/test_optionvalidation.py b/tests/test_optionvalidation.py |
520 | index f95ffed..4144c79 100644 |
521 | --- a/tests/test_optionvalidation.py |
522 | +++ b/tests/test_optionvalidation.py |
523 | @@ -1,8 +1,11 @@ |
524 | +import base64 |
525 | + |
526 | import pytest |
527 | |
528 | from exceptions import InvalidTypeException, UnknownTypeException |
529 | from optionvalidation import ( |
530 | OptionValidation, |
531 | + OptionValidationBase64Json, |
532 | OptionValidationEmail, |
533 | OptionValidationString, |
534 | OptionValidationURL, |
535 | @@ -21,6 +24,10 @@ def test_email_validation_type(): |
536 | assert isinstance(OptionValidation.new("email"), OptionValidationEmail) |
537 | |
538 | |
539 | +def test_base64_json_validation_type(): |
540 | + assert isinstance(OptionValidation.new("base64+json"), OptionValidationBase64Json) |
541 | + |
542 | + |
543 | def test_validation_raises_on_unknown(): |
544 | with pytest.raises(UnknownTypeException): |
545 | OptionValidation.new("clown") |
546 | @@ -102,7 +109,28 @@ def test_email_valid(email): |
547 | "user@domain", |
548 | ], |
549 | ) |
550 | -def test_email_imvalid(email): |
551 | +def test_email_invalid(email): |
552 | with pytest.raises(InvalidTypeException) as exc: |
553 | OptionValidation.new("email").validate("option", email) |
554 | assert email in str(exc.value) |
555 | + |
556 | + |
557 | +def test_base64_json_valid(): |
558 | + value = base64.b64encode("{}".encode("UTF-8")) |
559 | + OptionValidation.new("base64+json").validate("option", value) |
560 | + |
561 | + |
562 | +def test_base64_json_both_invalid(): |
563 | + value = "+_=-" |
564 | + with pytest.raises(InvalidTypeException) as exc: |
565 | + OptionValidation.new("base64+json").validate("option", value) |
566 | + assert value in str(exc.value) |
567 | + assert "base64" in str(exc.value) |
568 | + |
569 | + |
570 | +def test_base64_json_invalid(): |
571 | + value = base64.b64encode("{".encode("UTF-8")) |
572 | + with pytest.raises(InvalidTypeException) as exc: |
573 | + OptionValidation.new("base64+json").validate("option", value) |
574 | + assert value.decode("UTF-8") in str(exc.value) |
575 | + assert "json" in str(exc.value) |