"""Tests for the enhanced MuseHub user profile page.
Covers:
- test_profile_page_html_returns_200 — GET /users/{username} returns 200 HTML
- test_profile_page_no_auth_required — accessible without authentication
- test_profile_page_unknown_user_still_renders — unknown username still returns 200 HTML shell
- test_profile_page_html_contains_heatmap_js — page includes heatmap rendering JavaScript
- test_profile_page_html_contains_badge_js — page includes badge rendering JavaScript
- test_profile_page_html_contains_pinned_js — page includes pinned repos JavaScript
- test_profile_page_html_contains_activity_tab — page includes Activity tab
- test_profile_page_json_returns_200 — ?format=json returns 200 JSON
- test_profile_page_json_unknown_user_404 — ?format=json returns 404 for unknown user
- test_profile_page_json_heatmap_structure — JSON response has heatmap with days/stats
- test_profile_page_json_badges_structure — JSON response has 8 badges with expected fields
- test_profile_page_json_pinned_repos — JSON response includes pinned repo cards
- test_profile_page_json_activity_empty — JSON response returns empty activity for new user
- test_profile_page_json_activity_filter — ?tab=commits filters activity to commits only
- test_profile_page_json_badge_first_commit_earned — first_commit badge earned after seeding a commit
- test_profile_page_json_camel_case_keys — JSON keys are camelCase
"""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from musehub.db.musehub_identity_models import MusehubIdentity
from musehub.db.musehub_repo_models import MusehubCommit, MusehubCommitRef, MusehubRepo
from muse.core.types import long_id, now_utc_iso
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
async def _make_profile(
db: AsyncSession,
*,
username: str = "testuser",
user_id: str = "user-profile-test-001",
bio: str | None = "Test bio",
) -> MusehubIdentity:
"""Seed a minimal MusehubIdentity."""
profile = MusehubIdentity(
identity_id=user_id,
handle=username,
identity_type="human",
bio=bio,
avatar_url=None,
)
db.add(profile)
await db.commit()
await db.refresh(profile)
return profile
async def _make_repo(
db: AsyncSession,
*,
owner_user_id: str = "user-profile-test-001",
owner: str = "testuser",
name: str = "test-beats",
slug: str = "test-beats",
visibility: str = "public",
) -> MusehubRepo:
"""Seed a minimal MusehubRepo."""
from datetime import datetime, timezone
from musehub.core.genesis import compute_repo_id
repo_id = compute_repo_id(owner_user_id, slug, "code", now_utc_iso())
repo = MusehubRepo(
repo_id=repo_id,
name=name,
owner=owner,
slug=slug,
visibility=visibility,
owner_user_id=owner_user_id,
)
db.add(repo)
await db.commit()
await db.refresh(repo)
return repo
# ---------------------------------------------------------------------------
# HTML path tests
# ---------------------------------------------------------------------------
async def test_profile_page_html_returns_200(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""GET /users/{username} returns 200 HTML for any username."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
async def test_profile_page_no_auth_required(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Profile page is publicly accessible without authentication."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
async def test_profile_page_unknown_user_still_renders(
client: AsyncClient,
) -> None:
"""HTML shell renders even for unknown users — data fetched client-side."""
response = await client.get("/nobody-exists-xyzzy")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
async def test_profile_page_html_contains_heatmap_js(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""HTML dispatches the user-profile TypeScript module (heatmap rendered client-side)."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
body = response.text
# renderHeatmap moved to app.js; page dispatch JSON confirms module will run
assert '"page": "user-profile"' in body
assert '"username": "testuser"' in body
async def test_profile_page_html_contains_badge_js(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""HTML dispatches user-profile module which renders badges client-side."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
body = response.text
# renderBadges moved to app.js; verify page dispatch and profile container
assert '"page": "user-profile"' in body
assert "profile-container" in body or "content" in body
async def test_profile_page_html_contains_pinned_js(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""HTML dispatches user-profile module which renders pinned repos client-side."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
body = response.text
# renderPinned moved to app.js; verify page dispatch JSON
assert '"page": "user-profile"' in body
assert "testuser" in body
async def test_profile_page_html_contains_activity_tab(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""HTML renders the profile page with user-profile page dispatch (activity driven by JS)."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
body = response.text
# Reimagined template: activity sections are data-driven; module dispatch always present
assert '"page": "user-profile"' in body
assert "testuser" in body
# ---------------------------------------------------------------------------
# JSON path tests
# ---------------------------------------------------------------------------
async def test_profile_page_json_returns_200(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""GET /users/{username}?format=json returns 200 JSON."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json")
assert response.status_code == 200
assert "application/json" in response.headers["content-type"]
async def test_profile_page_json_unknown_user_404(
client: AsyncClient,
) -> None:
"""?format=json returns 404 for an unknown username."""
response = await client.get("/nobody-exists-xyzzy?format=json")
assert response.status_code == 404
async def test_profile_page_json_heatmap_structure(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response contains heatmap with days list and aggregate stats."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
assert "heatmap" in body
heatmap = body["heatmap"]
assert "days" in heatmap
assert "totalContributions" in heatmap
assert "longestStreak" in heatmap
assert "currentStreak" in heatmap
# Should have ~364 days (52 weeks × 7 days)
assert len(heatmap["days"]) >= 360
# Each day has date, count, intensity
first_day = heatmap["days"][0]
assert "date" in first_day
assert "count" in first_day
assert "intensity" in first_day
assert first_day["intensity"] in (0, 1, 2, 3)
async def test_profile_page_json_badges_structure(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response contains exactly 8 badges with required fields."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
assert "badges" in body
badges = body["badges"]
assert len(badges) == 8
for badge in badges:
assert "id" in badge
assert "name" in badge
assert "description" in badge
assert "icon" in badge
assert "earned" in badge
assert isinstance(badge["earned"], bool)
async def test_profile_page_json_pinned_repos(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response includes pinned repo cards when pinned_repo_ids are set."""
profile = await _make_profile(db_session)
repo = await _make_repo(db_session)
# Pin the repo
profile.pinned_repo_ids = [repo.repo_id]
db_session.add(profile)
await db_session.commit()
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
assert "pinnedRepos" in body
pinned = body["pinnedRepos"]
assert len(pinned) == 1
card = pinned[0]
assert card["name"] == "test-beats"
assert card["slug"] == "test-beats"
assert "forkCount" in card
async def test_profile_page_json_activity_empty(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response returns empty activity list for a new user with no events."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
assert "activity" in body
assert isinstance(body["activity"], list)
assert body["totalEvents"] == 0
assert body["page"] == 1
assert body["perPage"] == 20
async def test_profile_page_json_activity_filter(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""?tab=commits filters activity response to commits-only event types."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json&tab=commits")
assert response.status_code == 200
body = response.json()
assert body["activityFilter"] == "commits"
async def test_profile_page_json_badge_first_commit_earned(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""first_commit badge is earned after the user has at least one commit."""
from datetime import datetime, timezone
profile = await _make_profile(db_session)
repo = await _make_repo(db_session)
# Seed one commit owned by this user's repo
commit = MusehubCommit(
commit_id="abc123def456abc123def456abc123def456abc1",
branch="main",
parent_ids=[],
message="initial commit",
author="testuser",
timestamp=datetime.now(tz=timezone.utc),
)
db_session.add(commit)
db_session.add(MusehubCommitRef(repo_id=str(repo.repo_id), commit_id="abc123def456abc123def456abc123def456abc1"))
await db_session.commit()
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
badges = {b["id"]: b for b in body["badges"]}
assert "first_commit" in badges
assert badges["first_commit"]["earned"] is True
async def test_profile_page_json_camel_case_keys(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response uses camelCase keys throughout (no snake_case at top level)."""
await _make_profile(db_session)
response = await client.get("/testuser?format=json")
assert response.status_code == 200
body = response.json()
# Top-level camelCase keys
assert "avatarUrl" in body
assert "totalEvents" in body
assert "activityFilter" in body
assert "pinnedRepos" in body
# No snake_case variants
assert "avatar_url" not in body
assert "total_events" not in body
assert "pinned_repos" not in body
# ---------------------------------------------------------------------------
# AVAX address visibility
# ---------------------------------------------------------------------------
async def test_profile_page_html_hides_avax_when_null(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Profile HTML does not mention AVAX when avax_address is None."""
await _make_profile(db_session)
response = await client.get("/testuser")
assert response.status_code == 200
body = response.text
assert "AVAX" not in body
assert "avax" not in body.lower()
# ---------------------------------------------------------------------------
# Issue #448 — rich artist profiles with CC attribution fields
# ---------------------------------------------------------------------------
async def test_profile_model_rich_fields_stored_and_retrieved(
db_session: AsyncSession,
) -> None:
"""MusehubIdentity stores and retrieves all CC-attribution fields added.
Regression: before this fix, display_name / location / website_url /
social_url / is_verified / cc_license did not exist on the model or
schema; saving them would silently discard the data.
"""
profile = MusehubIdentity(
identity_id="user-test-cc-001",
handle="kevin_macleod_test",
display_name="Kevin MacLeod",
bio="Prolific composer. Every genre. Royalty-free forever.",
location="Sandpoint, Idaho",
website_url="https://incompetech.com",
social_url="kmacleod",
is_verified=True,
cc_license="CC BY 4.0",
)
db_session.add(profile)
await db_session.commit()
await db_session.refresh(profile)
assert profile.display_name == "Kevin MacLeod"
assert profile.location == "Sandpoint, Idaho"
assert profile.website_url == "https://incompetech.com"
assert profile.social_url == "kmacleod"
assert profile.is_verified is True
assert profile.cc_license == "CC BY 4.0"
async def test_profile_model_verified_defaults_false(
db_session: AsyncSession,
) -> None:
"""is_verified defaults to False for community users — no accidental verification."""
profile = MusehubIdentity(
identity_id="user-test-community-002",
handle="community_user_test",
bio="Just a regular community user.",
)
db_session.add(profile)
await db_session.commit()
await db_session.refresh(profile)
assert profile.is_verified is False
assert profile.cc_license is None
assert profile.display_name is None
assert profile.location is None
assert profile.social_url is None
async def test_profile_model_public_domain_artist(
db_session: AsyncSession,
) -> None:
"""Public Domain composers get is_verified=True and cc_license='Public Domain'."""
profile = MusehubIdentity(
identity_id="user-test-bach-003",
handle="bach_test",
display_name="Johann Sebastian Bach",
bio="Baroque composer. 48 preludes, 48 fugues.",
location="Leipzig, Saxony (1723-1750)",
website_url="https://www.bach-digital.de",
social_url=None,
is_verified=True,
cc_license="Public Domain",
)
db_session.add(profile)
await db_session.commit()
await db_session.refresh(profile)
assert profile.is_verified is True
assert profile.cc_license == "Public Domain"
assert profile.social_url is None
async def test_profile_page_json_includes_verified_and_license(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Profile JSON endpoint exposes isVerified and ccLicense fields for CC artists."""
profile = MusehubIdentity(
identity_id="user-test-cc-api-004",
handle="kai_engel_test",
display_name="Kai Engel",
bio="Ambient architect. Long-form textures.",
location="Germany",
website_url="https://freemusicarchive.org/music/Kai_Engel",
social_url=None,
is_verified=True,
cc_license="CC BY 4.0",
)
db_session.add(profile)
await db_session.commit()
response = await client.get("/kai_engel_test?format=json")
assert response.status_code == 200
body = response.json()
# The profile card must surface verification status and license so the
# frontend can render the CC badge without a secondary API call.
assert body.get("isVerified") is True
assert body.get("ccLicense") == "CC BY 4.0"
# ===========================================================================
# Profile Header Reimagination — TDD tests (Issue #1)
# Phase 1: repos pipeline (owner query)
# Phase 2: bio field
# Phase 3: AVAX address
# Phase 4: repo chip domain icons
# ===========================================================================
# ---------------------------------------------------------------------------
# Helpers shared by header tests
# ---------------------------------------------------------------------------
async def _make_identity_with_repos(
db: AsyncSession,
*,
handle: str = "herouser",
bio: str | None = None,
avax_address: str | None = None,
repo_slugs: list[str] | None = None,
) -> MusehubIdentity:
"""Seed a MusehubIdentity + repos where owner==handle (realistic data shape).
owner_user_id is set to the handle string — matching production data where
repos were created before the identity_id was stable. The repo pipeline fix
must resolve repos via owner==handle, not owner_user_id==identity_id.
"""
from datetime import datetime, timezone
from musehub.core.genesis import compute_repo_id
now_iso = now_utc_iso()
identity_id = long_id(handle.ljust(64, "0")[:64])
profile = MusehubIdentity(
identity_id=identity_id,
handle=handle,
identity_type="human",
bio=bio,
avax_address=avax_address,
avatar_url=None,
)
db.add(profile)
await db.flush()
for slug in (repo_slugs or []):
repo_id = compute_repo_id(identity_id, slug, "code", now_iso)
repo = MusehubRepo(
repo_id=repo_id,
name=slug,
owner=handle,
slug=slug,
visibility="public",
# owner_user_id stores the handle string (current production data shape)
owner_user_id=handle,
)
db.add(repo)
await db.commit()
await db.refresh(profile)
return profile
# ---------------------------------------------------------------------------
# Phase 1 — repos pipeline: repos appear in HTML when owner==handle
# ---------------------------------------------------------------------------
async def test_profile_header_repos_appear_when_owner_matches_handle(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Repo chips render in header when repos.owner == identity.handle.
Root cause being fixed: _fetch_repos queried owner_user_id==identity_id
(sha256:...) but DB stores owner_user_id==handle string. The fix queries
owner==handle so repos always resolve correctly.
"""
await _make_identity_with_repos(
db_session,
handle="chipuser",
repo_slugs=["muse", "stori", "maestro"],
)
resp = await client.get("/chipuser")
assert resp.status_code == 200
body = resp.text
# All three repo slugs must appear as chip text in the hero
assert "MUSE" in body
assert "STORI" in body
assert "MAESTRO" in body
async def test_profile_header_repo_count_in_json(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""JSON response repoCount matches seeded repos when owner==handle."""
await _make_identity_with_repos(
db_session,
handle="countuser",
repo_slugs=["alpha", "beta", "gamma"],
)
resp = await client.get("/countuser?format=json")
assert resp.status_code == 200
data = resp.json()
assert data.get("repoCount", 0) == 3
async def test_profile_header_repo_count_excludes_private(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""repoCount on the profile page must only count public repos.
Regression guard: the query previously lacked a visibility filter, causing
private repos to inflate the displayed count.
"""
from musehub.core.genesis import compute_repo_id
now_iso = now_utc_iso()
handle = "privacyuser"
identity_id = long_id(handle.ljust(64, "0")[:64])
profile = MusehubIdentity(
identity_id=identity_id,
handle=handle,
identity_type="human",
)
db_session.add(profile)
await db_session.flush()
for slug, visibility in [("pub1", "public"), ("pub2", "public"), ("priv1", "private")]:
repo = MusehubRepo(
repo_id=compute_repo_id(identity_id, slug, "code", now_iso),
name=slug,
owner=handle,
slug=slug,
visibility=visibility,
owner_user_id=handle,
)
db_session.add(repo)
await db_session.commit()
resp = await client.get(f"/{handle}?format=json")
assert resp.status_code == 200
data = resp.json()
assert data.get("repoCount") == 2, (
f"Expected 2 (public only), got {data.get('repoCount')} — "
"private repos must be excluded from the profile repo count"
)
# ---------------------------------------------------------------------------
# Phase 2 — bio field: bio renders in header when set
# ---------------------------------------------------------------------------
async def test_profile_header_bio_renders_when_set(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Bio string appears quoted in the hero body when identity.bio is set."""
await _make_identity_with_repos(
db_session,
handle="biouser",
bio="Building the sound of the future",
)
resp = await client.get("/biouser")
assert resp.status_code == 200
assert "Building the sound of the future" in resp.text
async def test_profile_header_bio_fallback_when_null(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""When bio is NULL, the fallback 'member since' line renders instead."""
await _make_identity_with_repos(db_session, handle="nobiouser", bio=None)
resp = await client.get("/nobiouser")
assert resp.status_code == 200
assert "member since" in resp.text
async def test_profile_header_avax_hidden_when_null(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""When avax_address is NULL, AVAX row is hidden entirely — no 'not set' placeholder."""
await _make_identity_with_repos(db_session, handle="noavaxuser", avax_address=None)
resp = await client.get("/noavaxuser")
assert resp.status_code == 200
assert "AVAX" not in resp.text
assert "not set" not in resp.text
# ---------------------------------------------------------------------------
# Auth key display — identity_id / fingerprint / public_key_b64
# ---------------------------------------------------------------------------
# These tests lock in the three-field identity model documented in
# /muse/identity#key-rotation:
# identity_id — immutable, sha256(first_registered_key_bytes)
# fingerprint — per-key, sha256(current_key_bytes), changes on rotation
# public_key_b64 — raw Ed25519 key, base64url, changes on rotation
# ---------------------------------------------------------------------------
async def _make_auth_key(
db: AsyncSession,
*,
identity_id: str,
fingerprint: str,
public_key_b64: str,
algorithm: str = "ed25519",
label: str = "",
created_at_offset_seconds: int = 0,
) -> None:
"""Insert a MusehubAuthKey row directly — bypasses the challenge-response flow."""
from datetime import datetime, timezone, timedelta
from musehub.db.musehub_auth_models import MusehubAuthKey
now = datetime.now(timezone.utc) + timedelta(seconds=created_at_offset_seconds)
key = MusehubAuthKey(
key_id=fingerprint, # key_id == fingerprint for simplicity in tests
identity_id=identity_id,
public_key_b64=public_key_b64,
fingerprint=fingerprint,
algorithm=algorithm,
label=label,
created_at=now,
)
db.add(key)
await db.flush()
async def test_profile_shows_auth_key_when_registered(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""Profile hero strip shows algorithm, public_key_b64, and fingerprint
when a MusehubAuthKey row exists for the identity."""
identity = await _make_identity_with_repos(db_session, handle="keyuser")
pubkey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
fp = "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=fp,
public_key_b64=pubkey,
)
await db_session.commit()
resp = await client.get("/keyuser")
assert resp.status_code == 200
body = resp.text
assert "ed25519" in body
assert pubkey in body
assert fp in body
async def test_profile_fallback_to_identity_id_when_no_key(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""When no MusehubAuthKey row exists, the profile falls back to displaying
the identity_id as the fingerprint — clearly a degraded state."""
identity = await _make_identity_with_repos(db_session, handle="nokeyuser")
resp = await client.get("/nokeyuser")
assert resp.status_code == 200
body = resp.text
# Falls back to identity.user_id (== identity_id)
assert identity.identity_id in body
# No pubkey row shown — ed25519 label should not appear in strip context
# (it may appear elsewhere in the page for other reasons, so we check
# that the strip row with the pubkey value is absent)
assert "strip-val--mono" in body # strip is rendered
# The fallback shows identity_id, not a separate pubkey line
assert f'ed25519' not in body
async def test_profile_shows_most_recent_key_after_rotation(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""After key rotation, the profile shows the newest key, not the original.
Both keys share the same identity_id — this is the rotation invariant.
The original key is still valid but the profile surfaces the current one.
"""
identity = await _make_identity_with_repos(db_session, handle="rotateduser")
old_pubkey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
old_fp = "sha256:aaaa000000000000000000000000000000000000000000000000000000000001"
new_pubkey = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
new_fp = "sha256:bbbb000000000000000000000000000000000000000000000000000000000002"
# Old key registered first
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=old_fp,
public_key_b64=old_pubkey,
label="original",
created_at_offset_seconds=0,
)
# New key registered 60s later — simulates muse auth rotate
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=new_fp,
public_key_b64=new_pubkey,
label="rotated",
created_at_offset_seconds=60,
)
await db_session.commit()
resp = await client.get("/rotateduser")
assert resp.status_code == 200
body = resp.text
# New key displayed
assert new_pubkey in body
assert new_fp in body
# Old key NOT displayed — it's registered but not the current one
assert old_pubkey not in body
assert old_fp not in body
async def test_identity_id_unchanged_across_rotation(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""The identity_id anchor never changes across key rotations.
Two keys exist with different fingerprints but the same identity_id —
both rows link back to the single musehub_identities row.
"""
from musehub.db.musehub_auth_models import MusehubAuthKey
from sqlalchemy import select
identity = await _make_identity_with_repos(db_session, handle="stableuser")
fp1 = "sha256:cccc000000000000000000000000000000000000000000000000000000000001"
fp2 = "sha256:dddd000000000000000000000000000000000000000000000000000000000002"
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=fp1,
public_key_b64="CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
created_at_offset_seconds=0,
)
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=fp2,
public_key_b64="DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
created_at_offset_seconds=60,
)
await db_session.commit()
# Both key rows must reference the same identity_id
rows = (await db_session.execute(
select(MusehubAuthKey)
.where(MusehubAuthKey.identity_id == identity.identity_id)
.order_by(MusehubAuthKey.created_at)
)).scalars().all()
assert len(rows) == 2
assert rows[0].identity_id == identity.identity_id
assert rows[1].identity_id == identity.identity_id
assert rows[0].fingerprint == fp1
assert rows[1].fingerprint == fp2
# identity_id is not a fingerprint of either current key
# (it is the fingerprint of the original registration key)
assert identity.identity_id not in (fp1, fp2)
async def test_profile_auth_key_algorithm_label_present(
client: AsyncClient,
db_session: AsyncSession,
) -> None:
"""The algorithm label ('ed25519') renders as a strip-label element,
not as raw text mixed into the fingerprint row."""
identity = await _make_identity_with_repos(db_session, handle="algolabeluser")
fp = "sha256:eeee000000000000000000000000000000000000000000000000000000000003"
await _make_auth_key(
db_session,
identity_id=identity.identity_id,
fingerprint=fp,
public_key_b64="EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE",
algorithm="ed25519",
)
await db_session.commit()
resp = await client.get("/algolabeluser")
assert resp.status_code == 200
body = resp.text
# Algorithm appears as a strip-label, fingerprint on its own row
assert 'ed25519' in body
assert 'fingerprint' in body
# ---------------------------------------------------------------------------