fix(nav): show the player game-data groups to staff #24

Merged
whitlocktech merged 1 commits from fix/staff-player-menu into main 2026-07-22 08:32:59 +00:00
2 changed files with 23 additions and 11 deletions

View File

@@ -21,7 +21,12 @@ enum class MenuAccess {
/** Visible to any signed-in account (§5, "My Account"). */ /** Visible to any signed-in account (§5, "My Account"). */
SIGNED_IN, 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, PLAYER,
/** Visible to any staff role (admin/editor/moderator) — the M10 staff surface (§1). */ /** Visible to any staff role (admin/editor/moderator) — the M10 staff surface (§1). */
@@ -70,7 +75,7 @@ fun visibleEntries(entries: List<MenuEntry>, session: Session): List<MenuEntry>
when (entry.access) { when (entry.access) {
MenuAccess.PUBLIC -> true MenuAccess.PUBLIC -> true
MenuAccess.SIGNED_IN -> session is Session.SignedIn 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.STAFF -> session is Session.SignedIn && session.user.isStaff
MenuAccess.MODERATOR -> session is Session.SignedIn && session.user.isModerator MenuAccess.MODERATOR -> session is Session.SignedIn && session.user.isModerator
} }

View File

@@ -37,12 +37,16 @@ class MenuAccessTest {
assertTrue(visible.contains(Routes.HOME)) assertTrue(visible.contains(Routes.HOME))
} }
@Test fun staffSeeAccountButNoPlayerOnlyGroups() { @Test fun staffSeeThePlayerGameDataGroups() {
val visible = routes(signedIn(Role.EDITOR)) // Staff are a superset of players: every staff role sees the PLAYER-access
assertTrue(visible.contains(Routes.ACCOUNT)) // game-data groups too (their own linked characters, via the role-agnostic
// No PLAYER-access entry (the M4 game-data groups) leaks to staff. // /player self-service surface), on top of their staff entries.
val playerOnly = APP_MENU.filter { it.access == MenuAccess.PLAYER }.map { it.route } val playerGroups = APP_MENU.filter { it.access == MenuAccess.PLAYER }.map { it.route }
assertTrue(playerOnly.none { visible.contains(it) }) 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() { @Test fun publicEntryCountIsStableAcrossSessions() {
@@ -58,10 +62,13 @@ class MenuAccessTest {
} }
@Test fun playerAccessGatedFunction() { @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)) val entries = listOf(MenuEntry("game", 0, MenuAccess.PLAYER))
assertTrue(visibleEntries(entries, signedIn(Role.PLAYER)).isNotEmpty()) for (role in listOf(Role.PLAYER, Role.ADMIN, Role.EDITOR, Role.MODERATOR)) {
assertTrue(visibleEntries(entries, signedIn(Role.ADMIN)).isEmpty()) 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()) assertTrue(visibleEntries(entries, Session.SignedOut).isEmpty())
} }