infrasynth-backend-kit/tests/test_security/test_api_keys.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

108 lines
4.2 KiB
Python

from django.contrib.auth.hashers import make_password
from rest_framework import status
from infrasynth.security.auth.middleware import JWTAuthenticationMiddleware
from infrasynth.security.models import APIKey
class TestAPIKeyViewSet:
def test_create_returns_full_key_once(self, authenticated_client):
resp = authenticated_client.post(
"/api/v1/auth/api-keys/",
{"name": "CI Key", "scopes": ["read:users"]},
format="json",
)
assert resp.status_code == status.HTTP_201_CREATED
data = resp.json()
assert "key" in data
assert "prefix" in data
assert data["key"].startswith(data["prefix"] + ".")
def test_created_key_authenticates(self, authenticated_client):
resp = authenticated_client.post(
"/api/v1/auth/api-keys/",
{"name": "CI Key", "scopes": ["read:users"]},
format="json",
)
full_key = resp.json()["key"]
from rest_framework.test import APIClient
client = APIClient()
client.credentials(HTTP_X_API_KEY=full_key)
check = client.get("/api/v1/auth/check/")
assert check.status_code == status.HTTP_200_OK
def test_list_keys(self, authenticated_client, user):
APIKey.objects.create(
name="Old Key",
prefix="prefix01",
key_hash=make_password("secret"),
created_by=user,
)
resp = authenticated_client.get("/api/v1/auth/api-keys/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
def test_list_never_exposes_hash(self, authenticated_client, user):
APIKey.objects.create(
name="Old Key",
prefix="prefix01",
key_hash=make_password("secret"),
created_by=user,
)
resp = authenticated_client.get("/api/v1/auth/api-keys/")
result = resp.json()["results"][0]
assert "key" not in result
assert "key_hash" not in result
assert result["prefix"] == "prefix01"
def test_delete_key(self, authenticated_client, user):
key = APIKey.objects.create(
name="Old Key",
prefix="prefix01",
key_hash=make_password("secret"),
created_by=user,
)
resp = authenticated_client.delete(f"/api/v1/auth/api-keys/{key.pk}/")
assert resp.status_code == status.HTTP_204_NO_CONTENT
assert not APIKey.objects.filter(pk=key.pk).exists()
def test_rotate_issues_new_secret_and_deactivates_old(self, authenticated_client, user):
created = APIKey.objects.create(
name="Rotate Me",
prefix="rot0aaaa",
key_hash=make_password("old-secret"),
scopes=["read:users"],
created_by=user,
)
resp = authenticated_client.post(f"/api/v1/auth/api-keys/{created.pk}/rotate/", format="json")
assert resp.status_code == status.HTTP_201_CREATED
created.refresh_from_db()
assert created.is_active is False
new = APIKey.objects.get(prefix=resp.json()["prefix"])
assert new.rotated_from_id == created.pk
assert new.is_active is True
assert resp.json()["key"].startswith(new.prefix + ".")
def test_requires_auth(self, api_client):
resp = api_client.get("/api/v1/auth/api-keys/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
class TestJWTAuthenticationMiddleware:
def test_ignores_anonymous_requests(self, rf, db):
request = rf.get("/api/v1/features/active/")
request.user = type("Anon", (), {"is_anonymous": True})()
JWTAuthenticationMiddleware(get_response=lambda r: None).process_request(request)
assert request.user.is_anonymous
def test_populates_user_from_cookie(self, rf, user):
from rest_framework_simplejwt.tokens import RefreshToken
from infrasynth.shared.crypto import encrypt
request = rf.get("/api/v1/features/active/")
request.COOKIES["access_token"] = encrypt(str(RefreshToken.for_user(user).access_token))
request.user = type("Anon", (), {"is_anonymous": True})()
JWTAuthenticationMiddleware(get_response=lambda r: None).process_request(request)
assert request.user == user