Merge ~maxiberta/snapstore-client:env-auth into snapstore-client:master
- Git
- lp:~maxiberta/snapstore-client
- env-auth
- Merge into master
Proposed by
Maximiliano Bertacchini
Status: | Merged |
---|---|
Approved by: | Maximiliano Bertacchini |
Approved revision: | 7266727019f5d033932617ab71d4d599829f748e |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~maxiberta/snapstore-client:env-auth |
Merge into: | snapstore-client:master |
Prerequisite: | ~maxiberta/snapstore-client:craft-store-2.1.1 |
Diff against target: |
343 lines (+162/-28) 8 files modified
store_admin/cli/common.py (+31/-0) store_admin/cli/runner.py (+1/-0) store_admin/cli/snaps.py (+2/-0) store_admin/cli/stores.py (+2/-0) store_admin/logic/accounts.py (+19/-1) store_admin/logic/http_client.py (+28/-20) store_admin/logic/tests/test_accounts.py (+24/-0) store_admin/logic/tests/test_http_client.py (+55/-7) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Przemysław Suliga | Approve | ||
Review via email: mp+425096@code.launchpad.net |
Commit message
Add support for exporting and using auth token for non-interactive use cases
If found, the STORE_ADMIN_TOKEN env is used as authentication token, with the same format as the "admin-
Description of the change
To post a comment you must log in.
Revision history for this message
Przemysław Suliga (suligap) wrote : | # |
- 1eb3f6c... by Maximiliano Bertacchini
-
Extend the documentation around export token
- 16d37a0... by Maximiliano Bertacchini
-
Add validation to the ttl option in export token
- 5b57c24... by Maximiliano Bertacchini
-
Use plain request.Session instead of craft_store.
StoreClient for authed sessions
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote : | # |
All comments addressed. Thanks!
Revision history for this message
Przemysław Suliga (suligap) wrote : | # |
+1 thanks!
Optional suggestion is to move both newly added export_token() functions to separate modules vs adding them to cli.stores and logic.stores.
review:
Approve
- fc4b705... by Maximiliano Bertacchini
-
Move the "export token" subcommand from cli.stores to cli.common
- 719faa3... by Maximiliano Bertacchini
-
Move export_token() from logic.stores to logic.accounts
- 7266727... by Maximiliano Bertacchini
-
Add some extra logging around exporting token
Revision history for this message
Przemysław Suliga (suligap) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/store_admin/cli/common.py b/store_admin/cli/common.py | |||
2 | index 754cc35..3033c61 100644 | |||
3 | --- a/store_admin/cli/common.py | |||
4 | +++ b/store_admin/cli/common.py | |||
5 | @@ -6,11 +6,14 @@ import click | |||
6 | 6 | import requests.exceptions | 6 | import requests.exceptions |
7 | 7 | 7 | ||
8 | 8 | from store_admin.defaults import get_sso_url | 8 | from store_admin.defaults import get_sso_url |
9 | 9 | from store_admin.logic import accounts | ||
10 | 9 | from store_admin.logic.login import login as logic_login | 10 | from store_admin.logic.login import login as logic_login |
11 | 10 | from store_admin.logic.registration import register as logic_register | 11 | from store_admin.logic.registration import register as logic_register |
12 | 11 | 12 | ||
13 | 12 | logger = logging.getLogger(__name__) | 13 | logger = logging.getLogger(__name__) |
14 | 13 | 14 | ||
15 | 15 | one_year_in_secs = 60 * 60 * 24 * 365 | ||
16 | 16 | |||
17 | 14 | 17 | ||
18 | 15 | @click.command() | 18 | @click.command() |
19 | 16 | @click.argument("proxy_url") | 19 | @click.argument("proxy_url") |
20 | @@ -102,3 +105,31 @@ def register(url, output, offline, channel, arch): | |||
21 | 102 | f"Could not write to the specified file path: {output}. Please " | 105 | f"Could not write to the specified file path: {output}. Please " |
22 | 103 | "provide a path that is writable" | 106 | "provide a path that is writable" |
23 | 104 | ) | 107 | ) |
24 | 108 | |||
25 | 109 | |||
26 | 110 | @click.command(name="token") | ||
27 | 111 | @click.option( | ||
28 | 112 | "--ttl", | ||
29 | 113 | type=int, | ||
30 | 114 | default=one_year_in_secs, # 1 year (seconds) | ||
31 | 115 | help=( | ||
32 | 116 | "How long the authentication token should be valid, in seconds " | ||
33 | 117 | f"(must be between 60s and 1 year) [default: {one_year_in_secs} (1 year)]" | ||
34 | 118 | ), | ||
35 | 119 | ) | ||
36 | 120 | def export_token(ttl): | ||
37 | 121 | """Export an admin token for your IoT App Store. | ||
38 | 122 | |||
39 | 123 | The resulting token is valid for `ttl` seconds (max. 1 year) and has permissions | ||
40 | 124 | "store_admin" and "package_access". | ||
41 | 125 | |||
42 | 126 | Set the environment variable `STORE_ADMIN_TOKEN=<token>` to skip | ||
43 | 127 | interactive login prompts in the `export snaps` and `export store` subcommands | ||
44 | 128 | (e.g. for CI/automation). | ||
45 | 129 | """ | ||
46 | 130 | if not (60 <= ttl <= one_year_in_secs): | ||
47 | 131 | raise click.BadParameter( | ||
48 | 132 | f"ttl must be between 60 and {one_year_in_secs} seconds (1 year)" | ||
49 | 133 | ) | ||
50 | 134 | |||
51 | 135 | accounts.export_token(ttl=ttl) | ||
52 | diff --git a/store_admin/cli/runner.py b/store_admin/cli/runner.py | |||
53 | index 9778936..03c172e 100644 | |||
54 | --- a/store_admin/cli/runner.py | |||
55 | +++ b/store_admin/cli/runner.py | |||
56 | @@ -81,4 +81,5 @@ remove.add_command(overrides.remove_overrides) | |||
57 | 81 | 81 | ||
58 | 82 | # Commands under export | 82 | # Commands under export |
59 | 83 | export.add_command(stores.export_store) | 83 | export.add_command(stores.export_store) |
60 | 84 | export.add_command(common.export_token) | ||
61 | 84 | export.add_command(snaps.export_snap) | 85 | export.add_command(snaps.export_snap) |
62 | diff --git a/store_admin/cli/snaps.py b/store_admin/cli/snaps.py | |||
63 | index 9be410c..0afac13 100644 | |||
64 | --- a/store_admin/cli/snaps.py | |||
65 | +++ b/store_admin/cli/snaps.py | |||
66 | @@ -76,6 +76,8 @@ def export_snap(snap_names, channels, architectures, store_id, export_dir): | |||
67 | 76 | 76 | ||
68 | 77 | To export IoT App Store snaps, use the --store option - you will be asked to | 77 | To export IoT App Store snaps, use the --store option - you will be asked to |
69 | 78 | log in. Only authorized accounts can export not publicly available snaps. | 78 | log in. Only authorized accounts can export not publicly available snaps. |
70 | 79 | |||
71 | 80 | See the `export token` subcommand for non-interactive authentication. | ||
72 | 79 | """ | 81 | """ |
73 | 80 | tarball_paths = snaps.export( | 82 | tarball_paths = snaps.export( |
74 | 81 | snap_names, channels, architectures, store_id, export_dir | 83 | snap_names, channels, architectures, store_id, export_dir |
75 | diff --git a/store_admin/cli/stores.py b/store_admin/cli/stores.py | |||
76 | index 47e0c34..053986a 100644 | |||
77 | --- a/store_admin/cli/stores.py | |||
78 | +++ b/store_admin/cli/stores.py | |||
79 | @@ -62,5 +62,7 @@ def export_store(store_id, channels, architectures, skip_snaps, account_key_ids) | |||
80 | 62 | """Export your IoT App Store data for import to the offline store. | 62 | """Export your IoT App Store data for import to the offline store. |
81 | 63 | 63 | ||
82 | 64 | STORE_ID: Store ID of a "device view" app store. | 64 | STORE_ID: Store ID of a "device view" app store. |
83 | 65 | |||
84 | 66 | See the `export token` subcommand for non-interactive authentication. | ||
85 | 65 | """ | 67 | """ |
86 | 66 | stores.export(store_id, channels, architectures, skip_snaps, account_key_ids) | 68 | stores.export(store_id, channels, architectures, skip_snaps, account_key_ids) |
87 | diff --git a/store_admin/logic/accounts.py b/store_admin/logic/accounts.py | |||
88 | index 8eca23d..5440af1 100644 | |||
89 | --- a/store_admin/logic/accounts.py | |||
90 | +++ b/store_admin/logic/accounts.py | |||
91 | @@ -8,7 +8,8 @@ from urllib.parse import urljoin | |||
92 | 8 | import requests | 8 | import requests |
93 | 9 | from pymacaroons import Macaroon | 9 | from pymacaroons import Macaroon |
94 | 10 | 10 | ||
96 | 11 | from .. import exceptions | 11 | from .http_client import STORE_ADMIN_ENV_AUTH_KEY, get_developer_session |
97 | 12 | from .. import defaults, exceptions | ||
98 | 12 | 13 | ||
99 | 13 | 14 | ||
100 | 14 | logger = logging.getLogger(__name__) | 15 | logger = logging.getLogger(__name__) |
101 | @@ -53,3 +54,20 @@ def get_account_details(dashboard_root, authed_session, auth_token): | |||
102 | 53 | "validation": "unproven", | 54 | "validation": "unproven", |
103 | 54 | }, | 55 | }, |
104 | 55 | } | 56 | } |
105 | 57 | |||
106 | 58 | |||
107 | 59 | def export_token(ttl): | ||
108 | 60 | logger.info("Logging in as store admin...") | ||
109 | 61 | # store_admin permission for accessing store data. | ||
110 | 62 | # package_access permission for downloading store snaps. | ||
111 | 63 | permissions = ["store_admin", "package_access"] | ||
112 | 64 | |||
113 | 65 | _, auth_token = get_developer_session( | ||
114 | 66 | defaults.get_dashboard_url(), | ||
115 | 67 | permissions=permissions, | ||
116 | 68 | ttl=ttl, | ||
117 | 69 | use_env=False, | ||
118 | 70 | ) | ||
119 | 71 | logger.info(f"Exported token with permissions={permissions!r} and ttl={ttl!r}") | ||
120 | 72 | logger.info("Usage:") | ||
121 | 73 | logger.info(f" export {STORE_ADMIN_ENV_AUTH_KEY}={auth_token}") | ||
122 | diff --git a/store_admin/logic/http_client.py b/store_admin/logic/http_client.py | |||
123 | index 067f9c8..110e278 100644 | |||
124 | --- a/store_admin/logic/http_client.py | |||
125 | +++ b/store_admin/logic/http_client.py | |||
126 | @@ -2,13 +2,20 @@ | |||
127 | 2 | # Copyright 2022 Canonical Ltd. This software is licensed under the | 2 | # Copyright 2022 Canonical Ltd. This software is licensed under the |
128 | 3 | # GNU General Public License version 3 (see the file LICENSE). | 3 | # GNU General Public License version 3 (see the file LICENSE). |
129 | 4 | 4 | ||
130 | 5 | import logging | ||
131 | 6 | import os | ||
132 | 7 | |||
133 | 5 | import requests | 8 | import requests |
134 | 6 | from requests.adapters import HTTPAdapter, Retry | 9 | from requests.adapters import HTTPAdapter, Retry |
135 | 7 | from craft_store import Auth, StoreClient, endpoints | 10 | from craft_store import Auth, StoreClient, endpoints |
136 | 8 | 11 | ||
137 | 9 | 12 | ||
138 | 13 | logger = logging.getLogger(__name__) | ||
139 | 14 | |||
140 | 10 | APP_NAME = "store-admin" | 15 | APP_NAME = "store-admin" |
141 | 11 | USER_AGENT = APP_NAME | 16 | USER_AGENT = APP_NAME |
142 | 17 | STORE_ADMIN_ENV_AUTH_KEY = "STORE_ADMIN_TOKEN" | ||
143 | 18 | CRAFT_STORE_ENV_AUTH_KEY = "CRAFT_STORE_TOKEN" | ||
144 | 12 | _session = None | 19 | _session = None |
145 | 13 | 20 | ||
146 | 14 | 21 | ||
147 | @@ -34,33 +41,34 @@ def get_session(unique=False): | |||
148 | 34 | return session | 41 | return session |
149 | 35 | 42 | ||
150 | 36 | 43 | ||
154 | 37 | def get_store_client(base_url): | 44 | def get_developer_session(dashboard_root, permissions=None, ttl=None, use_env=True): |
155 | 38 | store_client = StoreClient( | 45 | auth_token = None |
156 | 39 | base_url=base_url, | 46 | if use_env and STORE_ADMIN_ENV_AUTH_KEY in os.environ: |
157 | 47 | # Non-interactive authentication | ||
158 | 48 | logger.debug("Using auth token found in env for non-interactive login") | ||
159 | 49 | auth_token = os.getenv(STORE_ADMIN_ENV_AUTH_KEY) | ||
160 | 50 | os.environ[CRAFT_STORE_ENV_AUTH_KEY] = Auth.encode_credentials(auth_token) | ||
161 | 51 | |||
162 | 52 | client = StoreClient( | ||
163 | 53 | base_url=dashboard_root, | ||
164 | 40 | storage_base_url="", # No uploads expected. | 54 | storage_base_url="", # No uploads expected. |
165 | 41 | endpoints=endpoints.SNAP_STORE, # SCA. | 55 | endpoints=endpoints.SNAP_STORE, # SCA. |
166 | 42 | user_agent=USER_AGENT, | 56 | user_agent=USER_AGENT, |
167 | 43 | application_name=APP_NAME, | 57 | application_name=APP_NAME, |
168 | 58 | environment_auth=CRAFT_STORE_ENV_AUTH_KEY if use_env else None, | ||
169 | 44 | ephemeral=True, | 59 | ephemeral=True, |
170 | 45 | ) | 60 | ) |
171 | 46 | return store_client | ||
172 | 47 | |||
173 | 48 | |||
174 | 49 | def get_store_admin_auth_token(store_client, permissions): | ||
175 | 50 | credentials = store_client.login( | ||
176 | 51 | # https://dashboard.snapcraft.io/docs/v2/en/tokens.html | ||
177 | 52 | permissions=permissions, | ||
178 | 53 | description="store-admin export", | ||
179 | 54 | ttl=60 * 60, # 1h. | ||
180 | 55 | ) | ||
181 | 56 | # Decode from craft-store internal credential storage. | ||
182 | 57 | return Auth.decode_credentials(credentials) | ||
183 | 58 | |||
184 | 59 | 61 | ||
189 | 60 | def get_developer_session(dashboard_root, permissions=None): | 62 | if not auth_token: |
190 | 61 | client = get_store_client(dashboard_root) | 63 | # User/browser interaction |
191 | 62 | # User/browser interaction | 64 | logger.debug("No auth token found in env: proceeding with interactive login") |
192 | 63 | auth_token = get_store_admin_auth_token(client, permissions) | 65 | craft_store_auth = client.login( |
193 | 66 | # https://dashboard.snapcraft.io/docs/v2/en/tokens.html | ||
194 | 67 | permissions=permissions, | ||
195 | 68 | description="store-admin export", | ||
196 | 69 | ttl=ttl or 60 * 60, # 1h. | ||
197 | 70 | ) | ||
198 | 71 | auth_token = Auth.decode_credentials(craft_store_auth) | ||
199 | 64 | 72 | ||
200 | 65 | session = get_session(unique=True) | 73 | session = get_session(unique=True) |
201 | 66 | session.headers["Authorization"] = f"Macaroon {auth_token}" | 74 | session.headers["Authorization"] = f"Macaroon {auth_token}" |
202 | diff --git a/store_admin/logic/tests/test_accounts.py b/store_admin/logic/tests/test_accounts.py | |||
203 | index 72ef9c0..b11bec0 100644 | |||
204 | --- a/store_admin/logic/tests/test_accounts.py | |||
205 | +++ b/store_admin/logic/tests/test_accounts.py | |||
206 | @@ -5,10 +5,15 @@ | |||
207 | 5 | import pytest | 5 | import pytest |
208 | 6 | import requests | 6 | import requests |
209 | 7 | from pymacaroons import Macaroon, MACAROON_V2 | 7 | from pymacaroons import Macaroon, MACAROON_V2 |
210 | 8 | from unittest import mock | ||
211 | 8 | 9 | ||
212 | 9 | from store_admin import exceptions | 10 | from store_admin import exceptions |
213 | 10 | from store_admin.logic import accounts | 11 | from store_admin.logic import accounts |
214 | 11 | 12 | ||
215 | 13 | |||
216 | 14 | PATCH_PREFIX = "store_admin.logic.accounts." | ||
217 | 15 | |||
218 | 16 | |||
219 | 12 | # extract_external_id() tests | 17 | # extract_external_id() tests |
220 | 13 | 18 | ||
221 | 14 | 19 | ||
222 | @@ -60,3 +65,22 @@ def test_get_account_details_whoami_api_error( | |||
223 | 60 | assert caplog.records[-1].msg.startswith( | 65 | assert caplog.records[-1].msg.startswith( |
224 | 61 | "Failed to fetch account details: 503 Server Error" | 66 | "Failed to fetch account details: 503 Server Error" |
225 | 62 | ) | 67 | ) |
226 | 68 | |||
227 | 69 | |||
228 | 70 | # export_token() tests | ||
229 | 71 | |||
230 | 72 | |||
231 | 73 | def test_export_token(caplog, dashboard_root): | ||
232 | 74 | with mock.patch( | ||
233 | 75 | PATCH_PREFIX + "get_developer_session", | ||
234 | 76 | return_value=(requests.Session(), "mock-token"), | ||
235 | 77 | ) as mock_get_developer_session: | ||
236 | 78 | accounts.export_token(42) | ||
237 | 79 | |||
238 | 80 | mock_get_developer_session.assert_called_once_with( | ||
239 | 81 | dashboard_root, | ||
240 | 82 | permissions=["store_admin", "package_access"], | ||
241 | 83 | ttl=42, | ||
242 | 84 | use_env=False, | ||
243 | 85 | ) | ||
244 | 86 | assert caplog.records[-1].msg == " export STORE_ADMIN_TOKEN=mock-token" | ||
245 | diff --git a/store_admin/logic/tests/test_http_client.py b/store_admin/logic/tests/test_http_client.py | |||
246 | index 4497940..f805dbb 100644 | |||
247 | --- a/store_admin/logic/tests/test_http_client.py | |||
248 | +++ b/store_admin/logic/tests/test_http_client.py | |||
249 | @@ -2,6 +2,7 @@ | |||
250 | 2 | # Copyright 2022 Canonical Ltd. This software is licensed under the | 2 | # Copyright 2022 Canonical Ltd. This software is licensed under the |
251 | 3 | # GNU General Public License version 3 (see the file LICENSE). | 3 | # GNU General Public License version 3 (see the file LICENSE). |
252 | 4 | 4 | ||
253 | 5 | import os | ||
254 | 5 | from unittest import mock | 6 | from unittest import mock |
255 | 6 | 7 | ||
256 | 7 | from craft_store import Auth | 8 | from craft_store import Auth |
257 | @@ -9,9 +10,6 @@ from craft_store import Auth | |||
258 | 9 | from store_admin.logic import http_client | 10 | from store_admin.logic import http_client |
259 | 10 | 11 | ||
260 | 11 | 12 | ||
261 | 12 | PATCH_PREFIX = "store_admin.logic.http_client." | ||
262 | 13 | |||
263 | 14 | |||
264 | 15 | # get_session() tests | 13 | # get_session() tests |
265 | 16 | 14 | ||
266 | 17 | 15 | ||
267 | @@ -26,18 +24,17 @@ def test_get_session_unique(): | |||
268 | 26 | # get_developer_session() tests | 24 | # get_developer_session() tests |
269 | 27 | 25 | ||
270 | 28 | 26 | ||
271 | 27 | @mock.patch.dict(os.environ, {}, clear=True) | ||
272 | 29 | def test_get_developer_session(dashboard_root, requests_mock): | 28 | def test_get_developer_session(dashboard_root, requests_mock): |
273 | 30 | mock_credentials = Auth.encode_credentials("mock-credentials") | 29 | mock_credentials = Auth.encode_credentials("mock-credentials") |
277 | 31 | 30 | with mock.patch.object( | |
278 | 32 | with mock.patch( | 31 | http_client.StoreClient, "login", return_value=mock_credentials |
276 | 33 | PATCH_PREFIX + "StoreClient.login", return_value=mock_credentials | ||
279 | 34 | ) as mock_craft_store_client_login: | 32 | ) as mock_craft_store_client_login: |
280 | 35 | session, auth_token = http_client.get_developer_session( | 33 | session, auth_token = http_client.get_developer_session( |
281 | 36 | dashboard_root, permissions=["package_view"] | 34 | dashboard_root, permissions=["package_view"] |
282 | 37 | ) | 35 | ) |
283 | 38 | 36 | ||
284 | 39 | assert auth_token == "mock-credentials" | 37 | assert auth_token == "mock-credentials" |
285 | 40 | |||
286 | 41 | mock_craft_store_client_login.assert_called_once_with( | 38 | mock_craft_store_client_login.assert_called_once_with( |
287 | 42 | permissions=["package_view"], description="store-admin export", ttl=3600 | 39 | permissions=["package_view"], description="store-admin export", ttl=3600 |
288 | 43 | ) | 40 | ) |
289 | @@ -46,3 +43,54 @@ def test_get_developer_session(dashboard_root, requests_mock): | |||
290 | 46 | session.get(dashboard_root) | 43 | session.get(dashboard_root) |
291 | 47 | [call] = requests_mock.calls | 44 | [call] = requests_mock.calls |
292 | 48 | assert call.request.headers["Authorization"] == "Macaroon mock-credentials" | 45 | assert call.request.headers["Authorization"] == "Macaroon mock-credentials" |
293 | 46 | |||
294 | 47 | |||
295 | 48 | @mock.patch.dict(os.environ, {}, clear=True) | ||
296 | 49 | def test_get_developer_session_with_custom_ttl(dashboard_root, requests_mock): | ||
297 | 50 | mock_credentials = Auth.encode_credentials("mock-credentials") | ||
298 | 51 | with mock.patch.object( | ||
299 | 52 | http_client.StoreClient, "login", return_value=mock_credentials | ||
300 | 53 | ) as mock_craft_store_client_login: | ||
301 | 54 | session, auth_token = http_client.get_developer_session( | ||
302 | 55 | dashboard_root, permissions=["package_view"], ttl=42 | ||
303 | 56 | ) | ||
304 | 57 | |||
305 | 58 | assert auth_token == "mock-credentials" | ||
306 | 59 | mock_craft_store_client_login.assert_called_once_with( | ||
307 | 60 | permissions=["package_view"], description="store-admin export", ttl=42 | ||
308 | 61 | ) | ||
309 | 62 | |||
310 | 63 | |||
311 | 64 | @mock.patch.dict(os.environ, {http_client.STORE_ADMIN_ENV_AUTH_KEY: "mock-credentials"}) | ||
312 | 65 | def test_get_developer_session_with_store_admin_token(dashboard_root, requests_mock): | ||
313 | 66 | mock_credentials = Auth.encode_credentials("mock-credentials") | ||
314 | 67 | with mock.patch.object( | ||
315 | 68 | http_client.StoreClient, "login", return_value=mock_credentials | ||
316 | 69 | ) as mock_craft_store_client_login: | ||
317 | 70 | session, auth_token = http_client.get_developer_session( | ||
318 | 71 | dashboard_root, permissions=["package_view"] | ||
319 | 72 | ) | ||
320 | 73 | |||
321 | 74 | assert auth_token == "mock-credentials" | ||
322 | 75 | mock_craft_store_client_login.assert_not_called() | ||
323 | 76 | |||
324 | 77 | requests_mock.add("GET", dashboard_root, json={}) | ||
325 | 78 | session.get(dashboard_root) | ||
326 | 79 | [call] = requests_mock.calls | ||
327 | 80 | assert call.request.headers["Authorization"] == "Macaroon mock-credentials" | ||
328 | 81 | |||
329 | 82 | |||
330 | 83 | @mock.patch.dict(os.environ, {http_client.STORE_ADMIN_ENV_AUTH_KEY: "ignore-me"}) | ||
331 | 84 | def test_get_developer_session_with_no_env_flag(dashboard_root, requests_mock): | ||
332 | 85 | mock_credentials = Auth.encode_credentials("mock-credentials") | ||
333 | 86 | with mock.patch.object( | ||
334 | 87 | http_client.StoreClient, "login", return_value=mock_credentials | ||
335 | 88 | ) as mock_craft_store_client_login: | ||
336 | 89 | session, auth_token = http_client.get_developer_session( | ||
337 | 90 | dashboard_root, permissions=["package_view"], use_env=False | ||
338 | 91 | ) | ||
339 | 92 | |||
340 | 93 | assert auth_token == "mock-credentials" | ||
341 | 94 | mock_craft_store_client_login.assert_called_once_with( | ||
342 | 95 | permissions=["package_view"], description="store-admin export", ttl=3600 | ||
343 | 96 | ) |
Thanks! This works but has some issues I think. Left some comments.