|
7 | 7 | import pytest |
8 | 8 | from pytest_mock import MockerFixture |
9 | 9 | from sqlalchemy.ext.asyncio import AsyncSession |
| 10 | +from starlette.types import Message |
| 11 | +from starlette.websockets import WebSocket |
10 | 12 |
|
11 | 13 | from apps.audit import EventAction, EventOutcome |
12 | 14 | from apps.authentication.domain.login import UserLoginRequest |
|
25 | 27 | from apps.users.cruds.user import UsersCRUD |
26 | 28 | from apps.users.domain import User, UserCreate, UserCreateRequest |
27 | 29 | from config import settings |
| 30 | +from infrastructure.http.exceptions import session_token_invalid_error_handler |
28 | 31 |
|
29 | 32 | TEST_PASSWORD = "Test12345!" |
30 | 33 |
|
@@ -113,6 +116,50 @@ async def test_user_not_found(self, client: TestClient, mocker: MockerFixture): |
113 | 116 | assert resp.status_code == http.HTTPStatus.UNAUTHORIZED |
114 | 117 | assert resp.json()["result"][0]["message"] == SessionTokenInvalidError.message |
115 | 118 |
|
| 119 | + async def test_ws_session_invalid_audit_event(self, mocker: MockerFixture): |
| 120 | + """A `SessionTokenInvalidError` raised from a WebSocket connection (e.g. `/ws/alerts`) must |
| 121 | +
|
| 122 | + log the `user:session:invalid` audit event without crashing. `http_audit_fields` used to |
| 123 | + read the HTTP-only `request.method`, which a `WebSocket` lacks (regression, M2-10698). |
| 124 | + """ |
| 125 | + audit_log = mocker.patch("infrastructure.http.exceptions.log") |
| 126 | + user_id = uuid.uuid4() |
| 127 | + |
| 128 | + async def receive() -> Message: |
| 129 | + return {"type": "websocket.connect"} |
| 130 | + |
| 131 | + async def send(message: Message) -> None: |
| 132 | + return None |
| 133 | + |
| 134 | + websocket = WebSocket( |
| 135 | + { |
| 136 | + "type": "websocket", |
| 137 | + "scheme": "ws", |
| 138 | + "server": ("test.com", 80), |
| 139 | + "path": "/ws/alerts", |
| 140 | + "query_string": b"", |
| 141 | + "root_path": "", |
| 142 | + "headers": [(b"host", b"test.com"), (b"user-agent", b"pytest-ws-client")], |
| 143 | + "client": ("10.1.2.3", 54321), |
| 144 | + }, |
| 145 | + receive=receive, |
| 146 | + send=send, |
| 147 | + ) |
| 148 | + error = SessionTokenInvalidError(user_id=user_id) |
| 149 | + |
| 150 | + resp = await session_token_invalid_error_handler(websocket, error) |
| 151 | + |
| 152 | + audit_log.assert_awaited_once() |
| 153 | + event = audit_log.call_args[0][0] |
| 154 | + assert event.event_action == EventAction.USER_SESSION_INVALID |
| 155 | + assert event.event_outcome == EventOutcome.FAILURE |
| 156 | + assert event.user_id == user_id |
| 157 | + assert event.http_request_method is None # WebSocket has no HTTP method |
| 158 | + assert event.url_path == "/ws/alerts" |
| 159 | + assert event.client_ip == "10.1.2.3" |
| 160 | + assert event.user_agent == "pytest-ws-client" |
| 161 | + assert resp.status_code == http.HTTPStatus.UNAUTHORIZED |
| 162 | + |
116 | 163 | async def test_delete_access_token(self, client: TestClient, user: User, mocker: MockerFixture): |
117 | 164 | audit_log = mocker.patch("apps.authentication.api.auth.log") |
118 | 165 | client.login(user) |
|
0 commit comments