Initial commit

This commit is contained in:
ThePetrovich 2026-08-17 22:50:10 +08:00
commit 5cda7bc309
28 changed files with 1173 additions and 0 deletions

0
tests/__init__.py Normal file
View file

43
tests/settings.py Normal file
View file

@ -0,0 +1,43 @@
"""Minimal Django project, just enough to exercise the kit in isolation."""
SECRET_KEY = "test-only"
DEBUG = True
PRODUCTION = False
USE_TZ = True
YKSA_SERVICE = "kit"
INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.sessions",
"django.contrib.messages",
"yksa_kit",
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"yksa_kit.middleware.UserTimezoneMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
]
DATABASES = {
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"},
}
ROOT_URLCONF = "tests.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"OPTIONS": {"context_processors": [
"django.contrib.messages.context_processors.messages",
"yksa_kit.context_processors.ui_preferences",
]},
},
]
CACHES = {
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
}

85
tests/test_auth.py Normal file
View file

@ -0,0 +1,85 @@
from __future__ import annotations
import pytest
from django.test import RequestFactory
from yksa_kit.auth import (
extract_token,
request_can_ingest,
request_has_valid_token,
resolve_token,
store_session_token,
)
from yksa_kit.models import ApiToken, TokenKind
@pytest.fixture
def rf() -> RequestFactory:
return RequestFactory()
@pytest.mark.django_db
def test_returns_none_when_no_credentials(rf):
assert extract_token(rf.get("/api/v1/things/")) is None
assert request_has_valid_token(rf.get("/api/v1/things/")) is False
@pytest.mark.django_db
def test_authorization_bearer_header(rf):
token = ApiToken.objects.create(name="ci-job")
resolved = extract_token(rf.get("/", HTTP_AUTHORIZATION=f"Bearer {token.token}"))
assert resolved is not None and resolved.pk == token.pk
@pytest.mark.django_db
def test_authorization_raw_header_accepted(rf):
"""Plain header value without the 'Bearer ' prefix is tolerated."""
token = ApiToken.objects.create(name="legacy-client")
assert extract_token(rf.get("/", HTTP_AUTHORIZATION=token.token)) is not None
@pytest.mark.django_db
def test_query_string_token(rf):
token = ApiToken.objects.create(name="browser")
assert extract_token(rf.get("/", {"api_token": token.token})) is not None
@pytest.mark.django_db
def test_revoked_token_is_anonymous(rf):
token = ApiToken.objects.create(name="revoked", is_active=False)
assert extract_token(rf.get("/", HTTP_AUTHORIZATION=f"Bearer {token.token}")) is None
@pytest.mark.django_db
def test_unknown_token_is_anonymous(rf):
assert extract_token(rf.get("/", HTTP_AUTHORIZATION="Bearer nope")) is None
@pytest.mark.django_db
def test_extract_token_updates_last_used(rf):
token = ApiToken.objects.create(name="track-me")
assert token.last_used_at is None
extract_token(rf.get("/", HTTP_AUTHORIZATION=f"Bearer {token.token}"))
token.refresh_from_db()
assert token.last_used_at is not None
@pytest.mark.django_db
def test_only_ingest_tokens_may_push(rf):
read = ApiToken.objects.create(name="reader")
ingest = ApiToken.objects.create(name="station", kind=TokenKind.INGEST)
assert request_can_ingest(rf.get("/", {"api_token": read.token})) is False
assert request_can_ingest(rf.get("/", {"api_token": ingest.token})) is True
@pytest.mark.django_db
def test_session_token_is_ignored_by_the_api_path(rf):
"""The session carrier is for browser pages only; an API controller asking
request_has_valid_token() must not be authenticated by a stale session."""
token = ApiToken.objects.create(name="browser")
request = rf.get("/")
request.session = {}
store_session_token(request, token)
assert request_has_valid_token(request) is False
assert resolve_token(request) is not None

View file

