Keep git as source of truth: deep-link to Forgejo editor, manual refresh to drop cache, and optional signed push webhook for automatic invalidation.
143 lines
4.2 KiB
Python
143 lines
4.2 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from django.urls import reverse
|
|
|
|
from coulomb_social.apps.members.models import Member
|
|
from coulomb_social.apps.spaces.content import (
|
|
clear_content_cache,
|
|
clear_content_cache_for_repo,
|
|
forgejo_edit_url,
|
|
)
|
|
from coulomb_social.apps.spaces.forgejo import FetchedFile
|
|
from coulomb_social.apps.spaces.models import Space
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_forgejo_edit_url():
|
|
space = Space(
|
|
tenant_id="t",
|
|
slug="demo",
|
|
title="D",
|
|
forgejo_owner="coulomb",
|
|
forgejo_repo="coulomb-social",
|
|
default_branch="main",
|
|
content_root="docs/space-fixtures/demo/pages",
|
|
)
|
|
url = forgejo_edit_url(space, "index")
|
|
assert url.endswith(
|
|
"/coulomb/coulomb-social/_edit/main/docs/space-fixtures/demo/pages/index.md"
|
|
)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_refresh_clears_cache_and_refetches(client, settings):
|
|
clear_content_cache()
|
|
settings.DEBUG = True
|
|
settings.OIDC_ENABLED = False
|
|
client.post(
|
|
reverse("identity:dev_login"),
|
|
{
|
|
"subject": "writer",
|
|
"issuer": "https://local.dev/issuer",
|
|
"name": "Writer",
|
|
"tenant": "tenant:coulomb",
|
|
},
|
|
)
|
|
member = Member.objects.get(subject="writer")
|
|
Space.objects.create(
|
|
tenant_id="tenant:coulomb",
|
|
slug="demo",
|
|
title="Demo",
|
|
created_by=member,
|
|
forgejo_owner="coulomb",
|
|
forgejo_repo="coulomb-social",
|
|
content_root="docs/space-fixtures/demo/pages",
|
|
)
|
|
calls = {"n": 0}
|
|
|
|
def fake_fetch(**kwargs):
|
|
calls["n"] += 1
|
|
return FetchedFile(
|
|
path=kwargs["path"],
|
|
text=f"# Version {calls['n']}\n\nbody\n",
|
|
source="forgejo-raw",
|
|
)
|
|
|
|
with patch(
|
|
"coulomb_social.apps.spaces.content.fetch_raw_file", side_effect=fake_fetch
|
|
):
|
|
r1 = client.get(reverse("spaces:detail", kwargs={"slug": "demo"}))
|
|
assert r1.status_code == 200
|
|
assert b"Version 1" in r1.content
|
|
# cached — still v1
|
|
r2 = client.get(reverse("spaces:detail", kwargs={"slug": "demo"}))
|
|
assert b"Version 1" in r2.content
|
|
assert calls["n"] == 1
|
|
# refresh
|
|
r3 = client.post(reverse("spaces:refresh", kwargs={"slug": "demo"}))
|
|
assert r3.status_code == 302
|
|
r4 = client.get(reverse("spaces:detail", kwargs={"slug": "demo"}))
|
|
assert b"Version 2" in r4.content
|
|
assert calls["n"] == 2
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_webhook_hmac_invalidates_repo_cache(client, settings):
|
|
clear_content_cache()
|
|
settings.FORGEJO_WEBHOOK_SECRET = "test-webhook-secret-at-least-16"
|
|
Space.objects.create(
|
|
tenant_id="tenant:coulomb",
|
|
slug="demo",
|
|
title="Demo",
|
|
forgejo_owner="coulomb",
|
|
forgejo_repo="coulomb-social",
|
|
content_root="pages",
|
|
)
|
|
# prime cache
|
|
from coulomb_social.apps.spaces import content as content_mod
|
|
|
|
key = (
|
|
"coulomb",
|
|
"coulomb-social",
|
|
"main",
|
|
"pages/index.md",
|
|
settings.FORGEJO_BASE_URL,
|
|
)
|
|
content_mod._FETCH_CACHE[key] = FetchedFile(
|
|
path="pages/index.md", text="# old", source="forgejo-raw"
|
|
)
|
|
|
|
body = json.dumps(
|
|
{"repository": {"full_name": "coulomb/coulomb-social", "name": "coulomb-social"}}
|
|
).encode()
|
|
sig = hmac.new(
|
|
settings.FORGEJO_WEBHOOK_SECRET.encode(), body, hashlib.sha256
|
|
).hexdigest()
|
|
r = client.post(
|
|
reverse("spaces:forgejo_webhook"),
|
|
data=body,
|
|
content_type="application/json",
|
|
headers={"X-Gitea-Signature": sig},
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["ok"] is True
|
|
assert data["cache_entries_cleared"] >= 1
|
|
assert data["spaces_matched"] == 1
|
|
assert key not in content_mod._FETCH_CACHE
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_webhook_rejects_bad_secret(client, settings):
|
|
settings.FORGEJO_WEBHOOK_SECRET = "test-webhook-secret-at-least-16"
|
|
r = client.post(
|
|
reverse("spaces:forgejo_webhook"),
|
|
data=b"{}",
|
|
content_type="application/json",
|
|
headers={"X-Coulomb-Webhook-Secret": "wrong"},
|
|
)
|
|
assert r.status_code == 401
|