personal-lifecycle-tracker / backend / tests / test_metrics.py
test_metrics.py
Raw
from __future__ import annotations

from fastapi.testclient import TestClient


def test_metrics_endpoint_available(client: TestClient):
    """Verify that the /metrics endpoint returns HTTP 200 and Prometheus text format."""
    res = client.get("/metrics")
    assert res.status_code == 200
    assert "text/plain" in res.headers.get("content-type", "")
    content = res.text
    # Check that golden triangle labels are present
    assert "deployment_environment=" in content
    assert "service_version=" in content
    assert "service_name=" in content


def test_http_request_metrics_recorded(client: TestClient, auth_headers: dict):
    """Verify that requests to API endpoints update request counters and latency histograms."""
    # Execute a few requests
    res = client.get("/api/v1/items", headers=auth_headers)
    assert res.status_code == 200

    metrics_res = client.get("/metrics")
    assert metrics_res.status_code == 200
    text = metrics_res.text

    assert "app_http_requests_total" in text
    assert 'http_route="/api/v1/items"' in text
    assert 'http_status_code="200"' in text
    assert "app_http_server_duration_milliseconds" in text


def test_lifecycle_items_metrics_lifecycle(client: TestClient, auth_headers: dict):
    """Verify creation, completion, and deletion of lifecycle items update domain metrics."""
    # 1. Create renewal item
    create_payload = {
        "name": "Passport Renewal Test",
        "lifecycleType": "renewal",
        "dueDate": "2027-01-15",
    }
    create_res = client.post("/api/v1/items", json=create_payload, headers=auth_headers)
    assert create_res.status_code == 201
    item = create_res.json()
    item_id = item["id"]

    # 2. Check items created metric
    metrics_res = client.get("/metrics")
    text = metrics_res.text
    assert "app_items_created_total" in text
    assert 'lifecycle_type="renewal"' in text

    # 3. Complete item
    complete_res = client.post(f"/api/v1/items/{item_id}/complete", headers=auth_headers)
    assert complete_res.status_code == 200

    metrics_res2 = client.get("/metrics")
    text2 = metrics_res2.text
    assert "app_items_completed_total" in text2
    assert 'lifecycle_type="renewal"' in text2

    # 4. Delete item
    del_res = client.delete(f"/api/v1/items/{item_id}", headers=auth_headers)
    assert del_res.status_code == 204

    metrics_res3 = client.get("/metrics")
    text3 = metrics_res3.text
    assert "app_items_deleted_total" in text3


def test_auth_and_reminders_metrics(client: TestClient, auth_headers: dict):
    """Verify auth and reminder metrics tracking."""
    # 1. Signup metric
    new_user_payload = {
        "email": "metricstest@example.com",
        "name": "Metrics Test User",
        "password": "password123",
    }
    signup_res = client.post("/api/v1/auth/signup", json=new_user_payload)
    assert signup_res.status_code == 201

    # 2. Failed login metric
    client.post("/api/v1/auth/login", json={"email": "metricstest@example.com", "password": "wrongpassword"})

    # 3. Successful login metric
    client.post("/api/v1/auth/login", json={"email": "metricstest@example.com", "password": "password123"})

    # 4. Reminders summary
    summary_res = client.get("/api/v1/reminders/summary", headers=auth_headers)
    assert summary_res.status_code == 200

    # 5. Daily digest generation
    digest_res = client.post("/api/v1/reminders/digest/generate", headers=auth_headers)
    assert digest_res.status_code == 200

    # Inspect metrics output
    metrics_res = client.get("/metrics")
    text = metrics_res.text
    assert "app_user_signups_total" in text
    assert 'app_user_logins_total{deployment_environment="development",service_name="api-backend",service_version="dev-latest",status="success"}' in text
    assert 'app_user_logins_total{deployment_environment="development",service_name="api-backend",service_version="dev-latest",status="failure"}' in text
    assert "app_reminders_summary_requests_total" in text
    assert 'app_digests_generated_total{deployment_environment="development",service_name="api-backend",service_version="dev-latest",status="success"}' in text


def test_route_normalization_avoids_cardinality_explosion(client: TestClient, auth_headers: dict):
    """Verify that route paths with dynamic UUIDs and member emails are normalized to avoid high cardinality."""
    # Access an item by ID
    client.get("/api/v1/items/8f51a704-5853-41bb-9466-9e1234567890", headers=auth_headers)

    metrics_res = client.get("/metrics")
    text = metrics_res.text
    # The route should be normalized to /api/v1/items/{id} rather than raw UUID
    assert 'http_route="/api/v1/items/{id}"' in text
    assert "8f51a704-5853-41bb-9466-9e1234567890" not in text

    # Access a member route by email
    client.delete("/api/v1/groups/group_home/members/testuser@example.com", headers=auth_headers)
    metrics_res2 = client.get("/metrics")
    text2 = metrics_res2.text
    assert "testuser@example.com" not in text2
    assert "{memberEmail}" in text2


def test_item_creation_failure_metrics(client: TestClient, auth_headers: dict):
    """Verify that failed item creation attempts increment app_item_creation_failures_total."""
    # Trigger an item creation failure (invalid lifecycle type or empty name)
    fail_res = client.post(
        "/api/v1/items",
        json={"name": "   ", "lifecycleType": "renewal"},
        headers=auth_headers,
    )
    # The store/endpoint rejects empty names with 400
    assert fail_res.status_code == 400

    metrics_res = client.get("/metrics")
    assert metrics_res.status_code == 200
    text = metrics_res.text

    assert "app_item_creation_failures_total" in text
    assert 'app_item_creation_failures_total{deployment_environment="development",lifecycle_type="renewal",service_name="api-backend",service_version="dev-latest"}' in text