test: admin, sudo, notification, user and package scenarios
This commit is contained in:
1 parent
825c3c93e2
commit
29b12bfe0d
1 file changed
+270
@@ -0,0 +1,270 @@
|
||||
"""Scenarios: site administration, sudo, notifications, current-user features and packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from tests.integration.conftest import McpSession, RegularUser, RepoRef, drop_org, drop_user
|
||||
from tests.integration.harness import GiteaInstance
|
||||
|
||||
|
||||
def ssh_public_key() -> str:
|
||||
"""A fresh ed25519 public key in OpenSSH format (throwaway, the private half is dropped)."""
|
||||
key = Ed25519PrivateKey.generate().public_key()
|
||||
raw = key.public_bytes(serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH)
|
||||
return raw.decode("ascii")
|
||||
|
||||
|
||||
def logins(items: Any, key: str = "login") -> list[str]:
|
||||
"""The ``key`` member of every dict in a list result."""
|
||||
return [item[key] for item in items]
|
||||
|
||||
|
||||
# Cron tasks that are safe to trigger on a throwaway instance.
|
||||
SAFE_CRON_TASKS = ("cleanup_hook_task_table", "cleanup_expired_actions_assets", "check_repo_stats")
|
||||
|
||||
|
||||
async def test_admin_manages_users_orgs_keys_and_cron(
|
||||
mcp: McpSession, gitea: GiteaInstance, fresh_repo: RepoRef
|
||||
) -> None:
|
||||
# fresh_repo makes sure the repository root exists (the unadopted scan needs it).
|
||||
login = gitea.unique("it-adm")
|
||||
org = gitea.unique("it-adm-org")
|
||||
email = f"{login}@example.test"
|
||||
try:
|
||||
created = await mcp.ok(
|
||||
"gitea_admin_create_user",
|
||||
status=201,
|
||||
body={
|
||||
"username": login,
|
||||
"email": email,
|
||||
"password": secrets.token_urlsafe(18),
|
||||
"must_change_password": False,
|
||||
},
|
||||
)
|
||||
assert created.data["login"] == login
|
||||
edited = await mcp.ok(
|
||||
"gitea_admin_edit_user",
|
||||
username=login,
|
||||
body={"source_id": 0, "login_name": login, "full_name": "Integration User"},
|
||||
)
|
||||
assert edited.data["full_name"] == "Integration User"
|
||||
|
||||
found = await mcp.ok("gitea_admin_search_users", q=login)
|
||||
assert login in logins(found.data)
|
||||
assert email in logins(
|
||||
(await mcp.ok("gitea_admin_get_all_emails")).data, "email"
|
||||
) or email in logins((await mcp.ok("gitea_admin_search_emails", q=login)).data, "email")
|
||||
|
||||
# SSH key for the user, listed as the user, then removed.
|
||||
key = await mcp.ok(
|
||||
"gitea_admin_create_public_key",
|
||||
status=201,
|
||||
username=login,
|
||||
body={"title": "it key", "key": ssh_public_key()},
|
||||
)
|
||||
listed = await mcp.ok("gitea_user_list_keys", username=login)
|
||||
assert key.data["id"] in [item["id"] for item in listed.data]
|
||||
await mcp.ok("gitea_admin_delete_user_public_key", username=login, id=key.data["id"])
|
||||
assert (await mcp.ok("gitea_user_list_keys", username=login)).data == []
|
||||
|
||||
# An organization created on behalf of the user.
|
||||
await mcp.ok("gitea_admin_create_org", status=201, username=login, body={"username": org})
|
||||
assert org in logins((await mcp.ok("gitea_admin_get_all_orgs")).data, "name")
|
||||
assert org in logins((await mcp.ok("gitea_user_list_orgs", username=login)).data, "name")
|
||||
await mcp.ok("gitea_org_delete", org=org)
|
||||
|
||||
# Cron: list the tasks and run one safe task.
|
||||
tasks = await mcp.ok("gitea_admin_cron_list")
|
||||
task_names = logins(tasks.data, "name")
|
||||
assert task_names
|
||||
chosen = next((name for name in SAFE_CRON_TASKS if name in task_names), task_names[0])
|
||||
await mcp.ok("gitea_admin_cron_run", task=chosen)
|
||||
|
||||
# Unadopted repositories and runner registration token.
|
||||
await mcp.ok("gitea_admin_unadopted_list")
|
||||
token = await mcp.ok("gitea_admin_create_runner_registration_token", status=(200, 201))
|
||||
assert isinstance(token.data["token"], str) and token.data["token"]
|
||||
assert await mcp.ok("gitea_admin_get_runners")
|
||||
|
||||
await mcp.ok("gitea_admin_delete_user", username=login, purge=True)
|
||||
await mcp.fail("gitea_user_get", 404, username=login)
|
||||
finally:
|
||||
drop_org(gitea, org)
|
||||
drop_user(gitea, login)
|
||||
|
||||
|
||||
async def test_sudo_acts_as_the_named_user(
|
||||
mcp: McpSession, gitea: GiteaInstance, regular_user: RegularUser
|
||||
) -> None:
|
||||
me = regular_user.login
|
||||
plain = await mcp.ok("gitea_user_get_current")
|
||||
assert plain.data["login"] == gitea.admin_login
|
||||
assert plain.identity == {"login": gitea.admin_login, "mode": "admin-token", "sudo": None}
|
||||
|
||||
sudoed = await mcp.ok("gitea_user_get_current", sudo=me)
|
||||
assert sudoed.data["login"] == me
|
||||
assert sudoed.identity["sudo"] == me
|
||||
assert sudoed.identity["login"] == gitea.admin_login
|
||||
|
||||
repo = gitea.unique("sudo-repo")
|
||||
try:
|
||||
made = await mcp.ok(
|
||||
"gitea_user_current_create_repo", status=201, sudo=me, body={"name": repo}
|
||||
)
|
||||
assert made.data["owner"]["login"] == me
|
||||
assert gitea.api("GET", f"/repos/{me}/{repo}").status_code == 200
|
||||
finally:
|
||||
gitea.api("DELETE", f"/repos/{me}/{repo}")
|
||||
|
||||
# The audit log records who acted and who they acted as.
|
||||
entries = [e for e in mcp.audit_entries() if e.get("sudo") == me]
|
||||
assert {e["tool_name"] for e in entries} == {
|
||||
"gitea_user_get_current",
|
||||
"gitea_user_current_create_repo",
|
||||
}
|
||||
assert all(e["identity_login"] == gitea.admin_login for e in entries)
|
||||
assert all(e["identity_mode"] == "admin-token" for e in entries)
|
||||
|
||||
|
||||
async def test_notifications_list_and_mark_read(
|
||||
mcp: McpSession, gitea: GiteaInstance, regular_user: RegularUser, fresh_repo: RepoRef
|
||||
) -> None:
|
||||
repo = fresh_repo.args
|
||||
# A second user assigns the admin to a new issue, which notifies the admin.
|
||||
await mcp.ok(
|
||||
"gitea_repo_add_collaborator",
|
||||
collaborator=regular_user.login,
|
||||
body={"permission": "write"},
|
||||
**repo,
|
||||
)
|
||||
await mcp.ok(
|
||||
"gitea_issue_create_issue",
|
||||
status=201,
|
||||
sudo=regular_user.login,
|
||||
body={"title": "Ping", "assignees": [gitea.admin_login]},
|
||||
**repo,
|
||||
)
|
||||
deadline = time.monotonic() + 30
|
||||
notices: list[dict[str, Any]] = []
|
||||
while time.monotonic() < deadline and not notices:
|
||||
notices = (await mcp.ok("gitea_notification_get_list", **{"status-types": ["unread"]})).data
|
||||
if not notices:
|
||||
await asyncio.sleep(0.5)
|
||||
assert notices, "no notification arrived within 30 seconds"
|
||||
assert (await mcp.ok("gitea_notification_new_available")).data["new"] >= 1
|
||||
repo_list = await mcp.ok("gitea_notification_get_repo_list", **repo)
|
||||
assert repo_list.data
|
||||
|
||||
thread_id = str(notices[0]["id"])
|
||||
thread = await mcp.ok("gitea_notification_get_thread", id=thread_id)
|
||||
assert thread.data["unread"] is True
|
||||
await mcp.ok("gitea_notification_read_thread", status=(200, 205), id=thread_id)
|
||||
assert (await mcp.ok("gitea_notification_get_thread", id=thread_id)).data["unread"] is False
|
||||
|
||||
await mcp.ok("gitea_notification_read_list", status=(200, 205))
|
||||
assert (await mcp.ok("gitea_notification_get_list", **{"status-types": ["unread"]})).data == []
|
||||
|
||||
|
||||
async def test_current_user_features(
|
||||
mcp: McpSession, gitea: GiteaInstance, regular_user: RegularUser, fresh_repo: RepoRef
|
||||
) -> None:
|
||||
repo = fresh_repo.args
|
||||
other = regular_user.login
|
||||
me = await mcp.ok("gitea_user_get_current")
|
||||
assert me.data["is_admin"] is True
|
||||
assert (await mcp.ok("gitea_user_get", username=other)).data["login"] == other
|
||||
assert (await mcp.ok("gitea_user_get_settings")).data is not None
|
||||
|
||||
# Email addresses.
|
||||
address = f"extra-{secrets.token_hex(3)}@example.test"
|
||||
await mcp.ok("gitea_user_add_email", status=201, body={"emails": [address]})
|
||||
assert address in logins((await mcp.ok("gitea_user_list_emails")).data, "email")
|
||||
await mcp.ok("gitea_user_delete_email", body={"emails": [address]})
|
||||
assert address not in logins((await mcp.ok("gitea_user_list_emails")).data, "email")
|
||||
|
||||
# SSH keys and GPG keys.
|
||||
key = await mcp.ok(
|
||||
"gitea_user_current_post_key",
|
||||
status=201,
|
||||
body={"title": "it key", "key": ssh_public_key()},
|
||||
)
|
||||
key_id = key.data["id"]
|
||||
assert key_id in [k["id"] for k in (await mcp.ok("gitea_user_current_list_keys")).data]
|
||||
assert (await mcp.ok("gitea_user_current_get_key", id=key_id)).data["title"] == "it key"
|
||||
await mcp.ok("gitea_user_current_delete_key", id=key_id)
|
||||
assert (await mcp.ok("gitea_user_current_list_gpg_keys")).data == []
|
||||
assert (await mcp.ok("gitea_user_list_gpg_keys", username=other)).data == []
|
||||
|
||||
# Follow and unfollow.
|
||||
await mcp.ok("gitea_user_current_put_follow", username=other)
|
||||
await mcp.ok("gitea_user_current_check_following", username=other)
|
||||
await mcp.ok("gitea_user_check_following", username=gitea.admin_login, target=other)
|
||||
assert other in logins((await mcp.ok("gitea_user_current_list_following")).data)
|
||||
assert gitea.admin_login in logins(
|
||||
(await mcp.ok("gitea_user_list_followers", username=other)).data
|
||||
)
|
||||
await mcp.ok("gitea_user_current_delete_follow", username=other)
|
||||
await mcp.fail("gitea_user_current_check_following", 404, username=other)
|
||||
|
||||
# Block and unblock.
|
||||
await mcp.ok("gitea_user_block_user", username=other)
|
||||
await mcp.ok("gitea_user_check_user_block", username=other)
|
||||
assert other in logins((await mcp.ok("gitea_user_list_blocks")).data)
|
||||
await mcp.ok("gitea_user_unblock_user", username=other)
|
||||
await mcp.fail("gitea_user_check_user_block", 404, username=other)
|
||||
|
||||
# Starred and watched repositories.
|
||||
await mcp.ok("gitea_user_current_put_star", **repo)
|
||||
await mcp.ok("gitea_user_current_check_starring", **repo)
|
||||
assert fresh_repo.name in logins((await mcp.ok("gitea_user_current_list_starred")).data, "name")
|
||||
await mcp.ok("gitea_user_current_delete_star", **repo)
|
||||
await mcp.ok("gitea_repo_put_subscription", **repo)
|
||||
watched = await mcp.ok("gitea_user_current_list_subscriptions")
|
||||
assert fresh_repo.name in logins(watched.data, "name")
|
||||
await mcp.ok("gitea_repo_delete_subscription", **repo)
|
||||
|
||||
# Search.
|
||||
assert other in logins((await mcp.ok("gitea_user_search", q=other)).data["data"])
|
||||
|
||||
|
||||
async def test_generic_package_roundtrip(mcp: McpSession, gitea: GiteaInstance) -> None:
|
||||
owner = gitea.admin_login
|
||||
name = gitea.unique("it-pkg")
|
||||
content = b"package bytes " + secrets.token_bytes(16)
|
||||
upload = gitea.request(
|
||||
"PUT", f"/api/packages/{owner}/generic/{name}/1.0.0/payload.bin", content=content
|
||||
)
|
||||
assert upload.status_code == 201, upload.text
|
||||
|
||||
deadline = time.monotonic() + 15
|
||||
listed: list[Any] = []
|
||||
while time.monotonic() < deadline and not listed:
|
||||
listed = (
|
||||
await mcp.ok("gitea_package_list_packages", owner=owner, type="generic", q=name)
|
||||
).data
|
||||
if not listed:
|
||||
await asyncio.sleep(0.5)
|
||||
assert [item["name"] for item in listed] == [name]
|
||||
assert listed[0]["version"] == "1.0.0"
|
||||
|
||||
pkg = {"owner": owner, "type": "generic", "name": name}
|
||||
got = await mcp.ok("gitea_package_get_package", version="1.0.0", **pkg)
|
||||
assert got.data["name"] == name
|
||||
files = await mcp.ok("gitea_package_list_package_files", version="1.0.0", **pkg)
|
||||
assert [item["name"] for item in files.data] == ["payload.bin"]
|
||||
assert files.data[0]["size"] == len(content)
|
||||
latest = await mcp.ok("gitea_package_get_latest_package_version", **pkg)
|
||||
assert latest.data["version"] == "1.0.0"
|
||||
assert [
|
||||
v["version"] for v in (await mcp.ok("gitea_package_list_package_versions", **pkg)).data
|
||||
] == ["1.0.0"]
|
||||
|
||||
await mcp.ok("gitea_package_delete_package_version", version="1.0.0", **pkg)
|
||||
await mcp.fail("gitea_package_get_package", 404, version="1.0.0", **pkg)
|
||||
Reference in new issue
Block a user