2 Commits

Author SHA1 Message Date
f0a3b6c03e Merge pull request 'fix(nav): show the player game-data groups to staff' (#24) from fix/staff-player-menu into main
All checks were successful
SonarQube / analysis (push) Successful in 56s
Release APK / release (push) Successful in 9m55s
Reviewed-on: #24
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-07-22 08:32:57 +00:00
3aeb295342 fix(nav): show the player game-data groups to staff
All checks were successful
PR Checks / android-build (pull_request) Successful in 6m8s
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 <noreply@anthropic.com>
2026-07-22 02:18:33 -05: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())
} }