mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The server moved off /api/vN; the integration suite, handler tests, install.sh's health wait and the docs still called the old tree. Claude-Session: https://claude.ai/code/session_01TUxvfuta1Z44KVNYuAz71F
268 lines
9.7 KiB
Python
268 lines
9.7 KiB
Python
from collections.abc import Callable
|
|
from datetime import UTC, datetime, timedelta
|
|
from http import HTTPStatus
|
|
|
|
import pytest
|
|
import requests
|
|
from sqlalchemy import sql
|
|
from wiremock.resources.mappings import Mapping
|
|
|
|
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
|
from fixtures.metrics import Metrics
|
|
from fixtures.types import Operation, O11y, TestContainerDocker
|
|
|
|
|
|
def test_apply_license(
|
|
o11y: O11y,
|
|
create_user_admin: Operation, # pylint: disable=unused-argument
|
|
make_http_mocks: Callable[[TestContainerDocker, list[Mapping]], None],
|
|
get_token: Callable[[str, str], str],
|
|
) -> None:
|
|
"""
|
|
This applies a license to the o11y instance.
|
|
"""
|
|
add_license(o11y, make_http_mocks, get_token)
|
|
|
|
|
|
def test_create_and_get_public_dashboard(
|
|
o11y: O11y,
|
|
create_user_admin: Operation, # pylint: disable=unused-argument
|
|
get_token: Callable[[str, str], str],
|
|
):
|
|
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
|
|
|
response = requests.post(
|
|
o11y.self.host_configs["8080"].get("/v1/o11y/dashboards"),
|
|
json={"title": "Sample Title", "uploadedGrafana": False, "version": "v5"},
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.CREATED
|
|
assert response.json()["status"] == "success"
|
|
data = response.json()["data"]
|
|
dashboard_id = data["id"]
|
|
|
|
response = requests.post(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/dashboards/{dashboard_id}/public"),
|
|
json={
|
|
"timeRangeEnabled": True,
|
|
"defaultTimeRange": "10s",
|
|
},
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.CREATED
|
|
assert "id" in response.json()["data"]
|
|
|
|
response = requests.get(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/dashboards/{dashboard_id}/public"),
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json()["status"] == "success"
|
|
assert response.json()["data"]["timeRangeEnabled"] is True
|
|
assert response.json()["data"]["defaultTimeRange"] == "10s"
|
|
|
|
|
|
def test_public_dashboard_widget_query_range(
|
|
o11y: O11y,
|
|
create_user_admin: Operation, # pylint: disable=unused-argument
|
|
get_token: Callable[[str, str], str],
|
|
insert_metrics: Callable[[list[Metrics]], None],
|
|
):
|
|
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
|
|
|
# Insert metric data so the widget query returns results instead of 404
|
|
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
|
metrics: list[Metrics] = [
|
|
Metrics(
|
|
metric_name="container.cpu.time",
|
|
labels={"service": "test-service"},
|
|
timestamp=now - timedelta(minutes=5),
|
|
value=100.0,
|
|
temporality="Cumulative",
|
|
),
|
|
Metrics(
|
|
metric_name="container.cpu.time",
|
|
labels={"service": "test-service"},
|
|
timestamp=now - timedelta(minutes=3),
|
|
value=200.0,
|
|
temporality="Cumulative",
|
|
),
|
|
Metrics(
|
|
metric_name="container.cpu.time",
|
|
labels={"service": "test-service"},
|
|
timestamp=now - timedelta(minutes=1),
|
|
value=300.0,
|
|
temporality="Cumulative",
|
|
),
|
|
]
|
|
insert_metrics(metrics)
|
|
|
|
dashboard_req = {
|
|
"title": "Test Widget Query Range Dashboard",
|
|
"description": "For testing widget query range",
|
|
"version": "v5",
|
|
"widgets": [
|
|
{
|
|
"id": "6990c9d8-57ad-492f-8c63-039081e30d02",
|
|
"panelTypes": "graph",
|
|
"query": {
|
|
"builder": {
|
|
"queryData": [
|
|
{
|
|
"aggregations": [
|
|
{
|
|
"metricName": "container.cpu.time",
|
|
"reduceTo": "avg",
|
|
"spaceAggregation": "sum",
|
|
"temporality": "",
|
|
"timeAggregation": "rate",
|
|
}
|
|
],
|
|
"dataSource": "metrics",
|
|
"disabled": False,
|
|
"expression": "A",
|
|
"filter": {"expression": ""},
|
|
"functions": [],
|
|
"groupBy": [],
|
|
"having": {"expression": ""},
|
|
"legend": "",
|
|
"limit": 10,
|
|
"orderBy": [],
|
|
"queryName": "A",
|
|
"source": "",
|
|
"stepInterval": 10,
|
|
}
|
|
],
|
|
"queryFormulas": [],
|
|
"queryTraceOperator": [],
|
|
},
|
|
"datastore_sql": [{"disabled": False, "legend": "", "name": "A", "query": ""}],
|
|
"id": "80f12506-ef72-4013-8282-2713c8114c9e",
|
|
"promql": [{"disabled": False, "legend": "", "name": "A", "query": ""}],
|
|
"queryType": "builder",
|
|
},
|
|
}
|
|
],
|
|
}
|
|
create_response = requests.post(
|
|
o11y.self.host_configs["8080"].get("/v1/o11y/dashboards"),
|
|
json=dashboard_req,
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
assert create_response.status_code == HTTPStatus.CREATED
|
|
data = create_response.json()["data"]
|
|
dashboard_id = data["id"]
|
|
|
|
# create public dashboard
|
|
response = requests.post(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/dashboards/{dashboard_id}/public"),
|
|
json={
|
|
"timeRangeEnabled": False,
|
|
"defaultTimeRange": "10m",
|
|
},
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.CREATED
|
|
assert "id" in response.json()["data"]
|
|
|
|
response = requests.get(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/dashboards/{dashboard_id}/public"),
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json()["status"] == "success"
|
|
public_path = response.json()["data"]["publicPath"]
|
|
public_dashboard_id = public_path.split("/public/dashboard/")[-1]
|
|
|
|
resp = requests.get(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/public/dashboards/{public_dashboard_id}/widgets/0/query_range"),
|
|
timeout=2,
|
|
)
|
|
assert resp.status_code == HTTPStatus.OK
|
|
assert resp.json().get("status") == "success"
|
|
|
|
resp = requests.get(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/public/dashboards/{public_dashboard_id}/widgets/-1/query_range"),
|
|
timeout=2,
|
|
)
|
|
assert resp.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
resp = requests.get(
|
|
o11y.self.host_configs["8080"].get(f"/v1/o11y/public/dashboards/{public_dashboard_id}/widgets/1/query_range"),
|
|
timeout=2,
|
|
)
|
|
assert resp.status_code == HTTPStatus.BAD_REQUEST
|
|
|
|
|
|
def test_anonymous_role_has_public_dashboard_permission(
|
|
request: pytest.FixtureRequest,
|
|
o11y: O11y,
|
|
create_user_admin: Operation, # pylint: disable=unused-argument
|
|
get_token: Callable[[str, str], str],
|
|
):
|
|
"""
|
|
Verify that the o11y-anonymous role has the public-dashboard/* permission.
|
|
"""
|
|
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
|
|
|
# Get the roles to find the org_id
|
|
response = requests.get(
|
|
o11y.self.host_configs["8080"].get("/v1/o11y/roles"),
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
timeout=2,
|
|
)
|
|
|
|
assert response.status_code == HTTPStatus.OK
|
|
assert response.json()["status"] == "success"
|
|
|
|
roles = response.json()["data"]
|
|
anonymous_role = next((role for role in roles if role["name"] == "o11y-anonymous"), None)
|
|
assert anonymous_role is not None
|
|
org_id = anonymous_role["orgId"]
|
|
|
|
# Verify the tuple exists in the database that grants
|
|
# o11y-anonymous role read access to public-dashboard/*
|
|
with o11y.sqlstore.conn.connect() as conn:
|
|
tuple_object_id = f"organization/{org_id}/public-dashboard/*"
|
|
anonymous_role_subject = f"organization/{org_id}/role/o11y-anonymous"
|
|
tuple_result = conn.execute(
|
|
sql.text("SELECT * FROM tuple WHERE object_id = :object_id AND relation = :relation"),
|
|
{"object_id": tuple_object_id, "relation": "read"},
|
|
)
|
|
|
|
tuple_rows = tuple_result.mappings().fetchall()
|
|
assert len(tuple_rows) > 0
|
|
|
|
if request.config.getoption("--sqlstore-provider") == "sqlite":
|
|
tuple_row = next(
|
|
(row for row in tuple_rows if row["user_object_id"] == anonymous_role_subject),
|
|
None,
|
|
)
|
|
else:
|
|
tuple_row = next(
|
|
(row for row in tuple_rows if row["_user"] == f"role:{anonymous_role_subject}#assignee"),
|
|
None,
|
|
)
|
|
|
|
assert tuple_row is not None
|
|
assert tuple_row["object_type"] == "metaresource"
|
|
assert tuple_row["relation"] == "read"
|
|
|
|
if request.config.getoption("--sqlstore-provider") == "sqlite":
|
|
assert tuple_row["user_object_type"] == "role"
|
|
assert tuple_row["user_object_id"] == anonymous_role_subject
|
|
assert tuple_row["user_relation"] == "assignee"
|
|
else:
|
|
assert tuple_row["_user"] == f"role:{anonymous_role_subject}#assignee"
|