// The bridge as a SINK — what actually leaves the site (TEAMS.md §7.2, phase 8). // // teamIntegration.test.js proves the configuration rules; this proves the wiring // that consumes them, which is a different set of mistakes: // // 1. **the bridge never throws and never blocks its caller.** Every entry point // into teamNotify runs after a write has already been answered, so a bot // that is down, a config lookup that throws, or a client that rejects must // all come back as "no message sent" — an exception here would fail a forum // reply that succeeded; // 2. **the author is excluded from push and email and NOT from the bridge.** // Excluding is a per-recipient idea; a channel has no per-recipient anything, // and suppressing the message would silence it for everyone else; // 3. **push still runs when the bridge is unconfigured**, which is the ordinary // case on every deployment that never turns this on; // 4. **a roster event carries a count and never a name.** The count is public; // a character name is game-sourced text screened for a page, not a channel; // 5. **the excerpt strips markup**, because a forum body is sanitised HTML and // an embed description is text. const { test, beforeEach, afterEach } = require('node:test') const assert = require('node:assert/strict') const teamBridge = require('../src/utils/teamBridge') const teamIntegration = require('../src/model/teams/teamIntegration.model') const botInternalClient = require('../src/utils/botInternalClient') const teamNotify = require('../src/utils/teamNotify') const notifyDb = require('../src/model/teams/teamNotify.db') const forumSettings = require('../src/model/teams/teamForumSettings.model') const pushDispatch = require('../src/utils/pushDispatch') const mailer = require('../src/utils/mailer') const saved = new Map() function patch(mod, name, fn) { if (!saved.has(mod)) saved.set(mod, new Map()) if (!saved.get(mod).has(name)) saved.get(mod).set(name, mod[name]) mod[name] = fn } function restore() { for (const [mod, names] of saved) for (const [name, fn] of names) mod[name] = fn saved.clear() } const TEAM = { id: 3, name: 'Blackthorn’s Legion', slug: 'blackthorns-legion', external_id: 'g-1' } let sent let pushed beforeEach(() => { sent = [] pushed = [] patch(botInternalClient, 'teamNotify', async (payload) => { sent.push(payload) return { ok: true, status: 200, data: { posted: true } } }) patch(pushDispatch, 'publishToUsers', async (streamId, opts) => { pushed.push({ streamId, ...opts }) }) patch(notifyDb, 'recipientIds', async () => [11, 12]) patch(notifyDb, 'emailRecipients', async () => []) patch(forumSettings, 'forumsEnabled', async () => true) patch(mailer, 'isConfigured', async () => false) }) afterEach(restore) const bridgeTo = (channelRef, events) => patch(teamIntegration, 'destinationFor', async (teamId, streamId) => (events.includes(streamId) ? { channelRef, membersOnly: teamIntegration.isMembersOnly(streamId), platform: 'discord' } : null)) const noBridge = () => patch(teamIntegration, 'destinationFor', async () => null) // ── 1. Never throws, never blocks ────────────────────────────────────────── test('a bot that rejects the call does not stop the notification path', async () => { bridgeTo('999', ['team.member.joined']) patch(botInternalClient, 'teamNotify', async () => ({ ok: false, status: 503, error: 'bot responded 503' })) const recipients = await teamNotify.memberJoined(TEAM, { count: 2 }) assert.equal(recipients, 2, 'the push half still reported its recipients') assert.equal(pushed.length, 1) }) test('a config lookup that throws is a bridge that sends nothing, not an exception', async () => { patch(teamIntegration, 'destinationFor', async () => { throw new Error('connection lost') }) assert.equal(await teamBridge.deliver('team.member.joined', TEAM, { body: 'x' }), false) }) test('a client that throws outright is caught', async () => { bridgeTo('999', ['team.member.joined']) patch(botInternalClient, 'teamNotify', async () => { throw new Error('socket hang up') }) assert.equal(await teamBridge.deliver('team.member.joined', TEAM, { body: 'x' }), false) }) test('a team with no id is refused before anything is looked up', async () => { patch(teamIntegration, 'destinationFor', async () => { throw new Error('should not be reached') }) assert.equal(await teamBridge.deliver('team.member.joined', null, {}), false) assert.equal(await teamBridge.deliver('team.member.joined', {}, {}), false) }) // ── 2. The author exclusion stops at the channel ─────────────────────────── test('a forum post excludes its author from push but still bridges to the channel', async () => { bridgeTo('999', ['team.forum.post']) const seen = [] patch(notifyDb, 'recipientIds', async (teamId, opts) => { seen.push(opts.exclude); return [12] }) const result = await teamNotify.forumPost({ team: TEAM, threadId: 41, threadTitle: 'Siege tonight', type: 'discussion', authorUserId: 11, authorName: 'ana', bodyHtml: '

