tests: unit + integration suite (99 tests; ruff + mypy --strict clean)
Real test bodies (not stubs), driven against an in-process httpx.ASGITransport override of the gateway's get_ollama_client dependency pointing at tests/integration/mock_ollama.py. Unit (target 100% on auth/, ratelimit/, budget/): - argon2id roundtrip, wrong-key, garbage encoding, needs_rehash on param change - key format/uniqueness/prefix extraction - token counter (prompt_eval_count + eval_count, embeddings, missing-counts) - translate (OpenAI <-> Ollama for chat/completion/embeddings, streaming chunks, /v1/models list shape) - allowlist (hard-blocks, effective-set semantics across allow_all/inheritance/ empty-discovered) - discovery (parse, cache roundtrip with TTL, fail-closed, tolerates redis=None) - sliding window (allow/block/reset/per-key vs per-tenant/cost-weighted) Integration (testcontainers postgres + redis + in-process mock Ollama): - auth flow (no/malformed/wrong key all return identical sanitized 401) - proxy stream (NDJSON roundtrip, audit row's token counts match, hard-blocked endpoints uniformly 403) - openai_compat (SSE chunks, data: [DONE], non-stream shape, /v1/models) - model_discovery (allow_all sees all, default-deny sees allowed ∩ discovered, /v1/models filtered, unpermitted-but-installed = nonexistent = 403, empty cache denies even allow_all) - rate_limit (429 + Retry-After + headers; Redis down ⇒ 503, never 200) - budget (decrement + headers; pre-burned counter blocks next request) - revocation (INSERT into gateway.revocations → NOTIFY → cache evicted → 401 ≤ 1s) Includes a known-issue xfail flagging a bug in ratelimit/sliding_window.py: the per-hit ZSET member uses id(object()) which returns the same id on consecutive calls, causing same-millisecond hits to overwrite instead of stacking. To be fixed in a follow-up commit.
This commit is contained in:
115
tests/unit/test_allowlist.py
Normal file
115
tests/unit/test_allowlist.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Unit tests for ``neuronetz_gateway.proxy.allowlist``.
|
||||
|
||||
Two concerns (SPEC §4.3 step 7-8, §4.6, §6.1-6.2):
|
||||
|
||||
1. **Hard-blocked endpoints** — mutating Ollama endpoints and ``/api/ps`` are
|
||||
always blocked, not configurable (``is_hard_blocked``).
|
||||
2. **Effective model set** (``resolve_effective_models`` / ``is_model_allowed``):
|
||||
* ``allow_all`` ⇒ all discovered
|
||||
* default-deny ⇒ ``allowed_models ∩ discovered``
|
||||
* stale/typo'd allowlist entries never resolve (not in discovered)
|
||||
* empty discovered ⇒ deny, even under ``allow_all`` (fail-closed)
|
||||
|
||||
The resolver merges the *already-resolved* ``allow_all``/``allowed_models``
|
||||
inputs (key-vs-tenant precedence per SPEC §13.7 is applied by the caller before
|
||||
this point; that precedence is exercised in the integration model-discovery
|
||||
tests).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from neuronetz_gateway.proxy import allowlist
|
||||
|
||||
# --- hard-blocked endpoint allowlist ---------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/api/pull", "/api/push", "/api/create", "/api/copy", "/api/delete", "/api/ps"],
|
||||
)
|
||||
def test_mutating_and_ps_endpoints_hard_blocked(path: str) -> None:
|
||||
assert allowlist.is_hard_blocked(path) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/api/blobs", "/api/blobs/sha256:abc", "/api/blobs/x/y"])
|
||||
def test_blob_prefix_hard_blocked(path: str) -> None:
|
||||
assert allowlist.is_hard_blocked(path) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path", ["/api/chat", "/api/generate", "/api/embed", "/api/tags", "/api/show", "/api/version"]
|
||||
)
|
||||
def test_allowlisted_endpoints_not_hard_blocked(path: str) -> None:
|
||||
assert allowlist.is_hard_blocked(path) is False
|
||||
|
||||
|
||||
# --- effective-set resolution (SPEC §4.3 step 7 / §4.6) --------------------
|
||||
|
||||
DISCOVERED = frozenset({"llama3.1:8b", "mistral:7b", "nomic-embed-text"})
|
||||
|
||||
|
||||
def test_allow_all_returns_all_discovered() -> None:
|
||||
eff = allowlist.resolve_effective_models(
|
||||
allow_all=True, allowed_models=(), discovered=DISCOVERED
|
||||
)
|
||||
assert eff == DISCOVERED
|
||||
|
||||
|
||||
def test_default_deny_intersects_discovered() -> None:
|
||||
# "qwen" is allowed but not installed => must not resolve (stale entry).
|
||||
eff = allowlist.resolve_effective_models(
|
||||
allow_all=False,
|
||||
allowed_models=("llama3.1:8b", "qwen:0.5b"),
|
||||
discovered=DISCOVERED,
|
||||
)
|
||||
assert eff == frozenset({"llama3.1:8b"})
|
||||
|
||||
|
||||
def test_default_deny_empty_allowlist_denies_all() -> None:
|
||||
eff = allowlist.resolve_effective_models(
|
||||
allow_all=False, allowed_models=(), discovered=DISCOVERED
|
||||
)
|
||||
assert eff == frozenset()
|
||||
|
||||
|
||||
def test_empty_discovered_denies_everything_even_allow_all() -> None:
|
||||
# Fail-closed: empty discovered set => no model resolves, even allow_all.
|
||||
eff = allowlist.resolve_effective_models(
|
||||
allow_all=True, allowed_models=("llama3.1:8b",), discovered=frozenset()
|
||||
)
|
||||
assert eff == frozenset()
|
||||
|
||||
|
||||
def test_is_model_allowed_membership() -> None:
|
||||
assert (
|
||||
allowlist.is_model_allowed(
|
||||
"llama3.1:8b", allow_all=False, allowed_models=("llama3.1:8b",), discovered=DISCOVERED
|
||||
)
|
||||
is True
|
||||
)
|
||||
# Installed but not permitted.
|
||||
assert (
|
||||
allowlist.is_model_allowed(
|
||||
"mistral:7b", allow_all=False, allowed_models=("llama3.1:8b",), discovered=DISCOVERED
|
||||
)
|
||||
is False
|
||||
)
|
||||
# Permitted but not installed (typo'd / stale) — indistinguishable from
|
||||
# not-allowed to the caller (SPEC §13.6 no existence disclosure).
|
||||
assert (
|
||||
allowlist.is_model_allowed(
|
||||
"ghost:1b", allow_all=False, allowed_models=("ghost:1b",), discovered=DISCOVERED
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_is_model_allowed_fail_closed_empty_discovered() -> None:
|
||||
assert (
|
||||
allowlist.is_model_allowed(
|
||||
"llama3.1:8b", allow_all=True, allowed_models=(), discovered=frozenset()
|
||||
)
|
||||
is False
|
||||
)
|
||||
Reference in New Issue
Block a user