Skip to content

Commit

Permalink
Add tests for metrics events
Browse files Browse the repository at this point in the history
Now that there's a mock metrics event manager that can be used for
testing, add tests for the metrics events posted by Gafaelfawr.
Remove the separate test for the storage methods underlying the
maintenance events, since now the values can be checked directly.
  • Loading branch information
rra committed Dec 18, 2024
1 parent 5f12b52 commit e03608b
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 41 deletions.
6 changes: 3 additions & 3 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1520,9 +1520,9 @@ safir==9.1.0 \
--hash=sha256:38718de0d2e5f9623eddc511700a15fb56fee88da7bba0e1210dc831276aa9aa \
--hash=sha256:630b2e59ae87edab25fb2c3d5ccd6ca156c0d525eb543624532bbf4fd3c85536
# via gafaelfawr (pyproject.toml)
safir-logging==9.0.1 \
--hash=sha256:775ab8b2c1a62fe5779f8d4504797df7affb5dbebc70fcad00307cda419d637e \
--hash=sha256:e5dfdfbdafb0a60dd2b8cdd63f8171cb703830cd5d433d60c27028cc9a4044f1
safir-logging==9.1.0 \
--hash=sha256:614cf63ec2f54dec640df8da9ece69d8e5fb43b19f98a943430ceefac7b2f267 \
--hash=sha256:c486c4406ed9ffc41e3ff977ac8938cb89cd57ba7f20436a03f9b336bf0008e0
# via safir
sentry-sdk==2.19.2 \
--hash=sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d \
Expand Down
1 change: 1 addition & 0 deletions src/gafaelfawr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ async def maintenance(*, config_path: Path | None) -> None:
events = StateEvents()
await events.initialize(event_manager)
await token_service.gather_state_metrics(events)
await event_manager.aclose()
await engine.dispose()
logger.debug("Finished background maintenance")