@ -0,0 +1,51 @@
from __future__ import annotations
import json
import pytest
from django.test import Client, override_settings
from yksa_kit.conf import cache_key, user_agent
from yksa_kit.settings import env_bool, env_list, read_secret
@pytest.mark.django_db
def test_health_reports_the_database():
response = Client().get("/health/")
assert response.status_code == 200
assert json.loads(response.content)["database"] == "connected"
def test_cache_keys_are_namespaced_per_service():
"""Two services share one Redis; an unnamespaced key merges their budgets."""
assert cache_key("host_rl", "example.com") == "kit:host_rl:example.com"
def test_user_agent_falls_back_to_the_service_slug():
assert user_agent().startswith("yksa-kit/")
@override_settings(YKSA_USER_AGENT="yksa-tdas/0.1 (+https://tdas.tmtc.yksa.space)")
def test_user_agent_is_overridable():
assert "tdas" in user_agent()
def test_env_bool_accepts_the_spellings_compose_files_actually_use(monkeypatch):
for raw in ("1", "true", "TRUE", "True", "yes", "on"):
monkeypatch.setenv("YKSA_TEST_FLAG", raw)
assert env_bool("YKSA_TEST_FLAG") is True
monkeypatch.setenv("YKSA_TEST_FLAG", "0")
assert env_bool("YKSA_TEST_FLAG") is False
monkeypatch.delenv("YKSA_TEST_FLAG")
assert env_bool("YKSA_TEST_FLAG", default=True) is True
def test_env_list_splits_and_strips(monkeypatch):
monkeypatch.setenv("YKSA_TEST_HOSTS", "a.example, b.example ,")
assert env_list("YKSA_TEST_HOSTS") == ["a.example", "b.example"]
def test_read_secret_falls_back_to_the_environment(monkeypatch):
monkeypatch.setenv("YKSA_TEST_SECRET", "from-env")
assert read_secret("yksa_test_secret") == "from-env"
assert read_secret("yksa_absent_secret") is None

View file

@ -0,0 +1,40 @@
from __future__ import annotations
from django.core.cache import cache
from django.test import override_settings
from yksa_kit import host_rate_limit
def test_host_from_url():
assert host_rate_limit.host_from_url("https://Db.SatNOGS.org/api/") == "db.satnogs.org"
assert host_rate_limit.host_from_url("not a url") == ""
def test_development_never_blocks():
cache.clear()
host_rate_limit.acquire("example.com", per_minute=1)
host_rate_limit.acquire("example.com", per_minute=1)
assert cache.get("kit:host_rl:example.com") is None
@override_settings(PRODUCTION=True)
def test_production_counts_against_the_minute_window():
cache.clear()
host_rate_limit.acquire("example.com", per_minute=5)
assert cache.get("kit:host_rl:example.com") == 1
@override_settings(PRODUCTION=True)
def test_a_zero_budget_means_unlimited():
"""per_minute=0 is how a source says 'no published limit', not 'no requests'."""
cache.clear()
host_rate_limit.acquire("example.com", per_minute=0)
assert cache.get("kit:host_rl:example.com") is None
@override_settings(PRODUCTION=True)
def test_the_wait_gives_up_rather_than_stalling_the_worker():
cache.clear()
cache.set("kit:host_rl:example.com", 99, timeout=60)
host_rate_limit.acquire("example.com", per_minute=1, wait_seconds=0.0)

23
tests/test_states.py Normal file
View file

@ -0,0 +1,23 @@
from __future__ import annotations
import pytest
from yksa_kit.choices import RunStatus
from yksa_kit.states import health_state
@pytest.mark.parametrize("status,expected", [
("success", "ok"), ("ok", "ok"), ("partial", "warning"), ("failed", "failed"),
("error", "failed"), ("running", "running"), ("pending", "running"),
("queued", "running"), ("skipped", "skipped"),
("", "unknown"), (None, "unknown"), ("something-new", "unknown"),
])
def test_domain_status_maps_to_one_vocabulary(status, expected):
assert health_state(status) == expected
@pytest.mark.parametrize("value", RunStatus.values)
def test_every_run_status_has_a_state(value):
"""A new RunStatus member without a mapping would render as `unknown` on every
status page, which reads as a bug in the poller rather than a missing table row."""
assert health_state(value) != "unknown"

3
tests/urls.py Normal file
View file

@ -0,0 +1,3 @@
from django.urls import include, path
urlpatterns = [path("", include("yksa_kit.urls"))]