Skip to content

Commit

Permalink
Use fake HTTP server for the Patroni API in tests (WIP)
Browse files Browse the repository at this point in the history
We introduce a few fixtures, defined in tests/conftest.py:

- patroni_api, an HTTP server serving files in a temporary directory and
  with an 'installed()' context manager method to be used in actual
  tests to setup expected responses based on specified JSON files,
- runner, a CliRunner, configured with stdout and stderr separated as
  logs of the HTTP server thread would appear in stderr, and if mixed
  with stdout, the assertions in test case will fail.

Fixtures use some logging in order to improve debugging.

The direct advantage of this is that PatroniResource.rest_api() method
is now covered by the test suite.

For test_api.py, there was no 'patroni.json' file in tests/json, so we
use a temporary file when serving the API.
  • Loading branch information
dlax committed Sep 29, 2023
1 parent 7f6abb7 commit 43049e9
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 91 deletions.
43 changes: 43 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging
import shutil
from contextlib import contextmanager
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from typing import Any, Iterator, Union

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


class PatroniAPI(HTTPServer):
def __init__(self, directory: Path, *, datadir: Path) -> None:
self.directory = directory
self.datadir = datadir
handler_cls = partial(SimpleHTTPRequestHandler, directory=str(directory))
super().__init__(("", 0), handler_cls)

def serve_forever(self, *args: Any) -> None:
logger.info(
"starting fake Patroni API at %s (directory=%s)",
self.endpoint,
self.directory,
)
return super().serve_forever(*args)

@property
def endpoint(self) -> str:
return f"http://{self.server_name}:{self.server_port}"

@contextmanager
def installed(self, **installed: Union[Path, str]) -> Iterator[None]:
"""Temporarilly install specified files in served directory."""
for dest, src in installed.items():
if isinstance(src, str):
src = self.datadir / src
shutil.copy(src, self.directory / dest)
try:
yield None
finally:
for i in installed:
(self.directory / i).unlink()
34 changes: 33 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from functools import partial
from typing import Any, Callable
from pathlib import Path
from threading import Thread
from typing import Any, Callable, Iterator

import pytest
from click.testing import CliRunner
from pytest_mock import MockerFixture

from . import PatroniAPI
from .tools import my_mock


Expand All @@ -28,3 +32,31 @@ def fake_restapi(
mocker: MockerFixture, use_old_replica_state: bool
) -> Callable[..., Any]:
return partial(my_mock, mocker, use_old_replica_state=use_old_replica_state)


@pytest.fixture(scope="session")
def datadir() -> Path:
return Path(__file__).parent / "json"


@pytest.fixture(scope="session")
def patroni_api(
tmp_path_factory: pytest.TempPathFactory,
datadir: Path,
use_old_replica_state: bool, # XXX
) -> Iterator[PatroniAPI]:
"""A fake HTTP server for the Patroni API serving files from a temporary
directory.
"""
httpd = PatroniAPI(tmp_path_factory.mktemp("api"), datadir=datadir)
t = Thread(target=httpd.serve_forever)
t.start()
yield httpd
httpd.shutdown()
t.join()


@pytest.fixture
def runner() -> CliRunner:
"""A CliRunner with stdout and stderr not mixed."""
return CliRunner(mix_stderr=False)
25 changes: 14 additions & 11 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from pathlib import Path

from click.testing import CliRunner

from check_patroni.cli import main

from . import PatroniAPI

