From 0a493bf5887739f746885e58b2e56783f0ce0366 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 11:28:59 +0100 Subject: [PATCH 01/12] Stateless auth. --- qnexus/client/__init__.py | 75 +++++++++++++++------------------------ qnexus/client/auth.py | 3 -- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/qnexus/client/__init__.py b/qnexus/client/__init__.py index 7d85a5b..c9e6894 100644 --- a/qnexus/client/__init__.py +++ b/qnexus/client/__init__.py @@ -25,79 +25,62 @@ VERSION = version("qnexus") +def get_cookies_from_disk(): + cookies = httpx.Cookies() + try: + cookies.set("myqos_oat", read_token("refresh_token"), domain=CONFIG.domain) + except: + pass + try: + cookies.set("myqos_id", read_token("access_token"), domain=CONFIG.domain) + except: + pass + return cookies; + class AuthHandler(httpx.Auth): """Custom nexus auth handler""" - cookies: httpx.Cookies - - def __init__(self) -> None: - self.cookies = httpx.Cookies() - self.reload_tokens() - - super().__init__() - - def reload_tokens(self) -> None: - """Clear tokens and attempt to reload from the file system.""" - try: - self.cookies.clear() - token = read_token("refresh_token") - self.cookies.set("myqos_oat", token, domain=CONFIG.domain) - id_token = read_token("access_token") - self.cookies.set("myqos_id", id_token, domain=CONFIG.domain) - except FileNotFoundError: - pass def auth_flow( self, request: httpx.Request ) -> typing.Generator[httpx.Request, httpx.Response, None]: - self.cookies.set_cookie_header(request) + + cookies = get_cookies_from_disk() + cookies.set_cookie_header(request) response = yield request _check_sunset_header(request, response) if response.status_code == 401: - if self.cookies.get("myqos_oat") is None: - try: - token = read_token( - "refresh_token", - ) - self.cookies.set("myqos_oat", token, domain=CONFIG.domain) - except FileNotFoundError as exc: - raise AuthenticationError( - "Not authenticated. Please run `qnx login` in your terminal." - ) from exc - - auth_response = yield self.build_refresh_request() + auth_response = yield httpx.Request( + method="POST", + url=f"{CONFIG.url}/auth/tokens/refresh", + cookies=cookies, + headers={VERSION_HEADER: VERSION}, + ) + if auth_response.status_code == 401: raise AuthenticationError( "Not authenticated. Please run `qnx login` in your terminal." ) auth_response.raise_for_status() - self.cookies.extract_cookies(auth_response) + cookies.extract_cookies(auth_response) + request.headers.pop("cookie") + cookies.set_cookie_header(request) write_token( "access_token", - self.cookies.get("myqos_id", domain=CONFIG.domain) or "", + cookies.get("myqos_oat") or "", ) - if request.headers.get("cookie"): - request.headers.pop("cookie") - self.cookies.set_cookie_header(request) _check_version_headers(auth_response) + cookies.set_cookie_header(request) + yield request - def build_refresh_request(self) -> httpx.Request: - """Build the request for refreshing the id token.""" - self.cookies.delete("myqos_id") # We need to delete any existing id token first - return httpx.Request( - method="POST", - url=f"{CONFIG.url}/auth/tokens/refresh", - cookies=self.cookies, - headers={VERSION_HEADER: VERSION}, - ) _nexus_client: httpx.Client | None = None @@ -113,8 +96,6 @@ def get_nexus_client(reload: bool = False) -> httpx.Client: global _nexus_client if _nexus_client is None or reload: _auth_handler = AuthHandler() - _auth_handler.reload_tokens() - _nexus_client = httpx.Client( base_url=CONFIG.url, auth=_auth_handler, diff --git a/qnexus/client/auth.py b/qnexus/client/auth.py index 46adc7f..93bd212 100644 --- a/qnexus/client/auth.py +++ b/qnexus/client/auth.py @@ -104,7 +104,6 @@ def login() -> None: "access_token", resp_json["access_token"], ) - get_nexus_client(reload=True) # spinner.stop() print( f"āœ… Successfully logged in as {resp_json['email']} using the browser." @@ -142,7 +141,6 @@ def logout() -> None: """Clear tokens from file system and the client.""" remove_token("refresh_token") remove_token("access_token") - get_nexus_client(reload=True) print("Successfully logged out.") @@ -187,7 +185,6 @@ def _request_tokens(user: EmailStr, pwd: str) -> None: write_token("refresh_token", myqos_oat) write_token("access_token", myqos_id) - get_nexus_client(reload=True) _check_version_headers(resp) From 6b62e7c939c21ecae41a1866d0a669487bb46cb7 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 12:31:19 +0100 Subject: [PATCH 02/12] Fixes. --- qnexus/client/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/qnexus/client/__init__.py b/qnexus/client/__init__.py index c9e6894..b63b4ec 100644 --- a/qnexus/client/__init__.py +++ b/qnexus/client/__init__.py @@ -37,18 +37,20 @@ def get_cookies_from_disk(): pass return cookies; +def set_cookie_header(cookies: httpx.Cookies, request: httpx.Request): + """by default cookies.set_cookie_header(...) doesn't overwrite cookies if they already exist in the request header""" + if request.headers.get("cookie"): + request.headers.pop('cookie') + cookies.set_cookie_header(request) class AuthHandler(httpx.Auth): """Custom nexus auth handler""" - def auth_flow( self, request: httpx.Request ) -> typing.Generator[httpx.Request, httpx.Response, None]: - cookies = get_cookies_from_disk() - cookies.set_cookie_header(request) - + set_cookie_header(cookies, request) response = yield request _check_sunset_header(request, response) @@ -67,17 +69,16 @@ def auth_flow( ) auth_response.raise_for_status() - cookies.extract_cookies(auth_response) - request.headers.pop("cookie") - cookies.set_cookie_header(request) + auth_response_cookies = httpx.Cookies() + auth_response_cookies.extract_cookies(auth_response) write_token( "access_token", - cookies.get("myqos_oat") or "", + auth_response_cookies.get("myqos_id") or "", ) _check_version_headers(auth_response) - cookies.set_cookie_header(request) + set_cookie_header(auth_response_cookies, request) yield request From 85940d5c2678919e93a4e3f7cec25d0520b59200 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 12:43:55 +0100 Subject: [PATCH 03/12] Satisfy linter. --- qnexus/client/__init__.py | 23 ++++++++++++++--------- qnexus/client/auth.py | 1 - 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/qnexus/client/__init__.py b/qnexus/client/__init__.py index b63b4ec..23b28b1 100644 --- a/qnexus/client/__init__.py +++ b/qnexus/client/__init__.py @@ -25,24 +25,30 @@ VERSION = version("qnexus") -def get_cookies_from_disk(): + +def get_cookies_from_disk() -> httpx.Cookies: cookies = httpx.Cookies() try: - cookies.set("myqos_oat", read_token("refresh_token"), domain=CONFIG.domain) - except: + refresh_token = read_token("refresh_token") + cookies.set("myqos_oat", refresh_token, domain=CONFIG.domain) + except FileNotFoundError: pass try: - cookies.set("myqos_id", read_token("access_token"), domain=CONFIG.domain) - except: + access_token = read_token("refresh_token") + cookies.set("myqos_id", access_token, domain=CONFIG.domain) + except FileNotFoundError: pass - return cookies; -def set_cookie_header(cookies: httpx.Cookies, request: httpx.Request): + return cookies + + +def set_cookie_header(cookies: httpx.Cookies, request: httpx.Request) -> None: """by default cookies.set_cookie_header(...) doesn't overwrite cookies if they already exist in the request header""" if request.headers.get("cookie"): - request.headers.pop('cookie') + request.headers.pop("cookie") cookies.set_cookie_header(request) + class AuthHandler(httpx.Auth): """Custom nexus auth handler""" @@ -83,7 +89,6 @@ def auth_flow( yield request - _nexus_client: httpx.Client | None = None diff --git a/qnexus/client/auth.py b/qnexus/client/auth.py index 93bd212..3583679 100644 --- a/qnexus/client/auth.py +++ b/qnexus/client/auth.py @@ -16,7 +16,6 @@ VERSION, VERSION_HEADER, _check_version_headers, - get_nexus_client, ) from qnexus.client.utils import consolidate_error, remove_token, write_token from qnexus.config import CONFIG From 5b56e2b6be4e2dd9af1da5b7fa387828a5856780 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:29:27 +0100 Subject: [PATCH 04/12] Fix. --- qnexus/client/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qnexus/client/__init__.py b/qnexus/client/__init__.py index 23b28b1..180960d 100644 --- a/qnexus/client/__init__.py +++ b/qnexus/client/__init__.py @@ -34,7 +34,7 @@ def get_cookies_from_disk() -> httpx.Cookies: except FileNotFoundError: pass try: - access_token = read_token("refresh_token") + access_token = read_token("access_token") cookies.set("myqos_id", access_token, domain=CONFIG.domain) except FileNotFoundError: pass From 7bcf14f210089eb460a09ade76940e9ed175efc2 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:30:03 +0100 Subject: [PATCH 05/12] Fix tests. --- tests/test_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 7ea1e74..a854589 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -85,7 +85,6 @@ def test_token_refresh() -> None: # Confirm that the access token was updated assert read_token("access_token") == refreshed_access_token - assert get_nexus_client().auth.cookies.get("myqos_id") == refreshed_access_token # type: ignore # confirm that the request headers were updated first_cookie_header = list_project_route.calls[0].request.headers["cookie"] From 584f4fb162061ed5e1bf6461734de9993883c8f7 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:30:35 +0100 Subject: [PATCH 06/12] Remove reload tests. --- tests/test_auth.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index a854589..63e63d3 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -118,25 +118,7 @@ def test_token_refresh_expired() -> None: assert refresh_token_route.called -def test_nexus_client_reloads_tokens() -> None: - """Test the reload functionality of the nexus client. - - Test that if we write new tokens and reload the client, - that the new tokens are used.""" - oat_one = "dummy_oat_one" - oat_two = "dummy_oat_two" - - write_token("refresh_token", oat_one) - client_one = get_nexus_client(reload=True) - assert client_one.auth.cookies.get("myqos_oat") == oat_one # type: ignore - - write_token("refresh_token", oat_two) - client_two = get_nexus_client() - assert client_two.auth.cookies.get("myqos_oat") == oat_one # type: ignore - - client_two = get_nexus_client(reload=True) - assert client_two.auth.cookies.get("myqos_oat") == oat_two # type: ignore def test_nexus_client_reloads_domain() -> None: From 706562dd43a3946e103752de6c0e4788b212f1ed Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:31:27 +0100 Subject: [PATCH 07/12] Fix linter errors. --- tests/test_auth.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 63e63d3..e8e7b10 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -118,9 +118,6 @@ def test_token_refresh_expired() -> None: assert refresh_token_route.called - - - def test_nexus_client_reloads_domain() -> None: """Test the reload functionality of the nexus client. We should be able to change the domain in the config From 98b142d16f2e17ebb0e1e062e00fee09ae357419 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:33:21 +0100 Subject: [PATCH 08/12] Add ci_checks script. --- scripts/ci_checks.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 scripts/ci_checks.sh diff --git a/scripts/ci_checks.sh b/scripts/ci_checks.sh new file mode 100755 index 0000000..35978a3 --- /dev/null +++ b/scripts/ci_checks.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +uv run mypy qnexus/ tests/ integration/ +uv run ruff format --check +uv run ruff check From 3f6edbb16c9b8a026b0a2b977360984a74a12db2 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:42:08 +0100 Subject: [PATCH 09/12] Remove reload call. --- tests/test_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index e8e7b10..7002dd0 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -37,7 +37,6 @@ def clean_token_state() -> Generator[Any, Any, Any]: # Teardown - clean up after test remove_token("refresh_token") remove_token("access_token") - get_nexus_client(reload=True) CONFIG.token_path = old_token_path From 3d5d8ae07245abdc23d42a9db755ed82c1747714 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 13:45:08 +0100 Subject: [PATCH 10/12] Remove script. --- scripts/ci_checks.sh | 4 ---- 1 file changed, 4 deletions(-) delete mode 100755 scripts/ci_checks.sh diff --git a/scripts/ci_checks.sh b/scripts/ci_checks.sh deleted file mode 100755 index 35978a3..0000000 --- a/scripts/ci_checks.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -uv run mypy qnexus/ tests/ integration/ -uv run ruff format --check -uv run ruff check From 002809a4199ca489b7921fff5d35e6e74790cd58 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 14:24:48 +0100 Subject: [PATCH 11/12] Revert reload changes. --- qnexus/client/auth.py | 4 ++++ tests/test_auth.py | 1 + 2 files changed, 5 insertions(+) diff --git a/qnexus/client/auth.py b/qnexus/client/auth.py index 3583679..46adc7f 100644 --- a/qnexus/client/auth.py +++ b/qnexus/client/auth.py @@ -16,6 +16,7 @@ VERSION, VERSION_HEADER, _check_version_headers, + get_nexus_client, ) from qnexus.client.utils import consolidate_error, remove_token, write_token from qnexus.config import CONFIG @@ -103,6 +104,7 @@ def login() -> None: "access_token", resp_json["access_token"], ) + get_nexus_client(reload=True) # spinner.stop() print( f"āœ… Successfully logged in as {resp_json['email']} using the browser." @@ -140,6 +142,7 @@ def logout() -> None: """Clear tokens from file system and the client.""" remove_token("refresh_token") remove_token("access_token") + get_nexus_client(reload=True) print("Successfully logged out.") @@ -184,6 +187,7 @@ def _request_tokens(user: EmailStr, pwd: str) -> None: write_token("refresh_token", myqos_oat) write_token("access_token", myqos_id) + get_nexus_client(reload=True) _check_version_headers(resp) diff --git a/tests/test_auth.py b/tests/test_auth.py index 7002dd0..e8e7b10 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -37,6 +37,7 @@ def clean_token_state() -> Generator[Any, Any, Any]: # Teardown - clean up after test remove_token("refresh_token") remove_token("access_token") + get_nexus_client(reload=True) CONFIG.token_path = old_token_path From 43aebec44ee473bbd1d677db6a4912fa83d08213 Mon Sep 17 00:00:00 2001 From: aidanCQ Date: Tue, 12 Aug 2025 14:28:34 +0100 Subject: [PATCH 12/12] Fix tests. --- scripts/run_unit_tests.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/run_unit_tests.sh b/scripts/run_unit_tests.sh index d3675ee..1825adb 100755 --- a/scripts/run_unit_tests.sh +++ b/scripts/run_unit_tests.sh @@ -5,7 +5,6 @@ set -e # Order doesn't matter but auth tests manipulate environment variables # and should be run separately uv run pytest --cov-reset tests/test_auth.py::test_token_refresh -uv run pytest tests/test_auth.py::test_nexus_client_reloads_tokens uv run pytest tests/test_auth.py::test_nexus_client_reloads_domain uv run pytest tests/test_auth.py::test_token_refresh_expired @@ -13,4 +12,4 @@ uv run pytest tests/test_auth.py::test_token_refresh_expired echo "Running non-auth tests" uv run pytest tests/ -v --ignore=tests/test_auth.py -echo -e "\nšŸŽ‰ All tests passed successfully!" \ No newline at end of file +echo -e "\nšŸŽ‰ All tests passed successfully!"