From 05347fad74a8f33d8cf74cc0bfdd9232bcb20106 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Sun, 22 May 2022 01:33:46 +0100 Subject: [PATCH 01/21] Improve typing with newer mypy & behave-utils Fix PEP-612 features now mypy suports it. --- tests/environment.py | 12 +++--------- tests/steps/pages.py | 10 +--------- tests/wp.py | 7 ++++++- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index 3d839be..c1fbe89 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -17,7 +17,6 @@ from __future__ import annotations import sys from os import environ from typing import TYPE_CHECKING -from typing import Any from typing import Iterator from behave import fixture @@ -60,22 +59,17 @@ def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: context.session = use_fixture(requests_session, context) -# Todo(dom.sekotill): When PEP-612 is properly implemented in mypy the [*a, **k] and default -# values nonsense can be removed from fixtures - @fixture -def setup_test_cluster(context: Context, /, site_url: URL|None = None, *a: Any, **k: Any) -> Iterator[Site]: +def setup_test_cluster(context: Context, /, site_url: URL) -> Iterator[Site]: """ Prepare and return the details of a site fixture """ - assert site_url is not None, \ - "site_url is required, but default supplied until PEP-612 supported" with test_cluster(site_url) as site: yield site @fixture -def requests_session(context: ScenarioContext, /, *a: Any, **k: Any) -> Iterator[Session]: +def requests_session(context: ScenarioContext, /) -> Iterator[Session]: """ Create and configure a `requests` session for accessing site fixtures """ @@ -86,7 +80,7 @@ def requests_session(context: ScenarioContext, /, *a: Any, **k: Any) -> Iterator @fixture -def db_snapshot_rollback(context: FeatureContext, /, *a: Any, **k: Any) -> Iterator[None]: +def db_snapshot_rollback(context: FeatureContext, /) -> Iterator[None]: """ Manage the state of a site's database as a revertible fixture """ diff --git a/tests/steps/pages.py b/tests/steps/pages.py index 8375696..8baab0c 100644 --- a/tests/steps/pages.py +++ b/tests/steps/pages.py @@ -11,7 +11,6 @@ Step implementations involving creating and requesting WP posts (and pages) from __future__ import annotations from codecs import decode as utf8_decode -from typing import Any from typing import Iterator from behave import fixture @@ -123,17 +122,12 @@ def assert_contains( @fixture def wp_post( context: Context, /, - post_type: PostType|None = None, + post_type: PostType, content: str = DEFAULT_CONTENT, - *a: Any, - **k: Any, ) -> Iterator[JSONObject]: """ Create a WP post fixture of the given type with the given content """ - assert post_type is not None, \ - "post_type MUST be supplied to use_fixture when calling with wp_post" - wp = context.site.backend postid = wp.cli( "post", "create", @@ -162,8 +156,6 @@ def set_specials( context: Context, /, homepage: JSONObject|None = None, posts: JSONObject|None = None, - *a: Any, - **k: Any, ) -> Iterator[None]: """ Set the homepage and post index to new pages, creating default pages if needed diff --git a/tests/wp.py b/tests/wp.py index efc01b3..4408747 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -13,8 +13,10 @@ from __future__ import annotations from contextlib import contextmanager from os import environ from pathlib import Path +from typing import TYPE_CHECKING from typing import Iterator from typing import NamedTuple +from typing import TypeVar from behave_utils import URL from behave_utils import wait @@ -35,6 +37,9 @@ class Wordpress(Container): DEFAULT_ALIASES = ("upstream",) + if TYPE_CHECKING: + T = TypeVar("T", bound="Wordpress") + def __init__(self, site_url: URL, database: Mysql, network: Network|None = None): Container.__init__( self, @@ -66,7 +71,7 @@ class Wordpress(Container): return Cli(self, "wp") @contextmanager - def started(self) -> Iterator[Container]: + def started(self: T) -> Iterator[T]: with self: self.start() cmd = ["bash", "-c", "[[ /proc/1/exe -ef `which php-fpm` ]]"] -- GitLab From 4a5d48116ad22521403a9da12e38213b54ccdd56 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Thu, 26 May 2022 23:28:42 +0100 Subject: [PATCH 02/21] Split test site build and start for delayed starting --- tests/wp.py | 83 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/tests/wp.py b/tests/wp.py index 4408747..bc49c75 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -15,7 +15,6 @@ from os import environ from pathlib import Path from typing import TYPE_CHECKING from typing import Iterator -from typing import NamedTuple from typing import TypeVar from behave_utils import URL @@ -97,35 +96,73 @@ class Nginx(Container): ) -class Site(NamedTuple): +class Site: """ - A named-tuple of information about the containers for a site fixture + Manage all the containers of a site fixture """ - url: str - address: IPv4Address - frontend: Nginx - backend: Wordpress - database: Mysql + if TYPE_CHECKING: + T = TypeVar("T", bound="Site") + + def __init__( + self, + url: URL, + network: Network, + frontend: Nginx, + backend: Wordpress, + database: Mysql, + ): + self.url = url + self.network = network + self.frontend = frontend + self.backend = backend + self.database = database + self._address: IPv4Address|None = None + + @classmethod + @contextmanager + def build(cls: type[T], site_url: URL) -> Iterator[T]: + test_dir = Path(__file__).parent + db_init = test_dir / "mysql-init.sql" + + with Network() as network: + database = Mysql(network=network, init_files=[db_init]) + database.start() # Get a head start on initialising the database + backend = Wordpress(site_url, database, network=network) + frontend = Nginx(backend, network=network) + yield cls(site_url, network, frontend, backend, database) + + @contextmanager + def running(self) -> Iterator[None]: + """ + Start all the services and configure the network + """ + with self.database.started(), self.backend.started(), self.frontend.started(): + try: + yield + finally: + self._address = None + + @property + def address(self) -> IPv4Address: + if self._address is None: + if not self.frontend.is_running(): + raise RuntimeError( + "Site.address may only be accessed inside a Site.running() context", + ) + self._address = self.frontend.inspect().path( + f"$.NetworkSettings.Networks.{self.network}.IPAddress", + str, IPv4Address, + ) + return self._address @contextmanager def test_cluster(site_url: URL) -> Iterator[Site]: """ Configure and start all the necessary containers for use as test fixtures + + Deprecated: this is now a wrapper around Site.build() and Site.running() """ - test_dir = Path(__file__).parent - db_init = test_dir / "mysql-init.sql" - - with Network() as network: - database = Mysql(network=network, init_files=[db_init]) - database.start() # Get a head start on initialising the database - backend = Wordpress(site_url, database, network=network) - frontend = Nginx(backend, network=network) - - with database.started(), backend.started(), frontend.started(): - addr = frontend.inspect().path( - f"$.NetworkSettings.Networks.{network}.IPAddress", - str, IPv4Address, - ) - yield Site(site_url, addr, frontend, backend, database) + with Site.build(site_url) as site, site.running(): + yield site -- GitLab From 8d33950630e3485559c8b727cb6e4ba208e24fd8 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 27 May 2022 21:58:33 +0100 Subject: [PATCH 03/21] Split site fixture into running/non-running --- tests/environment.py | 20 ++++++-------------- tests/wp.py | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index c1fbe89..f70ccd6 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -28,8 +28,7 @@ from behave_utils import URL from behave_utils import redirect from behave_utils.mysql import snapshot_rollback from requests.sessions import Session -from wp import Site -from wp import test_cluster +from wp import running_site_fixture if TYPE_CHECKING: from behave.runner import FeatureContext @@ -42,7 +41,7 @@ def before_all(context: Context) -> None: """ Setup fixtures for all tests """ - context.site = use_fixture(setup_test_cluster, context, SITE_URL) + use_fixture(running_site_fixture, context, site_url=SITE_URL) def before_feature(context: FeatureContext, feature: Feature) -> None: @@ -59,21 +58,12 @@ def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: context.session = use_fixture(requests_session, context) -@fixture -def setup_test_cluster(context: Context, /, site_url: URL) -> Iterator[Site]: - """ - Prepare and return the details of a site fixture - """ - with test_cluster(site_url) as site: - yield site - - @fixture def requests_session(context: ScenarioContext, /) -> Iterator[Session]: """ Create and configure a `requests` session for accessing site fixtures """ - site = context.site + site = use_fixture(running_site_fixture, context) with Session() as session: redirect(session, site.url, site.address) yield session @@ -93,7 +83,9 @@ def db_snapshot_rollback(context: FeatureContext, /) -> Iterator[None]: if __name__ == "__main__": from subprocess import run - with test_cluster(SITE_URL) as site: + from wp import Site + + with Site.build(SITE_URL) as site, site.running(): run([environ.get("SHELL", "/bin/sh")]) elif not sys.stderr.isatty(): diff --git a/tests/wp.py b/tests/wp.py index bc49c75..9411650 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -17,6 +17,9 @@ from typing import TYPE_CHECKING from typing import Iterator from typing import TypeVar +from behave import fixture +from behave import use_fixture +from behave.runner import Context from behave_utils import URL from behave_utils import wait from behave_utils.docker import Cli @@ -27,6 +30,7 @@ from behave_utils.docker import Network from behave_utils.mysql import Mysql BUILD_CONTEXT = Path(__file__).parent.parent +DEFAULT_URL = URL("http://test.example.com") class Wordpress(Container): @@ -133,13 +137,13 @@ class Site: yield cls(site_url, network, frontend, backend, database) @contextmanager - def running(self) -> Iterator[None]: + def running(self: T) -> Iterator[T]: """ Start all the services and configure the network """ with self.database.started(), self.backend.started(), self.frontend.started(): try: - yield + yield self finally: self._address = None @@ -157,12 +161,32 @@ class Site: return self._address -@contextmanager -def test_cluster(site_url: URL) -> Iterator[Site]: +@fixture +def site_fixture(context: Context, /, site_url: URL|None = None) -> Iterator[Site]: """ - Configure and start all the necessary containers for use as test fixtures + Return a currently in-scope Site instance when used with `use_fixture` - Deprecated: this is now a wrapper around Site.build() and Site.running() + If "site_url" is provided and it doesn't match a current Site instance, a new instance + will be created in the current context. + + >>> use_fixture(site_fixture, context) + <<< + """ + if hasattr(context, "site"): + assert isinstance(context.site, Site) + if site_url is None or context.site.url == site_url: + yield context.site + return + with Site.build(site_url or DEFAULT_URL) as context.site: + yield context.site + + +@fixture +def running_site_fixture(context: Context, /, site_url: URL|None = None) -> Iterator[Site]: + """ + Return a currently in-scope Site instance that is running when used with `use_fixture` + + Like `site_fixture` but additionally entered into the `Site.running` context manager. """ - with Site.build(site_url) as site, site.running(): + with use_fixture(site_fixture, context, site_url=site_url).running() as site: yield site -- GitLab From e5bb6936fb45e7d17ccc95dd8a7015d886ec3531 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 27 May 2022 20:12:45 +0100 Subject: [PATCH 04/21] Move tests' request.Session completely within steps/request_steps.py --- tests/environment.py | 14 -------------- tests/steps/request_steps.py | 27 +++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index f70ccd6..7b8c182 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -25,9 +25,7 @@ from behave.model import Feature from behave.model import Scenario from behave.runner import Context from behave_utils import URL -from behave_utils import redirect from behave_utils.mysql import snapshot_rollback -from requests.sessions import Session from wp import running_site_fixture if TYPE_CHECKING: @@ -55,18 +53,6 @@ def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: """ Setup tools for each scenario """ - context.session = use_fixture(requests_session, context) - - -@fixture -def requests_session(context: ScenarioContext, /) -> Iterator[Session]: - """ - Create and configure a `requests` session for accessing site fixtures - """ - site = use_fixture(running_site_fixture, context) - with Session() as session: - redirect(session, site.url, site.address) - yield session @fixture diff --git a/tests/steps/request_steps.py b/tests/steps/request_steps.py index f146f9e..3c3d6e0 100644 --- a/tests/steps/request_steps.py +++ b/tests/steps/request_steps.py @@ -13,13 +13,19 @@ from __future__ import annotations import json from collections.abc import Collection from typing import Any +from typing import Iterator from typing import TypeVar +from behave import fixture from behave import then +from behave import use_fixture from behave import when from behave.runner import Context from behave_utils import URL from behave_utils import PatternEnum +from behave_utils.http import redirect +from requests import Session +from wp import running_site_fixture T = TypeVar("T") @@ -87,12 +93,28 @@ class ResponseCode(int, PatternEnum): attr.update(additional) +@fixture +def requests_session(context: Context, /) -> Iterator[Session]: + """ + Create and configure a `requests` session for accessing site fixtures + """ + if hasattr(context, "session"): + assert isinstance(context.session, Session) + yield context.session + return + site = use_fixture(running_site_fixture, context) + with Session() as context.session: + redirect(context.session, site.url, site.address) + yield context.session + + @when("{url:URL} is requested") def get_request(context: Context, url: URL) -> None: """ Assign the response from making a GET request to "url" to the context """ - context.response = context.session.get(context.site.url / url, allow_redirects=False) + session = use_fixture(requests_session, context) + context.response = session.get(context.site.url / url, allow_redirects=False) @when("data is sent with {method:Method} to {url:URL}") @@ -102,7 +124,8 @@ def post_request(context: Context, method: Method, url: URL) -> None: """ if context.text is None: raise ValueError("Missing data, please add as text to step definition") - context.response = context.session.request( + session = use_fixture(requests_session, context) + context.response = session.request( method.value, context.site.url / url, data=context.text.strip().format(context=context).encode("utf-8"), -- GitLab From 4aacffc93682f4f44751d2556afc13f57cb8bd1c Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 27 May 2022 21:50:03 +0100 Subject: [PATCH 05/21] Ensure test container fixtures clean up in the event of errors --- tests/wp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/wp.py b/tests/wp.py index 9411650..bf93c1d 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -129,12 +129,12 @@ class Site: test_dir = Path(__file__).parent db_init = test_dir / "mysql-init.sql" - with Network() as network: - database = Mysql(network=network, init_files=[db_init]) + with Network() as network, Mysql(network=network, init_files=[db_init]) as database: database.start() # Get a head start on initialising the database - backend = Wordpress(site_url, database, network=network) - frontend = Nginx(backend, network=network) - yield cls(site_url, network, frontend, backend, database) + with \ + Wordpress(site_url, database, network=network) as backend, \ + Nginx(backend, network=network) as frontend: + yield cls(site_url, network, frontend, backend, database) @contextmanager def running(self: T) -> Iterator[T]: -- GitLab From 4c33f5e59b9ecbfb187803fa728106bc26cf0f56 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Mon, 30 May 2022 20:33:12 +0100 Subject: [PATCH 06/21] Shortcut Site.running in tests --- tests/wp.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/wp.py b/tests/wp.py index bf93c1d..bd4ade2 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -122,6 +122,7 @@ class Site: self.backend = backend self.database = database self._address: IPv4Address|None = None + self._running = False @classmethod @contextmanager @@ -141,10 +142,15 @@ class Site: """ Start all the services and configure the network """ + if self._running: + yield self + return + self._running = True with self.database.started(), self.backend.started(), self.frontend.started(): try: yield self finally: + self._running = False self._address = None @property -- GitLab From 70e682938af38df966f94595ad0aec18c5e0d3d2 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 24 Feb 2023 22:18:47 +0000 Subject: [PATCH 07/21] Remove dead db_snapshot_rollback fixture --- tests/environment.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index 7b8c182..c4c6294 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 Dominik Sekotill +# Copyright 2021-2023 Dominik Sekotill # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -17,9 +17,7 @@ from __future__ import annotations import sys from os import environ from typing import TYPE_CHECKING -from typing import Iterator -from behave import fixture from behave import use_fixture from behave.model import Feature from behave.model import Scenario @@ -55,17 +53,6 @@ def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: """ -@fixture -def db_snapshot_rollback(context: FeatureContext, /) -> Iterator[None]: - """ - Manage the state of a site's database as a revertible fixture - """ - db = context.site.database - snapshot = db.mysqldump("--all-databases", deserialiser=bytes) - yield - db.mysql(input=snapshot) - - if __name__ == "__main__": from subprocess import run -- GitLab From efb6b5548d2c909ee91e00c7ca2eba5190d6c137 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 24 Feb 2023 22:27:03 +0000 Subject: [PATCH 08/21] Remove dead __main__ code --- tests/environment.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index c4c6294..8e0c25b 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -15,7 +15,6 @@ https://behave.readthedocs.io/en/stable/tutorial.html#environmental-controls from __future__ import annotations import sys -from os import environ from typing import TYPE_CHECKING from behave import use_fixture @@ -53,15 +52,7 @@ def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: """ -if __name__ == "__main__": - from subprocess import run - - from wp import Site - - with Site.build(SITE_URL) as site, site.running(): - run([environ.get("SHELL", "/bin/sh")]) - -elif not sys.stderr.isatty(): +if not sys.stderr.isatty(): import logging logging.basicConfig(level=logging.DEBUG, stream=sys.stderr) -- GitLab From c2c8a655dc77c53b5ff7e25a21379c036ae76a9b Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Mon, 27 Feb 2023 21:22:25 +0000 Subject: [PATCH 09/21] Get Site instances through fixture system --- .pre-commit-config.yaml | 2 +- tests/environment.py | 9 +++++---- tests/steps/commands.py | 7 +++++-- tests/steps/pages.py | 11 ++++++----- tests/steps/request_steps.py | 9 ++++++--- tests/steps/static.py | 4 +++- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aaf0022..d14d36c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,4 +93,4 @@ repos: args: ["--python-version=3.9"] additional_dependencies: - types-requests - - behave-utils ~=0.3.2 + - behave-utils ~=0.3.3 diff --git a/tests/environment.py b/tests/environment.py index 8e0c25b..a3e2a38 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -34,21 +34,22 @@ SITE_URL = URL("http://test.example.com") def before_all(context: Context) -> None: """ - Setup fixtures for all tests + Prepare fixtures for all tests """ use_fixture(running_site_fixture, context, site_url=SITE_URL) def before_feature(context: FeatureContext, feature: Feature) -> None: """ - Setup/revert fixtures before each feature + Prepare/revert fixtures before each feature """ - use_fixture(snapshot_rollback, context, context.site.database) + site = use_fixture(running_site_fixture, context) + use_fixture(snapshot_rollback, context, site.database) def before_scenario(context: ScenarioContext, scenario: Scenario) -> None: """ - Setup tools for each scenario + Prepare tools for each scenario """ diff --git a/tests/steps/commands.py b/tests/steps/commands.py index acd1f72..2664c8e 100644 --- a/tests/steps/commands.py +++ b/tests/steps/commands.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 Dominik Sekotill +# Copyright 2021-2023 Dominik Sekotill # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -15,10 +15,12 @@ import shlex from typing import TYPE_CHECKING from behave import then +from behave import use_fixture from behave import when from behave_utils.behave import PatternEnum from behave_utils.behave import register_pattern from wp import Container +from wp import running_site_fixture if TYPE_CHECKING: from behave.runner import Context @@ -54,8 +56,9 @@ def run_command(context: Context, args: Arguments) -> None: """ if len(args) == 0: raise ValueError("No arguments in argument list") + site = use_fixture(running_site_fixture, context) if args[0] in ('wp', 'php'): - container: Container = context.site.backend + container: Container = site.backend else: raise ValueError(f"Unknown command: {args[0]}") context.process = container.run(args, capture_output=True) diff --git a/tests/steps/pages.py b/tests/steps/pages.py index 8baab0c..c240b0a 100644 --- a/tests/steps/pages.py +++ b/tests/steps/pages.py @@ -24,6 +24,7 @@ from behave_utils import JSONArray from behave_utils import JSONObject from behave_utils import PatternEnum from request_steps import get_request +from wp import running_site_fixture DEFAULT_CONTENT = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut @@ -48,13 +49,13 @@ def assert_not_exist(context: Context, path: str) -> None: """ Assert that the path does not route to any resource """ + site = use_fixture(running_site_fixture, context) cmd = [ "post", "list", "--field=url", "--format=json", "--post_type=post,page", "--post_status=publish", ] - urls = {*context.site.backend.cli(*cmd, deserialiser=JSONArray.from_string)} - assert context.site.url / path not in urls, \ - f"{context.site.url / path} exists" + urls = {*site.backend.cli(*cmd, deserialiser=JSONArray.from_string)} + assert site.url / path not in urls, f"{site.url / path} exists" @given("a blank {post_type:PostType} exists") @@ -128,7 +129,7 @@ def wp_post( """ Create a WP post fixture of the given type with the given content """ - wp = context.site.backend + wp = use_fixture(running_site_fixture, context).backend postid = wp.cli( "post", "create", f"--post_type={post_type.value}", "--post_status=publish", @@ -162,7 +163,7 @@ def set_specials( Pages are reset at the end of a scenario """ - wp = context.site.backend + wp = use_fixture(running_site_fixture, context).backend options = { opt["option_name"]: opt["option_value"] diff --git a/tests/steps/request_steps.py b/tests/steps/request_steps.py index 3c3d6e0..59afa96 100644 --- a/tests/steps/request_steps.py +++ b/tests/steps/request_steps.py @@ -113,8 +113,9 @@ def get_request(context: Context, url: URL) -> None: """ Assign the response from making a GET request to "url" to the context """ + site = use_fixture(running_site_fixture, context) session = use_fixture(requests_session, context) - context.response = session.get(context.site.url / url, allow_redirects=False) + context.response = session.get(site.url / url, allow_redirects=False) @when("data is sent with {method:Method} to {url:URL}") @@ -124,10 +125,11 @@ def post_request(context: Context, method: Method, url: URL) -> None: """ if context.text is None: raise ValueError("Missing data, please add as text to step definition") + site = use_fixture(running_site_fixture, context) session = use_fixture(requests_session, context) context.response = session.request( method.value, - context.site.url / url, + site.url / url, data=context.text.strip().format(context=context).encode("utf-8"), allow_redirects=False, ) @@ -159,7 +161,8 @@ def assert_header(context: Context, header_name: str, header_value: str) -> None Assert that an expected header was received during a previous step """ if SAMPLE_SITE_NAME in header_value: - header_value = header_value.replace(SAMPLE_SITE_NAME, context.site.url) + site = use_fixture(running_site_fixture, context) + header_value = header_value.replace(SAMPLE_SITE_NAME, site.url) headers = context.response.headers assert header_name in headers, \ f"Expected header not found in response: {header_name!r}" diff --git a/tests/steps/static.py b/tests/steps/static.py index 76ce819..f4ce5d8 100644 --- a/tests/steps/static.py +++ b/tests/steps/static.py @@ -17,6 +17,7 @@ from behave import given from behave import use_fixture from behave.runner import Context from behave_utils.docker import Cli +from wp import running_site_fixture @given("{path} exists in the {container}") @@ -32,7 +33,8 @@ def container_file(context: Context, path: str, container_name: str) -> Iterator """ Create a file in a named container as a fixture """ - container = getattr(context.site, container_name) + site = use_fixture(running_site_fixture, context) + container = getattr(site, container_name) run = Cli(container) run("tee", path, input=(context.text or "This is a data file!")) yield -- GitLab From ba21409acc42eb1ab4edadf73f95473bc81e980a Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Tue, 28 Feb 2023 19:06:18 +0000 Subject: [PATCH 10/21] Add missing docstrings in test module --- tests/wp.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/wp.py b/tests/wp.py index bd4ade2..d6f5c8f 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 Dominik Sekotill +# Copyright 2021-2023 Dominik Sekotill # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -75,6 +75,9 @@ class Wordpress(Container): @contextmanager def started(self: T) -> Iterator[T]: + """ + Return a context in which the container is guaranteed to be started and running + """ with self: self.start() cmd = ["bash", "-c", "[[ /proc/1/exe -ef `which php-fpm` ]]"] @@ -127,6 +130,9 @@ class Site: @classmethod @contextmanager def build(cls: type[T], site_url: URL) -> Iterator[T]: + """ + Return a context that constructs a ready-to-go instance on entry + """ test_dir = Path(__file__).parent db_init = test_dir / "mysql-init.sql" @@ -140,7 +146,7 @@ class Site: @contextmanager def running(self: T) -> Iterator[T]: """ - Start all the services and configure the network + Return a context in which all containers are guaranteed to be started and running """ if self._running: yield self @@ -155,6 +161,9 @@ class Site: @property def address(self) -> IPv4Address: + """ + Return an IPv4 address through which test code can access the site + """ if self._address is None: if not self.frontend.is_running(): raise RuntimeError( -- GitLab From d3e7c0e6cfa15ecec7a8968e1e03c05098308d88 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Tue, 28 Feb 2023 19:06:32 +0000 Subject: [PATCH 11/21] Rewrite site_fixture to be idempotent(ish) --- tests/environment.py | 5 +---- tests/wp.py | 34 +++++++++++++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/tests/environment.py b/tests/environment.py index a3e2a38..5f2f04b 100644 --- a/tests/environment.py +++ b/tests/environment.py @@ -21,7 +21,6 @@ from behave import use_fixture from behave.model import Feature from behave.model import Scenario from behave.runner import Context -from behave_utils import URL from behave_utils.mysql import snapshot_rollback from wp import running_site_fixture @@ -29,14 +28,12 @@ if TYPE_CHECKING: from behave.runner import FeatureContext from behave.runner import ScenarioContext -SITE_URL = URL("http://test.example.com") - def before_all(context: Context) -> None: """ Prepare fixtures for all tests """ - use_fixture(running_site_fixture, context, site_url=SITE_URL) + use_fixture(running_site_fixture, context) def before_feature(context: FeatureContext, feature: Feature) -> None: diff --git a/tests/wp.py b/tests/wp.py index d6f5c8f..932113c 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -31,6 +31,7 @@ from behave_utils.mysql import Mysql BUILD_CONTEXT = Path(__file__).parent.parent DEFAULT_URL = URL("http://test.example.com") +CURRENT_SITE = URL("current://") class Wordpress(Container): @@ -177,31 +178,42 @@ class Site: @fixture -def site_fixture(context: Context, /, site_url: URL|None = None) -> Iterator[Site]: +def site_fixture(context: Context, /, url: URL = CURRENT_SITE) -> Iterator[Site]: """ Return a currently in-scope Site instance when used with `use_fixture` - If "site_url" is provided and it doesn't match a current Site instance, a new instance + If "url" is provided and it doesn't match a current Site instance, a new instance will be created in the current context. >>> use_fixture(site_fixture, context) <<< """ - if hasattr(context, "site"): - assert isinstance(context.site, Site) - if site_url is None or context.site.url == site_url: - yield context.site - return - with Site.build(site_url or DEFAULT_URL) as context.site: - yield context.site + if not hasattr(context, "sites"): + context.sites = dict[URL, Site]() + assert len(context.sites) == 0 or \ + len(context.sites) >= 2 and CURRENT_SITE in context.sites, \ + f'Both ["url" or DEFAULT_URL] and [CURRENT_SITE] must be added to sites: ' \ + f'{context.sites!r}' + if url in context.sites: + yield context.sites[url] + return + url = DEFAULT_URL if url == CURRENT_SITE else url + prev = context.sites.get(CURRENT_SITE) + with Site.build(url) as context.sites[url]: + context.sites[CURRENT_SITE] = context.sites[url] + yield context.sites[url] + del context.sites[url] + del context.sites[CURRENT_SITE] + if prev: + context.sites[CURRENT_SITE] = prev @fixture -def running_site_fixture(context: Context, /, site_url: URL|None = None) -> Iterator[Site]: +def running_site_fixture(context: Context, /, url: URL = CURRENT_SITE) -> Iterator[Site]: """ Return a currently in-scope Site instance that is running when used with `use_fixture` Like `site_fixture` but additionally entered into the `Site.running` context manager. """ - with use_fixture(site_fixture, context, site_url=site_url).running() as site: + with use_fixture(site_fixture, context, url=url).running() as site: yield site -- GitLab From c673cf22e30f06af58b4d6f02cf6f54c565ec19f Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Tue, 28 Feb 2023 19:30:05 +0000 Subject: [PATCH 12/21] Use PEP 673 "Self" type --- tests/requirements.txt | 1 + tests/wp.py | 15 ++++----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index bb41f9e..e8b777a 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,3 +3,4 @@ Python ~=3.9; python_version < '3.9' behave behave-utils ~=0.3.2 requests ~=2.26 +typing-extensions ~=4.0 diff --git a/tests/wp.py b/tests/wp.py index 932113c..d14d01c 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -13,9 +13,7 @@ from __future__ import annotations from contextlib import contextmanager from os import environ from pathlib import Path -from typing import TYPE_CHECKING from typing import Iterator -from typing import TypeVar from behave import fixture from behave import use_fixture @@ -28,6 +26,7 @@ from behave_utils.docker import Image from behave_utils.docker import IPv4Address from behave_utils.docker import Network from behave_utils.mysql import Mysql +from typing_extensions import Self BUILD_CONTEXT = Path(__file__).parent.parent DEFAULT_URL = URL("http://test.example.com") @@ -41,9 +40,6 @@ class Wordpress(Container): DEFAULT_ALIASES = ("upstream",) - if TYPE_CHECKING: - T = TypeVar("T", bound="Wordpress") - def __init__(self, site_url: URL, database: Mysql, network: Network|None = None): Container.__init__( self, @@ -75,7 +71,7 @@ class Wordpress(Container): return Cli(self, "wp") @contextmanager - def started(self: T) -> Iterator[T]: + def started(self) -> Iterator[Self]: """ Return a context in which the container is guaranteed to be started and running """ @@ -109,9 +105,6 @@ class Site: Manage all the containers of a site fixture """ - if TYPE_CHECKING: - T = TypeVar("T", bound="Site") - def __init__( self, url: URL, @@ -130,7 +123,7 @@ class Site: @classmethod @contextmanager - def build(cls: type[T], site_url: URL) -> Iterator[T]: + def build(cls, site_url: URL) -> Iterator[Self]: """ Return a context that constructs a ready-to-go instance on entry """ @@ -145,7 +138,7 @@ class Site: yield cls(site_url, network, frontend, backend, database) @contextmanager - def running(self: T) -> Iterator[T]: + def running(self) -> Iterator[Self]: """ Return a context in which all containers are guaranteed to be started and running """ -- GitLab From 0773738621529ea341c9cdccc6f4bc2c4085d23c Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Sun, 12 Mar 2023 02:38:02 +0000 Subject: [PATCH 13/21] Upgrade behave_utils to 0.4 --- .pre-commit-config.yaml | 2 +- tests/requirements.txt | 2 +- tests/wp.py | 23 +++++++++++------------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d14d36c..b828f2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,4 +93,4 @@ repos: args: ["--python-version=3.9"] additional_dependencies: - types-requests - - behave-utils ~=0.3.3 + - behave-utils ~=0.4.1 diff --git a/tests/requirements.txt b/tests/requirements.txt index e8b777a..6c2c89d 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ Python ~=3.9; python_version < '3.9' behave -behave-utils ~=0.3.2 +behave-utils ~=0.4.0 requests ~=2.26 typing-extensions ~=4.0 diff --git a/tests/wp.py b/tests/wp.py index d14d01c..6c6d2e7 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -25,6 +25,7 @@ from behave_utils.docker import Container as Container from behave_utils.docker import Image from behave_utils.docker import IPv4Address from behave_utils.docker import Network +from behave_utils.docker import inspect from behave_utils.mysql import Mysql from typing_extensions import Self @@ -78,7 +79,7 @@ class Wordpress(Container): with self: self.start() cmd = ["bash", "-c", "[[ /proc/1/exe -ef `which php-fpm` ]]"] - wait(lambda: self.is_running() and self.run(cmd).returncode == 0, timeout=600) + wait(lambda: self.run(cmd).returncode == 0, timeout=600) yield self @@ -127,15 +128,13 @@ class Site: """ Return a context that constructs a ready-to-go instance on entry """ - test_dir = Path(__file__).parent - db_init = test_dir / "mysql-init.sql" - - with Network() as network, Mysql(network=network, init_files=[db_init]) as database: - database.start() # Get a head start on initialising the database - with \ - Wordpress(site_url, database, network=network) as backend, \ - Nginx(backend, network=network) as frontend: - yield cls(site_url, network, frontend, backend, database) + with ( + Network() as network, + Mysql(network=network) as database, + Wordpress(site_url, database, network=network) as backend, + Nginx(backend, network=network) as frontend, + ): + yield cls(site_url, network, frontend, backend, database) @contextmanager def running(self) -> Iterator[Self]: @@ -146,7 +145,7 @@ class Site: yield self return self._running = True - with self.database.started(), self.backend.started(), self.frontend.started(): + with self.backend.started(), self.frontend.started(): try: yield self finally: @@ -163,7 +162,7 @@ class Site: raise RuntimeError( "Site.address may only be accessed inside a Site.running() context", ) - self._address = self.frontend.inspect().path( + self._address = inspect(self.frontend).path( f"$.NetworkSettings.Networks.{self.network}.IPAddress", str, IPv4Address, ) -- GitLab From 83cd775f2c9fc971ab2aac57738eb4578293e165 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Tue, 25 Apr 2023 22:58:10 +0100 Subject: [PATCH 14/21] Refactor container_file tests fixture --- tests/steps/static.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tests/steps/static.py b/tests/steps/static.py index f4ce5d8..3098cbb 100644 --- a/tests/steps/static.py +++ b/tests/steps/static.py @@ -10,6 +10,8 @@ Step implementations involving creating files in containers from __future__ import annotations +from pathlib import Path +from tempfile import NamedTemporaryFile from typing import Iterator from behave import fixture @@ -17,25 +19,39 @@ from behave import given from behave import use_fixture from behave.runner import Context from behave_utils.docker import Cli +from behave_utils.docker import Container from wp import running_site_fixture -@given("{path} exists in the {container}") -def step_impl(context: Context, path: str, container: str) -> None: +@given("{path:Path} exists in the {container_name}") +def step_impl(context: Context, path: Path, container_name: str) -> None: """ Create a file in the named container """ + site = use_fixture(running_site_fixture, context) + container = getattr(site, container_name) use_fixture(container_file, context, path, container) @fixture -def container_file(context: Context, path: str, container_name: str) -> Iterator[None]: +def container_file(context: Context, path: Path, container: Container) -> Iterator[None]: """ - Create a file in a named container as a fixture + Create a file in a container as a fixture """ - site = use_fixture(running_site_fixture, context) - container = getattr(site, container_name) - run = Cli(container) - run("tee", path, input=(context.text or "This is a data file!")) - yield - run("rm", path) + text = context.text.encode("utf-8") if context.text else b"This is a data file!" + + # For running containers, use commands within the container to make and delete the file + # This relies on "tee" and "rm" existing in the container image + if container.is_running(): + run = Cli(container) + run("tee", path, input=text) + yield + run("rm", path) + return + + # For unstarted containers, write to a temporary file and add it to the volumes mapping + with NamedTemporaryFile("wb") as temp: + temp.write(text) + temp.close() + container.volumes.append((path, Path(temp.name))) + yield -- GitLab From 0810eeaafac12cb9da5e2dc520838766367163da Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Sun, 23 Apr 2023 23:38:21 +0100 Subject: [PATCH 15/21] Add tests for configuration files settings --- .pre-commit-config.yaml | 1 + tests/configs/bad-plugins.txt | 1 + tests/configs/make-eggs.php | 9 ++ tests/configs/make-ham.php | 9 ++ tests/configs/make-spam.php | 9 ++ tests/configs/plugins.conf | 5 + tests/configs/plugins.txt | 1 + tests/configuration-files.feature | 121 ++++++++++++++++ tests/configuration-site.feature | 30 ++++ tests/steps/request_steps.py | 13 ++ tests/steps/site.py | 228 ++++++++++++++++++++++++++++++ tests/steps/static.py | 57 -------- tests/wp.py | 1 - 13 files changed, 427 insertions(+), 58 deletions(-) create mode 100644 tests/configs/bad-plugins.txt create mode 100644 tests/configs/make-eggs.php create mode 100644 tests/configs/make-ham.php create mode 100644 tests/configs/make-spam.php create mode 100644 tests/configs/plugins.conf create mode 100644 tests/configs/plugins.txt create mode 100644 tests/configuration-files.feature create mode 100644 tests/configuration-site.feature create mode 100644 tests/steps/site.py delete mode 100644 tests/steps/static.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b828f2d..bccbe62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,7 @@ repos: - id: check-for-squash - id: copyright-notice exclude: ^data/|^scripts/(compile-|install-) + args: [--min-size=1000] - id: protect-first-parent - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/tests/configs/bad-plugins.txt b/tests/configs/bad-plugins.txt new file mode 100644 index 0000000..50121a1 --- /dev/null +++ b/tests/configs/bad-plugins.txt @@ -0,0 +1 @@ +this-plugin-does-not-exist diff --git a/tests/configs/make-eggs.php b/tests/configs/make-eggs.php new file mode 100644 index 0000000..66a4189 --- /dev/null +++ b/tests/configs/make-eggs.php @@ -0,0 +1,9 @@ +// Awesome Website of King Arthur + """ + And the email address of arthur is "the.king@kodo.org.uk" + And the password of arthur is "password1" diff --git a/tests/steps/request_steps.py b/tests/steps/request_steps.py index 59afa96..ae98172 100644 --- a/tests/steps/request_steps.py +++ b/tests/steps/request_steps.py @@ -12,6 +12,7 @@ from __future__ import annotations import json from collections.abc import Collection +from textwrap import dedent from typing import Any from typing import Iterator from typing import TypeVar @@ -179,3 +180,15 @@ def assert_is_json(context: Context) -> None: context.response.json() except json.JSONDecodeError: raise AssertionError("Response is not a JSON document") + + +@then("the response body contains") +def assert_body_contains(context: Context) -> None: + """ + Assert the response body of a previous step contains the text attached to the step + """ + if context.text is None: + raise ValueError("this step needs text to check") + text = dedent(context.text).encode("utf-8") + assert text in context.response.content, \ + f"text not found in {context.response.content[:100]}" diff --git a/tests/steps/site.py b/tests/steps/site.py new file mode 100644 index 0000000..4b83fb9 --- /dev/null +++ b/tests/steps/site.py @@ -0,0 +1,228 @@ +# Copyright 2023 Dominik Sekotill +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +Step implementations involving setting configurations in the backend +""" + +from __future__ import annotations + +from base64 import b32encode as b32 +from collections.abc import Iterator +from pathlib import Path +from tempfile import NamedTemporaryFile + +from behave import fixture +from behave import given +from behave import then +from behave import use_fixture +from behave import when +from behave.runner import Context +from behave_utils.behave import PatternEnum +from behave_utils.behave import register_pattern +from behave_utils.docker import Cli +from behave_utils.docker import Container +from behave_utils.url import URL +from wp import CURRENT_SITE +from wp import Site +from wp import running_site_fixture +from wp import site_fixture + +CONFIG_DIR = Path(__file__).parent.parent / "configs" +DELAYED_SITE = URL("http://delayed.example.com") + + +register_pattern("\S+", Path) + + +class Addon(PatternEnum): + """ + Addon types for WP; i.e. themes or plugins + """ + + theme = "theme" + plugin = "plugin" + + +class Status(PatternEnum): + """ + Status values for `wp {plugin|theme} is-installed` commands + + The values of these enums are the expected return codes from executing one of the above + commands. + """ + + active = 0 + inactive = 1 + + +@fixture +def unstarted_site_fixture(context: Context, /, url: URL = CURRENT_SITE) -> Iterator[Site]: + """ + Return a wrapper around `wp.site_fixture` that checks the site has not been started + """ + site = use_fixture(site_fixture, context, url) + assert not site.backend.is_running(raise_on_exit=True), \ + 'Please run this step after the step "Given the site is not running"' + yield site + + +@fixture +def container_file( + context: Context, + container: Container, + path: Path, + contents: bytes, +) -> Iterator[None]: + """ + Create a file in a container as a fixture + """ + # For running containers, use commands within the container to make and delete the file + # This relies on "tee" and "rm" existing in the container image + if container.is_running(): + run = Cli(container) + run("tee", path, input=contents) + yield + run("rm", path) + return + + # For unstarted containers, write to a temporary file and add it to the volumes mapping + with NamedTemporaryFile("wb") as temp: + temp.write(contents) + temp.flush() + container.volumes.append((Path(temp.name), path)) + yield + + +@given("the site is not running") +def unstarted_site(context: Context) -> None: + """ + Mask any current site with a new, unstarted one for the rest of a feature or scenario + """ + uid = b32(id(context.feature).to_bytes(10, "big")).decode("ascii") + use_fixture(site_fixture, context, url=URL(f"http://{uid}.delayed.example.com")) + + +@given("{fixture:Path} is mounted in {directory:Path}") +@given("{fixture:Path} is mounted in {directory:Path} as {name}") +def mount_volume( + context: Context, + fixture: Path, + directory: Path, + name: str|None = None, +) -> None: + """ + Prepare volume mounts in the backend + """ + fixture = CONFIG_DIR / fixture + if not fixture.exists(): + raise FileNotFoundError(fixture) + if fixture.is_dir(): + raise IsADirectoryError(fixture) + if name is None: + name = fixture.name + + site = use_fixture(unstarted_site_fixture, context, CURRENT_SITE) + site.backend.volumes.append((fixture.absolute(), directory / name)) + + +@given("{path:Path} exists in the {container_name}") +def create_file(context: Context, path: Path, container_name: str) -> None: + """ + Create a file in the named container + """ + site = use_fixture(site_fixture, context) + container = getattr(site, container_name) + content = context.text.encode("utf-8") if context.text else b"This is a data file!" + use_fixture(container_file, context, container, path, content) + + +@given("{path:Path} contains") +def write_file(context: Context, path: Path) -> None: + """ + Write the contents of the step's text string to a fixture Path + """ + if context.text is None: + raise ValueError("A text value is needed for this step") + # If creating a file in /etc/wordpress there is not much point unless the site is + # unstarted, so use unstarted_site_fixture to ensure it's checked + site = use_fixture( + unstarted_site_fixture if Path("/etc/wordpress") in path.parents else site_fixture, + context, + ) + content = context.text.encode("utf-8") + b"\n" + use_fixture(container_file, context, site.backend, path, content) + + +@given("the environment variable {name} is \"{value}\"") +def set_environment(context: Context, name: str, value: str) -> None: + """ + Set the named environment variable in the backend + """ + site = use_fixture(unstarted_site_fixture, context, CURRENT_SITE) + site.backend.env[name] = value + + +@when("the site is started") +def start_backend(context: Context) -> None: + """ + Start the site backend (FPM) + """ + use_fixture(running_site_fixture, context, CURRENT_SITE) + + +@then("the {addon:Addon} {name} is installed") +@then("the {addon:Addon} {name} is {status:Status}") +def is_plugin_installed( + context: Context, + addon: Addon, + name: str, + status: Status|None = None, +) -> None: + """ + Check that the named theme or plugin is installed + """ + site = use_fixture(site_fixture, context, CURRENT_SITE) + assert site.backend.cli(addon.value, "is-installed", name, query=True) == 0, \ + f"{addon.name} {name} is not installed" + if status is not None: + assert site.backend.cli(addon.value, "is-active", name, query=True) == status.value + + +@then("the email address of {user} is \"{value}\"") +@then("the email address of {user} is '{value}'") +def is_user_email(context: Context, user: str, value: str) -> None: + """ + Check that the email address of an existing user matches the given value + """ + site = use_fixture(site_fixture, context, CURRENT_SITE) + email = site.backend.cli( + "user", "get", user, "--field=email", + deserialiser=lambda mv: str(mv, "utf-8").strip(), + ) + assert email == value, f"user's email {email} != {value}" + + +@then("the password of {user} is \"{value}\"") +@then("the password of {user} is '{value}'") +def is_user_password(context: Context, user: str, value: str) -> None: + """ + Check that the password of an existing user matches the given value + """ + site = use_fixture(site_fixture, context, CURRENT_SITE) + assert site.backend.cli("user", "check-password", user, value, query=True) == 0, \ + "passwords do not match" + + +@then("the password of {user} is not \"{value}\"") +@then("the password of {user} is not '{value}'") +def is_not_user_password(context: Context, user: str, value: str) -> None: + """ + Check that the password of an existing user does not match the given value + """ + site = use_fixture(site_fixture, context, CURRENT_SITE) + assert site.backend.cli("user", "check-password", user, value, query=True) != 0, \ + "passwords match" diff --git a/tests/steps/static.py b/tests/steps/static.py deleted file mode 100644 index 3098cbb..0000000 --- a/tests/steps/static.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2022-2023 Dominik Sekotill -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -""" -Step implementations involving creating files in containers -""" - -from __future__ import annotations - -from pathlib import Path -from tempfile import NamedTemporaryFile -from typing import Iterator - -from behave import fixture -from behave import given -from behave import use_fixture -from behave.runner import Context -from behave_utils.docker import Cli -from behave_utils.docker import Container -from wp import running_site_fixture - - -@given("{path:Path} exists in the {container_name}") -def step_impl(context: Context, path: Path, container_name: str) -> None: - """ - Create a file in the named container - """ - site = use_fixture(running_site_fixture, context) - container = getattr(site, container_name) - use_fixture(container_file, context, path, container) - - -@fixture -def container_file(context: Context, path: Path, container: Container) -> Iterator[None]: - """ - Create a file in a container as a fixture - """ - text = context.text.encode("utf-8") if context.text else b"This is a data file!" - - # For running containers, use commands within the container to make and delete the file - # This relies on "tee" and "rm" existing in the container image - if container.is_running(): - run = Cli(container) - run("tee", path, input=text) - yield - run("rm", path) - return - - # For unstarted containers, write to a temporary file and add it to the volumes mapping - with NamedTemporaryFile("wb") as temp: - temp.write(text) - temp.close() - container.volumes.append((path, Path(temp.name))) - yield diff --git a/tests/wp.py b/tests/wp.py index 6c6d2e7..e0542a3 100644 --- a/tests/wp.py +++ b/tests/wp.py @@ -55,7 +55,6 @@ class Wordpress(Container): ], env=dict( SITE_URL=site_url, - SITE_ADMIN_EMAIL="test@kodo.org.uk", DB_NAME=database.name, DB_USER=database.user, DB_PASS=database.password, -- GitLab From 74b3c0d0d4fc303a6e7a6dc7459ddfa1e67c0163 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Wed, 26 Apr 2023 19:57:22 +0100 Subject: [PATCH 16/21] Fix convenience file reading This feature appeared broken for unknown reasons, which was discovered by behaviour testing. --- scripts/entrypoint.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 4089fb0..4e267be 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -276,6 +276,18 @@ run_background_cron() exec -a wp-cron /bin/bash <<&2 "WARNING: unterminated line in" `readlink /proc/self/fd/0` + ARRAY+=( "$line" ) + fi +} + mkdir -p ${CONFIG_DIR} cd ${CONFIG_DIR} @@ -284,13 +296,13 @@ for file in **/*.conf; do done if [[ -e ${PLUGINS_LIST:=${CONFIG_DIR}/plugins.txt} ]]; then - PLUGINS+=( $(<"${PLUGINS_LIST}") ) + readlines PLUGINS <"${PLUGINS_LIST}" fi if [[ -e ${THEMES_LIST:=${CONFIG_DIR}/themes.txt} ]]; then - THEMES+=( $(<"${THEMES_LIST}") ) + readlines THEMES <"${THEMES_LIST}" fi if [[ -e ${LANGUAGES_LIST:=${CONFIG_DIR}/languages.txt} ]]; then - LANGUAGES+=( $(<"${LANGUAGES_LIST}") ) + readlines LANGUAGES <"${LANGUAGES_LIST}" fi declare -a extra_args -- GitLab From 245654fae89d361f3f7cda7801db80ffff9940b5 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Wed, 26 Apr 2023 20:14:14 +0100 Subject: [PATCH 17/21] Clarify format of convenience files in docs --- doc/configuration.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index bfa468d..2c59b89 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -55,6 +55,10 @@ options are optional plain text files listing additional entries (one per line) to append to the [**PLUGINS**](#plugins), [**THEMES**](#themes) and [**LANGUAGES**](#languages) option arrays respectively. +> **Note:** Every line in these files MUST be correctly terminated in the Unix +> style (with a line-feed character). Please pay special attention to the final +> line as some text editors do not correctly terminate them. + Options ------- @@ -115,7 +119,7 @@ the missing language pack will be silently ignored. **Required**: no\ **Default**: /etc/wordpress/languages.txt -The path to a file containing lines to append to +The path to a plain text file containing lines to append to [**LANGUAGES**](#languages). ### PLUGINS @@ -133,7 +137,8 @@ be the latest stable available in the wordpress.org registry. **Required**: no\ **Default**: /etc/wordpress/plugins.txt -The path to a file containing lines to append to [**PLUGINS**](#plugins). +The path to a plain text file containing lines to append to +[**PLUGINS**](#plugins). ### PHP_DIRECTIVES @@ -231,7 +236,8 @@ latest stable available in the wordpress.org registry. **Required**: no\ **Default**: /etc/wordpress/themes.txt -The path to a file containing lines to append to [**THEMES**](#themes). +The path to a plain text file containing lines to append to +[**THEMES**](#themes). ### WP_CONFIGS -- GitLab From c8476624b1c0e09b81e1528825edc6f0f11e074b Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Wed, 26 Apr 2023 20:41:50 +0100 Subject: [PATCH 18/21] Correct the documentation for environment variables --- doc/configuration.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 2c59b89..87e6191 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -39,10 +39,14 @@ to the container, however: - Unlike the way most configuration systems treat environment variables, they do not overwrite options provided in the configuration files unless - the files are specifically written to honour the environment variables. - -- For array options the value provided in the environment variable will - become the first item (index 0). + the files are specifically written to honour the environment variables, + for instance by using the "+=" operator to append to arrays. + +- For array options the value provided will be split on whitespace. + Unfortunately at the moment there is no way to escape or quote whitespace + within items. If you find yourself needing to add items with whitespace, + consider using configuration files instead, which are interpreted with + Bash, or one of the [convenience files](#convenience-files). Convenience Files -- GitLab From 56b45c24a2cbf410dcdc3696622079f276d9c341 Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Fri, 28 Apr 2023 01:48:06 +0100 Subject: [PATCH 19/21] Pin pipeline build template --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3442e0a..0858d8b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,6 +48,7 @@ Build Images: trigger: include: - project: dom/project-templates + ref: f7997d0d file: /pipeline-templates/build-image.yml strategy: depend -- GitLab From d4b215520bd250b2f6aadee7d7aaba5c7c3f21cc Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Mon, 17 Jun 2024 13:41:03 +0100 Subject: [PATCH 20/21] Stop test configs from breaking wp-cli --- tests/configs/make-eggs.php | 2 +- tests/configs/make-ham.php | 2 +- tests/configs/make-spam.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/configs/make-eggs.php b/tests/configs/make-eggs.php index 66a4189..3791571 100644 --- a/tests/configs/make-eggs.php +++ b/tests/configs/make-eggs.php @@ -3,7 +3,7 @@ // Serves the word "eggs" at /eggs // Used for checking that this file has been loaded correctly -if ($_SERVER['REQUEST_URI'] == '/eggs') { +if ( !defined('WP_CLI') && $_SERVER['REQUEST_URI'] == '/eggs' ) { echo("eggs"); exit; } diff --git a/tests/configs/make-ham.php b/tests/configs/make-ham.php index b2d1c3e..7342d09 100644 --- a/tests/configs/make-ham.php +++ b/tests/configs/make-ham.php @@ -3,7 +3,7 @@ // Serves the word "ham" at /ham // Used for checking that this file has been loaded correctly -if ($_SERVER['REQUEST_URI'] == '/ham') { +if ( !defined('WP_CLI') && $_SERVER['REQUEST_URI'] == '/ham' ) { echo("ham"); exit; } diff --git a/tests/configs/make-spam.php b/tests/configs/make-spam.php index 64bf045..786e1be 100644 --- a/tests/configs/make-spam.php +++ b/tests/configs/make-spam.php @@ -3,7 +3,7 @@ // Serves the word "spam" at /spam // Used for checking that this file has been loaded correctly -if ($_SERVER['REQUEST_URI'] == '/spam') { +if ( !defined('WP_CLI') && $_SERVER['REQUEST_URI'] == '/spam' ) { echo("spam"); exit; } -- GitLab From 7e886499109386936f7ce657f9c56a2cd75fae5a Mon Sep 17 00:00:00 2001 From: Dom Sekotill Date: Mon, 17 Jun 2024 13:48:25 +0100 Subject: [PATCH 21/21] Use an off-the-shelf plugin for testing --- tests/configs/plugins.conf | 2 +- tests/configs/plugins.txt | 2 +- tests/configuration-files.feature | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/configs/plugins.conf b/tests/configs/plugins.conf index 0c347ae..9ac4f8d 100644 --- a/tests/configs/plugins.conf +++ b/tests/configs/plugins.conf @@ -1,5 +1,5 @@ PLUGINS=( - "https://code.kodo.org.uk/api/v4/projects/94/jobs/artifacts/main/raw/wp-test-plugin.zip?job=build" + "wp-dummy-content-generator" ) # vim: ft=bash diff --git a/tests/configs/plugins.txt b/tests/configs/plugins.txt index c8943d9..4779f15 100644 --- a/tests/configs/plugins.txt +++ b/tests/configs/plugins.txt @@ -1 +1 @@ -https://code.kodo.org.uk/api/v4/projects/94/jobs/artifacts/main/raw/wp-test-plugin.zip?job=build +wp-dummy-content-generator diff --git a/tests/configuration-files.feature b/tests/configuration-files.feature index 64fc2a9..c6a89dd 100644 --- a/tests/configuration-files.feature +++ b/tests/configuration-files.feature @@ -15,7 +15,7 @@ Feature: Configuration files and environment And plugins.conf is mounted in /etc/wordpress/extras/ When the site is started Then the theme sela is active - And the plugin wp-test-plugin is installed + And the plugin wp-dummy-content-generator is installed Scenario: Nested directories of "*config.php" files are found and integrated Given make-spam.php is mounted in /etc/wordpress/ as spam-config.php @@ -61,7 +61,7 @@ Feature: Configuration files and environment Scenario: A "plugins.txt" file is used as a source of plugins Given plugins.txt is mounted in /etc/wordpress/ When the site is started - Then the plugin wp-test-plugin is installed + Then the plugin wp-dummy-content-generator is installed Scenario: A "themes.txt" file is used as a source of plugins Given /etc/wordpress/themes.txt contains: @@ -88,7 +88,7 @@ Feature: Configuration files and environment And plugins.txt is mounted in /etc/wordpress/ as real-plugins.txt And the environment variable PLUGINS_LIST is "/etc/wordpress/real-plugins.txt" When the site is started - Then the plugin wp-test-plugin is installed + Then the plugin wp-dummy-content-generator is installed Scenario: When THEMES_LIST is provided, it replaces "themes.txt" Given /etc/wordpress/themes.txt contains: -- GitLab