Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e24d7db17 | ||
|
|
43a2804562 | ||
|
|
42b659e807 | ||
|
|
240d962ec3 | ||
|
|
2aba489036 | ||
|
|
10181aa019 | ||
|
|
2d492d9d2e | ||
|
|
1fe16c1342 | ||
|
|
68c2048724 | ||
|
|
fcdca7a37c | ||
|
|
77404cfd03 | ||
|
|
49ce2d9945 | ||
|
|
5f86b58b88 | ||
|
|
888c03e4c1 | ||
|
|
bce46843b3 | ||
|
|
89b14872e6 | ||
|
|
090a059c17 | ||
|
|
b2f8eaf89a | ||
|
|
8001933692 | ||
|
|
d9ad68b72b |
No files matched your search
+2
-1
@@ -47,5 +47,6 @@ async def activity_events() -> list[dict]:
|
||||
"repo": event.get("repo") or {},
|
||||
"created_at": event.get("created", ""),
|
||||
}
|
||||
for event in events
|
||||
for event in (events or [])
|
||||
if isinstance(event, dict)
|
||||
]
|
||||
+44
-2
@@ -43,6 +43,8 @@ async def readiness():
|
||||
user = await asyncio.wait_for(
|
||||
current_user(), timeout=READINESS_TIMEOUT_SECONDS
|
||||
)
|
||||
if not isinstance(user, dict) or not user.get("login"):
|
||||
raise ValueError("Gitea current-user response did not include a login")
|
||||
except Exception as exc:
|
||||
timed_out = isinstance(exc, TimeoutError)
|
||||
error_message = (
|
||||
@@ -75,6 +77,10 @@ async def context() -> JSONResponse:
|
||||
asyncio.gather(current_user(), repos(), issues(), pull_requests()),
|
||||
timeout=CONTEXT_TIMEOUT_SECONDS,
|
||||
)
|
||||
if not isinstance(user_data, dict):
|
||||
raise ValueError("Gitea current-user response was not an object")
|
||||
if not all(field in user_data for field in ("id", "login")):
|
||||
raise ValueError("Gitea current-user response did not include id and login")
|
||||
except Exception as e:
|
||||
error_message = (
|
||||
f"Gitea context request timed out after {CONTEXT_TIMEOUT_SECONDS:g}s"
|
||||
@@ -100,16 +106,52 @@ async def context() -> JSONResponse:
|
||||
Repo(id=r["id"], name=r["name"], full_name=r["full_name"], description=r.get("description") or "", url=r["html_url"], updated_at=r.get("updated_at", ""))
|
||||
for r in (repo_data or [])[:50]
|
||||
if isinstance(r, dict)
|
||||
and all(field in r for field in ("id", "name", "full_name", "html_url"))
|
||||
]
|
||||
issue_models = [
|
||||
Issue(id=i["id"], number=i["number"], title=i["title"], state=i["state"], labels=[l.get("name", "") for l in (i.get("labels") or [])], assignees=[a.get("login", "") for a in (i.get("assignees") or [])], url=i["html_url"])
|
||||
Issue(
|
||||
id=i["id"],
|
||||
number=i["number"],
|
||||
title=i["title"],
|
||||
state=i["state"],
|
||||
labels=[
|
||||
label.get("name", "")
|
||||
for label in (i.get("labels") or [])
|
||||
if isinstance(label, dict)
|
||||
],
|
||||
assignees=[
|
||||
assignee.get("login", "")
|
||||
for assignee in (i.get("assignees") or [])
|
||||
if isinstance(assignee, dict)
|
||||
],
|
||||
url=i["html_url"],
|
||||
)
|
||||
for i in (issues_data or [])[:50]
|
||||
if isinstance(i, dict)
|
||||
and all(
|
||||
field in i
|
||||
for field in ("id", "number", "title", "state", "html_url")
|
||||
)
|
||||
]
|
||||
pr_models = [
|
||||
PullRequest(id=p["id"], number=p["number"], title=p["title"], state=p["state"], user=(p.get("user") or {}).get("login", ""), url=p["html_url"])
|
||||
PullRequest(
|
||||
id=p["id"],
|
||||
number=p["number"],
|
||||
title=p["title"],
|
||||
state=p["state"],
|
||||
user=(
|
||||
p["user"].get("login", "")
|
||||
if isinstance(p.get("user"), dict)
|
||||
else ""
|
||||
),
|
||||
url=p["html_url"],
|
||||
)
|
||||
for p in (prs_data or [])[:50]
|
||||
if isinstance(p, dict)
|
||||
and all(
|
||||
field in p
|
||||
for field in ("id", "number", "title", "state", "html_url")
|
||||
)
|
||||
]
|
||||
ctx = compute(user_model, repo_models, issue_models, pr_models)
|
||||
return JSONResponse(ctx.model_dump())
|
||||
|
||||
@@ -65,6 +65,37 @@ async def test_context_normalizes_nullable_issue_collections(monkeypatch):
|
||||
assert issue["assignees"] == []
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_skips_malformed_nested_issue_metadata(monkeypatch):
|
||||
async def user():
|
||||
return {"id": 1, "login": "timmy"}
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
async def issue_collection():
|
||||
return [{
|
||||
"id": 85,
|
||||
"number": 85,
|
||||
"title": "Malformed nested metadata",
|
||||
"state": "open",
|
||||
"labels": [None, {"name": "bug"}, "invalid"],
|
||||
"assignees": [None, {"login": "timmy"}, "invalid"],
|
||||
"html_url": "https://forge.example/issues/85",
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", issue_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", empty_collection)
|
||||
|
||||
response = await main.context()
|
||||
issue = json.loads(response.body)["issues"][0]
|
||||
|
||||
assert issue["labels"] == ["bug"]
|
||||
assert issue["assignees"] == ["timmy"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_normalizes_nullable_repository_description(monkeypatch):
|
||||
async def user():
|
||||
@@ -122,6 +153,35 @@ async def test_context_normalizes_nullable_pull_request_author(monkeypatch):
|
||||
assert pull_request["user"] == ""
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_normalizes_malformed_pull_request_author(monkeypatch):
|
||||
async def user():
|
||||
return {"id": 1, "login": "timmy"}
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
async def pull_request_collection():
|
||||
return [{
|
||||
"id": 87,
|
||||
"number": 87,
|
||||
"title": "Malformed author",
|
||||
"state": "open",
|
||||
"user": "unknown",
|
||||
"html_url": "https://forge.example/pulls/87",
|
||||
}]
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", empty_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", pull_request_collection)
|
||||
|
||||
response = await main.context()
|
||||
pull_request = json.loads(response.body)["pull_requests"][0]
|
||||
|
||||
assert pull_request["user"] == ""
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_normalizes_null_collection_payloads(monkeypatch):
|
||||
async def user():
|
||||
@@ -167,3 +227,140 @@ async def test_context_normalizes_nullable_user_profile_fields(monkeypatch):
|
||||
assert response.status_code == 200
|
||||
assert context["user"]["full_name"] == ""
|
||||
assert context["user"]["email"] == ""
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_returns_fallback_for_null_current_user_payload(monkeypatch):
|
||||
async def null_user():
|
||||
return None
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(main, "current_user", null_user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", empty_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", empty_collection)
|
||||
|
||||
response = await main.context()
|
||||
context = json.loads(response.body)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert context["repos"] == []
|
||||
assert context["error"] == "Gitea current-user response was not an object"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_returns_fallback_for_incomplete_current_user_payload(monkeypatch):
|
||||
async def incomplete_user():
|
||||
return {"id": 1}
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(main, "current_user", incomplete_user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", empty_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", empty_collection)
|
||||
|
||||
response = await main.context()
|
||||
context = json.loads(response.body)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert context["repos"] == []
|
||||
assert context["error"] == "Gitea current-user response did not include id and login"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_skips_malformed_repository_entries(monkeypatch):
|
||||
async def user():
|
||||
return {"id": 1, "login": "timmy"}
|
||||
|
||||
async def repository_collection():
|
||||
return [
|
||||
{"id": 79, "name": "missing-required-fields"},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "dashboard",
|
||||
"full_name": "stackchain/dashboard",
|
||||
"description": "Operations dashboard",
|
||||
"html_url": "https://forge.example/stackchain/dashboard",
|
||||
},
|
||||
]
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "repos", repository_collection)
|
||||
monkeypatch.setattr(main, "issues", empty_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", empty_collection)
|
||||
|
||||
response = await main.context()
|
||||
repositories = json.loads(response.body)["repos"]
|
||||
|
||||
assert [repository["name"] for repository in repositories] == ["dashboard"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_skips_malformed_issue_entries(monkeypatch):
|
||||
async def user():
|
||||
return {"id": 1, "login": "timmy"}
|
||||
|
||||
async def issue_collection():
|
||||
return [
|
||||
{"id": 81, "title": "missing-required-fields"},
|
||||
{
|
||||
"id": 1,
|
||||
"number": 81,
|
||||
"title": "Valid issue",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/dashboard/issues/81",
|
||||
},
|
||||
]
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", issue_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", empty_collection)
|
||||
|
||||
response = await main.context()
|
||||
issues = json.loads(response.body)["issues"]
|
||||
|
||||
assert [issue["title"] for issue in issues] == ["Valid issue"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_context_skips_malformed_pull_request_entries(monkeypatch):
|
||||
async def user():
|
||||
return {"id": 1, "login": "timmy"}
|
||||
|
||||
async def pull_request_collection():
|
||||
return [
|
||||
{"id": 83, "title": "missing-required-fields"},
|
||||
{
|
||||
"id": 1,
|
||||
"number": 83,
|
||||
"title": "Valid pull request",
|
||||
"state": "open",
|
||||
"html_url": "https://forge.example/stackchain/stackchain-dashboard/pulls/83",
|
||||
},
|
||||
]
|
||||
|
||||
async def empty_collection():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(main, "current_user", user)
|
||||
monkeypatch.setattr(main, "repos", empty_collection)
|
||||
monkeypatch.setattr(main, "issues", empty_collection)
|
||||
monkeypatch.setattr(main, "pull_requests", pull_request_collection)
|
||||
|
||||
response = await main.context()
|
||||
pull_requests = json.loads(response.body)["pull_requests"]
|
||||
|
||||
assert [pull_request["title"] for pull_request in pull_requests] == [
|
||||
"Valid pull request"
|
||||
]
|
||||
@@ -118,3 +118,47 @@ async def test_activity_events_fetches_feed_for_authenticated_user(monkeypatch):
|
||||
"created_at": "2026-08-05T03:00:00Z",
|
||||
}]
|
||||
assert paths == ["users/timmy/activities/feeds?limit=20"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_activity_events_skips_malformed_feed_entries(monkeypatch):
|
||||
async def fake_current_user():
|
||||
return {"login": "timmy"}
|
||||
|
||||
async def fake_fetch(path):
|
||||
return [
|
||||
None,
|
||||
{
|
||||
"op_type": "push",
|
||||
"act_user": {"login": "timmy"},
|
||||
"repo": {"full_name": "stackchain/stackchain-dashboard"},
|
||||
"created": "2026-08-06T08:00:00Z",
|
||||
},
|
||||
"invalid",
|
||||
]
|
||||
|
||||
monkeypatch.setattr(gitea_proxy, "current_user", fake_current_user)
|
||||
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
|
||||
|
||||
result = await gitea_proxy.activity_events()
|
||||
|
||||
assert result == [{
|
||||
"type": "push",
|
||||
"actor": {"login": "timmy"},
|
||||
"repo": {"full_name": "stackchain/stackchain-dashboard"},
|
||||
"created_at": "2026-08-06T08:00:00Z",
|
||||
}]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_activity_events_normalizes_null_feed_payload(monkeypatch):
|
||||
async def fake_current_user():
|
||||
return {"login": "timmy"}
|
||||
|
||||
async def fake_fetch(path):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gitea_proxy, "current_user", fake_current_user)
|
||||
monkeypatch.setattr(gitea_proxy, "fetch", fake_fetch)
|
||||
|
||||
assert await gitea_proxy.activity_events() == []
|
||||
@@ -42,6 +42,23 @@ async def test_readiness_endpoint_returns_503_when_gitea_is_unavailable(monkeypa
|
||||
assert b'"error":"connection refused"' in response.body
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_readiness_endpoint_returns_503_for_null_current_user_payload(monkeypatch):
|
||||
async def null_user():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(main, "current_user", null_user)
|
||||
|
||||
response = await main.readiness()
|
||||
|
||||
assert response.status_code == 503
|
||||
assert json.loads(response.body) == {
|
||||
"status": "not_ready",
|
||||
"service": "stackchain-dashboard",
|
||||
"error": "Gitea current-user response did not include a login",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_readiness_endpoint_times_out_and_cancels_stalled_gitea_check(monkeypatch):
|
||||
cancelled = asyncio.Event()
|
||||
|
||||
Reference in new issue
Block a user