Expand Down
1 change: 1 addition & 0 deletions tests/data/config/github.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ quota: {}
metrics:
enabled: false
application: "gafaelfawr"
mock: true
1 change: 1 addition & 0 deletions tests/data/config/oidc-enrollment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ oidc:
metrics:
enabled: false
application: "gafaelfawr"
mock: true
50 changes: 46 additions & 4 deletions tests/handlers/ingress_logging_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Tests for logging in the ``/ingress/auth`` route."""
"""Tests for logging and metrics in the ``/ingress/auth`` route."""

from __future__ import annotations

import base64
from typing import Any
from unittest.mock import ANY

import pytest
from httpx import AsyncClient
from safir.datetime import format_datetime_for_logging
from safir.metrics import MockEventPublisher

from gafaelfawr.dependencies.context import context_dependency
from gafaelfawr.factory import Factory
from gafaelfawr.models.token import Token

Expand All @@ -35,7 +38,7 @@ async def test_success(
},
)
assert r.status_code == 200
expected_log = {
expected_log: dict[str, Any] = {
"auth_uri": "/foo",
"event": "Token authorized",
"httpRequest": {
Expand All @@ -61,14 +64,16 @@ async def test_success(
caplog.clear()
r = await client.get(
"/ingress/auth",
params={"scope": "exec:admin"},
params={"scope": "exec:admin", "service": "service-one"},
headers={
"Authorization": f"Basic {basic_b64}",
"X-Original-Uri": "/foo",
"X-Forwarded-For": "192.0.2.1",
},
)
assert r.status_code == 200
url = expected_log["httpRequest"]["requestUrl"]
expected_log["httpRequest"]["requestUrl"] += "&service=service-one"
expected_log["token_source"] = "basic-username"
assert parse_log(caplog) == [expected_log]

Expand All @@ -78,17 +83,32 @@ async def test_success(
caplog.clear()
r = await client.get(
"/ingress/auth",
params={"scope": "exec:admin"},
params={"scope": "exec:admin", "service": "service-two"},
headers={
"Authorization": f"Basic {basic_b64}",
"X-Original-Uri": "/foo",
"X-Forwarded-For": "192.0.2.1",
},
)
assert r.status_code == 200
expected_log["httpRequest"]["requestUrl"] = url + "&service=service-two"
expected_log["token_source"] = "basic-password"
assert parse_log(caplog) == [expected_log]

# Check the logged metrics events.
events = context_dependency._events
assert events
assert isinstance(events.auth_user, MockEventPublisher)
events.auth_user.published.assert_published_all(
[
{"username": token_data.username, "service": None},
{"username": token_data.username, "service": "service-one"},
{"username": token_data.username, "service": "service-two"},
]
)
assert isinstance(events.auth_bot, MockEventPublisher)
events.auth_bot.published.assert_published_all([])


@pytest.mark.asyncio
async def test_authz_failed(
Expand Down Expand Up @@ -395,3 +415,25 @@ async def test_internal(
"user": token_data.username,
},
]


@pytest.mark.asyncio
async def test_bot_metrics(client: AsyncClient, factory: Factory) -> None:
token_data = await create_session_token(
factory, username="bot-something", scopes={"read:all"}
)
r = await client.get(
"/ingress/auth",
params={"scope": "read:all", "service": "service"},
headers={"Authorization": f"Bearer {token_data.token}"},
)
assert r.status_code == 200

events = context_dependency._events
assert events
assert isinstance(events.auth_bot, MockEventPublisher)
events.auth_bot.published.assert_published_all(
[{"username": "bot-something", "service": "service"}]
)
assert isinstance(events.auth_user, MockEventPublisher)
events.auth_user.published.assert_published_all([])
28 changes: 28 additions & 0 deletions tests/handlers/login_github_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import pytest
import respx
from httpx import AsyncClient, Response
from safir.metrics import NOT_NONE, MockEventPublisher
from safir.testing.slack import MockSlackWebhook

from gafaelfawr.config import Config
from gafaelfawr.constants import COOKIE_NAME
from gafaelfawr.dependencies.config import config_dependency
from gafaelfawr.dependencies.context import context_dependency
from gafaelfawr.factory import Factory
from gafaelfawr.models.github import GitHubTeam, GitHubUserInfo
from gafaelfawr.models.state import State
Expand Down Expand Up @@ -200,6 +202,20 @@ async def test_login(
],
}

# Check that the correct metrics events were logged.
events = context_dependency._events
assert events
assert isinstance(events.login_attempt, MockEventPublisher)
events.login_attempt.published.assert_published_all([{}])
assert isinstance(events.login_success, MockEventPublisher)
events.login_success.published.assert_published_all(
[{"username": "githubuser", "elapsed": NOT_NONE}]
)
assert isinstance(events.login_enrollment, MockEventPublisher)
events.login_enrollment.published.assert_published_all([])
assert isinstance(events.login_failure, MockEventPublisher)
events.login_failure.published.assert_published_all([])


@pytest.mark.asyncio
async def test_redirect_header(
Expand Down Expand Up @@ -520,6 +536,18 @@ async def test_no_valid_groups(
# None of these errors should have resulted in Slack alerts.
assert mock_slack.messages == []

# Check that the correct metrics events were logged.
events = context_dependency._events
assert events
assert isinstance(events.login_attempt, MockEventPublisher)
events.login_attempt.published.assert_published_all([{}])
assert isinstance(events.login_success, MockEventPublisher)
events.login_success.published.assert_published_all([])
assert isinstance(events.login_enrollment, MockEventPublisher)
events.login_enrollment.published.assert_published_all([])
assert isinstance(events.login_failure, MockEventPublisher)
events.login_failure.published.assert_published_all([{}])


@pytest.mark.asyncio
async def test_unicode_name(
Expand Down
14 changes: 14 additions & 0 deletions tests/handlers/login_oidc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import pytest
import respx
from httpx import AsyncClient, ConnectError
from safir.metrics import MockEventPublisher
from safir.testing.slack import MockSlackWebhook

from gafaelfawr.constants import GID_MIN, UID_USER_MIN
from gafaelfawr.dependencies.context import context_dependency
from gafaelfawr.factory import Factory
from gafaelfawr.models.userinfo import Group, UserInfo

Expand Down Expand Up @@ -1009,6 +1011,18 @@ async def test_enrollment_url(
assert r.status_code == 307
assert r.headers["Cache-Control"] == "no-cache, no-store"

# Check that the correct metrics events were logged.
events = context_dependency._events
assert events
assert isinstance(events.login_attempt, MockEventPublisher)
events.login_attempt.published.assert_published_all([{}])
assert isinstance(events.login_success, MockEventPublisher)
events.login_success.published.assert_published_all([])
assert isinstance(events.login_enrollment, MockEventPublisher)
events.login_enrollment.published.assert_published_all([{}])
assert isinstance(events.login_failure, MockEventPublisher)
events.login_failure.published.assert_published_all([])


@pytest.mark.asyncio
async def test_missing_username(
Expand Down
19 changes: 19 additions & 0 deletions tests/services/token_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from cryptography.fernet import Fernet
from pydantic import ValidationError
from safir.datetime import current_datetime
from safir.metrics import MockEventPublisher
from safir.testing.slack import MockSlackWebhook

from gafaelfawr.config import Config
Expand Down Expand Up @@ -1754,6 +1755,17 @@ async def test_audit(factory: Factory) -> None:
@pytest.mark.asyncio
async def test_state_metrics(config: Config, factory: Factory) -> None:
token_service = factory.create_token_service()
event_manager = config.metrics.make_manager()
await event_manager.initialize()
events = StateEvents()
await events.initialize(event_manager)
await token_service.gather_state_metrics(events)

assert isinstance(events.active_user_sessions, MockEventPublisher)
events.active_user_sessions.published.assert_published_all([{"count": 0}])
assert isinstance(events.active_user_tokens, MockEventPublisher)
events.active_user_tokens.published.assert_published_all([{"count": 0}])

await create_session_token(factory, username="someone")
await create_session_token(factory, username="someone")
token_data = await create_session_token(factory, username="other")
Expand All @@ -1765,9 +1777,16 @@ async def test_state_metrics(config: Config, factory: Factory) -> None:
ip_address="127.0.0.1",
)

await event_manager.aclose()
event_manager = config.metrics.make_manager()
await event_manager.initialize()
events = StateEvents()
await events.initialize(event_manager)
await token_service.gather_state_metrics(events)

assert isinstance(events.active_user_sessions, MockEventPublisher)
events.active_user_sessions.published.assert_published_all([{"count": 2}])
assert isinstance(events.active_user_tokens, MockEventPublisher)
events.active_user_tokens.published.assert_published_all([{"count": 1}])

await event_manager.aclose()
Empty file removed tests/storage/__init__.py
Empty file.
34 changes: 0 additions & 34 deletions tests/storage/token_test.py

This file was deleted.

0 comments on commit e03608b

Please sign in to comment.