From 3aeb295342b70235846d3fb6fa0e70654fe1dd69 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 22 Jul 2026 02:18:33 -0500 Subject: [PATCH] fix(nav): show the player game-data groups to staff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Staff are a superset of players (all player abilities plus their staff tools), and the backend's player self-service surface is role-agnostic, but MenuAccess.PLAYER gated "My characters/vendors/houses" on role == player — so a signed-in admin/editor/moderator saw neither the menu items nor, via the greyed personal streams, their own notification options, even with linked characters. Gate MenuAccess.PLAYER on isPlayer OR isStaff. The notifications screen needs no change: once the backend returns the caller's linked accounts (paired with RunicGateway/website), hasLinkedAccount resolves and the personal streams enable themselves. Tests: MenuAccessTest now asserts every staff role sees the player game-data groups and a PLAYER entry, and an unrecognized role / anon still cannot. Full unit suite passes. Co-Authored-By: Claude --- .../runicgateway/app/ui/navigation/Menu.kt | 9 +++++-- .../app/ui/navigation/MenuAccessTest.kt | 25 ++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt b/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt index 283a6dd..32ab76c 100644 --- a/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt +++ b/app/src/main/java/com/runicgateway/app/ui/navigation/Menu.kt @@ -21,7 +21,12 @@ enum class MenuAccess { /** Visible to any signed-in account (§5, "My Account"). */ SIGNED_IN, - /** Visible only to a player — the linked game-data groups (§6.3). */ + /** + * The linked game-data groups (§6.3). Visible to any player **or** staff: + * staff are a superset of players (all player abilities plus their staff + * tools), and the backend's player self-service surface is role-agnostic, so + * a signed-in admin/editor/moderator sees + uses their own characters too. + */ PLAYER, /** Visible to any staff role (admin/editor/moderator) — the M10 staff surface (§1). */ @@ -70,7 +75,7 @@ fun visibleEntries(entries: List, session: Session): List when (entry.access) { MenuAccess.PUBLIC -> true MenuAccess.SIGNED_IN -> session is Session.SignedIn - MenuAccess.PLAYER -> session is Session.SignedIn && session.user.isPlayer + MenuAccess.PLAYER -> session is Session.SignedIn && (session.user.isPlayer || session.user.isStaff) MenuAccess.STAFF -> session is Session.SignedIn && session.user.isStaff MenuAccess.MODERATOR -> session is Session.SignedIn && session.user.isModerator } diff --git a/app/src/test/java/com/runicgateway/app/ui/navigation/MenuAccessTest.kt b/app/src/test/java/com/runicgateway/app/ui/navigation/MenuAccessTest.kt index 40ec162..dd94f2d 100644 --- a/app/src/test/java/com/runicgateway/app/ui/navigation/MenuAccessTest.kt +++ b/app/src/test/java/com/runicgateway/app/ui/navigation/MenuAccessTest.kt @@ -37,12 +37,16 @@ class MenuAccessTest { assertTrue(visible.contains(Routes.HOME)) } - @Test fun staffSeeAccountButNoPlayerOnlyGroups() { - val visible = routes(signedIn(Role.EDITOR)) - assertTrue(visible.contains(Routes.ACCOUNT)) - // No PLAYER-access entry (the M4 game-data groups) leaks to staff. - val playerOnly = APP_MENU.filter { it.access == MenuAccess.PLAYER }.map { it.route } - assertTrue(playerOnly.none { visible.contains(it) }) + @Test fun staffSeeThePlayerGameDataGroups() { + // Staff are a superset of players: every staff role sees the PLAYER-access + // game-data groups too (their own linked characters, via the role-agnostic + // /player self-service surface), on top of their staff entries. + val playerGroups = APP_MENU.filter { it.access == MenuAccess.PLAYER }.map { it.route } + for (role in listOf(Role.ADMIN, Role.EDITOR, Role.MODERATOR)) { + val visible = routes(signedIn(role)) + assertTrue("$role should see Account", visible.contains(Routes.ACCOUNT)) + assertTrue("$role should see the player game-data groups", playerGroups.all { visible.contains(it) }) + } } @Test fun publicEntryCountIsStableAcrossSessions() { @@ -58,10 +62,13 @@ class MenuAccessTest { } @Test fun playerAccessGatedFunction() { - // A synthetic PLAYER-gated entry is visible to a player, hidden from staff/anon. + // A PLAYER-gated entry is visible to a player AND to every staff role + // (staff superset), hidden only from an unrecognized role and anon. val entries = listOf(MenuEntry("game", 0, MenuAccess.PLAYER)) - assertTrue(visibleEntries(entries, signedIn(Role.PLAYER)).isNotEmpty()) - assertTrue(visibleEntries(entries, signedIn(Role.ADMIN)).isEmpty()) + for (role in listOf(Role.PLAYER, Role.ADMIN, Role.EDITOR, Role.MODERATOR)) { + assertTrue("$role should see a PLAYER entry", visibleEntries(entries, signedIn(role)).isNotEmpty()) + } + assertTrue(visibleEntries(entries, signedIn(Role.UNKNOWN)).isEmpty()) assertTrue(visibleEntries(entries, Session.SignedOut).isEmpty()) } -- 2.49.1