Meet at the moongate.

', }) assert.deepEqual(seen[0], [11], 'the author is excluded from the recipient set') assert.equal(result.bridged, true) assert.equal(sent.length, 1) assert.equal(sent[0].title, 'Siege tonight') assert.equal(sent[0].body, 'Meet at the moongate.') }) test('an announcement bridges on its own stream, not the discussion one', async () => { bridgeTo('999', ['team.announcement']) await teamNotify.forumPost({ team: TEAM, threadId: 7, threadTitle: 'Rules', type: 'announcement', bodyHtml: '

Read this.

', }) assert.equal(sent.length, 1) assert.equal(sent[0].streamId, 'team.announcement') // The same event under the other stream is not carried by this configuration. sent.length = 0 await teamNotify.forumPost({ team: TEAM, threadId: 8, threadTitle: 'Chat', type: 'discussion', bodyHtml: '

Hi.

', }) assert.equal(sent.length, 0) }) test('forums switched off silence the bridge as well as the push', async () => { bridgeTo('999', ['team.forum.post']) patch(forumSettings, 'forumsEnabled', async () => false) const result = await teamNotify.forumPost({ team: TEAM, threadId: 41, threadTitle: 'Siege', type: 'discussion', bodyHtml: '

x

', }) assert.deepEqual(result, { push: 0, emitted: false, bridged: false }) assert.equal(sent.length, 0) }) // ── 3. The ordinary deployment: nothing configured ───────────────────────── test('an unconfigured bridge is silent and costs the push path nothing', async () => { noBridge() const recipients = await teamNotify.memberJoined(TEAM, { count: 1 }) assert.equal(recipients, 2) assert.equal(pushed.length, 1) assert.equal(sent.length, 0) }) // ── 4. A roster event says how many, never who ───────────────────────────── test('the roster message carries a count and no member name', async () => { bridgeTo('999', ['team.member.joined']) await teamNotify.memberJoined(TEAM, { count: 3 }) assert.equal(sent[0].body, '3 new members joined.') assert.equal(sent[0].title, null, 'a roster event has no title to put a name in') }) test('the count is singular at one, and degrades honestly with no count at all', async () => { assert.equal(teamBridge.memberJoinedBody(1), 'A new member joined.') assert.equal(teamBridge.memberJoinedBody(4), '4 new members joined.') assert.equal(teamBridge.memberJoinedBody(undefined), 'The roster has changed.') assert.equal(teamBridge.memberJoinedBody(0), 'The roster has changed.') }) test('the tickle stays content-free even when the bridge beside it carries a body', async () => { bridgeTo('999', ['team.member.joined']) await teamNotify.memberJoined(TEAM, { count: 3 }) assert.deepEqual(pushed[0], { streamId: 'team.member.joined', ref: 'team:3', userIds: [11, 12] }) }) // ── 5. The excerpt ───────────────────────────────────────────────────────── test('the excerpt strips markup, collapses whitespace and decodes entities', async () => { assert.equal(teamBridge.excerpt('

Meet at\nthe moongate & wait.

'), 'Meet at the moongate & wait.') }) test('the excerpt is bounded, because an embed description that overflows is rejected wholesale', async () => { const long = teamBridge.excerpt(`

${'x'.repeat(5000)}

`) assert.equal(long.length, teamBridge.EXCERPT_CHARS) assert.ok(long.endsWith('…')) }) test('the label prefers a staff display-name override, as every other surface does', async () => { assert.equal(teamBridge.teamLabel({ name: 'Real', display_name_override: 'Shown' }), 'Shown') assert.equal(teamBridge.teamLabel(null), 'a team') })