def test_api_status_code_200(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("node_is_pending_restart_ok")
result = runner.invoke(
main, ["-e", "https://10.20.199.3:8008", "node_is_pending_restart"]
)
def test_api_status_code_200(
runner: CliRunner, patroni_api: PatroniAPI, tmp_path: Path
) -> None:
patroni_json = tmp_path / "patroni.json"
patroni_json.write_text('{"pending_restart": false}')
with patroni_api.installed(patroni=patroni_json):
result = runner.invoke(
main, ["-e", patroni_api.endpoint, "node_is_pending_restart"]
)
assert result.exit_code == 0


def test_api_status_code_404(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("Fake test", status=404)
def test_api_status_code_404(runner: CliRunner, patroni_api: PatroniAPI) -> None:
result = runner.invoke(
main, ["-e", "https://10.20.199.3:8008", "node_is_pending_restart"]
main, ["-e", patroni_api.endpoint, "node_is_pending_restart"]
)
assert result.exit_code == 3
51 changes: 26 additions & 25 deletions tests/test_cluster_config_has_changed.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
from pathlib import Path
from typing import Iterator

import nagiosplugin
import pytest
from click.testing import CliRunner

from check_patroni.cli import main

from . import PatroniAPI

def test_cluster_config_has_changed_ok_with_hash(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("cluster_config_has_changed")
@pytest.fixture(scope="module", autouse=True)
def _setup_patroni_api(patroni_api: PatroniAPI) -> Iterator[None]:
with patroni_api.installed(config="cluster_config_has_changed.json"):
yield None


def test_cluster_config_has_changed_ok_with_hash(
runner: CliRunner, patroni_api: PatroniAPI
) -> None:
result = runner.invoke(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--hash",
"96b12d82571473d13e890b893734e731",
Expand All @@ -28,20 +37,17 @@ def test_cluster_config_has_changed_ok_with_hash(fake_restapi) -> None:


def test_cluster_config_has_changed_ok_with_state_file(
fake_restapi, tmp_path: Path
runner: CliRunner, patroni_api: PatroniAPI, tmp_path: Path
) -> None:
runner = CliRunner()

state_file = tmp_path / "cluster_config_has_changed.state_file"
with state_file.open("w") as f:
f.write('{"hash": "96b12d82571473d13e890b893734e731"}')

fake_restapi("cluster_config_has_changed")
result = runner.invoke(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--state-file",
str(state_file),
Expand All @@ -54,15 +60,14 @@ def test_cluster_config_has_changed_ok_with_state_file(
)


def test_cluster_config_has_changed_ko_with_hash(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("cluster_config_has_changed")
def test_cluster_config_has_changed_ko_with_hash(
runner: CliRunner, patroni_api: PatroniAPI
) -> None:
result = runner.invoke(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--hash",
"96b12d82571473d13e890b8937ffffff",
Expand All @@ -76,21 +81,18 @@ def test_cluster_config_has_changed_ko_with_hash(fake_restapi) -> None:


def test_cluster_config_has_changed_ko_with_state_file_and_save(
fake_restapi, tmp_path: Path
runner: CliRunner, patroni_api: PatroniAPI, tmp_path: Path
) -> None:
runner = CliRunner()

state_file = tmp_path / "cluster_config_has_changed.state_file"
with state_file.open("w") as f:
f.write('{"hash": "96b12d82571473d13e890b8937ffffff"}')

fake_restapi("cluster_config_has_changed")
# test without saving the new hash
result = runner.invoke(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--state-file",
str(state_file),
Expand All @@ -115,7 +117,7 @@ def test_cluster_config_has_changed_ko_with_state_file_and_save(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--state-file",
str(state_file),
Expand All @@ -136,17 +138,16 @@ def test_cluster_config_has_changed_ko_with_state_file_and_save(
assert new_config_hash == "96b12d82571473d13e890b893734e731"


def test_cluster_config_has_changed_params(fake_restapi, tmp_path: Path) -> None:
def test_cluster_config_has_changed_params(
runner: CliRunner, patroni_api: PatroniAPI, tmp_path: Path
) -> None:
# This one is placed last because it seems like the exceptions are not flushed from stderr for the next tests.
runner = CliRunner()

fake_state_file = tmp_path / "fake_file_name.state_file"
fake_restapi("cluster_config_has_changed")
result = runner.invoke(
main,
[
"-e",
"https://10.20.199.3:8008",
patroni_api.endpoint,
"cluster_config_has_changed",
"--hash",
"640df9f0211c791723f18fc3ed9dbb95",
Expand Down
32 changes: 12 additions & 20 deletions tests/test_cluster_has_leader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,34 @@

from check_patroni.cli import main

from . import PatroniAPI

def test_cluster_has_leader_ok(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("cluster_has_leader_ok")
result = runner.invoke(
main, ["-e", "https://10.20.199.3:8008", "cluster_has_leader"]
)
def test_cluster_has_leader_ok(runner: CliRunner, patroni_api: PatroniAPI) -> None:
with patroni_api.installed(cluster="cluster_has_leader_ok.json"):
result = runner.invoke(main, ["-e", patroni_api.endpoint, "cluster_has_leader"])
assert result.exit_code == 0
assert (
result.stdout
== "CLUSTERHASLEADER OK - The cluster has a running leader. | has_leader=1;;@0\n"
)


def test_cluster_has_leader_ok_standby_leader(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("cluster_has_leader_ok_standby_leader")
result = runner.invoke(
main, ["-e", "https://10.20.199.3:8008", "cluster_has_leader"]
)
def test_cluster_has_leader_ok_standby_leader(
runner: CliRunner, patroni_api: PatroniAPI
) -> None:
with patroni_api.installed(cluster="cluster_has_leader_ok_standby_leader.json"):
result = runner.invoke(main, ["-e", patroni_api.endpoint, "cluster_has_leader"])
assert result.exit_code == 0
assert (
result.stdout
== "CLUSTERHASLEADER OK - The cluster has a running leader. | has_leader=1;;@0\n"
)


def test_cluster_has_leader_ko(fake_restapi) -> None:
runner = CliRunner()

fake_restapi("cluster_has_leader_ko")
result = runner.invoke(
main, ["-e", "https://10.20.199.3:8008", "cluster_has_leader"]
)
def test_cluster_has_leader_ko(runner: CliRunner, patroni_api: PatroniAPI) -> None:
with patroni_api.installed(cluster="cluster_has_leader_ko.json"):
result = runner.invoke(main, ["-e", patroni_api.endpoint, "cluster_has_leader"])
assert result.exit_code == 2
assert (
result.stdout
Expand Down
Loading

0 comments on commit 43049e9

Please sign in to comment.