chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
import { createEnvMock, databaseMock } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { EmailType } from '@/lib/messaging/email/mailer'
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
eq: vi.fn((a, b) => ({ type: 'eq', left: a, right: b })),
|
||||
}))
|
||||
|
||||
const mockDb = databaseMock.db as Record<string, ReturnType<typeof vi.fn>>
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => createEnvMock({ BETTER_AUTH_SECRET: 'test-secret-key' }))
|
||||
|
||||
import {
|
||||
generateUnsubscribeToken,
|
||||
getEmailPreferences,
|
||||
isTransactionalEmail,
|
||||
isUnsubscribed,
|
||||
resubscribe,
|
||||
unsubscribeFromAll,
|
||||
updateEmailPreferences,
|
||||
verifyUnsubscribeToken,
|
||||
} from '@/lib/messaging/email/unsubscribe'
|
||||
|
||||
describe('unsubscribe utilities', () => {
|
||||
const testEmail = 'test@example.com'
|
||||
const testEmailType = 'marketing'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('generateUnsubscribeToken', () => {
|
||||
it.concurrent('should generate a token with salt:hash:emailType format', () => {
|
||||
const token = generateUnsubscribeToken(testEmail, testEmailType)
|
||||
const parts = token.split(':')
|
||||
|
||||
expect(parts).toHaveLength(3)
|
||||
expect(parts[0]).toHaveLength(32) // Salt should be 32 chars (16 bytes hex)
|
||||
expect(parts[1]).toHaveLength(64) // SHA256 hash should be 64 chars
|
||||
expect(parts[2]).toBe(testEmailType)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should generate different tokens for the same email (due to random salt)',
|
||||
() => {
|
||||
const token1 = generateUnsubscribeToken(testEmail, testEmailType)
|
||||
const token2 = generateUnsubscribeToken(testEmail, testEmailType)
|
||||
|
||||
expect(token1).not.toBe(token2)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should default to marketing email type', () => {
|
||||
const token = generateUnsubscribeToken(testEmail)
|
||||
const parts = token.split(':')
|
||||
|
||||
expect(parts[2]).toBe('marketing')
|
||||
})
|
||||
|
||||
it.concurrent('should generate different tokens for different email types', () => {
|
||||
const marketingToken = generateUnsubscribeToken(testEmail, 'marketing')
|
||||
const updatesToken = generateUnsubscribeToken(testEmail, 'updates')
|
||||
|
||||
expect(marketingToken).not.toBe(updatesToken)
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifyUnsubscribeToken', () => {
|
||||
it.concurrent('should verify a valid token', () => {
|
||||
const token = generateUnsubscribeToken(testEmail, testEmailType)
|
||||
const result = verifyUnsubscribeToken(testEmail, token)
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.emailType).toBe(testEmailType)
|
||||
})
|
||||
|
||||
it.concurrent('should reject an invalid token', () => {
|
||||
const invalidToken = 'invalid:token:format'
|
||||
const result = verifyUnsubscribeToken(testEmail, invalidToken)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.emailType).toBe('format')
|
||||
})
|
||||
|
||||
it.concurrent('should reject a token for wrong email', () => {
|
||||
const token = generateUnsubscribeToken(testEmail, testEmailType)
|
||||
const result = verifyUnsubscribeToken('wrong@example.com', token)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle legacy tokens (2 parts) and default to marketing', () => {
|
||||
const salt = 'abc123'
|
||||
const secret = 'test-secret-key'
|
||||
const { createHash } = require('crypto')
|
||||
const hash = createHash('sha256').update(`${testEmail}:${salt}:${secret}`).digest('hex')
|
||||
const legacyToken = `${salt}:${hash}`
|
||||
|
||||
const result = verifyUnsubscribeToken(testEmail, legacyToken)
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.emailType).toBe('marketing')
|
||||
})
|
||||
|
||||
it.concurrent('should reject malformed tokens', () => {
|
||||
const malformedTokens = ['', 'single-part', 'too:many:parts:here:invalid', ':empty:parts:']
|
||||
|
||||
malformedTokens.forEach((token) => {
|
||||
const result = verifyUnsubscribeToken(testEmail, token)
|
||||
expect(result.valid).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTransactionalEmail', () => {
|
||||
it.concurrent('should identify transactional emails correctly', () => {
|
||||
expect(isTransactionalEmail('transactional')).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should identify non-transactional emails correctly', () => {
|
||||
const nonTransactionalTypes: EmailType[] = ['marketing', 'updates', 'notifications']
|
||||
|
||||
nonTransactionalTypes.forEach((type) => {
|
||||
expect(isTransactionalEmail(type)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getEmailPreferences', () => {
|
||||
it('should return email preferences for a user', async () => {
|
||||
const mockPreferences = {
|
||||
unsubscribeAll: false,
|
||||
unsubscribeMarketing: true,
|
||||
}
|
||||
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: mockPreferences }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await getEmailPreferences(testEmail)
|
||||
|
||||
expect(result).toEqual(mockPreferences)
|
||||
})
|
||||
|
||||
it('should return null when user is not found', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await getEmailPreferences(testEmail)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('should return empty object when emailPreferences is null', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: null }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await getEmailPreferences(testEmail)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should return null on database error', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockRejectedValue(new Error('Database connection failed')),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await getEmailPreferences(testEmail)
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateEmailPreferences', () => {
|
||||
it('should update email preferences for existing user', async () => {
|
||||
const userId = 'user-123'
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ id: userId }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: { unsubscribeAll: false } }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
mockDb.insert.mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await updateEmailPreferences(testEmail, { unsubscribeMarketing: true })
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockDb.insert).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return false when user is not found', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await updateEmailPreferences(testEmail, { unsubscribeMarketing: true })
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should merge with existing preferences', async () => {
|
||||
const userId = 'user-123'
|
||||
const existingPrefs = { unsubscribeAll: false, unsubscribeUpdates: true }
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ id: userId }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: existingPrefs }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const mockInsertValues = vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
mockDb.insert.mockReturnValue({
|
||||
values: mockInsertValues,
|
||||
})
|
||||
|
||||
await updateEmailPreferences(testEmail, { unsubscribeMarketing: true })
|
||||
|
||||
expect(mockInsertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
emailPreferences: {
|
||||
unsubscribeAll: false,
|
||||
unsubscribeUpdates: true,
|
||||
unsubscribeMarketing: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false on database error', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockRejectedValue(new Error('Database error')),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await updateEmailPreferences(testEmail, { unsubscribeMarketing: true })
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isUnsubscribed', () => {
|
||||
it('should return false when user has no preferences', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'marketing')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when unsubscribeAll is true', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: { unsubscribeAll: true } }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'marketing')
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when specific type is unsubscribed', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
{ emailPreferences: { unsubscribeMarketing: true, unsubscribeUpdates: false } },
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const resultMarketing = await isUnsubscribed(testEmail, 'marketing')
|
||||
expect(resultMarketing).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when specific type is not unsubscribed', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi
|
||||
.fn()
|
||||
.mockResolvedValue([
|
||||
{ emailPreferences: { unsubscribeMarketing: false, unsubscribeUpdates: true } },
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'marketing')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should check updates unsubscribe status', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ emailPreferences: { unsubscribeUpdates: true } }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'updates')
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should check notifications unsubscribe status', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ emailPreferences: { unsubscribeNotifications: true } }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'notifications')
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for unknown email type', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: {} }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'all')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false on database error', async () => {
|
||||
mockDb.select.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
leftJoin: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockRejectedValue(new Error('Database error')),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await isUnsubscribed(testEmail, 'marketing')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsubscribeFromAll', () => {
|
||||
it('should call updateEmailPreferences with unsubscribeAll: true', async () => {
|
||||
const userId = 'user-123'
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ id: userId }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ emailPreferences: {} }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const mockInsertValues = vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
mockDb.insert.mockReturnValue({
|
||||
values: mockInsertValues,
|
||||
})
|
||||
|
||||
const result = await unsubscribeFromAll(testEmail)
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockInsertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
emailPreferences: expect.objectContaining({ unsubscribeAll: true }),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resubscribe', () => {
|
||||
it('should reset all unsubscribe flags to false', async () => {
|
||||
const userId = 'user-123'
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([{ id: userId }]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
mockDb.select.mockReturnValueOnce({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue([
|
||||
{
|
||||
emailPreferences: {
|
||||
unsubscribeAll: true,
|
||||
unsubscribeMarketing: true,
|
||||
unsubscribeUpdates: true,
|
||||
unsubscribeNotifications: true,
|
||||
},
|
||||
},
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const mockInsertValues = vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
mockDb.insert.mockReturnValue({
|
||||
values: mockInsertValues,
|
||||
})
|
||||
|
||||
const result = await resubscribe(testEmail)
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockInsertValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
emailPreferences: {
|
||||
unsubscribeAll: false,
|
||||
unsubscribeMarketing: false,
|
||||
unsubscribeUpdates: false,
|
||||
unsubscribeNotifications: false,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user