Publish curated MobilityOps source snapshot
This commit is contained in:
commit
7adc072f5e
317 files changed
+48851
No files matched your search
@@ -0,0 +1,341 @@
|
||||
"""Regression tests for the hardening pass (security, robustness, data-quality edge cases)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import UTC, datetime
|
||||
from threading import Barrier
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import delete, func, select
|
||||
|
||||
from app.core.config import Settings, get_settings, insecure_default_secrets
|
||||
from app.core.db import SessionLocal
|
||||
from app.core.errors import AppError
|
||||
from app.core.observability import UNMATCHED_ROUTE_LABEL
|
||||
from app.core.ratelimit import FailedAttemptLimiter, SlidingWindowLimiter
|
||||
from app.models.audit import AuditEvent
|
||||
from app.models.customer import Customer
|
||||
from app.models.data_quality import DataQualityIssue
|
||||
from app.schemas import CurrentUser
|
||||
from app.services.data_quality import defer_issue, run_scan
|
||||
from tests.test_return import _activate_booking, _return_body
|
||||
|
||||
|
||||
def test_insecure_defaults_include_mounted_mcp_boundary_even_when_status_check_is_disabled():
|
||||
defaults = Settings(_env_file=None)
|
||||
assert "app_secret" in insecure_default_secrets(defaults)
|
||||
assert "mcp_hub_service_token" in insecure_default_secrets(defaults)
|
||||
hardened = Settings(
|
||||
_env_file=None,
|
||||
app_secret="x" * 32,
|
||||
n8n_callback_token="c" * 32,
|
||||
mcp_hub_service_token="m" * 32,
|
||||
)
|
||||
assert insecure_default_secrets(hardened) == []
|
||||
|
||||
|
||||
def test_production_refuses_to_start_with_placeholder_secrets(monkeypatch):
|
||||
monkeypatch.setenv("MOBILITYOPS_ENV", "production")
|
||||
monkeypatch.setenv("APP_SECRET", "replace-in-production")
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match="placeholder secrets"):
|
||||
get_settings()
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
monkeypatch.undo()
|
||||
get_settings.cache_clear()
|
||||
# The cached settings the running app relies on must be intact afterwards.
|
||||
assert get_settings().mobilityops_env == "test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("public_url", "secure_cookie", "message"),
|
||||
[
|
||||
("http://fleetops.example.test", "true", "must use HTTPS"),
|
||||
("https://fleetops.example.test", "false", "must be true"),
|
||||
],
|
||||
)
|
||||
def test_production_refuses_cleartext_or_insecure_session_cookie(
|
||||
monkeypatch, public_url, secure_cookie, message
|
||||
):
|
||||
values = {
|
||||
"MOBILITYOPS_ENV": "production",
|
||||
"APP_SECRET": "a" * 32,
|
||||
"N8N_CALLBACK_TOKEN": "c" * 32,
|
||||
"MCP_HUB_SERVICE_TOKEN": "m" * 32,
|
||||
"MOBILITYOPS_PUBLIC_URL": public_url,
|
||||
"SESSION_COOKIE_SECURE": secure_cookie,
|
||||
}
|
||||
for name, value in values.items():
|
||||
monkeypatch.setenv(name, value)
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
get_settings()
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
monkeypatch.undo()
|
||||
get_settings.cache_clear()
|
||||
assert get_settings().mobilityops_env == "test"
|
||||
|
||||
|
||||
def test_failed_attempt_limiter_blocks_after_threshold_and_resets():
|
||||
limiter = FailedAttemptLimiter(max_failures=3, window_seconds=60)
|
||||
for _ in range(3):
|
||||
assert limiter.retry_after_seconds("1.2.3.4") == 0
|
||||
limiter.record_failure("1.2.3.4")
|
||||
assert limiter.retry_after_seconds("1.2.3.4") > 0
|
||||
assert limiter.retry_after_seconds("5.6.7.8") == 0
|
||||
limiter.reset("1.2.3.4")
|
||||
assert limiter.retry_after_seconds("1.2.3.4") == 0
|
||||
|
||||
|
||||
def test_sliding_window_limiter_counts_successes_and_isolates_keys():
|
||||
limiter = SlidingWindowLimiter(max_requests=2, window_seconds=60)
|
||||
assert limiter.consume("session-a") == 0
|
||||
assert limiter.consume("session-a") == 0
|
||||
assert limiter.consume("session-a") > 0
|
||||
assert limiter.consume("session-b") == 0
|
||||
|
||||
|
||||
def test_login_client_key_uses_proxy_appended_peer_not_spoofed_leading_value():
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.api.routers.auth import _client_key
|
||||
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"headers": [(b"x-forwarded-for", b"203.0.113.99, 198.51.100.7")],
|
||||
"client": ("172.20.0.3", 12345),
|
||||
}
|
||||
)
|
||||
assert _client_key(request) == "198.51.100.7"
|
||||
|
||||
|
||||
def test_audit_export_accepts_naive_datetimes(ops_client):
|
||||
response = ops_client.get(
|
||||
"/api/v1/audit/export.csv",
|
||||
params={"occurred_from": "2026-01-01T00:00:00", "occurred_to": "2026-01-15T00:00:00"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_audit_list_rejects_non_uuid_correlation_id(ops_client):
|
||||
response = ops_client.get("/api/v1/audit", params={"correlation_id": "not-a-uuid"})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_unmatched_routes_do_not_create_metric_series(client):
|
||||
probe = f"/api/v1/does-not-exist-{uuid.uuid4().hex}"
|
||||
assert client.get(probe).status_code == 404
|
||||
metrics = client.get("/metrics").text
|
||||
assert probe not in metrics
|
||||
assert UNMATCHED_ROUTE_LABEL in metrics
|
||||
|
||||
|
||||
def test_bookings_page_for_unknown_vehicle_keeps_page_shape(ops_client):
|
||||
response = ops_client.get("/api/v1/bookings", params={"vehicle_ref": "MO-NOPE", "page": 1})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"items": [],
|
||||
"page": 1,
|
||||
"page_size": 25,
|
||||
"total": 0,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_return_idempotency_key_rejects_different_body(ops_client):
|
||||
booking_ref = _activate_booking("MO-011", start_odometer_km=30000)
|
||||
key = "test-return-fingerprint-001"
|
||||
first = ops_client.post(
|
||||
f"/api/v1/bookings/{booking_ref}/return",
|
||||
json=_return_body(end_odometer_km=30500),
|
||||
headers={"Idempotency-Key": key},
|
||||
)
|
||||
assert first.status_code == 201
|
||||
replay = ops_client.post(
|
||||
f"/api/v1/bookings/{booking_ref}/return",
|
||||
json=_return_body(end_odometer_km=30500),
|
||||
headers={"Idempotency-Key": key},
|
||||
)
|
||||
assert replay.status_code == 201
|
||||
mismatch = ops_client.post(
|
||||
f"/api/v1/bookings/{booking_ref}/return",
|
||||
json=_return_body(end_odometer_km=30999),
|
||||
headers={"Idempotency-Key": key},
|
||||
)
|
||||
assert mismatch.status_code == 409
|
||||
assert mismatch.json()["error"]["code"] == "IDEMPOTENCY_KEY_REUSED"
|
||||
|
||||
|
||||
def test_return_callback_rejects_malformed_correlation_id(client):
|
||||
event_id = str(uuid.uuid4())
|
||||
response = client.post(
|
||||
"/api/v1/integrations/n8n/return-callback",
|
||||
json={"event_id": event_id, "follow_up": "cleaning", "correlation_id": "nope"},
|
||||
headers={
|
||||
"Idempotency-Key": event_id,
|
||||
"X-Service-Token": get_settings().n8n_callback_token,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_blocked_checkout_booking_can_be_cancelled(ops_client):
|
||||
window = {"starts_at": "2051-03-01T10:00:00Z", "ends_at": "2051-03-02T12:00:00Z"}
|
||||
available = ops_client.get("/api/v1/bookings/availability", params=window).json()
|
||||
vehicle_option = next(item for item in available if item["operational_status"] == "available")
|
||||
vehicle = ops_client.get(f"/api/v1/vehicles/{vehicle_option['public_ref']}").json()
|
||||
booking = ops_client.post(
|
||||
"/api/v1/bookings",
|
||||
json={
|
||||
"customer_ref": "CUS-0002",
|
||||
"vehicle_ref": vehicle["public_ref"],
|
||||
**window,
|
||||
},
|
||||
).json()
|
||||
confirmed = ops_client.post(
|
||||
f"/api/v1/bookings/{booking['public_ref']}/complete-requirements",
|
||||
json={"confirmation": "Licence and rental conditions checked"},
|
||||
)
|
||||
assert confirmed.status_code == 200
|
||||
checkout = ops_client.post(
|
||||
f"/api/v1/bookings/{booking['public_ref']}/checkout",
|
||||
json={
|
||||
"start_odometer_km": vehicle["odometer_km"],
|
||||
"fuel_level_percent": 80,
|
||||
"cleanliness_ok": True,
|
||||
"damage_reported": True,
|
||||
"technical_warning": False,
|
||||
},
|
||||
)
|
||||
assert checkout.status_code == 200
|
||||
assert checkout.json()["booking_status"] == "blocked"
|
||||
cancelled = ops_client.post(
|
||||
f"/api/v1/bookings/{booking['public_ref']}/cancel",
|
||||
json={"reason": "Vehicle damaged at departure inspection"},
|
||||
)
|
||||
assert cancelled.status_code == 200
|
||||
assert cancelled.json()["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_scan_does_not_flag_anonymised_customers_as_missing_fields(ops_client):
|
||||
with SessionLocal() as db:
|
||||
customer = Customer(
|
||||
public_ref="CUS-ANON-SCAN",
|
||||
first_name="Anoniem",
|
||||
last_name="Klant",
|
||||
email=None,
|
||||
phone=None,
|
||||
postal_code=None,
|
||||
city=None,
|
||||
date_of_birth=None,
|
||||
anonymized_at=datetime.now(UTC),
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
customer_id = customer.id
|
||||
try:
|
||||
with SessionLocal() as db:
|
||||
run_scan(db)
|
||||
db.commit()
|
||||
flagged = db.scalar(
|
||||
select(DataQualityIssue.id).where(
|
||||
DataQualityIssue.entity_type == "customer",
|
||||
DataQualityIssue.entity_id == customer_id,
|
||||
)
|
||||
)
|
||||
assert flagged is None
|
||||
finally:
|
||||
with SessionLocal() as db:
|
||||
db.execute(delete(DataQualityIssue).where(DataQualityIssue.entity_id == customer_id))
|
||||
db.execute(delete(AuditEvent).where(AuditEvent.entity_id == customer_id))
|
||||
db.execute(delete(Customer).where(Customer.id == customer_id))
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_concurrent_quality_scans_leave_only_one_open_issue_per_condition():
|
||||
barrier = Barrier(2)
|
||||
|
||||
def scan() -> None:
|
||||
with SessionLocal() as db:
|
||||
barrier.wait()
|
||||
run_scan(db)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
list(executor.map(lambda _index: scan(), range(2)))
|
||||
|
||||
with SessionLocal() as db:
|
||||
duplicates = db.execute(
|
||||
select(
|
||||
DataQualityIssue.rule_type,
|
||||
DataQualityIssue.entity_type,
|
||||
DataQualityIssue.entity_id,
|
||||
func.count(DataQualityIssue.id),
|
||||
)
|
||||
.where(DataQualityIssue.status == "open")
|
||||
.group_by(
|
||||
DataQualityIssue.rule_type,
|
||||
DataQualityIssue.entity_type,
|
||||
DataQualityIssue.entity_id,
|
||||
)
|
||||
.having(func.count(DataQualityIssue.id) > 1)
|
||||
).all()
|
||||
assert duplicates == []
|
||||
|
||||
|
||||
def test_concurrent_issue_resolution_records_exactly_one_decision():
|
||||
issue_id = uuid.uuid4()
|
||||
issue_ref = f"DQ-RACE-{uuid.uuid4().hex[:8].upper()}"
|
||||
with SessionLocal() as db:
|
||||
db.add(
|
||||
DataQualityIssue(
|
||||
id=issue_id,
|
||||
public_ref=issue_ref,
|
||||
rule_type="missing_required_field",
|
||||
entity_type="customer",
|
||||
entity_id=uuid.uuid4(),
|
||||
severity="low",
|
||||
status="open",
|
||||
evidence_json={},
|
||||
proposed_action_json={},
|
||||
detected_at=datetime.now(UTC),
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
actor = CurrentUser(
|
||||
public_ref="USR-RACE", display_name="Race Manager", role="operations_manager"
|
||||
)
|
||||
barrier = Barrier(2)
|
||||
|
||||
def resolve() -> str:
|
||||
with SessionLocal() as db:
|
||||
barrier.wait()
|
||||
try:
|
||||
return defer_issue(db, issue_ref, actor).status
|
||||
except AppError as exc:
|
||||
return exc.code
|
||||
|
||||
try:
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
outcomes = list(executor.map(lambda _index: resolve(), range(2)))
|
||||
assert sorted(outcomes) == ["ISSUE_NOT_OPEN", "deferred"]
|
||||
with SessionLocal() as db:
|
||||
decisions = db.scalar(
|
||||
select(func.count(AuditEvent.id)).where(
|
||||
AuditEvent.entity_id == issue_id,
|
||||
AuditEvent.action == "data_quality_issue_deferred",
|
||||
)
|
||||
)
|
||||
assert decisions == 1
|
||||
finally:
|
||||
with SessionLocal() as db:
|
||||
db.execute(delete(AuditEvent).where(AuditEvent.entity_id == issue_id))
|
||||
db.execute(delete(DataQualityIssue).where(DataQualityIssue.id == issue_id))
|
||||
db.commit()
|
||||
Reference in new issue
Block a user