File size: 10,410 Bytes
b4fd35c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""Offline checks of actual API function bodies extracted with AST.

This does not import or simulate FastAPI/Pydantic routing. It verifies bounded
body handling, admin state transitions, and lifespan cancellation with minimal
response/request doubles. Run full pytest separately in the installed runtime.
"""
import ast
import asyncio
from contextlib import asynccontextmanager, suppress
import __future__
import hmac
import json
import logging
from pathlib import Path
import secrets
import sys
import tempfile
from types import SimpleNamespace
import unittest

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.services.runtime_store import JsonRuntimeStore


class ResponseDouble:
    def __init__(self, content=None, status_code=200, headers=None, **kwargs):
        self.content = content
        self.status_code = status_code
        self.headers = headers or {}
        self.cookies = {}

    def set_cookie(self, key, value, **kwargs):
        self.cookies[key] = value

    def delete_cookie(self, key):
        self.cookies.pop(key, None)

    async def __call__(self, scope, receive, send):
        await send({"type": "http.response.start", "status": self.status_code})
        await send({"type": "http.response.body", "body": json.dumps(self.content).encode()})


class HTTPExceptionDouble(Exception):
    def __init__(self, status_code, detail, headers=None):
        super().__init__(detail)
        self.status_code = status_code
        self.headers = headers or {}


def extract(relative_path, names, namespace):
    parsed = ast.parse((ROOT / relative_path).read_text())
    nodes = [node for node in parsed.body if getattr(node, "name", None) in names]
    if len(nodes) != len(names):
        raise AssertionError("One of the expected functions has moved or disappeared")
    for node in nodes:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name != "lifespan":
            node.decorator_list = []
    module = ast.Module(body=nodes, type_ignores=[])
    code = compile(module, str(ROOT / relative_path), "exec", flags=__future__.annotations.compiler_flag)
    exec(code, namespace)
    return namespace


class OfflineAPIChecks(unittest.TestCase):
    def test_body_limiter_measures_chunks_and_replays_valid_body(self):
        namespace = extract("app.py", {"RequestBodyLimitMiddleware"}, {"JSONResponse": ResponseDouble})
        middleware_class = namespace["RequestBodyLimitMiddleware"]
        async def check(chunks, limit, lengths=()):
            forwarded = []
            sent = []
            messages = iter({"type": "http.request", "body": chunk, "more_body": index < len(chunks)-1}
                            for index, chunk in enumerate(chunks))
            async def receive():
                return next(messages)
            async def send(message):
                sent.append(message)
            async def downstream(scope, receive, send):
                forwarded.append((await receive())["body"])
            middleware = middleware_class(downstream, max_bytes=limit)
            await middleware({"type": "http", "method": "POST", "headers": list(lengths)}, receive, send)
            return forwarded, sent
        forwarded, sent = asyncio.run(check([b"a"*6, b"b"*6], 10))
        self.assertEqual(sent[0]["status"], 413)
        self.assertFalse(forwarded)
        forwarded, sent = asyncio.run(check([b"a"*20000, b"b"*20000], 524288))
        self.assertEqual(forwarded, [b"a"*20000+b"b"*20000])
        self.assertFalse(sent)
        forwarded, sent = asyncio.run(check([], 10, [(b"content-length", b"99")]))
        self.assertEqual(sent[0]["status"], 413)

    def test_admin_lockout_logout_and_defaults(self):
        with tempfile.TemporaryDirectory() as directory:
            clock = SimpleNamespace(time=lambda: 1000)
            settings = SimpleNamespace(admin_password="correct", environment="test", runtime_path=Path(directory),
                                       config_path=Path(directory)/"config.json", runtime_defaults={"maintenance_mode": True, "max_results": 9})
            namespace = {
                "JsonRuntimeStore": JsonRuntimeStore, "get_settings": lambda: settings,
                "json": json, "time": clock, "secrets": secrets,
                "HTTPException": HTTPExceptionDouble, "Response": ResponseDouble,
                "RedirectResponse": ResponseDouble, "logger": logging.getLogger("offline"),
                "admin_sessions": {}, "_login_attempts": {}, "_login_lockouts": {},
                "_MAX_LOGIN_ATTEMPTS": 5, "_LOCKOUT_WINDOW": 300, "_LOCKOUT_DURATION": 900,
            }
            extract("src/api/admin.py", {"admin_auth", "admin_logout_get", "_sessions_file", "_load_sessions",
                "_save_sessions", "_cleanup_sessions", "_require_session", "_config_store"}, namespace)
            request = SimpleNamespace(client=SimpleNamespace(host="one-client"), cookies={})
            for attempt in range(5):
                with self.assertRaises(HTTPExceptionDouble) as caught:
                    asyncio.run(namespace["admin_auth"](SimpleNamespace(password="wrong"), request))
                self.assertEqual(caught.exception.status_code, 429 if attempt == 4 else 401)
            clock.time = lambda: 1301
            with self.assertRaises(HTTPExceptionDouble) as caught:
                asyncio.run(namespace["admin_auth"](SimpleNamespace(password="correct"), request))
            self.assertEqual(caught.exception.status_code, 429)
            clock.time = lambda: 1901
            response = asyncio.run(namespace["admin_auth"](SimpleNamespace(password="correct"), request))
            request.cookies["admin_session"] = response.cookies["admin_session"]
            namespace["_require_session"](request)
            asyncio.run(namespace["admin_logout_get"](request))
            with self.assertRaises(HTTPExceptionDouble) as caught:
                namespace["_require_session"](request)
            self.assertEqual(caught.exception.status_code, 401)
            self.assertEqual(namespace["_config_store"]().load(), settings.runtime_defaults)

    def test_config_validation_rejects_silent_skips(self):
        ns = extract("src/api/admin.py", {"_validated_config_values"}, {"HTTPException": HTTPExceptionDouble})
        validate = ns["_validated_config_values"]
        self.assertEqual(validate({"max_results": "9", "maintenance_mode": "true"}), {"max_results": 9, "maintenance_mode": True})
        for value in ({"max_results": "abc"}, {"max_results": 2.5}, {"maintenance_mode": "perhaps"}, {"announcement": "x"*501}):
            with self.assertRaises(HTTPExceptionDouble) as caught:
                validate(value)
            self.assertEqual(caught.exception.status_code, 422)

    def test_lifespan_serves_before_initialization_completes_and_cancels_tasks(self):
        async def scenario():
            began = asyncio.Event()
            cancelled = []
            async def initialize():
                began.set()
                try:
                    await asyncio.Event().wait()
                finally:
                    cancelled.append("startup")
            async def metrics_loop():
                try:
                    await asyncio.Event().wait()
                finally:
                    cancelled.append("metrics")
            flushed = []
            async def flush():
                flushed.append(True)
            ns = extract("app.py", {"lifespan"}, {
                "asynccontextmanager": asynccontextmanager, "suppress": suppress, "asyncio": asyncio,
                "_initialize_services": initialize, "_metrics_flush_loop": metrics_loop,
                "metrics_service": SimpleNamespace(flush=flush), "logger": logging.getLogger("offline"),
            })
            async with ns["lifespan"](None):
                await asyncio.wait_for(began.wait(), timeout=1)
                self.assertEqual(cancelled, [])
            self.assertCountEqual(cancelled, ["startup", "metrics"])
            self.assertEqual(flushed, [True])
        asyncio.run(scenario())

    def test_readiness_distinguishes_initializing_maintenance_and_missing_secret(self):
        rag = SimpleNamespace(is_ready=lambda: False, get_stats=lambda: {"retrieval_mode": "unavailable"})
        settings = SimpleNamespace(origin_token="origin", environment="production", openai_api_key="key")
        config = {"maintenance_mode": False}
        ns = extract("app.py", {"health_ready"}, {
            "_runtime_config": lambda: config, "rag_service": rag, "settings": settings,
            "_startup_state": "initializing", "LIBBEE_VERSION": "test", "JSONResponse": ResponseDouble,
        })
        self.assertEqual(ns["health_ready"]().status_code, 503)
        rag.is_ready = lambda: True
        self.assertEqual(ns["health_ready"]().status_code, 200)
        settings.origin_token = ""
        self.assertEqual(ns["health_ready"]().content["status"], "misconfigured")
        config["maintenance_mode"] = True
        self.assertEqual(ns["health_ready"]().status_code, 503)

    def test_origin_subpaths_fail_closed_and_admin_cross_site_posts_fail(self):
        settings = SimpleNamespace(origin_token="origin-secret", environment="production")
        ns = extract("app.py", {"secure_origin"}, {
            "settings": settings, "hmac": hmac, "asyncio": asyncio, "JSONResponse": ResponseDouble,
            "_runtime_config": lambda: {"maintenance_mode": False}, "_HF_PREVIEW_PATHS": set(),
        })
        async def next_handler(request):
            return ResponseDouble()
        request = SimpleNamespace(url=SimpleNamespace(path="/agent/test"), method="GET", headers={})
        self.assertEqual(asyncio.run(ns["secure_origin"](request, next_handler)).status_code, 401)
        request.headers = {"x-libbee-origin-token": "origin-secret"}
        self.assertEqual(asyncio.run(ns["secure_origin"](request, next_handler)).status_code, 200)
        settings.origin_token = ""
        self.assertEqual(asyncio.run(ns["secure_origin"](request, next_handler)).status_code, 401)
        request.url.path = "/admin/config"
        request.method = "POST"
        request.headers = {"sec-fetch-site": "cross-site"}
        self.assertEqual(asyncio.run(ns["secure_origin"](request, next_handler)).status_code, 403)


if __name__ == "__main__":
    unittest.main(verbosity=2)