mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-08-21 07:47:27 +00:00
Compare commits
3
Commits
78288befb8
...
aef7fa5f93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef7fa5f93 | ||
|
|
c412b08135 | ||
|
|
a3b6372c11 |
@@ -5,7 +5,7 @@
|
|||||||
"@mui/icons-material": ">=7.3.0 <7.4.0",
|
"@mui/icons-material": ">=7.3.0 <7.4.0",
|
||||||
"@mui/material": ">=7.3.0 <7.4.0",
|
"@mui/material": ">=7.3.0 <7.4.0",
|
||||||
"@mui/x-data-grid": ">=8.28.0 <8.29.0",
|
"@mui/x-data-grid": ">=8.28.0 <8.29.0",
|
||||||
"@tanstack/react-query": ">=5.94.0 <5.95.0",
|
"@tanstack/react-query": ">=5.101.0 <5.102.0",
|
||||||
"chart.js": ">=4.5.0 <4.6.0",
|
"chart.js": ">=4.5.0 <4.6.0",
|
||||||
"react": ">=19.2.0 <19.3.0",
|
"react": ">=19.2.0 <19.3.0",
|
||||||
"react-chartjs-2": ">=5.3.0 <5.4.0",
|
"react-chartjs-2": ">=5.3.0 <5.4.0",
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
"@vitejs/plugin-react": ">=6.0.0 <6.1.0",
|
"@vitejs/plugin-react": ">=6.0.0 <6.1.0",
|
||||||
"eslint": ">=9.39.0 <9.40.0",
|
"eslint": ">=9.39.0 <9.40.0",
|
||||||
"eslint-plugin-react": ">=7.37.0 <7.38.0",
|
"eslint-plugin-react": ">=7.37.0 <7.38.0",
|
||||||
"eslint-plugin-react-hooks": ">=7.0.0 <7.1.0",
|
"eslint-plugin-react-hooks": ">=7.1.0 <7.2.0",
|
||||||
"eslint-plugin-react-refresh": ">=0.5.0 <0.6.0",
|
"eslint-plugin-react-refresh": ">=0.5.0 <0.6.0",
|
||||||
"eslint-plugin-simple-import-sort": ">=12.1.0 <12.2.0",
|
"eslint-plugin-simple-import-sort": ">=12.1.0 <12.2.0",
|
||||||
"typescript": ">=5.9.0 <5.10.0",
|
"typescript": ">=5.9.0 <5.10.0",
|
||||||
"typescript-eslint": ">=8.57.0 <8.58.0",
|
"typescript-eslint": ">=8.57.0 <8.58.0",
|
||||||
"vite": ">=8.0.0 <8.1.0"
|
"vite": ">=8.1.0 <8.2.0"
|
||||||
},
|
},
|
||||||
"name": "ahriman-frontend",
|
"name": "ahriman-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const queryClient = new QueryClient({
|
|||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
retry: 1,
|
retry: 1,
|
||||||
staleTime: 30_000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, Request, StreamResponse
|
from aiohttp.web import HTTPBadRequest, Request, Response, StreamResponse
|
||||||
from aiohttp_sse import EventSourceResponse, sse_response
|
from aiohttp_sse import EventSourceResponse, sse_response
|
||||||
from asyncio import Queue, QueueShutDown, wait_for
|
from asyncio import Queue, QueueShutDown, wait_for
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
@@ -66,16 +66,13 @@ class EventBusView(BaseView):
|
|||||||
return await BaseView.get_permission(request)
|
return await BaseView.get_permission(request)
|
||||||
|
|
||||||
permission = UserAccess.Full
|
permission = UserAccess.Full
|
||||||
event_filter = request.query.getall("event", []) if request.query is not None else []
|
try:
|
||||||
|
topics = cls(request)._topics()
|
||||||
|
except HTTPBadRequest:
|
||||||
|
topics = None
|
||||||
|
|
||||||
if event_filter:
|
if topics is not None and set(topics).issubset(cls.READ_EVENTS):
|
||||||
try:
|
permission = UserAccess.Read
|
||||||
topics = {EventType(event) for event in event_filter}
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if topics.issubset(cls.READ_EVENTS):
|
|
||||||
permission = UserAccess.Read
|
|
||||||
|
|
||||||
return permission
|
return permission
|
||||||
|
|
||||||
@@ -98,6 +95,24 @@ class EventBusView(BaseView):
|
|||||||
|
|
||||||
await response.send(json.dumps(data), event=event_type)
|
await response.send(json.dumps(data), event=event_type)
|
||||||
|
|
||||||
|
def _topics(self) -> list[EventType] | None:
|
||||||
|
"""
|
||||||
|
parse event filter from request query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[EventType] | None: event filter if any
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPBadRequest: if invalid event type is supplied
|
||||||
|
"""
|
||||||
|
if self.request.query is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return [EventType(event) for event in self.request.query.getall("event", [])] or None
|
||||||
|
except ValueError as ex:
|
||||||
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
@apidocs(
|
@apidocs(
|
||||||
tags=["Audit log"],
|
tags=["Audit log"],
|
||||||
summary="Live updates",
|
summary="Live updates",
|
||||||
@@ -116,14 +131,8 @@ class EventBusView(BaseView):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
StreamResponse: 200 with streaming updates
|
StreamResponse: 200 with streaming updates
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPBadRequest: if invalid event type is supplied
|
|
||||||
"""
|
"""
|
||||||
try:
|
topics = self._topics()
|
||||||
topics = [EventType(event) for event in self.request.query.getall("event", [])] or None
|
|
||||||
except ValueError as ex:
|
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
|
||||||
object_id = self.request.query.get("object_id")
|
object_id = self.request.query.get("object_id")
|
||||||
event_bus = self.service().event_bus
|
event_bus = self.service().event_bus
|
||||||
|
|
||||||
@@ -138,3 +147,18 @@ class EventBusView(BaseView):
|
|||||||
await event_bus.unsubscribe(subscription_id)
|
await event_bus.unsubscribe(subscription_id)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
async def head(self) -> StreamResponse:
|
||||||
|
"""
|
||||||
|
HEAD method implementation based on the result of GET method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StreamResponse: generated response for the request
|
||||||
|
"""
|
||||||
|
self._topics()
|
||||||
|
self.service()
|
||||||
|
|
||||||
|
return Response(headers={
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
from aiohttp.web import HTTPBadRequest
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
from multidict import MultiDict
|
from multidict import MultiDict
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
@@ -110,6 +111,37 @@ async def test_run_timeout() -> None:
|
|||||||
await EventBusView._run(response, queue)
|
await EventBusView._run(response, queue)
|
||||||
|
|
||||||
|
|
||||||
|
def test_topics() -> None:
|
||||||
|
"""
|
||||||
|
must parse event filters
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "GET", params=MultiDict([
|
||||||
|
("event", EventType.PackageUpdated),
|
||||||
|
("event", EventType.PackageRemoved),
|
||||||
|
]))
|
||||||
|
|
||||||
|
assert EventBusView(request)._topics() == [EventType.PackageUpdated, EventType.PackageRemoved]
|
||||||
|
|
||||||
|
|
||||||
|
def test_topics_empty() -> None:
|
||||||
|
"""
|
||||||
|
must return None for missing event filters
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "GET", params=MultiDict())
|
||||||
|
|
||||||
|
assert EventBusView(request)._topics() is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_topics_invalid() -> None:
|
||||||
|
"""
|
||||||
|
must raise bad request for invalid event filters
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "GET", params=MultiDict(event="invalid"))
|
||||||
|
|
||||||
|
with pytest.raises(HTTPBadRequest):
|
||||||
|
EventBusView(request)._topics()
|
||||||
|
|
||||||
|
|
||||||
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must stream events via SSE
|
must stream events via SSE
|
||||||
@@ -192,3 +224,31 @@ async def test_get_connection_reset(client: TestClient, mocker: MockerFixture) -
|
|||||||
mocker.patch.object(EventBusView, "_run", side_effect=ConnectionResetError)
|
mocker.patch.object(EventBusView, "_run", side_effect=ConnectionResetError)
|
||||||
response = await client.get("/api/v1/events/stream")
|
response = await client.get("/api/v1/events/stream")
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_head(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must check stream availability without opening SSE stream
|
||||||
|
"""
|
||||||
|
response = await client.head("/api/v1/events/stream", params={"event": EventType.PackageUpdated})
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.headers["Content-Type"] == "text/event-stream"
|
||||||
|
assert not await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_head_bad_request(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must return bad request for invalid event type
|
||||||
|
"""
|
||||||
|
response = await client.head("/api/v1/events/stream", params={"event": "invalid"})
|
||||||
|
assert response.status == 400
|
||||||
|
assert not await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_head_not_found(client: TestClient) -> None:
|
||||||
|
"""
|
||||||
|
must return not found for unknown repository
|
||||||
|
"""
|
||||||
|
response = await client.head("/api/v1/events/stream", params={"architecture": "unknown", "repository": "unknown"})
|
||||||
|
assert response.status == 404
|
||||||
|
assert not await response.text()
|
||||||
|
|||||||
Reference in New Issue
Block a user