from __future__ import annotations import json from pathlib import Path from codexpi.config import load_or_create_secret from codexpi.security import LoginLimiter def test_progressive_login_delay_is_capped(): limiter = LoginLimiter(attempts=20) delays = [] for _ in range(8): assert limiter.allow("100.64.0.10") delays.append(limiter.penalty("100.64.0.10")) assert delays[:5] == [0.25, 0.5, 1.0, 2.0, 4.0] assert delays[-1] == 4.0 def test_login_security_log_contains_no_credentials(logged_in): _, app, *_ = logged_in records = [json.loads(line) for line in (app.state.settings.data_dir / "security.log").read_text(encoding="utf-8").splitlines()] assert [row["event"] for row in records[:2]] == ["auth.login_succeeded", "session.created"] rendered = json.dumps(records) assert "a-very-long-test-password" not in rendered assert "csrf" not in rendered.lower() assert "cookie" not in rendered.lower() def test_full_access_change_is_audited(logged_in): client, app, projects, _, headers = logged_in root = projects / "full-access" root.mkdir() project = client.post("/api/projects", headers=headers, json={"name": "Full", "path": str(root)}).json() response = client.put( f"/api/projects/{project['id']}/full-access", headers=headers, json={"enabled": True, "acknowledged": True}, ) assert response.status_code == 200 log = (app.state.settings.data_dir / "security.log").read_text(encoding="utf-8") assert '"event":"security.full_access_changed"' in log assert '"outcome":"enabled"' in log def test_application_secret_can_come_from_external_file(tmp_path: Path, monkeypatch): external = tmp_path / "mounted-secret" external.write_text("x" * 48, encoding="utf-8") generated_data = tmp_path / "data" generated_data.mkdir() monkeypatch.setenv("CODEXPI_SECRET_KEY_FILE", str(external)) assert load_or_create_secret(generated_data) == "x" * 48 assert not (generated_data / "secret_key").exists()