"""TDD — musehub#224: MCP parity for user profile create and account delete. create/delete had zero MCP executor coverage (only read/update existed) -- musehub_create_user_profile mirrors POST /api/users; musehub_delete_user_account mirrors DELETE /api/me (self) and DELETE /api/admin/users/{handle} (admin-only, another account) via the same shared musehub_account.delete_identity_account. """ from __future__ import annotations import base64 import hashlib as _hashlib import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from muse.core.types import encode_pubkey, fake_id from musehub.core.genesis import compute_key_id from musehub.crypto.keys import key_fingerprint from musehub.db.musehub_auth_models import MusehubAuthKey from musehub.db.musehub_identity_models import MusehubIdentity from musehub.services.musehub_mcp_executor import ( execute_create_user_profile, execute_delete_user_account, execute_read_user_profile, ) pytestmark = pytest.mark.asyncio _COUNTER: list[int] = [0] def _uid(tag: str = "") -> str: _COUNTER[0] += 1 return f"mcpacct{tag}{_COUNTER[0]}" async def _make_identity_with_key( session: AsyncSession, handle: str, is_admin: bool = False ) -> MusehubIdentity: identity = MusehubIdentity( identity_id=fake_id(f"identity:{handle}"), handle=handle, is_admin=is_admin, agent_capabilities=[], pinned_repo_ids=[], ) session.add(identity) await session.flush() raw = _hashlib.sha256(handle.encode()).digest() pubkey_b64 = encode_pubkey("ed25519", raw) _, raw_b64 = pubkey_b64.split(":", 1) raw_bytes = base64.urlsafe_b64decode(raw_b64 + "==") fp = key_fingerprint(raw_bytes) key = MusehubAuthKey( key_id=compute_key_id(identity.identity_id, pubkey_b64), identity_id=identity.identity_id, algorithm="ed25519", public_key_b64=pubkey_b64, fingerprint=fp, label="test-key", ) session.add(key) await session.flush() return identity async def _key_count(session: AsyncSession, identity_id: str) -> int: rows = (await session.execute( select(MusehubAuthKey).where(MusehubAuthKey.identity_id == identity_id) )).scalars().all() return len(rows) class TestCreateUserProfile: async def test_create_succeeds_for_new_username(self, db_session: AsyncSession) -> None: await _make_identity_with_key(db_session, "creator-actor") await db_session.commit() result = await execute_create_user_profile( username="brand-new-handle", bio="Hi", actor="creator-actor" ) assert result.ok, result.error_message assert result.data["username"] == "brand-new-handle" read_back = await execute_read_user_profile("brand-new-handle") assert read_back.ok assert read_back.data["bio"] == "Hi" async def test_create_rejects_duplicate_username(self, db_session: AsyncSession) -> None: handle = f"taken-{_uid()}" await _make_identity_with_key(db_session, handle) await db_session.commit() result = await execute_create_user_profile(username=handle, actor="someone-else") assert not result.ok assert result.error_code == "already_exists" async def test_create_rejects_second_profile_for_same_actor( self, db_session: AsyncSession ) -> None: actor = f"double-{_uid()}" first = await execute_create_user_profile(username=actor, actor=actor) assert first.ok, first.error_message result = await execute_create_user_profile( username=f"{actor}-second", actor=actor ) assert not result.ok assert result.error_code == "already_exists" class TestDeleteUserAccount: async def test_self_delete_hard_deletes_keys_and_soft_deletes_identity( self, db_session: AsyncSession ) -> None: handle = f"selfdel-{_uid()}" identity = await _make_identity_with_key(db_session, handle) await db_session.commit() identity_id = identity.identity_id result = await execute_delete_user_account(username=handle, actor=handle) assert result.ok, result.error_message await db_session.refresh(identity) assert identity.deleted_at is not None assert await _key_count(db_session, identity_id) == 0 async def test_admin_can_delete_a_different_account(self, db_session: AsyncSession) -> None: admin_handle = f"admin-{_uid()}" await _make_identity_with_key(db_session, admin_handle, is_admin=True) target_handle = f"target-{_uid()}" target = await _make_identity_with_key(db_session, target_handle) await db_session.commit() identity_id = target.identity_id result = await execute_delete_user_account(username=target_handle, actor=admin_handle) assert result.ok, result.error_message await db_session.refresh(target) assert target.deleted_at is not None assert await _key_count(db_session, identity_id) == 0 async def test_non_admin_cannot_delete_a_different_account( self, db_session: AsyncSession ) -> None: actor_handle = f"nonadmin-{_uid()}" await _make_identity_with_key(db_session, actor_handle, is_admin=False) target_handle = f"target-{_uid()}" await _make_identity_with_key(db_session, target_handle) await db_session.commit() result = await execute_delete_user_account(username=target_handle, actor=actor_handle) assert not result.ok assert result.error_code == "forbidden" async def test_unknown_username_returns_user_not_found( self, db_session: AsyncSession ) -> None: admin_handle = f"admin-{_uid()}" await _make_identity_with_key(db_session, admin_handle, is_admin=True) await db_session.commit() result = await execute_delete_user_account( username=f"ghost-{_uid()}", actor=admin_handle ) assert not result.ok assert result.error_code == "user_not_found"