test(teams): the refusals, which is most of what a notification feature is
A notification feature is mostly things that correctly do NOT happen, and each of these is invisible until it goes wrong in production: a departed member and a revoked guest are not recipients; a mute subtracts per Team and leaves the user's other Teams alone; the author of a post never receives the notification about it; forums switched off silences the forum streams including the digest; a Team's first roster wakes nobody; a failed send does not stamp `last_digest_at`. Two real defects came out of writing them. `Number(null)` is 0 and 0 is an integer, so a null in a caller's id list survived `filter(Number.isInteger)` and rode into an IN clause as user id 0. No row has id 0, so it was harmless — which is exactly why it would never have been noticed. Fixed in all three places that filter ids. `recipientIds: db.recipientIds` in the model captured the function OBJECT at require time, so the layer below could never be substituted. That is not only untestable; it means the model was not really the seam it claimed to be. Wrapped so `db.x` resolves at call time. The registries catalog assertion is now an exact five-element list, so a shard-content stream creeping back into core's registration fails here rather than shipping. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -126,3 +126,53 @@ test('publish skips endpoints that fail the SSRF guard', async () => {
|
||||
assert.equal(calls[0].url, 'https://relay.test/ok')
|
||||
})
|
||||
})
|
||||
|
||||
// ── publishToUsers — the third fan-out shape (TEAMS.md §6.2) ───────────────
|
||||
//
|
||||
// `publish` answers "everyone subscribed" and "this one owner". Team
|
||||
// notifications need "these N users", because the four `team.*` streams are
|
||||
// global and which Team an event belongs to lives in the SET, not the stream id.
|
||||
|
||||
test('publishToUsers tickles the given set, and asks for exactly that set', async () => {
|
||||
await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
|
||||
const { calls, fetchImpl } = captureFetch()
|
||||
const asked = []
|
||||
const pushDevices = {
|
||||
endpointsForUsersStream: async (ids, stream) => {
|
||||
asked.push({ ids, stream })
|
||||
return [{ endpoint: 'https://relay.test/a' }, { endpoint: 'https://relay.test/b' }]
|
||||
},
|
||||
}
|
||||
await pushDispatch.publishToUsers('team.forum.post', { ref: 'team:1:thread:7', userIds: [4, 9] }, { pushDevices, fetchImpl })
|
||||
assert.deepEqual(asked, [{ ids: [4, 9], stream: 'team.forum.post' }])
|
||||
assert.equal(calls.length, 2)
|
||||
assert.deepEqual(JSON.parse(calls[0].opts.body), { stream: 'team.forum.post', ref: 'team:1:thread:7' })
|
||||
})
|
||||
})
|
||||
|
||||
test('publishToUsers with an empty set never touches the database', async () => {
|
||||
const { calls, fetchImpl } = captureFetch()
|
||||
let looked = false
|
||||
const pushDevices = { endpointsForUsersStream: async () => { looked = true; return [] } }
|
||||
await pushDispatch.publishToUsers('team.forum.post', { ref: 'x', userIds: [] }, { pushDevices, fetchImpl })
|
||||
assert.equal(looked, false, 'an empty IN () is a syntax error, so the query must not be made at all')
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
|
||||
test('publishToUsers de-duplicates and drops non-numeric ids', async () => {
|
||||
await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
|
||||
const { fetchImpl } = captureFetch()
|
||||
const asked = []
|
||||
const pushDevices = {
|
||||
endpointsForUsersStream: async (ids) => { asked.push(ids); return [] },
|
||||
}
|
||||
await pushDispatch.publishToUsers('team.forum.post', { ref: 'x', userIds: [4, 4, null, 'nope', 9] }, { pushDevices, fetchImpl })
|
||||
assert.deepEqual(asked, [[4, 9]])
|
||||
})
|
||||
})
|
||||
|
||||
test('publishToUsers never throws when the lookup fails', async () => {
|
||||
const { fetchImpl } = captureFetch()
|
||||
const pushDevices = { endpointsForUsersStream: async () => { throw new Error('down') } }
|
||||
await pushDispatch.publishToUsers('team.forum.post', { ref: 'x', userIds: [1] }, { pushDevices, fetchImpl })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user