From 405066db83747c7af91444c2b9be007aa4d55553 Mon Sep 17 00:00:00 2001 From: Philip Couling Date: Tue, 16 Jan 2024 16:23:35 +0000 Subject: [PATCH] Actions fully unit tested --- pyproject.toml | 1 + source/mkdocs_deploy/abstract.py | 29 ++- source/mkdocs_deploy/actions.py | 68 ++++--- source/mkdocs_deploy/plugins/aws_s3.py | 8 +- source/mkdocs_deploy/plugins/html_redirect.py | 74 +++++++ .../mkdocs_deploy/plugins/local_filesystem.py | 6 +- .../mkdocs_deploy/shared_implementations.py | 76 +------ source/tests/conftest.py | 1 + source/tests/mock_plugin.py | 32 +-- source/tests/mock_wrapper.py | 3 +- source/tests/test_modules/test_actions.py | 186 ------------------ .../test_modules/test_actions/__init__.py | 0 .../test_modules/test_actions/conftest.py | 27 +++ .../test_actions/test_create_alias.py | 83 ++++++++ .../test_actions/test_delete_alias.py | 93 +++++++++ .../test_actions/test_delete_version.py | 36 ++++ .../test_actions/test_load_plugins.py | 68 +++++++ .../test_actions/test_refresh_alias.py | 51 +++++ .../test_modules/test_actions/test_upload.py | 77 ++++++++ 19 files changed, 605 insertions(+), 314 deletions(-) create mode 100644 source/mkdocs_deploy/plugins/html_redirect.py delete mode 100644 source/tests/test_modules/test_actions.py create mode 100644 source/tests/test_modules/test_actions/__init__.py create mode 100644 source/tests/test_modules/test_actions/conftest.py create mode 100644 source/tests/test_modules/test_actions/test_create_alias.py create mode 100644 source/tests/test_modules/test_actions/test_delete_alias.py create mode 100644 source/tests/test_modules/test_actions/test_delete_version.py create mode 100644 source/tests/test_modules/test_actions/test_load_plugins.py create mode 100644 source/tests/test_modules/test_actions/test_refresh_alias.py create mode 100644 source/tests/test_modules/test_actions/test_upload.py diff --git a/pyproject.toml b/pyproject.toml index 7e7eef8..884dd23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ mypy = "^1.8.0" [tool.poetry.plugins."mkdocs_deploy.plugins"] "local" = "mkdocs_deploy.plugins.local_filesystem:enable_plugin" "s3" = "mkdocs_deploy.plugins.aws_s3:enable_plugin" +"html-redirect" = "mkdocs_deploy.plugins.html_redirect:enable_plugin" [tool.poetry.group.dev.dependencies] build = "^0.10.0" diff --git a/source/mkdocs_deploy/abstract.py b/source/mkdocs_deploy/abstract.py index 39962a8..cf150c6 100644 --- a/source/mkdocs_deploy/abstract.py +++ b/source/mkdocs_deploy/abstract.py @@ -17,6 +17,9 @@ class RedirectMechanismNotFound(Exception): class _DefaultVersionType(Enum): DEFAULT_VERSION = 0 + def __repr__(self) -> str: + return self.name + DEFAULT_VERSION = _DefaultVersionType.DEFAULT_VERSION @@ -268,4 +271,28 @@ def target_for_url(target_url: str) -> Target: :return: """ handler = _TARGETS[urllib.parse.urlparse(target_url).scheme] - return handler(target_url) \ No newline at end of file + return handler(target_url) + + +_SHARED_REDIRECT_MECHANISMS: dict[str, RedirectMechanism] = {} + + +def get_redirect_mechanisms(session: TargetSession) -> dict[str, RedirectMechanism]: + """Get all available redirect mechanisms for a target session + + Unlike the property returned by the target session itself, this will also include shared redirect mechanisms. + """ + result = _SHARED_REDIRECT_MECHANISMS.copy() + result.update(session.available_redirect_mechanisms) + return result + + +def register_shared_redirect_mechanism(mechanism_key: str, mechanism: RedirectMechanism) -> None: + """Register a redirect mechanism which can work with any target type from any plugin. + + DO NOT use this to simply add a mechanism to your own plugin. The target session should return mechanisms that + only work on that target. + + There are believed to be very few of these: html is the only inbuilt mechanism. This can work with any target + because it only generates html files and all target types support uploading html files.""" + _SHARED_REDIRECT_MECHANISMS[mechanism_key] = mechanism diff --git a/source/mkdocs_deploy/actions.py b/source/mkdocs_deploy/actions.py index c65378d..7f3a348 100644 --- a/source/mkdocs_deploy/actions.py +++ b/source/mkdocs_deploy/actions.py @@ -8,9 +8,9 @@ """ import importlib.metadata import logging -from typing import Iterable, Optional +from typing import Collection -from .abstract import DEFAULT_VERSION, Source, TargetSession, Version, VersionNotFound +from .abstract import DEFAULT_VERSION, Source, TargetSession, Version, VersionNotFound, get_redirect_mechanisms from .versions import DeploymentAlias _logger = logging.getLogger(__name__) @@ -71,6 +71,8 @@ def delete_version(target: TargetSession, version_id: str) -> None: :param version_id: """ deployment_spec = target.deployment_spec + if deployment_spec.default_version is not None and deployment_spec.default_version.version_id == version_id: + delete_alias(target, DEFAULT_VERSION) for alias_id in deployment_spec.aliases_for_version(version_id): delete_alias(target, alias_id) _logger.info("Deleting version %s", version_id) @@ -82,7 +84,7 @@ def delete_version(target: TargetSession, version_id: str) -> None: def create_alias( - target: TargetSession, alias_id: Version, version: str, mechanisms: Iterable[str] | None = None + target: TargetSession, alias_id: Version, version: str, mechanisms: Collection[str] | None = None ) -> None: """ Create a new alias for a version. @@ -93,14 +95,14 @@ def create_alias( :param target: The target session to create the alias on :param alias_id: The new alias id :param version: The version_id to point to - :param mechanisms: The named mechanisms to use. If None then 'html' target will choose the mechanism. + :param mechanisms: The named mechanisms to use. If None then all available mechanisms will be used. """ # Check if the given mechanisms can be implemented by this target - available_redirect_mechanisms = target.available_redirect_mechanisms + available_redirect_mechanisms = get_redirect_mechanisms(target) if mechanisms is not None: for mechanism in mechanisms: if mechanism not in available_redirect_mechanisms: - raise ValueError(f"LocalFileTreeTarget does not support redirect mechanism: {mechanism}") + raise ValueError(f"{type(TargetSession).__name__} does not support redirect mechanism: {mechanism}") # Check if the alias already exists ... # If mechanisms wasn't specified use whatever is on the existing one. @@ -108,51 +110,53 @@ def create_alias( if alias_id in deployment_spec.versions: raise ValueError(f"Cannot create an alias with the same name as an existing version. " f"Delete the version first! Alias name: {alias_id}") - if alias_id is ... and deployment_spec.default_version is not None: + if alias_id is DEFAULT_VERSION and deployment_spec.default_version is not None: # This is the "default" alias alias = deployment_spec.default_version - if mechanisms is None: - mechanisms = alias.redirect_mechanisms - if alias_id in deployment_spec.aliases: + elif alias_id in deployment_spec.aliases: alias = deployment_spec.aliases[alias_id] - if mechanisms is None: - mechanisms = alias.redirect_mechanisms else: # No existing alias was found. Make a new one. alias = DeploymentAlias(version_id=version, redirect_mechanisms=set()) target.set_alias(alias_id, alias) - # Must set the alias first or creating the mechanism will fail. - if mechanisms is None: - mechanisms = ["html"] + + if mechanisms is None: + mechanisms = available_redirect_mechanisms.keys() _logger.info("Creating %s alias redirect %s to %s", ", ".join(mechanisms), alias_id, version) # Remove any redirect mechanisms to a different version that we are not going to replace - if alias.version_id != version: - for mechanism in alias.redirect_mechanisms.copy(): - if mechanism not in mechanisms: - try: - available_redirect_mechanisms[mechanism].delete_redirect(target, alias_id) - except KeyError: - raise ValueError(f"LocalFileTreeTarget does not support redirect mechanism: {mechanism}. " - f"Unable to remove redirect for {alias_id}-->{alias.version_id}") - alias.redirect_mechanisms.discard(mechanism) - alias.version_id = version + for mechanism in alias.redirect_mechanisms.copy(): + if mechanism not in mechanisms: + try: + _logger.warning( + "Implicitly deleting redirect %s mechanism %s to %s", alias_id, mechanism, alias.version_id + ) + available_redirect_mechanisms[mechanism].delete_redirect(target, alias_id) + except KeyError: + raise ValueError( + f"{type(TargetSession).__name__} does not support redirect mechanism: {mechanism}. " + f"Unable to remove redirect for {alias_id}-->{alias.version_id}" + ) + alias.redirect_mechanisms.discard(mechanism) # Create the redirects or refresh them to their new location. for mechanism in mechanisms: if mechanism in alias.redirect_mechanisms: if alias.version_id != version: + _logger.debug("Modifying %s mechanism %s from %s to %s", alias_id, mechanism, alias.version_id, version) available_redirect_mechanisms[mechanism].refresh_redirect(target, alias_id, version) else: _logger.debug("mechanism %s already in place, skipping", mechanism) else: + _logger.debug("Creating %s mechanism %s to %s", alias_id, mechanism, version) available_redirect_mechanisms[mechanism].create_redirect(target, alias_id, version) alias.redirect_mechanisms.add(mechanism) + alias.version_id = version target.set_alias(alias_id, alias) -def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[str] | None = None) -> None: +def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Collection[str] | None = None) -> None: """ Delete an alias. @@ -167,20 +171,20 @@ def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[ if alias_id is DEFAULT_VERSION: alias = target.deployment_spec.default_version if alias is None: - _logger.debug("Default alias not set") + _logger.warning("Cannot delete default alias as it is not set") return else: try: alias = target.deployment_spec.aliases[alias_id] except KeyError: - _logger.debug("Alias %s not set, skipping", alias_id) + _logger.warning("Cannot delete alias %s not set, it has not been set", alias_id) return if mechanisms is not None: - to_delete = [mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms] + to_delete: list | set = [mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms] else: to_delete = alias.redirect_mechanisms.copy() - available_mechanisms = target.available_redirect_mechanisms + available_mechanisms = get_redirect_mechanisms(target) for mechanism in to_delete: try: available_mechanisms[mechanism].delete_redirect( @@ -196,7 +200,7 @@ def delete_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[ target.set_alias(alias_id, None) -def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable[str] | None = None) -> None: +def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Collection[str] | None = None) -> None: """ Refresh redirects. @@ -218,7 +222,7 @@ def refresh_alias(target: TargetSession, alias_id: Version, mechanisms: Iterable to_refresh = {mechanism for mechanism in mechanisms if mechanism in alias.redirect_mechanisms} else: to_refresh = alias.redirect_mechanisms - available_mechanisms = target.available_redirect_mechanisms + available_mechanisms = get_redirect_mechanisms(target) for mechanism in to_refresh: available_mechanisms[mechanism].refresh_redirect( session=target, diff --git a/source/mkdocs_deploy/plugins/aws_s3.py b/source/mkdocs_deploy/plugins/aws_s3.py index cb80de2..7c6fd12 100644 --- a/source/mkdocs_deploy/plugins/aws_s3.py +++ b/source/mkdocs_deploy/plugins/aws_s3.py @@ -18,10 +18,6 @@ logging.getLogger("s3transfer").setLevel("INFO") -AWS_S3_REDIRECT_MECHANISMS = shared_implementations.SHARED_REDIRECT_MECHANISMS.copy() -# TODO add S3 redirect - - def enable_plugin() -> None: """ Enables the plugin. @@ -179,7 +175,9 @@ def set_alias(self, alias_id: Union[str, type(...)], alias: Optional[versions.De @property def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]: - return AWS_S3_REDIRECT_MECHANISMS.copy() + # TODO add S3 redirect + # TODO cloud front redirect rules + return {} @property def deployment_spec(self) -> versions.DeploymentSpec: diff --git a/source/mkdocs_deploy/plugins/html_redirect.py b/source/mkdocs_deploy/plugins/html_redirect.py new file mode 100644 index 0000000..bb33c93 --- /dev/null +++ b/source/mkdocs_deploy/plugins/html_redirect.py @@ -0,0 +1,74 @@ +from io import BytesIO + +from ..abstract import DEFAULT_VERSION, RedirectMechanism, TargetSession, Version, register_shared_redirect_mechanism + + +def enable_plugin() -> None: + """Enables the plugin. + + Registers HTML redirect mechanism""" + register_shared_redirect_mechanism("html", HtmlRedirect()) + + +class HtmlRedirect(RedirectMechanism): + + def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: + if alias is DEFAULT_VERSION: + session.upload_file( + version_id=DEFAULT_VERSION, + filename="index.html", + file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=version_id+"/").encode("utf-8")) + ) + else: + files_created = set() + for filename in session.iter_files(version_id): + if filename.endswith(".html") or filename.endswith(".htm"): + if filename == "404.html" or filename.endswith("/404.htm"): + session.upload_file( + version_id=alias, + filename=filename, + file_obj=session.download_file(version_id=version_id, filename=filename) + ) + else: + parts = filename.split("/") + depth = len(parts) + url = ("../" * depth + version_id + "/" + "/".join(parts[:-1])) + session.upload_file( + version_id=alias, # Yes that's correct! + filename=filename, + file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=url).encode("utf-8")) + ) + files_created.add(filename) + for filename in session.iter_files(alias): + if filename not in files_created and (filename.endswith("html") or filename.endswith("htm")): + session.delete_file(alias, filename) + + def refresh_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: + # create_redirect already cleans up so no need to explicitly delete the old one + self.create_redirect(session, alias, version_id) + + def delete_redirect(self, session: TargetSession, alias: Version) -> None: + if alias is ...: + session.delete_file(version_id=..., filename="index.html") + else: + for filename in session.iter_files(alias): + if filename.endswith("html") or filename.endswith("htm"): + session.delete_file(version_id=alias, filename=filename) + +_HTML_REDIRECT_PATTERN=""" + + + + Redirecting + + + + + Redirecting to {url}... + + +""" diff --git a/source/mkdocs_deploy/plugins/local_filesystem.py b/source/mkdocs_deploy/plugins/local_filesystem.py index 3c972c3..cc803e6 100644 --- a/source/mkdocs_deploy/plugins/local_filesystem.py +++ b/source/mkdocs_deploy/plugins/local_filesystem.py @@ -10,8 +10,6 @@ from .. import abstract, shared_implementations from ..versions import DeploymentAlias, DeploymentSpec, DeploymentVersion -LOCAL_FILE_REDIRECT_MECHANISMS = shared_implementations.SHARED_REDIRECT_MECHANISMS.copy() - _logger = logging.getLogger(__name__) @@ -66,6 +64,7 @@ def open_file_for_read(self, filename: str) -> IO[bytes]: result = self._tar_file.extractfile(self._prefix + filename) if result is None: raise RuntimeError(f"Requested file is not a regular file: {filename} in {self._file_path}") + return result def close(self): self._tar_file.close() @@ -233,8 +232,7 @@ def set_alias(self, alias_id: Union[str, type(...)], alias: Optional[DeploymentA @property def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]: - return LOCAL_FILE_REDIRECT_MECHANISMS.copy() - + return {} @property def deployment_spec(self) -> DeploymentSpec: diff --git a/source/mkdocs_deploy/shared_implementations.py b/source/mkdocs_deploy/shared_implementations.py index a027146..b7d71a3 100644 --- a/source/mkdocs_deploy/shared_implementations.py +++ b/source/mkdocs_deploy/shared_implementations.py @@ -1,11 +1,9 @@ import contextlib import logging import os -from io import BytesIO from tempfile import SpooledTemporaryFile -from typing import IO, Union +from typing import IO -from .abstract import RedirectMechanism, TargetSession from .versions import DEPLOYMENTS_FILENAME, DeploymentSpec, MIKE_VERSIONS_FILENAME _logger = logging.getLogger(__name__) @@ -26,76 +24,6 @@ def generate_meta_data(deployment_spec: DeploymentSpec) -> dict[str, bytes]: } -class HtmlRedirect(RedirectMechanism): - - def create_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None: - if alias is ...: - session.upload_file( - version_id=..., - filename="index.html", - file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=version_id+"/").encode("utf-8")) - ) - else: - files_created = set() - for filename in session.iter_files(version_id): - if filename.endswith(".html") or filename.endswith(".htm"): - if filename == "404.html" or filename.endswith("/404.htm"): - session.upload_file( - version_id=alias, - filename=filename, - file_obj=session.download_file(version_id=version_id, filename=filename) - ) - else: - parts = filename.split("/") - depth = len(parts) - url = ("../" * depth + version_id + "/" + "/".join(parts[:-1])) - session.upload_file( - version_id=alias, # Yes that's correct! - filename=filename, - file_obj=BytesIO(_HTML_REDIRECT_PATTERN.format(url=url).encode("utf-8")) - ) - files_created.add(filename) - for filename in session.iter_files(alias): - if filename not in files_created and (filename.endswith("html") or filename.endswith("htm")): - session.delete_file(alias, filename) - - def refresh_redirect(self, session: TargetSession, alias: Union[str, type(...)], version_id: str) -> None: - # create_redirect already cleans up so no need to explicitly delete the old one - self.create_redirect(session, alias, version_id) - - def delete_redirect(self, session: TargetSession, alias: Union[str, type(...)]) -> None: - if alias is ...: - session.delete_file(version_id=..., filename="index.html") - else: - for filename in session.iter_files(alias): - if filename.endswith("html") or filename.endswith("htm"): - session.delete_file(version_id=alias, filename=filename) - - -_HTML_REDIRECT_PATTERN=""" - - - - Redirecting - - - - - Redirecting to {url}... - - -""" - - -SHARED_REDIRECT_MECHANISMS = { - 'html': HtmlRedirect() -} - - class SeekableFileWrapper(contextlib.closing): """ Acts as a wrapper on IO[bytes] which should always be seekable. @@ -141,4 +69,4 @@ def __getattr__(self, item: str): return self.__wrapped_file.__getattribute__(item) def close(self) -> None: - self.__exit_stack.close() \ No newline at end of file + self.__exit_stack.close() diff --git a/source/tests/conftest.py b/source/tests/conftest.py index 7e9eaf4..78bfdd0 100644 --- a/source/tests/conftest.py +++ b/source/tests/conftest.py @@ -8,3 +8,4 @@ def _clean_plugins(monkeypatch: pytest.MonkeyPatch): """Ensure that all tests run with uninitialized plugins""" monkeypatch.setattr(abstract, "_SOURCES", {}) monkeypatch.setattr(abstract, "_TARGETS", {}) + monkeypatch.setattr(abstract, "_SHARED_REDIRECT_MECHANISMS", {}) diff --git a/source/tests/mock_plugin.py b/source/tests/mock_plugin.py index 56d98b1..e863965 100644 --- a/source/tests/mock_plugin.py +++ b/source/tests/mock_plugin.py @@ -31,10 +31,18 @@ def open_file_for_read(self, filename: str) -> IO[bytes]: class MockRedirectMechanism(abstract.RedirectMechanism): def create_redirect(self, session: TargetSession, alias: Version, version_id: str) -> None: - pass + if alias is abstract.DEFAULT_VERSION: + if session.deployment_spec.default_version is None: + raise RuntimeError("Some plugins might not accept creating a redirect if the version doesn't exist") + elif alias not in session.deployment_spec.aliases: + raise RuntimeError("Some plugins might not accept creating a redirect if the version doesn't exist") def delete_redirect(self, session: TargetSession, alias: Version) -> None: - pass + if alias is abstract.DEFAULT_VERSION: + if session.deployment_spec.default_version is None: + raise RuntimeError("Some plugins might not accept deleting a redirect if the version doesn't exist") + elif alias not in session.deployment_spec.aliases: + raise RuntimeError("Some plugins might not accept deleting a redirect if the version doesn't exist") class MockTargetSession(abstract.TargetSession): @@ -42,14 +50,12 @@ class MockTargetSession(abstract.TargetSession): internal_deployment_spec: abstract.DeploymentSpec closed: bool = False close_success: bool = False - aliases: dict[Version, versions.DeploymentAlias] redirect_mechanisms: dict[str, abstract.RedirectMechanism] = {'mock': MockRedirectMechanism()} def __init__(self): self.files = {} self.deleted_files = set() self.internal_deployment_spec = abstract.DeploymentSpec() - self.aliases = {} self.redirect_mechanisms = self.redirect_mechanisms.copy() def start_version(self, version_id: str, title: str) -> None: @@ -57,7 +63,10 @@ def start_version(self, version_id: str, title: str) -> None: def delete_version(self, version_id: str) -> None: existing_files = [f for v, f in self.files.keys() if v == version_id] - del self.internal_deployment_spec.versions[version_id] + try: + del self.internal_deployment_spec.versions[version_id] + except KeyError: + raise abstract.VersionNotFound() for file in existing_files: del self.files[(version_id, file)] @@ -86,14 +95,15 @@ def close(self, success: bool = False) -> None: self.close_success = success def set_alias(self, alias_id: Version, alias: DeploymentAlias | None) -> None: - if alias is None: - del self.aliases[alias_id] - return - self.aliases[alias_id] = deepcopy(alias) + if alias is not None: + alias = deepcopy(alias) if alias_id is abstract.DEFAULT_VERSION: - self.internal_deployment_spec.default_version = self.aliases[alias_id] + self.internal_deployment_spec.default_version = alias else: - self.internal_deployment_spec.aliases[alias_id] = self.aliases[alias_id] + if alias is not None: + self.internal_deployment_spec.aliases[alias_id] = alias + else: + del self.internal_deployment_spec.aliases[alias_id] @property def available_redirect_mechanisms(self) -> dict[str, abstract.RedirectMechanism]: diff --git a/source/tests/mock_wrapper.py b/source/tests/mock_wrapper.py index 19ecdb1..51a3db0 100644 --- a/source/tests/mock_wrapper.py +++ b/source/tests/mock_wrapper.py @@ -1,4 +1,5 @@ from typing import Any, NamedTuple, TypeVar +from copy import deepcopy _T = TypeVar("_T") @@ -38,5 +39,5 @@ def __setattr__(self, key, value): return setattr(self._wrapped, key, value) def __call__(self, *args, **kwargs): - self._method_calls.append(MethodCall(self._name, args, kwargs)) + self._method_calls.append(MethodCall(self._name, deepcopy(args), deepcopy(kwargs))) return self._wrapped(*args, **kwargs) diff --git a/source/tests/test_modules/test_actions.py b/source/tests/test_modules/test_actions.py deleted file mode 100644 index 74bad84..0000000 --- a/source/tests/test_modules/test_actions.py +++ /dev/null @@ -1,186 +0,0 @@ -import logging -import uuid - -import pytest - -from mkdocs_deploy import abstract, actions, versions -from ..mock_plugin import MockSource, MockTargetSession -from ..mock_wrapper import mock_wrapper - -MOCK_SOURCE_FILES = { - "index.html": str(uuid.uuid4()).encode(), - "subdir/foo.txt": str(uuid.uuid4()).encode(), -} - - -@pytest.mark.parametrize("title", ["Version 1.1", None], ids=["Explicit Title", "Implicit Title"]) -def test_upload(title: str | None): - VERSION = "1.1" - source = MockSource(MOCK_SOURCE_FILES) - session, session_method_calls = mock_wrapper(MockTargetSession()) - - try: - actions.upload(source=source, target=session, version_id=VERSION, title=title) - - assert session_method_calls[0].name == "MockTargetSession.start_version" - - assert session.files == {(VERSION, file): content for file, content in source.files.items()} - - assert session.deployment_spec.versions == { - VERSION: versions.DeploymentVersion(title=title or VERSION), - } - except: - session.close(success=False) - raise - else: - session.close(success=True) - - -def test_upload_implicit_title_does_not_override_existing_one(): - VERSION = "1.1" - source = MockSource(MOCK_SOURCE_FILES) - session, session_method_calls = mock_wrapper(MockTargetSession()) - session.start_version(VERSION, "foo bar") - - actions.upload(source=source, target=session, version_id=VERSION, title=None) - - assert session.deployment_spec.versions == { - VERSION: versions.DeploymentVersion(title="foo bar"), - } - - -def test_upload_explicit_title_overrides_existing_one(): - VERSION = "1.1" - VERSION_TITLE = "Version 1.1" - source = MockSource(MOCK_SOURCE_FILES) - session, session_method_calls = mock_wrapper(MockTargetSession()) - session.start_version(VERSION, "foo bar") - - actions.upload(source=source, target=session, version_id=VERSION, title=VERSION_TITLE) - - assert session.deployment_spec.versions == { - VERSION: versions.DeploymentVersion(title=VERSION_TITLE), - } - - -@pytest.mark.parametrize("alias", ["latest", abstract.DEFAULT_VERSION], ids=["named", "default"]) -def test_refresh_all_redirects_on_alias(alias: abstract.Version): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - session.available_redirect_mechanisms['mock'].create_redirect(session, abstract.DEFAULT_VERSION, "1.1") - session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock'})) - - method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) - method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) - session.redirect_mechanisms['mock'] = method_1 - session.redirect_mechanisms['mock_2'] = method_2 - - actions.refresh_alias(session, alias) - assert not method_calls_2 # Only existing aliases are refreshed - assert len(method_calls_1) == 1 - assert method_calls_1[0].name == 'MockRedirectMechanism.refresh_redirect' - assert method_calls_1[0].kwargs["alias"] == alias - assert method_calls_1[0].kwargs["version_id"] == "1.1" - - -@pytest.mark.parametrize("alias", ["latest", abstract.DEFAULT_VERSION], ids=["Named", "Default"]) -def test_refresh_specific_redirect_on_alias(alias: abstract.Version): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock_2'})) - session.available_redirect_mechanisms['mock'].create_redirect(session, abstract.DEFAULT_VERSION, "1.1") - - method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) - method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) - session.redirect_mechanisms['mock'] = method_1 - session.redirect_mechanisms['mock_2'] = method_2 - - actions.refresh_alias(session, alias, {"mock_2"}) - assert not method_calls_1 # Only named aliases are refreshed - assert len(method_calls_2) == 1 - assert method_calls_2[0].name == 'MockRedirectMechanism.refresh_redirect' - assert method_calls_2[0].kwargs["alias"] == alias - assert method_calls_2[0].kwargs["version_id"] == "1.1" - - - -@pytest.mark.parametrize("alias", ["latest", abstract.DEFAULT_VERSION], ids=["named", "default"]) -def test_delete_all_redirects_on_alias(alias: abstract.Version): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - session.available_redirect_mechanisms['mock'].create_redirect(session, abstract.DEFAULT_VERSION, "1.1") - session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock'})) - - method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) - method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) - session.redirect_mechanisms['mock'] = method_1 - session.redirect_mechanisms['mock_2'] = method_2 - - actions.delete_alias(session, alias) - assert not method_calls_2 # Only existing aliases are refreshed - assert len(method_calls_1) == 1 - assert method_calls_1[0].name == 'MockRedirectMechanism.delete_redirect' - assert method_calls_1[0].kwargs["alias"] == alias - - assert alias not in session.aliases - - -def test_refresh_missing_alias_logs_warning(caplog: pytest.LogCaptureFixture): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - - session.redirect_mechanisms['mock'], method_calls = mock_wrapper(session.redirect_mechanisms['mock']) - with caplog.at_level(level=logging.WARNING): - actions.refresh_alias(session, "not_an_alias", {"mock"}) - - assert len(caplog.records) == 1 - assert caplog.records[0].levelno == logging.WARNING - assert not method_calls - - -@pytest.mark.parametrize("alias", ["latest", abstract.DEFAULT_VERSION], ids=["Named", "Default"]) -def test_delete_last_redirect_on_alias(alias: abstract.Version): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock_2'})) - session.available_redirect_mechanisms['mock'].create_redirect(session, abstract.DEFAULT_VERSION, "1.1") - - method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) - method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) - session.redirect_mechanisms['mock'] = method_1 - session.redirect_mechanisms['mock_2'] = method_2 - - actions.delete_alias(session, alias, {"mock_2"}) - assert not method_calls_1 # Only named aliases are refreshed - assert len(method_calls_2) == 1 - assert method_calls_2[0].name == 'MockRedirectMechanism.delete_redirect' - assert method_calls_2[0].kwargs["alias"] == alias - - assert alias not in session.aliases - - -@pytest.mark.parametrize("alias", ["latest", abstract.DEFAULT_VERSION], ids=["Named", "Default"]) -def test_delete_one_of_two_redirects_on_alias(alias: abstract.Version): - session = MockTargetSession() - for version in ("1.0", "1.1", "2.0"): - session.start_version(version, version) - session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock' ,'mock_2'})) - session.available_redirect_mechanisms['mock'].create_redirect(session, abstract.DEFAULT_VERSION, "1.1") - - method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) - method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) - session.redirect_mechanisms['mock'] = method_1 - session.redirect_mechanisms['mock_2'] = method_2 - - actions.delete_alias(session, alias, {"mock_2"}) - assert not method_calls_1 # Only named aliases are refreshed - assert len(method_calls_2) == 1 - assert method_calls_2[0].name == 'MockRedirectMechanism.delete_redirect' - assert method_calls_2[0].kwargs["alias"] == alias - - assert alias in session.aliases diff --git a/source/tests/test_modules/test_actions/__init__.py b/source/tests/test_modules/test_actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/source/tests/test_modules/test_actions/conftest.py b/source/tests/test_modules/test_actions/conftest.py new file mode 100644 index 0000000..6e50f87 --- /dev/null +++ b/source/tests/test_modules/test_actions/conftest.py @@ -0,0 +1,27 @@ +import uuid + +import pytest + +from mkdocs_deploy import abstract +from ...mock_plugin import MockTargetSession + + +@pytest.fixture() +def mock_source_files() -> dict[str, bytes]: + return { + "index.html": str(uuid.uuid4()).encode(), + "subdir/foo.txt": str(uuid.uuid4()).encode(), + } + + +@pytest.fixture() +def mock_session() -> MockTargetSession: + session = MockTargetSession() + for version in ("1.0", "1.1", "2.0"): + session.start_version(version, version) + return session + + +@pytest.fixture(params=["latest", abstract.DEFAULT_VERSION], ids=["Named_alias", "Default_alias"]) +def alias(request) -> abstract.Version: + return request.param diff --git a/source/tests/test_modules/test_actions/test_create_alias.py b/source/tests/test_modules/test_actions/test_create_alias.py new file mode 100644 index 0000000..f51b55b --- /dev/null +++ b/source/tests/test_modules/test_actions/test_create_alias.py @@ -0,0 +1,83 @@ +from typing import Collection + +import pytest + +from mkdocs_deploy import abstract +from mkdocs_deploy.actions import create_alias +from ...mock_plugin import MockTargetSession +from ...mock_wrapper import mock_wrapper + + +@pytest.mark.parametrize("mechanisms", [("mock",), None], ids=["explicit_mechanism", "implicit_mechanism"]) +def test_create_new_alias(mock_session: MockTargetSession, alias: abstract.Version, mechanisms: Collection[str] | None): + mock_session.redirect_mechanisms["mock"], mechanism_calls = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mock_session, session_calls = mock_wrapper(mock_session) + create_alias(mock_session, alias, "1.1", mechanisms) + assert session_calls[-1].name == "MockTargetSession.set_alias" + assert len(mechanism_calls) == 1 + assert mechanism_calls[0].name == "MockRedirectMechanism.create_redirect" + + +def test_add_mechanism(mock_session: MockTargetSession, alias: abstract.Version): + create_alias(mock_session, alias, "1.1", ["mock"]) + mechanism_1, calls_1 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mechanism_2, calls_2 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mock_session.redirect_mechanisms["mock"] = mechanism_1 + mock_session.redirect_mechanisms["mock_2"] = mechanism_2 + create_alias(mock_session, alias, "1.1", ["mock", "mock_2"]) + assert not calls_1 + assert len(calls_2) == 1 + assert calls_2[0].name == "MockRedirectMechanism.create_redirect" + + +def test_change_version_change_mechanism(mock_session: MockTargetSession, alias: abstract.Version): + create_alias(mock_session, alias, "1.1", ["mock"]) + + # Notice that mock_2 is added AFTER the version is created. + # The intended behaviour here is to create all available mechanisms + mechanism_1, calls_1 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mechanism_2, calls_2 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mock_session.redirect_mechanisms["mock"] = mechanism_1 + mock_session.redirect_mechanisms["mock_2"] = mechanism_2 + create_alias(mock_session, alias, "2.0", ["mock_2"]) + + assert len(calls_1) == 1 + assert calls_1[0].name == "MockRedirectMechanism.delete_redirect" + assert len(calls_2) == 1 + assert calls_2[0].name == "MockRedirectMechanism.create_redirect" + + +def test_change_version_only(mock_session: MockTargetSession, alias: abstract.Version): + create_alias(mock_session, alias, "1.1", ["mock"]) + + # Notice that mock_2 is added AFTER the version is created. + # The intended behaviour here is to create all available mechanisms + mechanism_1, calls_1 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mechanism_2, calls_2 = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + mock_session.redirect_mechanisms["mock"] = mechanism_1 + mock_session.redirect_mechanisms["mock_2"] = mechanism_2 + create_alias(mock_session, alias, "2.0", ["mock"]) + + assert len(calls_1) == 1 + assert calls_1[0].name == "MockRedirectMechanism.refresh_redirect" + assert not calls_2 + + +def test_clobbering_version_with_alias_fails(mock_session: MockTargetSession): + with pytest.raises(ValueError): + create_alias(mock_session, "2.0", "1.1") + + +def test_creating_non_existent_mechanism_fails(mock_session: MockTargetSession): + with pytest.raises(ValueError): + create_alias(mock_session, "2.0", "1.1", ["foo"]) + + +def test_deleting_non_existent_mechanism_fails(mock_session: MockTargetSession, alias: abstract.Version): + """If a code change means configuration has redirects that cannot now be deleted, we should error""" + mock_session.redirect_mechanisms["phantom"] = mock_session.redirect_mechanisms["mock"] + create_alias(mock_session, alias, "2.0", ["mock", "phantom"]) + del mock_session.redirect_mechanisms["phantom"] + + with pytest.raises(ValueError): + create_alias(mock_session, alias, "2.0", ["mock"]) diff --git a/source/tests/test_modules/test_actions/test_delete_alias.py b/source/tests/test_modules/test_actions/test_delete_alias.py new file mode 100644 index 0000000..17310d2 --- /dev/null +++ b/source/tests/test_modules/test_actions/test_delete_alias.py @@ -0,0 +1,93 @@ +import logging + +import pytest + +from mkdocs_deploy import abstract, actions +from ...mock_plugin import MockTargetSession +from ...mock_wrapper import mock_wrapper + + +def test_delete_all_redirects_on_alias(alias: abstract.Version, mock_session: MockTargetSession): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock'})) + mock_session.available_redirect_mechanisms['mock'].create_redirect(mock_session, alias, "1.1") + + method_1, method_calls_1 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + mock_session.redirect_mechanisms['mock'] = method_1 + mock_session.redirect_mechanisms['mock_2'] = method_2 + + actions.delete_alias(mock_session, alias) + assert not method_calls_2 # Only existing aliases are refreshed + assert len(method_calls_1) == 1 + assert method_calls_1[0].name == 'MockRedirectMechanism.delete_redirect' + assert method_calls_1[0].kwargs["alias"] == alias + + if alias is abstract.DEFAULT_VERSION: + assert mock_session.internal_deployment_spec.default_version is None + else: + assert alias not in mock_session.internal_deployment_spec.aliases + + +def test_delete_last_redirect_on_alias(alias: abstract.Version, mock_session: MockTargetSession): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock_2'})) + mock_session.available_redirect_mechanisms['mock'].create_redirect(mock_session, alias, "1.1") + + method_1, method_calls_1 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + mock_session.redirect_mechanisms['mock'] = method_1 + mock_session.redirect_mechanisms['mock_2'] = method_2 + + actions.delete_alias(mock_session, alias, {"mock_2"}) + assert not method_calls_1 # Only named aliases are refreshed + assert len(method_calls_2) == 1 + assert method_calls_2[0].name == 'MockRedirectMechanism.delete_redirect' + assert method_calls_2[0].kwargs["alias"] == alias + + if alias is abstract.DEFAULT_VERSION: + assert mock_session.internal_deployment_spec.default_version is None + else: + assert alias not in mock_session.internal_deployment_spec.aliases + + +def test_delete_one_of_two_redirects_on_alias(alias: abstract.Version, mock_session: MockTargetSession): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock' ,'mock_2'})) + mock_session.available_redirect_mechanisms['mock'].create_redirect(mock_session, alias, "1.1") + + method_1, method_calls_1 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + mock_session.redirect_mechanisms['mock'] = method_1 + mock_session.redirect_mechanisms['mock_2'] = method_2 + + actions.delete_alias(mock_session, alias, {"mock_2"}) + assert not method_calls_1 # Only named aliases are refreshed + assert len(method_calls_2) == 1 + assert method_calls_2[0].name == 'MockRedirectMechanism.delete_redirect' + assert method_calls_2[0].kwargs["alias"] == alias + + if alias is abstract.DEFAULT_VERSION: + assert mock_session.internal_deployment_spec.default_version is not None + else: + assert alias in mock_session.internal_deployment_spec.aliases + + +def test_delete_missing_alias_generates_warning( + alias: abstract.Version, caplog: pytest.LogCaptureFixture, mock_session: MockTargetSession +): + mock_session.redirect_mechanisms['mock'], redirect_method_calls = mock_wrapper( + mock_session.redirect_mechanisms['mock'] + ) + + with caplog.at_level(logging.WARNING): + actions.delete_alias(mock_session, alias) + + assert not redirect_method_calls + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.WARNING + + +def test_delete_non_existent_mechanism_raises_value_error( + alias: abstract.Version, caplog: pytest.LogCaptureFixture, mock_session: MockTargetSession +): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'does_not_exist'})) + with pytest.raises(ValueError): + actions.delete_alias(mock_session, alias, ["does_not_exist"]) diff --git a/source/tests/test_modules/test_actions/test_delete_version.py b/source/tests/test_modules/test_actions/test_delete_version.py new file mode 100644 index 0000000..3c9f82c --- /dev/null +++ b/source/tests/test_modules/test_actions/test_delete_version.py @@ -0,0 +1,36 @@ +import pytest +import logging +from mkdocs_deploy import abstract +from mkdocs_deploy.actions import create_alias, delete_version +from ...mock_plugin import MockTargetSession +from ...mock_wrapper import mock_wrapper, MethodCall + + +def test_delete_version_with_aliases(mock_session: MockTargetSession, alias: abstract.Version): + mock_session.redirect_mechanisms["mock"], mechanism_calls = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + create_alias(mock_session, alias, "1.1") + + mock_session, session_calls = mock_wrapper(mock_session) + + delete_version(mock_session, "1.1") + + assert "1.1" not in mock_session.internal_deployment_spec.versions + if alias is abstract.DEFAULT_VERSION: + assert mock_session.internal_deployment_spec.default_version is None + else: + assert alias not in mock_session.internal_deployment_spec.aliases + + assert MethodCall("MockTargetSession.set_alias", (alias, None), {}) in session_calls + + +def test_delete_missing_version(mock_session: MockTargetSession, alias: abstract.Version, caplog: pytest.LogCaptureFixture): + mock_session.redirect_mechanisms["mock"], mechanism_calls = mock_wrapper(mock_session.redirect_mechanisms["mock"]) + + mock_session, session_calls = mock_wrapper(mock_session) + + with caplog.at_level(logging.WARNING): + delete_version(mock_session, "foo") + + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.WARNING + diff --git a/source/tests/test_modules/test_actions/test_load_plugins.py b/source/tests/test_modules/test_actions/test_load_plugins.py new file mode 100644 index 0000000..50ef8d9 --- /dev/null +++ b/source/tests/test_modules/test_actions/test_load_plugins.py @@ -0,0 +1,68 @@ +import importlib.metadata + +import pytest + +from mkdocs_deploy import actions +from mkdocs_deploy.plugins import aws_s3, local_filesystem, html_redirect +import functools + +def test_load_plugins_calls_entry_point(monkeypatch: pytest.MonkeyPatch): + def mock_entry_points_discovery(group: str): + assert group == "mkdocs_deploy.plugins" + return [mock_entry_point] + + class MockEntryPoint: + name = "mock_entry_point" + has_run = False + + def load(self): + return self.run + + def run(self): + self.has_run = True + + mock_entry_point = MockEntryPoint() + monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points_discovery) + + actions.load_plugins() + + assert mock_entry_point.has_run + + +def test_load_plugin_cascades_error(monkeypatch: pytest.MonkeyPatch): + def mock_entry_points_discovery(group: str): + assert group == "mkdocs_deploy.plugins" + return [mock_entry_point] + + class MockError(Exception): + ... + + class MockEntryPoint: + name = "mock_entry_point" + + def load(self): + return self.run + + def run(self): + raise MockError() + + mock_entry_point = MockEntryPoint() + monkeypatch.setattr(importlib.metadata, "entry_points", mock_entry_points_discovery) + + with pytest.raises(MockError): + actions.load_plugins() + + +def test_inbuilt_plugins_are_loaded(monkeypatch: pytest.MonkeyPatch): + """The aim of this is to ensure that we don't mess up entry points in pyproject.toml""" + all_plugins = (aws_s3, local_filesystem, html_redirect) + executed: set[str] = set() + + def enable_plugin(name: str): + executed.add(name) + + for plugin in all_plugins: + monkeypatch.setattr(plugin, "enable_plugin", functools.partial(enable_plugin, plugin.__name__)) + + actions.load_plugins() + assert executed == {plugin.__name__ for plugin in all_plugins} diff --git a/source/tests/test_modules/test_actions/test_refresh_alias.py b/source/tests/test_modules/test_actions/test_refresh_alias.py new file mode 100644 index 0000000..9a62ce8 --- /dev/null +++ b/source/tests/test_modules/test_actions/test_refresh_alias.py @@ -0,0 +1,51 @@ +import logging + +import pytest + +from mkdocs_deploy import abstract, actions +from ...mock_plugin import MockTargetSession +from ...mock_wrapper import mock_wrapper + + +def test_refresh_all_redirects_on_alias(alias: abstract.Version, mock_session: MockTargetSession): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock'})) + mock_session.available_redirect_mechanisms['mock'].create_redirect(mock_session, alias, "1.1") + + method_1, method_calls_1 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + mock_session.redirect_mechanisms['mock'] = method_1 + mock_session.redirect_mechanisms['mock_2'] = method_2 + + actions.refresh_alias(mock_session, alias) + assert not method_calls_2 # Only existing aliases are refreshed + assert len(method_calls_1) == 1 + assert method_calls_1[0].name == 'MockRedirectMechanism.refresh_redirect' + assert method_calls_1[0].kwargs["alias"] == alias + assert method_calls_1[0].kwargs["version_id"] == "1.1" + + +def test_refresh_specific_redirect_on_alias(alias: abstract.Version, mock_session: MockTargetSession): + mock_session.set_alias(alias, abstract.DeploymentAlias(version_id="1.1", redirect_mechanisms={'mock_2'})) + mock_session.available_redirect_mechanisms['mock'].create_redirect(mock_session, alias, "1.1") + + method_1, method_calls_1 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(mock_session.redirect_mechanisms['mock']) + mock_session.redirect_mechanisms['mock'] = method_1 + mock_session.redirect_mechanisms['mock_2'] = method_2 + + actions.refresh_alias(mock_session, alias, {"mock_2"}) + assert not method_calls_1 # Only named aliases are refreshed + assert len(method_calls_2) == 1 + assert method_calls_2[0].name == 'MockRedirectMechanism.refresh_redirect' + assert method_calls_2[0].kwargs["alias"] == alias + assert method_calls_2[0].kwargs["version_id"] == "1.1" + + +def test_refresh_missing_alias_logs_warning(caplog: pytest.LogCaptureFixture, mock_session: MockTargetSession): + mock_session.redirect_mechanisms['mock'], method_calls = mock_wrapper(mock_session.redirect_mechanisms['mock']) + with caplog.at_level(level=logging.WARNING): + actions.refresh_alias(mock_session, "not_an_alias", {"mock"}) + + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.WARNING + assert not method_calls diff --git a/source/tests/test_modules/test_actions/test_upload.py b/source/tests/test_modules/test_actions/test_upload.py new file mode 100644 index 0000000..9a9a09f --- /dev/null +++ b/source/tests/test_modules/test_actions/test_upload.py @@ -0,0 +1,77 @@ +import pytest + +from mkdocs_deploy import abstract, actions, versions +from ...mock_plugin import MockSource, MockTargetSession +from ...mock_wrapper import mock_wrapper + + +@pytest.mark.parametrize("title", ["Version 1.1", None], ids=["Explicit Title", "Implicit Title"]) +def test_upload(title: str | None, mock_source_files: dict[str, bytes]): + VERSION = "1.1" + source = MockSource(mock_source_files) + session, session_method_calls = mock_wrapper(MockTargetSession()) + + try: + actions.upload(source=source, target=session, version_id=VERSION, title=title) + + assert session_method_calls[0].name == "MockTargetSession.start_version" + + assert session.files == {(VERSION, file): content for file, content in source.files.items()} + + assert session.deployment_spec.versions == { + VERSION: versions.DeploymentVersion(title=title or VERSION), + } + except: + session.close(success=False) + raise + else: + session.close(success=True) + + +def test_upload_implicit_title_does_not_override_existing_one(mock_source_files: dict[str, bytes]): + VERSION = "1.1" + source = MockSource(mock_source_files) + session, session_method_calls = mock_wrapper(MockTargetSession()) + session.start_version(VERSION, "foo bar") + + actions.upload(source=source, target=session, version_id=VERSION, title=None) + + assert session.deployment_spec.versions == { + VERSION: versions.DeploymentVersion(title="foo bar"), + } + + +def test_upload_explicit_title_overrides_existing_one(mock_source_files: dict[str, bytes]): + VERSION = "1.1" + VERSION_TITLE = "Version 1.1" + source = MockSource(mock_source_files) + session, session_method_calls = mock_wrapper(MockTargetSession()) + session.start_version(VERSION, "foo bar") + + actions.upload(source=source, target=session, version_id=VERSION, title=VERSION_TITLE) + + assert session.deployment_spec.versions == { + VERSION: versions.DeploymentVersion(title=VERSION_TITLE), + } + + +def test_upload_refreshes_aliases(mock_source_files: dict[str, bytes]): + VERSION = "1.1" + ALIAS = "latest" + source = MockSource(mock_source_files) + session = MockTargetSession() + session.set_alias(ALIAS, abstract.DeploymentAlias(version_id=VERSION, redirect_mechanisms={'mock'})) + method_1, method_calls_1 = mock_wrapper(session.redirect_mechanisms['mock']) + method_2, method_calls_2 = mock_wrapper(session.redirect_mechanisms['mock']) + session.redirect_mechanisms['mock'] = method_1 + session.redirect_mechanisms['mock_2'] = method_2 + + session.start_version(VERSION, "foo bar") + + actions.upload(source=source, target=session, version_id=VERSION, title=None) + + assert not method_calls_2 + assert len(method_calls_1) == 1 + assert method_calls_1[0].name == 'MockRedirectMechanism.refresh_redirect' + assert method_calls_1[0].kwargs["alias"] == ALIAS + assert method_calls_1[0].kwargs["version_id"] == VERSION