Ctrl K
ComunidadGuías…

backend/tests/test_auth.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Tests for auth.py — API key authentication."""

import os
from unittest.mock import MagicMock, patch

import pytest
from flask import Flask

from auth import init_auth, require_api_key
from config import reload_settings
from tests.ui_session_helpers import grant_ui_session


def test_no_auth_when_key_empty():
    """Test that auth is disabled when API key is empty."""
    if "SUBLARR_API_KEY" in os.environ:
        del os.environ["SUBLARR_API_KEY"]

    settings = reload_settings()
    assert settings.api_key == ""

    app = Flask(__name__)
    init_auth(app)
    # Should not raise any errors


def test_auth_required_when_key_set():
    """Test that auth is enabled when API key is set."""
    os.environ["SUBLARR_API_KEY"] = "test-key-123"
    settings = reload_settings()
    assert settings.api_key == "test-key-123"

    app = Flask(__name__)
    init_auth(app)

    # Cleanup
    del os.environ["SUBLARR_API_KEY"]
    reload_settings()


def test_require_api_key_decorator():
    """When API key is set, require it on protected endpoints."""
    app = Flask(__name__)

    @app.route("/test")
    @require_api_key
    def test_route():
        return {"status": "ok"}

    # Case 1: no API key configured — request should succeed
    os.environ.pop("SUBLARR_API_KEY", None)
    reload_settings()
    with app.test_client() as client:
        response = client.get("/test")
        assert response.status_code == 200

    # Case 2: API key configured — request without key should fail
    os.environ["SUBLARR_API_KEY"] = "test-key-123"
    reload_settings()
    try:
        with app.test_client() as client:
            response = client.get("/test")
            assert response.status_code == 401

            response_with_key = client.get("/test", headers={"X-Api-Key": "test-key-123"})
            assert response_with_key.status_code == 200
    finally:
        os.environ.pop("SUBLARR_API_KEY", None)
        reload_settings()


def test_ip_allowlist_blocks_unlisted_ip():
    """Requests from IPs outside allowed_ip_ranges must receive 403."""
    app = Flask(__name__)

    @app.route("/api/v1/health")
    def health():
        return {"status": "ok"}

    init_auth(app)

    settings_mock = MagicMock()
    settings_mock.allowed_ip_ranges = "10.0.0.0/8"
    settings_mock.api_key = ""

    with patch("auth.get_settings", return_value=settings_mock), app.test_client() as client:
        # Flask test client sends from 127.0.0.1 by default
        resp = client.get("/api/v1/health")
        assert resp.status_code == 403


def test_ip_allowlist_allows_listed_ip():
    """Requests from IPs inside allowed_ip_ranges must pass through."""
    app = Flask(__name__)

    @app.route("/api/v1/health")
    def health():
        return {"status": "ok"}

    init_auth(app)

    settings_mock = MagicMock()
    settings_mock.allowed_ip_ranges = "127.0.0.1/32"
    settings_mock.api_key = ""

    with patch("auth.get_settings", return_value=settings_mock), app.test_client() as client:
        resp = client.get("/api/v1/health")
        assert resp.status_code != 403


def test_ip_allowlist_empty_allows_all():
    """Empty allowed_ip_ranges allows all IPs."""
    app = Flask(__name__)

    @app.route("/api/v1/health")
    def health():
        return {"status": "ok"}

    init_auth(app)

    settings_mock = MagicMock()
    settings_mock.allowed_ip_ranges = ""
    settings_mock.api_key = ""

    with patch("auth.get_settings", return_value=settings_mock), app.test_client() as client:
        resp = client.get("/api/v1/health")
        assert resp.status_code != 403


def _protected_app():
    """Flask app with init_auth and a protected /api/v1 route + session support."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"

    @app.route("/api/v1/protected")
    def protected():
        return {"status": "ok"}

    init_auth(app)
    return app


def _settings(api_key="server-key", allowed_ip_ranges=""):
    settings_mock = MagicMock()
    settings_mock.api_key = api_key
    settings_mock.allowed_ip_ranges = allowed_ip_ranges
    settings_mock.max_login_attempts = 20
    return settings_mock


def test_ui_session_satisfies_api_key_gate():
    """A logged-in UI session must pass the API gate without an X-Api-Key.

    Regression test for the 1.6.0 login loop: the SPA logs in (session cookie
    set) but has no key in localStorage yet; every API call got 401 and the
    frontend's 401 interceptor bounced back to /login forever.
    """
    app = _protected_app()
    with patch("auth.get_settings", return_value=_settings()), app.test_client() as client:
        with client.session_transaction() as sess:
            grant_ui_session(sess)
        resp = client.get("/api/v1/protected")
        assert resp.status_code == 200


def test_no_session_no_key_still_401():
    """Without a session and without a key the gate must still reject."""
    app = _protected_app()
    with patch("auth.get_settings", return_value=_settings()), app.test_client() as client:
        resp = client.get("/api/v1/protected")
        assert resp.status_code == 401


def test_unauthenticated_session_cookie_does_not_bypass():
    """A session cookie without the ui_authenticated flag must not pass."""
    app = _protected_app()
    with patch("auth.get_settings", return_value=_settings()), app.test_client() as client:
        with client.session_transaction() as sess:
            sess["some_other_flag"] = True
        resp = client.get("/api/v1/protected")
        assert resp.status_code == 401


def test_valid_proxy_auth_satisfies_api_key_gate():
    """A trusted-proxy-authenticated request must pass without an X-Api-Key."""
    app = _protected_app()
    with (
        patch("auth.get_settings", return_value=_settings()),
        patch("proxy_auth.request_has_valid_proxy_auth", return_value=True),
        app.test_client() as client,
    ):
        resp = client.get("/api/v1/protected")
        assert resp.status_code == 200


def test_require_api_key_decorator_accepts_ui_session():
    """The decorator must honour the same session-OR-key contract as the hook."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"

    @app.route("/decorated")
    @require_api_key
    def decorated_route():
        return {"status": "ok"}

    os.environ["SUBLARR_API_KEY"] = "test-key-123"
    reload_settings()
    try:
        with app.test_client() as client:
            resp = client.get("/decorated")
            assert resp.status_code == 401

            with client.session_transaction() as sess:
                grant_ui_session(sess)
            resp = client.get("/decorated")
            assert resp.status_code == 200
    finally:
        os.environ.pop("SUBLARR_API_KEY", None)
        reload_settings()


def test_require_api_key_decorator_accepts_proxy_auth():
    """Trusted-proxy SSO requests must pass decorated routes without a key."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"

    @app.route("/decorated")
    @require_api_key
    def decorated_route():
        return {"status": "ok"}

    os.environ["SUBLARR_API_KEY"] = "test-key-123"
    reload_settings()
    try:
        with (
            patch("proxy_auth.request_has_valid_proxy_auth", return_value=True),
            app.test_client() as client,
        ):
            resp = client.get("/decorated")
            assert resp.status_code == 200
    finally:
        os.environ.pop("SUBLARR_API_KEY", None)
        reload_settings()