fix(events): staff could not reach their own participation history
Found by the live walk, signed in as an admin: /account/events redirected to the dashboard. `GET /player/events/history` is behind requireAuth alone and self-scoped on req.user.id -- staff are a superset of players -- but the WEB has two logged-in shells, and RequirePlayer sends anyone who is not a `player` out of /account. A single mount there is a screen the reviewing admin can never open. Engagement Phase 7 hit this exact wall with the inbox and answered it with two routes, one pair of components and one mapping. `eventHistoryPath` joins `inboxPath` and `notificationSettingsPath` in notificationPaths.js rather than starting a second file with the same comment at the top of it. The staff path is /admin/events/mine, in the Events section of the sidebar, and it is the one row in that group with no `roles`. Also: the eventAnnounce fixture carried no slug, state or `listed`, so `eventUrl` answered undefined in every test in that file and the new code was exercised by none of them. The fixture now looks like a definition row, and three tests cover the link, the unlisted case and the draft case. The run.failed assertion that came with them was reading the wrong layer: `baseFor` assembles eventUrl for every trigger and the SEAM drops the keys a trigger does not declare, so the declaration test is what proves it. Removed, with a note saying where the rule actually lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -45,9 +45,16 @@ after(() => db.close())
|
||||
const DEFINITION = {
|
||||
id: 3,
|
||||
title: 'The Yew Invasion',
|
||||
slug: 'the-yew-invasion',
|
||||
summary: 'Orcish warbands are massing north of Yew.',
|
||||
series_name: 'The Yew Campaign',
|
||||
timezone: 'America/New_York',
|
||||
// Both are load-bearing for `eventUrl` (Phase 14a): an event with no public
|
||||
// page gets no link. They were absent from this fixture, which meant the url
|
||||
// was undefined in every test here and the new code was exercised by none of
|
||||
// them.
|
||||
state: 'ready',
|
||||
listed: true,
|
||||
}
|
||||
|
||||
const RUN = {
|
||||
@@ -201,6 +208,12 @@ test('run.cancelled carries the operator\'s reason, and omits it when none was g
|
||||
assert.equal(only().envelope.data.reason, undefined)
|
||||
})
|
||||
|
||||
// `run.failed` alone gets no public page, and the DECLARATION is what enforces
|
||||
// that rather than anything here: `baseFor` assembles `eventUrl` for every
|
||||
// trigger and the seam drops the keys a trigger does not declare. The test above
|
||||
// that asserts run.failed's url variables are exactly `['runUrl']` is therefore
|
||||
// the one that proves it — an assertion on this envelope would be reading the
|
||||
// wrong layer, because the filtering has not happened yet at this point.
|
||||
test('run.failed links the run console — the one destination that exists today', async () => {
|
||||
await announce.runFailed(RUN, 'sidecar responded 503')
|
||||
const { data } = only().envelope
|
||||
@@ -209,6 +222,29 @@ test('run.failed links the run console — the one destination that exists today
|
||||
assert.equal(data.runUrl, '/admin/events/runs/3692')
|
||||
})
|
||||
|
||||
test('every public emit carries the page for THIS occurrence', async () => {
|
||||
await announce.runStarted(RUN)
|
||||
// The slug is the definition's and the run is in the query string. Without
|
||||
// `?run=` a mail about last Friday's occurrence would open next Friday's.
|
||||
assert.equal(only().envelope.data.eventUrl, '/site/events/the-yew-invasion?run=3692')
|
||||
})
|
||||
|
||||
test('an UNLISTED event announces with no link rather than a link that 404s', async () => {
|
||||
// `eventUrl` is declared optional exactly so `email.button` can drop itself.
|
||||
// A path here would render as a dead button in every mail — worse than none,
|
||||
// because it advertises a link the reader cannot follow. `news.post` paid for
|
||||
// that once already.
|
||||
definitionsDb.getById = async () => ({ ...DEFINITION, listed: false })
|
||||
await announce.runStarted(RUN)
|
||||
assert.equal(only().envelope.data.eventUrl, undefined)
|
||||
})
|
||||
|
||||
test('a definition that is not yet `ready` has no page either', async () => {
|
||||
definitionsDb.getById = async () => ({ ...DEFINITION, state: 'draft' })
|
||||
await announce.runStarted(RUN)
|
||||
assert.equal(only().envelope.data.eventUrl, undefined)
|
||||
})
|
||||
|
||||
test('run.failed falls back to the run\'s own last error', async () => {
|
||||
await announce.runFailed({ ...RUN, last_error: 'the pinned version has no phases' }, null)
|
||||
assert.equal(only().envelope.data.error, 'the pinned version has no phases')
|
||||
|
||||
Reference in New Issue
Block a user