diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3d75ea9a..389ab11a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: path: frontend/test-results/screenshots/ # ─── OCR Service Unit Tests ─────────────────────────────────────────────────── - # Only spell_check.py and test_confidence.py — no ML stack required. + # Only spell_check.py, test_confidence.py, test_sender_registry.py — no ML stack required. ocr-tests: name: OCR Service Tests runs-on: ubuntu-latest @@ -60,11 +60,11 @@ jobs: python-version: '3.11' - name: Install test dependencies - run: pip install "pyspellchecker==0.9.0" pytest + run: pip install "pyspellchecker==0.9.0" pytest pytest-asyncio working-directory: ocr-service - name: Run OCR unit tests (no ML stack required) - run: python -m pytest test_spell_check.py test_confidence.py -v + run: python -m pytest test_spell_check.py test_confidence.py test_sender_registry.py -v working-directory: ocr-service # ─── Backend Unit & Slice Tests ─────────────────────────────────────────────── diff --git a/ocr-service/test_training_auth.py b/ocr-service/test_training_auth.py index e8ad23ad..76ae1060 100644 --- a/ocr-service/test_training_auth.py +++ b/ocr-service/test_training_auth.py @@ -67,3 +67,42 @@ async def test_train_returns_403_when_token_wrong(): ) assert response.status_code == 403 + + +# ─── /train-sender authentication ───────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_train_sender_returns_503_when_training_token_not_configured(): + """POST /train-sender must return 503 when TRAINING_TOKEN env var is empty. + + An empty token means the service was started without training configured. + Allowing requests through would grant unauthenticated access to the + training endpoint, contradicting the principle of failing closed. + """ + with patch("main.TRAINING_TOKEN", ""), \ + patch("main._models_ready", True): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + "/train-sender", + data={"output_model_path": "/app/models/sender_test.mlmodel"}, + files={"file": ("training.zip", _minimal_zip(), "application/zip")}, + ) + + assert response.status_code == 503 + + +@pytest.mark.asyncio +async def test_train_sender_returns_403_when_token_wrong(): + """POST /train-sender must return 403 when TRAINING_TOKEN is set but header is wrong.""" + with patch("main.TRAINING_TOKEN", "secret-token"), \ + patch("main._models_ready", True): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.post( + "/train-sender", + data={"output_model_path": "/app/models/sender_test.mlmodel"}, + files={"file": ("training.zip", _minimal_zip(), "application/zip")}, + headers={"X-Training-Token": "wrong-token"}, + ) + + assert response.status_code == 403