feat(slack): autoFlagPreExistingSlackAccess helper

Best-effort lookup of an activating member's email in the Slack
workspace. On a hit, flips slackInvited:true and stamps slackInvitedAt
without sending a fresh invite. Races against a 3s timeout and swallows
all errors so activation never blocks on Slack.

- Promotes SlackService.findUserByEmail from private to public so the
  helper can call it without a wrapper.
- New activity-log action: slack_access_auto_detected (actor = subject).
- Idempotent: short-circuits when slackInvited is already true.

Callers wired in next commit.
This commit is contained in:
Jennie Robinson Faber 2026-04-29 12:13:59 +01:00
parent 2f6a92ac61
commit b1d8cb1966
4 changed files with 263 additions and 1 deletions

View file

@ -0,0 +1,67 @@
// Spec: docs/specs/wave-based-slack-onboarding.md
// Test plan: docs/specs/wave-based-slack-onboarding-tests.md §1
//
// SCAFFOLD: `describe.skip` until the schema migration lands. Tests use the
// schema's path metadata only — no DB connection required.
import { describe, it, expect } from 'vitest'
import mongoose from 'mongoose'
import Member from '../../../server/models/member.js'
describe.skip('Member schema — Slack fields (post-migration)', () => {
it('does not define slackInviteStatus (1.2)', () => {
expect(Member.schema.path('slackInviteStatus')).toBeUndefined()
})
it('defines slackInvited as Boolean with default false (1.1)', () => {
const path = Member.schema.path('slackInvited')
expect(path).toBeDefined()
expect(path.instance).toBe('Boolean')
expect(path.defaultValue).toBe(false)
})
it('defines slackInvitedAt as an optional Date (1.3)', () => {
const path = Member.schema.path('slackInvitedAt')
expect(path).toBeDefined()
expect(path.instance).toBe('Date')
expect(path.isRequired).toBeFalsy()
})
it('retains slackUserId as String (1.4)', () => {
const path = Member.schema.path('slackUserId')
expect(path).toBeDefined()
expect(path.instance).toBe('String')
})
it('does not auto-stamp slackInvitedAt via pre-save hook (1.5)', () => {
// Constructing a doc and flipping slackInvited should NOT set slackInvitedAt
// — call sites are responsible (project convention).
const doc = new Member({
email: 't@example.com',
name: 'T',
circle: 'community',
contributionAmount: 0
})
doc.slackInvited = true
expect(doc.slackInvitedAt).toBeUndefined()
})
it('new member defaults: slackInvited false, slackInvitedAt unset (1.1)', () => {
const doc = new Member({
email: 'new@example.com',
name: 'New',
circle: 'community',
contributionAmount: 0
})
expect(doc.slackInvited).toBe(false)
expect(doc.slackInvitedAt).toBeUndefined()
})
})
// Sanity: import doesn't introduce mongoose connection side-effects.
describe('mongoose import sanity', () => {
it('imports without error', () => {
expect(mongoose).toBeDefined()
expect(Member).toBeDefined()
})
})