/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from 'vitest'
const sendEmail = vi.fn()
const isConfigured = vi.fn()
vi.mock('@/lib/email/service', () => ({
getEmailService: () => ({ sendEmail, isConfigured }),
}))
import {
shouldSendBackupAlert,
sendBackupFailureAlert,
ALERT_FAILURE_THRESHOLD,
ALERT_THROTTLE_MS,
} from '../backup-alert'
const NOW = new Date('2026-07-12T04:00:00.000Z')
describe('shouldSendBackupAlert', () => {
it('alerts immediately on needs_reauth regardless of failure count', () => {
expect(
shouldSendBackupAlert({
kind: 'needs_reauth',
consecutiveFailures: 1,
lastAlertAt: null,
now: NOW,
})
).toBe(true)
})
it('requires the failure threshold for repeated_failures', () => {
expect(
shouldSendBackupAlert({
kind: 'repeated_failures',
consecutiveFailures: ALERT_FAILURE_THRESHOLD - 1,
lastAlertAt: null,
now: NOW,
})
).toBe(false)
expect(
shouldSendBackupAlert({
kind: 'repeated_failures',
consecutiveFailures: ALERT_FAILURE_THRESHOLD,
lastAlertAt: null,
now: NOW,
})
).toBe(true)
})
it('throttles both kinds against last_alert_at', () => {
const recent = new Date(NOW.getTime() - ALERT_THROTTLE_MS + 60_000).toISOString()
expect(
shouldSendBackupAlert({
kind: 'needs_reauth',
consecutiveFailures: 0,
lastAlertAt: recent,
now: NOW,
})
).toBe(false)
expect(
shouldSendBackupAlert({
kind: 'repeated_failures',
consecutiveFailures: 10,
lastAlertAt: recent,
now: NOW,
})
).toBe(false)
const stale = new Date(NOW.getTime() - ALERT_THROTTLE_MS - 60_000).toISOString()
expect(
shouldSendBackupAlert({
kind: 'needs_reauth',
consecutiveFailures: 0,
lastAlertAt: stale,
now: NOW,
})
).toBe(true)
})
})
/**
* Supabase stub: company_members lookup resolves a member with an email,
* company_settings resolves a company name.
*/
function makeSupabase(options: { member?: unknown; companyName?: string | null } = {}) {
const from = vi.fn().mockImplementation((table: string) => {
const chain: any = {
select: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
maybeSingle: vi.fn().mockImplementation(() => {
if (table === 'company_members') {
return Promise.resolve({
data:
options.member !== undefined
? options.member
: { user_id: 'u-1', profiles: { email: 'emil@example.com' } },
})
}
return Promise.resolve({
data: { company_name: options.companyName ?? 'Testbolag AB' },
})
}),
}
return chain
})
return { from } as any
}
const baseInput = {
companyId: 'c-1',
userId: 'u-1',
consecutiveFailures: 3,
errorMessage: 'Drive quota exceeded',
origin: 'https://app.test',
} as const
describe('sendBackupFailureAlert', () => {
beforeEach(() => {
vi.clearAllMocks()
isConfigured.mockReturnValue(true)
sendEmail.mockResolvedValue({ success: true })
})
it('does nothing when the email service is not configured', async () => {
isConfigured.mockReturnValue(false)
const result = await sendBackupFailureAlert(makeSupabase(), {
...baseInput,
kind: 'repeated_failures',
})
expect(result).toEqual({ sent: false, reason: 'email_not_configured' })
expect(sendEmail).not.toHaveBeenCalled()
})
it('does not email a user who is no longer a company member', async () => {
const result = await sendBackupFailureAlert(makeSupabase({ member: null }), {
...baseInput,
kind: 'repeated_failures',
})
expect(result).toEqual({ sent: false, reason: 'no_recipient' })
expect(sendEmail).not.toHaveBeenCalled()
})
it('sends a repeated-failures email with count, error and link', async () => {
const result = await sendBackupFailureAlert(makeSupabase(), {
...baseInput,
kind: 'repeated_failures',
})
expect(result).toEqual({ sent: true })
expect(sendEmail).toHaveBeenCalledTimes(1)
const options = sendEmail.mock.calls[0][0]
expect(options.to).toBe('emil@example.com')
expect(options.subject).toContain('misslyckas')
expect(options.text).toContain('3 nätter i rad')
expect(options.text).toContain('Drive quota exceeded')
expect(options.text).toContain('https://app.test/import#cloud-backup')
expect(options.html).toContain('Testbolag AB')
})
it('sends a needs_reauth email pointing at the reconnect flow', async () => {
const result = await sendBackupFailureAlert(makeSupabase(), {
...baseInput,
kind: 'needs_reauth',
errorMessage: null,
})
expect(result).toEqual({ sent: true })
const options = sendEmail.mock.calls[0][0]
expect(options.subject).toContain('pausad')
expect(options.text).toContain('Koppla om Google Drive')
expect(options.text).toContain('https://app.test/import#cloud-backup')
})
it('reports send failures without throwing', async () => {
sendEmail.mockResolvedValue({ success: false, error: 'smtp down' })
const result = await sendBackupFailureAlert(makeSupabase(), {
...baseInput,
kind: 'repeated_failures',
})
expect(result).toEqual({ sent: false, reason: 'send_failed' })
})
it('escapes HTML in the error message', async () => {
await sendBackupFailureAlert(makeSupabase(), {
...baseInput,
kind: 'repeated_failures',
errorMessage: '',
})
const options = sendEmail.mock.calls[0][0]
expect(options.html).not.toContain('