From 5f316afce50e3d1363d45076e2237399fd516e75 Mon Sep 17 00:00:00 2001 From: Jesper Zedlitz Date: Sun, 25 Jan 2026 08:34:37 +0100 Subject: [PATCH 1/8] add pytest-asyncio as test dependency --- pyproject.toml | 1 + uv.lock | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 466ca5e..822685d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ version-file = "src/apkit/_version.py" dev = [ "coverage>=7.10.7", "pytest>=8.4.1", + "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", ] docs = [ diff --git a/uv.lock b/uv.lock index b00e690..a7b43af 100644 --- a/uv.lock +++ b/uv.lock @@ -185,6 +185,7 @@ server = [ dev = [ { name = "coverage" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, ] docs = [ @@ -216,6 +217,7 @@ provides-extras = ["redis", "server"] dev = [ { name = "coverage", specifier = ">=7.10.7" }, { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, ] docs = [ @@ -1682,6 +1684,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "pytest-cov" version = "7.0.0" From 070c1f975cffc1516aaf7d7d1ab216723e7d47fb Mon Sep 17 00:00:00 2001 From: Jesper Zedlitz Date: Sun, 25 Jan 2026 08:59:49 +0100 Subject: [PATCH 2/8] add test for InboxVerifier.verify --- tests/helper/test_inbox.py | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/helper/test_inbox.py diff --git a/tests/helper/test_inbox.py b/tests/helper/test_inbox.py new file mode 100644 index 0000000..b5469af --- /dev/null +++ b/tests/helper/test_inbox.py @@ -0,0 +1,72 @@ +from cryptography.hazmat.primitives.asymmetric import rsa +from apkit.config import AppConfig +from apkit.helper.inbox import InboxVerifier +from apsig import draft, ld_signature +from apkit.models import CryptographicKey, Person +from cryptography.hazmat.primitives import serialization as crypto_serialization + +import json +import pytest + + +def _prepare_signed_request(): + # prepare private and public keys + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_key_pem = ( + private_key.public_key() + .public_bytes( + encoding=crypto_serialization.Encoding.PEM, + format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo, + ) + .decode("utf-8") + ) + + # create an activity with an embedded Actor + body_json = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/likes", + "type": "Like", + "actor": { + "id": "https://example.com/actor", + "type": "Person", + "publicKey": { + "id": "http://example.com/actor#key", + "owner": "https://example.com/actor", + "publicKeyPem": public_key_pem, + }, + }, + "object": "https://example.com/5", + } + + body = json.dumps(body_json).encode("utf-8") + + url = "http://example.com/ap" + method = "POST" + headers = {"host": "example.com"} + + # sign the request + signer = draft.Signer( + headers=headers, + method=method, + url=url, + key_id="http://example.com/actor#key", + private_key=private_key, + body=body, + ) + headers = signer.sign() + + # the verifier expects all HTTP header names in lower case + headers["signature"] = headers["Signature"] + + return (body, url, method, headers) + + +@pytest.mark.asyncio +async def test_verify_draft_http_signature(): + (body, url, method, headers) = _prepare_signed_request() + + config = AppConfig() + inbox_verifier = InboxVerifier(config) + + result = await inbox_verifier.verify(body, url, method, headers) + assert result == True From 28190f18baac9f7652528ce1d5c8337741c25a02 Mon Sep 17 00:00:00 2001 From: Jesper Zedlitz Date: Mon, 26 Jan 2026 06:59:07 +0100 Subject: [PATCH 3/8] add test for apkit.cache --- tests/test_cache.py | 112 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/test_cache.py diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..a653683 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,112 @@ +import time +import pytest + +from apkit.cache import Cache + + +class FakeKeyValueStore: + """Minimal in-memory KeyValueStore for tests.""" + + def __init__(self): + self._data = {} + + def get(self, key): + return self._data.get(key) + + def set(self, key, value): + self._data[key] = value + + def delete(self, key): + self._data.pop(key, None) + + +@pytest.fixture +def store(): + return FakeKeyValueStore() + + +@pytest.fixture +def cache(store): + return Cache(store) + + +def test_set_and_get_without_ttl(cache): + cache.set("a", "value", ttl=None) + assert cache.get("a") == "value" + + +def test_set_and_get_with_ttl_not_expired(cache, monkeypatch): + monkeypatch.setattr(time, "time", lambda: 1000.0) + cache.set("a", "value", ttl=10) + + monkeypatch.setattr(time, "time", lambda: 1005.0) + assert cache.get("a") == "value" + + +def test_get_expired_item(cache, monkeypatch): + monkeypatch.setattr(time, "time", lambda: 1000.0) + cache.set("a", "value", ttl=5) + + monkeypatch.setattr(time, "time", lambda: 1006.0) + assert cache.get("a") is None + assert not cache.exists("a") + + +def test_set_with_non_positive_ttl_deletes(cache): + cache.set("a", "value", ttl=0) + assert cache.get("a") is None + + cache.set("b", "value", ttl=-5) + assert cache.get("b") is None + + +def test_delete(cache): + cache.set("a", "value", ttl=None) + cache.delete("a") + assert cache.get("a") is None + + +def test_exists_true(cache): + cache.set("a", "value", ttl=None) + assert cache.exists("a") is True + + +def test_exists_false_for_missing_key(cache): + assert cache.exists("missing") is False + + +def test_exists_false_for_expired_item(cache, monkeypatch): + monkeypatch.setattr(time, "time", lambda: 1000.0) + cache.set("a", "value", ttl=1) + + monkeypatch.setattr(time, "time", lambda: 1002.0) + assert cache.exists("a") is False + + +@pytest.mark.asyncio +async def test_async_set_and_get(cache): + await cache.async_set("a", "value", ttl=None) + result = await cache.async_get("a") + assert result == "value" + + +@pytest.mark.asyncio +async def test_async_get_expired(cache, monkeypatch): + monkeypatch.setattr(time, "time", lambda: 1000.0) + await cache.async_set("a", "value", ttl=1) + + monkeypatch.setattr(time, "time", lambda: 1002.0) + assert await cache.async_get("a") is None + + +@pytest.mark.asyncio +async def test_async_exists(cache): + await cache.async_set("a", "value", ttl=None) + assert await cache.async_exists("a") is True + + +@pytest.mark.asyncio +async def test_async_delete(cache): + await cache.async_set("a", "value", ttl=None) + await cache.async_delete("a") + assert await cache.async_get("a") is None From cf12f6c9d4c46829f41a93e8f804eb243c628455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?= Date: Mon, 26 Jan 2026 22:50:59 +0900 Subject: [PATCH 4/8] fix(tests): FakeKeyValueStore should inheritance KeyValueStore class --- tests/test_cache.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index a653683..70b1761 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,10 +1,12 @@ import time +from typing import Any import pytest +from apkit.kv import KeyValueStore from apkit.cache import Cache -class FakeKeyValueStore: +class FakeKeyValueStore(KeyValueStore[Any, Any]): """Minimal in-memory KeyValueStore for tests.""" def __init__(self): @@ -19,6 +21,20 @@ def set(self, key, value): def delete(self, key): self._data.pop(key, None) + def exists(self, key): + return key in self._data + + async def async_get(self, key): + return self.get(key) + + async def async_set(self, key, value): + self.set(key, value) + + async def async_delete(self, key): + self.delete(key) + + async def async_exists(self, key): + return self.exists(key) @pytest.fixture def store(): From e2a824735581b01d5fc517826afafdad324592b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?= Date: Mon, 26 Jan 2026 22:52:42 +0900 Subject: [PATCH 5/8] fix(tests): Add dummy TTL parameter to set method --- tests/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index 70b1761..bf3dbb8 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -15,7 +15,7 @@ def __init__(self): def get(self, key): return self._data.get(key) - def set(self, key, value): + def set(self, key, value, ttl_seconds = None): self._data[key] = value def delete(self, key): From 67089ccfe42a844bfb6115d19e7ddbddc2a07adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?= Date: Mon, 26 Jan 2026 22:53:29 +0900 Subject: [PATCH 6/8] fix(tests): Add dummy TTL parameter to async_set method --- tests/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index bf3dbb8..b675959 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -27,7 +27,7 @@ def exists(self, key): async def async_get(self, key): return self.get(key) - async def async_set(self, key, value): + async def async_set(self, key, value, ttl_seconds = None): self.set(key, value) async def async_delete(self, key): From 9dbe2e920ccbb9fa493aebcca2e7a9dd3dd1b96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?= Date: Mon, 26 Jan 2026 22:54:22 +0900 Subject: [PATCH 7/8] refactor(tests): remove unused codes and format imports Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/helper/test_inbox.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/helper/test_inbox.py b/tests/helper/test_inbox.py index b5469af..35326c3 100644 --- a/tests/helper/test_inbox.py +++ b/tests/helper/test_inbox.py @@ -1,12 +1,11 @@ -from cryptography.hazmat.primitives.asymmetric import rsa +import json + +import pytest from apkit.config import AppConfig from apkit.helper.inbox import InboxVerifier -from apsig import draft, ld_signature -from apkit.models import CryptographicKey, Person +from apsig import draft from cryptography.hazmat.primitives import serialization as crypto_serialization - -import json -import pytest +from cryptography.hazmat.primitives.asymmetric import rsa def _prepare_signed_request(): From f35ad527e3925ca43467e8d55de3963d4ffe3b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=98=E7=80=AC=E3=81=93=E3=81=93=E3=81=82?= Date: Mon, 26 Jan 2026 22:55:38 +0900 Subject: [PATCH 8/8] refactor(tests): simplify truthiness assertions Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- tests/helper/test_inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helper/test_inbox.py b/tests/helper/test_inbox.py index 35326c3..54d7472 100644 --- a/tests/helper/test_inbox.py +++ b/tests/helper/test_inbox.py @@ -68,4 +68,4 @@ async def test_verify_draft_http_signature(): inbox_verifier = InboxVerifier(config) result = await inbox_verifier.verify(body, url, method, headers) - assert result == True + assert result