diff --git a/README.md b/README.md index fee2dff..0c5acd2 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,24 @@ Or if it is a commandline tool then you might put some example commands here: ``` python -m python_interface_to_workflows --version ``` + +# Using Copier +```bash +mkdir new_directory_path +cd new_directory_path +git init +git remote add origin {origin ssh} +git branch -M main + +cd .. +``` +then either: +```bash +git clone git@github.com:DiamondLightSource/python-interface-to-workflows.git +copier copy {this_repo's_path} {new_directory_path} +``` +or: +```bash +copier copy git@github.com:DiamondLightSource/python-copier-template.git new directory path +``` +rebuild in dev container without cache diff --git a/scripts/lintyaml.sh b/scripts/lintyaml.sh index 3de474e..9ebc381 100644 --- a/scripts/lintyaml.sh +++ b/scripts/lintyaml.sh @@ -1,6 +1,6 @@ #!/bin/bash cd src/python_interface_to_workflows/templates -for file in * +for file in *.yaml; do argo lint "$file" --offline SUCCESSFULLINT=$? diff --git a/scripts/makecopiercorrect.sh b/scripts/makecopiercorrect.sh index 0f2a966..973bd00 100644 --- a/scripts/makecopiercorrect.sh +++ b/scripts/makecopiercorrect.sh @@ -7,16 +7,36 @@ cp ../templates/*example*.txt "../../copier_template/src/{{ project_name }}/temp for file in "../../copier_template/src/{{ project_name }}/workflow_definitions"/* do [[ $file == *.jinja ]] && continue + sed -i 's/python-interface-to-workflows/{{repo_name}}/g' "$file" sed -i 's/DiamondLightSource/{{github_org}}/g' "$file" + + sed -i '1i{% raw %}' "$file" + echo '{% endraw %}' >> "$file" + + sed -i \ + -e 's/{{repo_name}}/{% endraw %}{{repo_name}}{% raw %}/g' \ + -e 's/{{github_org}}/{% endraw %}{{github_org}}{% raw %}/g' \ + "$file" + mv "$file" "$file.jinja" done for file in "../../copier_template/src/{{ project_name }}/templates"/* do [[ $file == *.jinja ]] && continue + sed -i 's/python-interface-to-workflows/{{repo_name}}/g' "$file" sed -i 's/DiamondLightSource/{{github_org}}/g' "$file" + + sed -i '1i{% raw %}' "$file" + echo '{% endraw %}' >> "$file" + + sed -i \ + -e 's/{{repo_name}}/{% endraw %}{{repo_name}}{% raw %}/g' \ + -e 's/{{github_org}}/{% endraw %}{{github_org}}{% raw %}/g' \ + "$file" + mv "$file" "$file.jinja" done git add "../../copier_template/src/{{ project_name }}/workflow_definitions"/* diff --git a/scripts/runthefiles.sh b/scripts/runthefiles.sh index a7453b4..960a7d8 100644 --- a/scripts/runthefiles.sh +++ b/scripts/runthefiles.sh @@ -5,4 +5,5 @@ do uv run "$file" done mv *.yaml ../templates/ +mv *.txt ../templates/ git add -u ../templates/ diff --git a/src/copier_template/pyproject.toml.jinja b/src/copier_template/pyproject.toml.jinja index cad7d38..9eec731 100644 --- a/src/copier_template/pyproject.toml.jinja +++ b/src/copier_template/pyproject.toml.jinja @@ -68,7 +68,6 @@ reportMissingImports = false # Ignore missing stubs in imported modules reportMissingTypeStubs = "none" reportMissingModuleSource="warning" reportInvalidTypeForm="warning" -reportMissingImports="warning" reportUndefinedVariable="warning" reportAbstractUsage="warning" reportArgumentType="warning" diff --git a/src/copier_template/scripts/lintyaml.sh.jinja b/src/copier_template/scripts/lintyaml.sh.jinja index 7b0b6e8..5c984df 100644 --- a/src/copier_template/scripts/lintyaml.sh.jinja +++ b/src/copier_template/scripts/lintyaml.sh.jinja @@ -1,6 +1,6 @@ #!/bin/bash cd src/{{project_name}}/templates -for file in * +for file in *.yaml; do argo lint "$file" --offline SUCCESSFULLINT=$? diff --git a/src/copier_template/scripts/runthefiles.sh.jinja b/src/copier_template/scripts/runthefiles.sh.jinja index ca0bea2..960a7d8 100644 --- a/src/copier_template/scripts/runthefiles.sh.jinja +++ b/src/copier_template/scripts/runthefiles.sh.jinja @@ -1,8 +1,9 @@ #!/bin/bash -cd src/{{project_name}}/workflow_definitions +cd src/python_interface_to_workflows/workflow_definitions for file in * do uv run "$file" done mv *.yaml ../templates/ +mv *.txt ../templates/ git add -u ../templates/ diff --git a/src/copier_template/src/{{ project_name }}/auth/keycloak_checker.py.jinja b/src/copier_template/src/{{ project_name }}/auth/keycloak_checker.py.jinja index 04dd688..2607840 100644 --- a/src/copier_template/src/{{ project_name }}/auth/keycloak_checker.py.jinja +++ b/src/copier_template/src/{{ project_name }}/auth/keycloak_checker.py.jinja @@ -9,12 +9,12 @@ from {{project_name}}.auth.open_auth_url import open_auth_url def set_token_env_variable() -> str: keycloak_openid = KeycloakOpenID( - client_id="workflows-dashboard", - server_url="https://identity.diamond.ac.uk/", - realm_name="dls", - client_secret_key="", - pool_maxsize=1, - ) + client_id="workflows-cli", + server_url="https://identity.diamond.ac.uk/", + realm_name="dls", + client_secret_key="", + pool_maxsize=1, + ) port = 8000 code_verifier = generate_code_verifier() code_challenge, code_challenge_method = generate_code_challenge(code_verifier) @@ -25,14 +25,26 @@ def set_token_env_variable() -> str: code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) - open_auth_url(auth_url) - token: dict[str, str] = ( # pyright: ignore[reportUnknownVariableType] - keycloak_openid.token( # pyright: ignore[reportUnknownMemberType] - grant_type="authorization_code", - code=os.environ["AUTH"], - redirect_uri=f"http://localhost:{port}/", - code_verifier=code_verifier, - ) + match open_auth_url(auth_url, port): + case True: + token: dict[str, str] = ( # pyright: ignore[reportUnknownVariableType] + keycloak_openid.token( # pyright: ignore[reportUnknownMemberType] + grant_type="authorization_code", + code=os.environ.get("AUTH"), # pyright: ignore + redirect_uri=f"http://localhost:{port}/", + code_verifier=code_verifier, + ) + ) + case False: + token: dict[str, str] = keycloak_openid.refresh_token( # pyright: ignore + str(os.environ.get("REFRESHTOKEN")) # pyright: ignore + ) + token_info: dict[str, int | str | dict[str, list[str]]] = ( # pyright: ignore + keycloak_openid.decode_token(token["access_token"]) # pyright: ignore ) + expire_time = int(token_info["exp"]) + 1500 # pyright: ignore + dotenv.set_key("src/.env", "EXPIRY", str(expire_time).strip("'")) dotenv.set_key("src/.env", "TOKEN", token["access_token"].strip("'")) + dotenv.set_key("src/.env", "REFRESHTOKEN", token["refresh_token"].strip("'")) + dotenv.load_dotenv(dotenv_path="src/.env", override=True) return token["access_token"] # pyright: ignore[reportUnknownArgumentType] diff --git a/src/copier_template/src/{{ project_name }}/auth/open_auth_url.py b/src/copier_template/src/{{ project_name }}/auth/open_auth_url.py index 5a18266..9acae95 100644 --- a/src/copier_template/src/{{ project_name }}/auth/open_auth_url.py +++ b/src/copier_template/src/{{ project_name }}/auth/open_auth_url.py @@ -1,10 +1,13 @@ import os import socket +import time import urllib.parse import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer from typing import cast +import dotenv + class _ReusingHTTPServer(HTTPServer): allow_reuse_address = True @@ -26,16 +29,23 @@ def do_GET(self): self.wfile.write(b"Missing authorization code.") -def open_auth_url(auth_url: str) -> None: - httpd = _ReusingHTTPServer(("localhost", 8000), CallbackHandler) - webbrowser.open(auth_url) - try: - httpd.handle_request() - os.environ["AUTH"] = httpd.auth_code - except OSError: - os.environ["AUTH"] = "" - print("ERROR: Port in use. Please restart your terminal.") - exit(1) - finally: - httpd.socket.shutdown(socket.SHUT_RDWR) - httpd.server_close() +def open_auth_url(auth_url: str, port: int) -> bool: + dotenv.load_dotenv(dotenv_path="src/.env", override=True) + expiry_str: str = os.environ.get("EXPIRY").strip("'") # pyright: ignore + if (expiry_str == "" or int(expiry_str)) <= float(time.time()): + httpd = _ReusingHTTPServer(("localhost", port), CallbackHandler) + webbrowser.open(auth_url) + try: + httpd.handle_request() + os.environ["AUTH"] = httpd.auth_code + dotenv.set_key("src/.env", "AUTH", httpd.auth_code) + except OSError: + os.environ["AUTH"] = "" + print("ERROR: Port in use. Please restart your terminal.") + exit(1) + finally: + httpd.socket.shutdown(socket.SHUT_RDWR) + httpd.server_close() + return True + else: + return False diff --git a/src/copier_template/src/{{ project_name }}/templates/example.txt.jinja b/src/copier_template/src/{{ project_name }}/templates/example.txt.jinja index 736759b..3e74853 100644 --- a/src/copier_template/src/{{ project_name }}/templates/example.txt.jinja +++ b/src/copier_template/src/{{ project_name }}/templates/example.txt.jinja @@ -1,3 +1,4 @@ +{% raw %} apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: @@ -7,7 +8,7 @@ metadata: Replicates the functionality of example.yaml workflows.argoproj.io/title: example remade via hera - workflows.diamond.ac.uk/repository: https://github.com/{{github_org}}/{{repo_name}} + workflows.diamond.ac.uk/repository: https://github.com/{% endraw %}{{github_org}}{% raw %}/{% endraw %}{{repo_name}}{% raw %} labels: workflows.diamond.ac.uk/science-group-examples: 'true' spec: @@ -212,3 +213,4 @@ spec: resources: requests: storage: 1Gi +{% endraw %} diff --git a/src/copier_template/src/{{ project_name }}/workflow_definitions/create_example_template.py.jinja b/src/copier_template/src/{{ project_name }}/workflow_definitions/create_example_template.py.jinja index 6a6aa90..65be398 100644 --- a/src/copier_template/src/{{ project_name }}/workflow_definitions/create_example_template.py.jinja +++ b/src/copier_template/src/{{ project_name }}/workflow_definitions/create_example_template.py.jinja @@ -1,3 +1,4 @@ +{% raw %} from hera.workflows import ( DAG, Artifact, @@ -153,7 +154,7 @@ with Workflow( "workflows.argoproj.io/title": "example remade via hera", "workflows.argoproj.io/description": """Replicates the functionality of example.yaml""", - "workflows.diamond.ac.uk/repository": "https://github.com/{{github_org}}/{{repo_name}}", + "workflows.diamond.ac.uk/repository": "https://github.com/{% endraw %}{{github_org}}{% raw %}/{% endraw %}{{repo_name}}{% raw %}", }, volumes=Volume(name="tmpdir", mount_path="/tmp/", size="1Gi"), ) as w: @@ -178,5 +179,6 @@ example.yaml""", [install, params] >> makeimages >> makehdf5 # pyright: ignore -with open("example.yaml", "w") as div: +with open("example.txt", "w") as div: div.write(w.to_yaml()) # pyright: ignore[reportUnknownMemberType] +{% endraw %} diff --git a/src/copier_template/tests/test_keycloak_checker.py.jinja b/src/copier_template/tests/test_keycloak_checker.py.jinja index 2eda176..322b17b 100644 --- a/src/copier_template/tests/test_keycloak_checker.py.jinja +++ b/src/copier_template/tests/test_keycloak_checker.py.jinja @@ -1,35 +1,75 @@ import os -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch + +from pytest import mark from {{project_name}}.auth.keycloak_checker import set_token_env_variable +@mark.parametrize( + "staging,port,return_present", + [ + (True, 5173, True), + (False, 8000, False), + (True, 5173, False), + (False, 8000, True), + ], +) +@patch("{{project_name}}.auth.keycloak_checker.dotenv.load_dotenv") +@patch("{{project_name}}.auth.keycloak_checker.dotenv.set_key") @patch("{{project_name}}.auth.keycloak_checker.KeycloakOpenID") @patch("{{project_name}}.auth.keycloak_checker.generate_code_verifier") @patch("{{project_name}}.auth.keycloak_checker.generate_code_challenge") @patch("{{project_name}}.auth.keycloak_checker.open_auth_url") -def test_return_key( +def test_set_token_env_variable( mock_open_auth_url: MagicMock, mock_gen_code_challenge: MagicMock, mock_gen_code_verifier: MagicMock, mock_gen_keycloak_id: MagicMock, + mock_set_key: MagicMock, + mock_load_env: MagicMock, + staging: bool, + port: int, + return_present: bool, ): mock_gen_code_verifier.return_value = "verifier" mock_gen_code_challenge.return_value = ("challenge", "S256") os.environ["AUTH"] = "auth_url_code" + os.environ["REFRESHTOKEN"] = "refresh" + keycloak = MagicMock() mock_gen_keycloak_id.return_value = keycloak - + mock_open_auth_url.return_value = return_present keycloak.auth_url.return_value = "https://mock.site" - keycloak.token.return_value = { + token = { "access_token": "fake_token", "refresh_token": "fake_refresh", } - assert set_token_env_variable() == "fake_token" - mock_open_auth_url.assert_called_once_with("https://mock.site", 8000) - keycloak.token.assert_called_once_with( - grant_type="authorization_code", - code="auth_url_code", - redirect_uri="http://localhost:8000/", - code_verifier="verifier", + keycloak.token.return_value = token + keycloak.refresh_token.return_value = token + keycloak.decode_token.return_value = {"exp": 123456789} + assert set_token_env_variable(staging) == "fake_token" + + mock_open_auth_url.assert_called_once_with("https://mock.site", port) + if return_present: + keycloak.token.assert_called_once_with( + grant_type="authorization_code", + code="auth_url_code", + redirect_uri=f"http://localhost:{port}/", + code_verifier="verifier", + ) + keycloak.refresh_token.assert_not_called() + else: + keycloak.refresh_token.assert_called_once_with("refresh") + keycloak.token.assert_not_called() + mock_set_key.assert_has_calls( + [ + call("src/.env", "EXPIRY", str(123456789 + 1500)), + call("src/.env", "TOKEN", "fake_token"), + call("src/.env", "REFRESHTOKEN", "fake_refresh"), + ] + ) + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, ) diff --git a/src/copier_template/tests/test_open_auth_url.py.jinja b/src/copier_template/tests/test_open_auth_url.py.jinja index f6d3393..bc3799a 100644 --- a/src/copier_template/tests/test_open_auth_url.py.jinja +++ b/src/copier_template/tests/test_open_auth_url.py.jinja @@ -7,22 +7,45 @@ from {{project_name}}.auth.open_auth_url import ( ) +@patch("{{project_name}}.auth.open_auth_url.dotenv.set_key") +@patch("{{project_name}}.auth.open_auth_url.dotenv.load_dotenv") +@patch("{{project_name}}.auth.open_auth_url.time.time") @patch("{{project_name}}.auth.open_auth_url.webbrowser.open") @patch("{{project_name}}.auth.open_auth_url._ReusingHTTPServer") def test_open_auth_url_normal_function( mock_http_server: MagicMock, mock_open_browser: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_set_key: MagicMock, ): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "" + server = mock_http_server.return_value server.auth_code = "this_is_your_code" - open_auth_url("url") + + assert open_auth_url("url", 5173) is True + + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, + ) server.handle_request.assert_called_once() mock_open_browser.assert_called_once_with("url") + mock_set_key.assert_called_once_with( + "src/.env", + "AUTH", + "this_is_your_code", + ) server.socket.shutdown.assert_called_once() server.server_close.assert_called_once() assert os.environ["AUTH"] == "this_is_your_code" +@patch("{{project_name}}.auth.open_auth_url.dotenv.set_key") +@patch("{{project_name}}.auth.open_auth_url.dotenv.load_dotenv") +@patch("{{project_name}}.auth.open_auth_url.time.time") @patch("{{project_name}}.auth.open_auth_url.exit") @patch("{{project_name}}.auth.open_auth_url.webbrowser.open") @patch("{{project_name}}.auth.open_auth_url._ReusingHTTPServer") @@ -30,18 +53,48 @@ def test_open_auth_url_raises_error( mock_http_server: MagicMock, mock_open_browser: MagicMock, mock_exit: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_set_key: MagicMock, ): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "" + server = mock_http_server.return_value - server.auth_code = "this_is_your_code" server.handle_request.side_effect = OSError - open_auth_url("url") + + assert open_auth_url("url", 5173) is True mock_open_browser.assert_called_once_with("url") + mock_load_env.assert_called_once() assert os.environ["AUTH"] == "" mock_exit.assert_called_once_with(1) + mock_set_key.assert_not_called() server.socket.shutdown.assert_called_once() server.server_close.assert_called_once() +@patch("{{project_name}}.auth.open_auth_url.webbrowser.open") +@patch("{{project_name}}.auth.open_auth_url.dotenv.load_dotenv") +@patch("{{project_name}}.auth.open_auth_url.time.time") +@patch("{{project_name}}.auth.open_auth_url._ReusingHTTPServer") +def test_open_auth_url_refresh_token( + mock_http_server: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_open_browser: MagicMock, +): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "200" + assert open_auth_url("url", 5173) is False + + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, + ) + mock_http_server.assert_not_called() + mock_open_browser.assert_not_called() + + def test_handler_normal_function(): handler = CallbackHandler.__new__(CallbackHandler) handler.path = "/?code=this_is_your_code" diff --git a/src/python_interface_to_workflows/auth/keycloak_checker.py b/src/python_interface_to_workflows/auth/keycloak_checker.py index 71390fb..44e486e 100644 --- a/src/python_interface_to_workflows/auth/keycloak_checker.py +++ b/src/python_interface_to_workflows/auth/keycloak_checker.py @@ -36,14 +36,26 @@ def set_token_env_variable(staging: bool) -> str: code_challenge=code_challenge, code_challenge_method=code_challenge_method, ) - open_auth_url(auth_url, port) - token: dict[str, str] = ( # pyright: ignore[reportUnknownVariableType] - keycloak_openid.token( # pyright: ignore[reportUnknownMemberType] - grant_type="authorization_code", - code=os.environ["AUTH"], - redirect_uri=f"http://localhost:{port}/", - code_verifier=code_verifier, - ) + match open_auth_url(auth_url, port): + case True: + token: dict[str, str] = ( # pyright: ignore[reportUnknownVariableType] + keycloak_openid.token( # pyright: ignore[reportUnknownMemberType] + grant_type="authorization_code", + code=os.environ.get("AUTH"), # pyright: ignore + redirect_uri=f"http://localhost:{port}/", + code_verifier=code_verifier, + ) + ) + case False: + token: dict[str, str] = keycloak_openid.refresh_token( # pyright: ignore + str(os.environ.get("REFRESHTOKEN")) # pyright: ignore + ) + token_info: dict[str, int | str | dict[str, list[str]]] = ( # pyright: ignore + keycloak_openid.decode_token(token["access_token"]) # pyright: ignore ) + expire_time = int(token_info["exp"]) + 1500 # pyright: ignore + dotenv.set_key("src/.env", "EXPIRY", str(expire_time).strip("'")) dotenv.set_key("src/.env", "TOKEN", token["access_token"].strip("'")) + dotenv.set_key("src/.env", "REFRESHTOKEN", token["refresh_token"].strip("'")) + dotenv.load_dotenv(dotenv_path="src/.env", override=True) return token["access_token"] # pyright: ignore[reportUnknownArgumentType] diff --git a/src/python_interface_to_workflows/auth/open_auth_url.py b/src/python_interface_to_workflows/auth/open_auth_url.py index 2f0dcf9..9acae95 100644 --- a/src/python_interface_to_workflows/auth/open_auth_url.py +++ b/src/python_interface_to_workflows/auth/open_auth_url.py @@ -1,5 +1,6 @@ import os import socket +import time import urllib.parse import webbrowser from http.server import BaseHTTPRequestHandler, HTTPServer @@ -28,17 +29,23 @@ def do_GET(self): self.wfile.write(b"Missing authorization code.") -def open_auth_url(auth_url: str, port: int) -> None: - httpd = _ReusingHTTPServer(("localhost", port), CallbackHandler) - webbrowser.open(auth_url) - try: - httpd.handle_request() - os.environ["AUTH"] = httpd.auth_code - dotenv.set_key("src/.env", "AUTH", httpd.auth_code) - except OSError: - os.environ["AUTH"] = "" - print("ERROR: Port in use. Please restart your terminal.") - exit(1) - finally: - httpd.socket.shutdown(socket.SHUT_RDWR) - httpd.server_close() +def open_auth_url(auth_url: str, port: int) -> bool: + dotenv.load_dotenv(dotenv_path="src/.env", override=True) + expiry_str: str = os.environ.get("EXPIRY").strip("'") # pyright: ignore + if (expiry_str == "" or int(expiry_str)) <= float(time.time()): + httpd = _ReusingHTTPServer(("localhost", port), CallbackHandler) + webbrowser.open(auth_url) + try: + httpd.handle_request() + os.environ["AUTH"] = httpd.auth_code + dotenv.set_key("src/.env", "AUTH", httpd.auth_code) + except OSError: + os.environ["AUTH"] = "" + print("ERROR: Port in use. Please restart your terminal.") + exit(1) + finally: + httpd.socket.shutdown(socket.SHUT_RDWR) + httpd.server_close() + return True + else: + return False diff --git a/src/python_interface_to_workflows/templates/example.yaml b/src/python_interface_to_workflows/templates/example.txt similarity index 100% rename from src/python_interface_to_workflows/templates/example.yaml rename to src/python_interface_to_workflows/templates/example.txt diff --git a/src/python_interface_to_workflows/workflow_definitions/create_example_template.py b/src/python_interface_to_workflows/workflow_definitions/create_example_template.py index e718511..c8f4ce7 100644 --- a/src/python_interface_to_workflows/workflow_definitions/create_example_template.py +++ b/src/python_interface_to_workflows/workflow_definitions/create_example_template.py @@ -178,5 +178,5 @@ def to_hdf5(paths: str): [install, params] >> makeimages >> makehdf5 # pyright: ignore -with open("example.yaml", "w") as div: +with open("example.txt", "w") as div: div.write(w.to_yaml()) # pyright: ignore[reportUnknownMemberType] diff --git a/tests/test_keycloak_checker.py b/tests/test_keycloak_checker.py index bb5c776..6f6bc21 100644 --- a/tests/test_keycloak_checker.py +++ b/tests/test_keycloak_checker.py @@ -1,12 +1,22 @@ import os -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from pytest import mark from python_interface_to_workflows.auth.keycloak_checker import set_token_env_variable -@mark.parametrize("dev,port", [(True, 5173), (False, 8000)]) +@mark.parametrize( + "staging,port,return_present", + [ + (True, 5173, True), + (False, 8000, False), + (True, 5173, False), + (False, 8000, True), + ], +) +@patch("python_interface_to_workflows.auth.keycloak_checker.dotenv.load_dotenv") +@patch("python_interface_to_workflows.auth.keycloak_checker.dotenv.set_key") @patch("python_interface_to_workflows.auth.keycloak_checker.KeycloakOpenID") @patch("python_interface_to_workflows.auth.keycloak_checker.generate_code_verifier") @patch("python_interface_to_workflows.auth.keycloak_checker.generate_code_challenge") @@ -16,25 +26,50 @@ def test_set_token_env_variable( mock_gen_code_challenge: MagicMock, mock_gen_code_verifier: MagicMock, mock_gen_keycloak_id: MagicMock, - dev: bool, + mock_set_key: MagicMock, + mock_load_env: MagicMock, + staging: bool, port: int, + return_present: bool, ): mock_gen_code_verifier.return_value = "verifier" mock_gen_code_challenge.return_value = ("challenge", "S256") os.environ["AUTH"] = "auth_url_code" + os.environ["REFRESHTOKEN"] = "refresh" + keycloak = MagicMock() mock_gen_keycloak_id.return_value = keycloak - + mock_open_auth_url.return_value = return_present keycloak.auth_url.return_value = "https://mock.site" - keycloak.token.return_value = { + token = { "access_token": "fake_token", "refresh_token": "fake_refresh", } - assert set_token_env_variable(dev) == "fake_token" + keycloak.token.return_value = token + keycloak.refresh_token.return_value = token + keycloak.decode_token.return_value = {"exp": 123456789} + assert set_token_env_variable(staging) == "fake_token" + mock_open_auth_url.assert_called_once_with("https://mock.site", port) - keycloak.token.assert_called_once_with( - grant_type="authorization_code", - code="auth_url_code", - redirect_uri=f"http://localhost:{port}/", - code_verifier="verifier", + if return_present: + keycloak.token.assert_called_once_with( + grant_type="authorization_code", + code="auth_url_code", + redirect_uri=f"http://localhost:{port}/", + code_verifier="verifier", + ) + keycloak.refresh_token.assert_not_called() + else: + keycloak.refresh_token.assert_called_once_with("refresh") + keycloak.token.assert_not_called() + mock_set_key.assert_has_calls( + [ + call("src/.env", "EXPIRY", str(123456789 + 1500)), + call("src/.env", "TOKEN", "fake_token"), + call("src/.env", "REFRESHTOKEN", "fake_refresh"), + ] + ) + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, ) diff --git a/tests/test_open_auth_url.py b/tests/test_open_auth_url.py index 59fa0d3..e05fb49 100644 --- a/tests/test_open_auth_url.py +++ b/tests/test_open_auth_url.py @@ -7,22 +7,45 @@ ) +@patch("python_interface_to_workflows.auth.open_auth_url.dotenv.set_key") +@patch("python_interface_to_workflows.auth.open_auth_url.dotenv.load_dotenv") +@patch("python_interface_to_workflows.auth.open_auth_url.time.time") @patch("python_interface_to_workflows.auth.open_auth_url.webbrowser.open") @patch("python_interface_to_workflows.auth.open_auth_url._ReusingHTTPServer") def test_open_auth_url_normal_function( mock_http_server: MagicMock, mock_open_browser: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_set_key: MagicMock, ): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "" + server = mock_http_server.return_value server.auth_code = "this_is_your_code" - open_auth_url("url", 5173) + + assert open_auth_url("url", 5173) is True + + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, + ) server.handle_request.assert_called_once() mock_open_browser.assert_called_once_with("url") + mock_set_key.assert_called_once_with( + "src/.env", + "AUTH", + "this_is_your_code", + ) server.socket.shutdown.assert_called_once() server.server_close.assert_called_once() assert os.environ["AUTH"] == "this_is_your_code" +@patch("python_interface_to_workflows.auth.open_auth_url.dotenv.set_key") +@patch("python_interface_to_workflows.auth.open_auth_url.dotenv.load_dotenv") +@patch("python_interface_to_workflows.auth.open_auth_url.time.time") @patch("python_interface_to_workflows.auth.open_auth_url.exit") @patch("python_interface_to_workflows.auth.open_auth_url.webbrowser.open") @patch("python_interface_to_workflows.auth.open_auth_url._ReusingHTTPServer") @@ -30,18 +53,48 @@ def test_open_auth_url_raises_error( mock_http_server: MagicMock, mock_open_browser: MagicMock, mock_exit: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_set_key: MagicMock, ): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "" + server = mock_http_server.return_value - server.auth_code = "this_is_your_code" server.handle_request.side_effect = OSError - open_auth_url("url", 5173) + + assert open_auth_url("url", 5173) is True mock_open_browser.assert_called_once_with("url") + mock_load_env.assert_called_once() assert os.environ["AUTH"] == "" mock_exit.assert_called_once_with(1) + mock_set_key.assert_not_called() server.socket.shutdown.assert_called_once() server.server_close.assert_called_once() +@patch("python_interface_to_workflows.auth.open_auth_url.webbrowser.open") +@patch("python_interface_to_workflows.auth.open_auth_url.dotenv.load_dotenv") +@patch("python_interface_to_workflows.auth.open_auth_url.time.time") +@patch("python_interface_to_workflows.auth.open_auth_url._ReusingHTTPServer") +def test_open_auth_url_refresh_token( + mock_http_server: MagicMock, + mock_time: MagicMock, + mock_load_env: MagicMock, + mock_open_browser: MagicMock, +): + mock_time.return_value = 100 + os.environ["EXPIRY"] = "200" + assert open_auth_url("url", 5173) is False + + mock_load_env.assert_called_once_with( + dotenv_path="src/.env", + override=True, + ) + mock_http_server.assert_not_called() + mock_open_browser.assert_not_called() + + def test_handler_normal_function(): handler = CallbackHandler.__new__(CallbackHandler) handler.path = "/?code=this_is_your_code"