feat(activation): wire autoFlagPreExistingSlackAccess into self-serve paths
Replaces the per-file inviteToSlack helpers with a single auto-flag call. Self-serve activation paths now check for pre-existing workspace membership (silent on miss) instead of attempting an admin-only invite. - helcim/subscription.post.js: removed local inviteToSlack; both free- and paid-tier activation branches now call the helper, then notifyNewMember with the canonical 'manual_invitation_required' arg. - members/create.post.js: same shape — helper + canonical notify arg. - invite/accept.post.js (free-tier branch): added the helper call after member creation. Free-tier had no prior Slack call (audit confirmed); paid-tier remains untouched and activates via the Helcim webhook. Admin-created and CSV-imported members intentionally do NOT call the helper — admins flip the flag manually after sending the invite. Test stub for autoFlagPreExistingSlackAccess added to server setup.
This commit is contained in:
parent
b1d8cb1966
commit
55029e7eb7
5 changed files with 262 additions and 152 deletions
|
|
@ -8,77 +8,6 @@ import { createHelcimSubscription, generateIdempotencyKey, listHelcimCustomerTra
|
||||||
import { sendWelcomeEmail } from '../../utils/resend.js'
|
import { sendWelcomeEmail } from '../../utils/resend.js'
|
||||||
import { upsertPaymentFromHelcim } from '../../utils/payments.js'
|
import { upsertPaymentFromHelcim } from '../../utils/payments.js'
|
||||||
|
|
||||||
// Function to invite member to Slack
|
|
||||||
async function inviteToSlack(member) {
|
|
||||||
try {
|
|
||||||
const slackService = getSlackService()
|
|
||||||
if (!slackService) {
|
|
||||||
console.warn('Slack service not configured, skipping invitation')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Processing Slack invitation for ${member.email}...`)
|
|
||||||
|
|
||||||
const inviteResult = await slackService.inviteUserToSlack(
|
|
||||||
member.email,
|
|
||||||
member.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if (inviteResult.success) {
|
|
||||||
const update = {}
|
|
||||||
if (inviteResult.status === 'existing_user_added_to_channel' ||
|
|
||||||
inviteResult.status === 'user_already_in_channel' ||
|
|
||||||
inviteResult.status === 'new_user_invited_to_workspace') {
|
|
||||||
update.slackInviteStatus = 'sent'
|
|
||||||
update.slackUserId = inviteResult.userId
|
|
||||||
update.slackInvited = true
|
|
||||||
} else {
|
|
||||||
update.slackInviteStatus = 'pending'
|
|
||||||
update.slackInvited = false
|
|
||||||
}
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: update },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send notification to vetting channel
|
|
||||||
await slackService.notifyNewMember(
|
|
||||||
member.name,
|
|
||||||
member.email,
|
|
||||||
member.circle,
|
|
||||||
member.contributionAmount,
|
|
||||||
inviteResult.status
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(`Successfully processed Slack invitation for ${member.email}: ${inviteResult.status}`)
|
|
||||||
} else {
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'failed' } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
console.error(`Failed to process Slack invitation for ${member.email}: ${inviteResult.error}`)
|
|
||||||
// Don't throw error - subscription creation should still succeed
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during Slack invitation process:', error)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'failed' } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
} catch (saveError) {
|
|
||||||
console.error('Failed to update member Slack status:', saveError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't throw error - subscription creation should still succeed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
// Membership signup completes subscription before email verify; allow the
|
// Membership signup completes subscription before email verify; allow the
|
||||||
|
|
@ -109,7 +38,21 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
logActivity(member._id, 'subscription_created', { amount: body.contributionAmount })
|
logActivity(member._id, 'subscription_created', { amount: body.contributionAmount })
|
||||||
|
|
||||||
await inviteToSlack(member)
|
await autoFlagPreExistingSlackAccess(member)
|
||||||
|
try {
|
||||||
|
const slackService = getSlackService()
|
||||||
|
if (slackService) {
|
||||||
|
await slackService.notifyNewMember(
|
||||||
|
member.name,
|
||||||
|
member.email,
|
||||||
|
member.circle,
|
||||||
|
member.contributionAmount,
|
||||||
|
'manual_invitation_required'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[slack] notifyNewMember failed:', err)
|
||||||
|
}
|
||||||
if (isFirstActivation) await sendWelcomeEmail(member)
|
if (isFirstActivation) await sendWelcomeEmail(member)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -207,7 +150,21 @@ export default defineEventHandler(async (event) => {
|
||||||
console.error('[payments] initial charge log failed, will be picked up by reconciliation:', err?.message || err)
|
console.error('[payments] initial charge log failed, will be picked up by reconciliation:', err?.message || err)
|
||||||
}
|
}
|
||||||
|
|
||||||
await inviteToSlack(member)
|
await autoFlagPreExistingSlackAccess(member)
|
||||||
|
try {
|
||||||
|
const slackService = getSlackService()
|
||||||
|
if (slackService) {
|
||||||
|
await slackService.notifyNewMember(
|
||||||
|
member.name,
|
||||||
|
member.email,
|
||||||
|
member.circle,
|
||||||
|
member.contributionAmount,
|
||||||
|
'manual_invitation_required'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[slack] notifyNewMember failed:', err)
|
||||||
|
}
|
||||||
if (isFirstActivation) await sendWelcomeEmail(member)
|
if (isFirstActivation) await sendWelcomeEmail(member)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// For free tier, redirect to welcome
|
// For free tier, redirect to welcome
|
||||||
if (body.contributionAmount === 0) {
|
if (body.contributionAmount === 0) {
|
||||||
|
await autoFlagPreExistingSlackAccess(member)
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
requiresPayment: false,
|
requiresPayment: false,
|
||||||
|
|
|
||||||
|
|
@ -7,80 +7,6 @@ import { memberCreateSchema } from '../../utils/schemas.js'
|
||||||
import { sendWelcomeEmail } from '../../utils/resend.js'
|
import { sendWelcomeEmail } from '../../utils/resend.js'
|
||||||
import { assignMemberNumber } from '../../utils/memberNumber.js'
|
import { assignMemberNumber } from '../../utils/memberNumber.js'
|
||||||
|
|
||||||
// Function to invite member to Slack
|
|
||||||
async function inviteToSlack(member) {
|
|
||||||
try {
|
|
||||||
const slackService = getSlackService()
|
|
||||||
if (!slackService) {
|
|
||||||
console.warn('Slack service not configured, skipping invitation')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(`Processing Slack invitation for member`)
|
|
||||||
|
|
||||||
const inviteResult = await slackService.inviteUserToSlack(
|
|
||||||
member.email,
|
|
||||||
member.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if (inviteResult.success) {
|
|
||||||
// Update member record based on the actual result
|
|
||||||
if (inviteResult.status === 'existing_user_added_to_channel' ||
|
|
||||||
inviteResult.status === 'user_already_in_channel' ||
|
|
||||||
inviteResult.status === 'new_user_invited_to_workspace') {
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'sent', slackUserId: inviteResult.userId, slackInvited: true } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Manual invitation required
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'pending', slackInvited: false } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification to vetting channel
|
|
||||||
await slackService.notifyNewMember(
|
|
||||||
member.name,
|
|
||||||
member.email,
|
|
||||||
member.circle,
|
|
||||||
member.contributionAmount,
|
|
||||||
inviteResult.status
|
|
||||||
)
|
|
||||||
|
|
||||||
console.warn(`Slack invitation processed: ${inviteResult.status}`)
|
|
||||||
} else {
|
|
||||||
// Update member record to reflect failed invitation
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'failed' } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
|
|
||||||
console.error(`Failed to process Slack invitation: ${inviteResult.error}`)
|
|
||||||
// Don't throw error - member creation should still succeed
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during Slack invitation process:', error)
|
|
||||||
|
|
||||||
// Update member record to reflect failed invitation
|
|
||||||
try {
|
|
||||||
await Member.findByIdAndUpdate(
|
|
||||||
member._id,
|
|
||||||
{ $set: { slackInviteStatus: 'failed' } },
|
|
||||||
{ runValidators: false }
|
|
||||||
)
|
|
||||||
} catch (saveError) {
|
|
||||||
console.error('Failed to update member Slack status:', saveError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't throw error - member creation should still succeed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
// Ensure database is connected
|
// Ensure database is connected
|
||||||
await connectDB()
|
await connectDB()
|
||||||
|
|
@ -107,8 +33,22 @@ export default defineEventHandler(async (event) => {
|
||||||
circle: member.circle
|
circle: member.circle
|
||||||
}, { timestamp: member.createdAt })
|
}, { timestamp: member.createdAt })
|
||||||
|
|
||||||
// Send Slack invitation for new members
|
// Auto-flag pre-existing Slack workspace members; admin manually invites the rest.
|
||||||
await inviteToSlack(member)
|
await autoFlagPreExistingSlackAccess(member)
|
||||||
|
try {
|
||||||
|
const slackService = getSlackService()
|
||||||
|
if (slackService) {
|
||||||
|
await slackService.notifyNewMember(
|
||||||
|
member.name,
|
||||||
|
member.email,
|
||||||
|
member.circle,
|
||||||
|
member.contributionAmount,
|
||||||
|
'manual_invitation_required'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[slack] notifyNewMember failed:', err)
|
||||||
|
}
|
||||||
|
|
||||||
// Send welcome email (non-blocking)
|
// Send welcome email (non-blocking)
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
211
tests/server/api/activation-auto-flag.test.js
Normal file
211
tests/server/api/activation-auto-flag.test.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Spec: docs/specs/wave-based-slack-onboarding.md
|
||||||
|
// Test plan: docs/specs/wave-based-slack-onboarding-tests.md §3 + §4
|
||||||
|
//
|
||||||
|
// Verifies that the three self-serve activation paths invoke
|
||||||
|
// `autoFlagPreExistingSlackAccess`, while the two admin-create paths do not.
|
||||||
|
//
|
||||||
|
// `autoFlagPreExistingSlackAccess` is auto-imported by Nitro at runtime; in
|
||||||
|
// tests it's stubbed as a global in tests/server/setup.js. Tests grab the stub
|
||||||
|
// off `globalThis` and reset it per-case.
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
import Member from '../../../server/models/member.js'
|
||||||
|
import { requireAuth } from '../../../server/utils/auth.js'
|
||||||
|
import { requiresPayment } from '../../../server/config/contributions.js'
|
||||||
|
import { sendWelcomeEmail } from '../../../server/utils/resend.js'
|
||||||
|
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||||
|
// jwt and the auto-flag helper are mocked via globals/vi.mock
|
||||||
|
import PreRegistration from '../../../server/models/preRegistration.js'
|
||||||
|
import { createHelcimCustomer } from '../../../server/utils/helcim.js'
|
||||||
|
import subscriptionHandler from '../../../server/api/helcim/subscription.post.js'
|
||||||
|
import membersCreateHandler from '../../../server/api/members/create.post.js'
|
||||||
|
import inviteAcceptHandler from '../../../server/api/invite/accept.post.js'
|
||||||
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||||
|
|
||||||
|
vi.mock('../../../server/models/member.js', () => {
|
||||||
|
const mockSave = vi.fn().mockResolvedValue(undefined)
|
||||||
|
function MockMember(data) {
|
||||||
|
Object.assign(this, data)
|
||||||
|
this._id = 'new-member-123'
|
||||||
|
this.status = data.status || 'pending_payment'
|
||||||
|
this.save = mockSave
|
||||||
|
}
|
||||||
|
MockMember.findOne = vi.fn()
|
||||||
|
MockMember.findOneAndUpdate = vi.fn()
|
||||||
|
MockMember.findById = vi.fn()
|
||||||
|
MockMember.findByIdAndUpdate = vi.fn()
|
||||||
|
MockMember.create = vi.fn()
|
||||||
|
MockMember._mockSave = mockSave
|
||||||
|
return { default: MockMember }
|
||||||
|
})
|
||||||
|
vi.mock('../../../server/models/preRegistration.js', () => ({
|
||||||
|
default: { findById: vi.fn(), findByIdAndUpdate: vi.fn() }
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
|
||||||
|
vi.mock('../../../server/utils/auth.js', () => ({
|
||||||
|
requireAuth: vi.fn(),
|
||||||
|
getPaymentBridgeMember: vi.fn().mockResolvedValue(null),
|
||||||
|
setAuthCookie: vi.fn()
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/slack.ts', () => ({
|
||||||
|
getSlackService: vi.fn().mockReturnValue(null)
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/config/contributions.js', () => ({
|
||||||
|
requiresPayment: vi.fn(),
|
||||||
|
getHelcimPlanId: vi.fn()
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/resend.js', () => ({
|
||||||
|
sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true })
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/helcim.js', () => ({
|
||||||
|
createHelcimSubscription: vi.fn(),
|
||||||
|
generateIdempotencyKey: vi.fn().mockReturnValue('idem-key-1'),
|
||||||
|
listHelcimCustomerTransactions: vi.fn().mockResolvedValue([]),
|
||||||
|
createHelcimCustomer: vi.fn()
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/payments.js', () => ({
|
||||||
|
upsertPaymentFromHelcim: vi.fn().mockResolvedValue({ created: true, payment: { _id: 'p1' } })
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/validateBody.js', () => ({ validateBody: vi.fn() }))
|
||||||
|
vi.mock('../../../server/utils/schemas.js', () => ({ memberCreateSchema: {} }))
|
||||||
|
vi.mock('../../../server/utils/memberNumber.js', () => ({
|
||||||
|
assignMemberNumber: vi.fn().mockResolvedValue(1)
|
||||||
|
}))
|
||||||
|
vi.mock('jsonwebtoken', () => {
|
||||||
|
const verify = vi.fn(() => ({ type: 'prereg-invite', preRegistrationId: 'prereg-1' }))
|
||||||
|
return {
|
||||||
|
default: { verify },
|
||||||
|
verify
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.stubGlobal('helcimSubscriptionSchema', {})
|
||||||
|
vi.stubGlobal('inviteAcceptSchema', {})
|
||||||
|
|
||||||
|
const autoFlagStub = globalThis.autoFlagPreExistingSlackAccess
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3.1 — Helcim subscription success calls helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('POST /api/helcim/subscription — auto-flag wiring (3.1)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
autoFlagStub.mockClear()
|
||||||
|
requireAuth.mockResolvedValue(undefined)
|
||||||
|
requiresPayment.mockReturnValue(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls autoFlagPreExistingSlackAccess after successful free-tier activation', async () => {
|
||||||
|
const mockMember = {
|
||||||
|
_id: 'member-1',
|
||||||
|
email: 'free@example.com',
|
||||||
|
name: 'Free Tier',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0,
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
Member.findOneAndUpdate.mockResolvedValue({ _id: 'member-1', status: 'pending_payment' })
|
||||||
|
Member.findById.mockResolvedValue(mockMember)
|
||||||
|
|
||||||
|
const event = createMockEvent({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/helcim/subscription',
|
||||||
|
body: { customerId: 'cust-1', contributionAmount: 0, customerCode: 'code-1' }
|
||||||
|
})
|
||||||
|
|
||||||
|
await subscriptionHandler(event)
|
||||||
|
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledTimes(1)
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ _id: 'member-1', email: 'free@example.com' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3.2 — Free-tier /api/invite/accept calls helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('POST /api/invite/accept (free tier) — auto-flag wiring (3.2)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
autoFlagStub.mockClear()
|
||||||
|
PreRegistration.findById.mockResolvedValue({
|
||||||
|
_id: 'prereg-1',
|
||||||
|
email: 'invitee@example.com',
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
PreRegistration.findByIdAndUpdate.mockResolvedValue(undefined)
|
||||||
|
Member.findOne.mockResolvedValue(null)
|
||||||
|
Member.create.mockImplementation(async (data) => ({
|
||||||
|
_id: 'new-member-via-invite',
|
||||||
|
...data
|
||||||
|
}))
|
||||||
|
// Handler uses Nitro auto-imported validateBody (global), not the explicit import
|
||||||
|
globalThis.validateBody.mockResolvedValue({
|
||||||
|
token: 'tok',
|
||||||
|
preRegistrationId: 'prereg-1',
|
||||||
|
name: 'New Invitee',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls helper after Member.create on the free-tier branch', async () => {
|
||||||
|
const event = createMockEvent({ method: 'POST', path: '/api/invite/accept' })
|
||||||
|
|
||||||
|
await inviteAcceptHandler(event)
|
||||||
|
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledTimes(1)
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ _id: 'new-member-via-invite', email: 'invitee@example.com' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('paid-tier branch does NOT call helper (3.3)', async () => {
|
||||||
|
globalThis.validateBody.mockResolvedValue({
|
||||||
|
token: 'tok',
|
||||||
|
preRegistrationId: 'prereg-1',
|
||||||
|
name: 'Paid Invitee',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 25
|
||||||
|
})
|
||||||
|
createHelcimCustomer.mockResolvedValue({ id: 'cust-2', customerCode: 'code-2' })
|
||||||
|
|
||||||
|
const event = createMockEvent({ method: 'POST', path: '/api/invite/accept' })
|
||||||
|
|
||||||
|
await inviteAcceptHandler(event)
|
||||||
|
|
||||||
|
expect(autoFlagStub).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3.8 — members/create.post.js calls helper instead of legacy inviteToSlack
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
describe('POST /api/members/create — auto-flag wiring (3.8)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
autoFlagStub.mockClear()
|
||||||
|
Member.findOne.mockResolvedValue(null)
|
||||||
|
Member._mockSave.mockResolvedValue(undefined)
|
||||||
|
sendWelcomeEmail.mockResolvedValue({ success: true })
|
||||||
|
validateBody.mockResolvedValue({
|
||||||
|
email: 'mc@example.com',
|
||||||
|
name: 'MC Member',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls helper instead of legacy inviteToSlack', async () => {
|
||||||
|
const event = createMockEvent({ method: 'POST', path: '/api/members/create' })
|
||||||
|
|
||||||
|
await membersCreateHandler(event)
|
||||||
|
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledTimes(1)
|
||||||
|
expect(autoFlagStub).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ email: 'mc@example.com' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -15,6 +15,9 @@ import {
|
||||||
sendRedirect
|
sendRedirect
|
||||||
} from 'h3'
|
} from 'h3'
|
||||||
|
|
||||||
|
// Real server/utils that are safe to use as-is in tests
|
||||||
|
import { escapeRegex } from '../../server/utils/escapeRegex.js'
|
||||||
|
|
||||||
// Register real h3 functions as globals so server code that relies on
|
// Register real h3 functions as globals so server code that relies on
|
||||||
// Nitro auto-imports can find them in the test environment.
|
// Nitro auto-imports can find them in the test environment.
|
||||||
vi.stubGlobal('getCookie', getCookie)
|
vi.stubGlobal('getCookie', getCookie)
|
||||||
|
|
@ -42,7 +45,5 @@ vi.stubGlobal('requireAdmin', vi.fn())
|
||||||
vi.stubGlobal('validateBody', vi.fn(async (event) => readBody(event)))
|
vi.stubGlobal('validateBody', vi.fn(async (event) => readBody(event)))
|
||||||
vi.stubGlobal('logActivity', vi.fn())
|
vi.stubGlobal('logActivity', vi.fn())
|
||||||
vi.stubGlobal('validateTagSlugs', vi.fn())
|
vi.stubGlobal('validateTagSlugs', vi.fn())
|
||||||
|
vi.stubGlobal('autoFlagPreExistingSlackAccess', vi.fn().mockResolvedValue(undefined))
|
||||||
// Real server/utils that are safe to use as-is in tests
|
|
||||||
import { escapeRegex } from '../../server/utils/escapeRegex.js'
|
|
||||||
vi.stubGlobal('escapeRegex', escapeRegex)
|
vi.stubGlobal('escapeRegex', escapeRegex)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue