Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const testConfig = {
client: 'pg',
connection: {
host: 'localhost',
port: isCI ? 5432 : 5433,
port: parseInt(process.env.TEST_DB_PORT || (isCI ? '5432' : '5433')),
user: 'test_user',
password: 'test_pass',
database: 'ebb_test'
Expand Down
4 changes: 2 additions & 2 deletions repos/__tests__/License.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ describe('LicenseRepo', () => {
})


describe('updateLicenseByStripePaymentId', () => {
describe('updateLicenseByCustomerId', () => {
it('should update a license', async () => {
const result = await LicenseRepo.updateLicenseByStripePaymentId('pi_test123', 'expired')
const result = await LicenseRepo.updateLicenseByCustomerId('cus_test123', 'expired')
expect(result).not.toBeNull()
expect(result?.user_id).toBe(user1Id)
expect(result?.status).toBe('expired')
Expand Down
14 changes: 11 additions & 3 deletions services/SlackOAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,15 @@ const BOT_SCOPES = [
'users:read'
].join(',')

const ENCRYPTION = new EbbEncryption(process.env.SLACK_ENCRYPTION_KEY)
// Constructed lazily so importing this module (e.g. in tests or in environments
// without Slack configured) does not throw when SLACK_ENCRYPTION_KEY is unset.
let encryption: EbbEncryption | undefined
const getEncryption = (): EbbEncryption => {
if (!encryption) {
encryption = new EbbEncryption(process.env.SLACK_ENCRYPTION_KEY)
}
return encryption
}

const generateAuthUrl = async (userId: string, teamId?: string, redirectType?: 'dev' | 'prod'): Promise<string> => {
if (!CLIENT_ID) {
Expand Down Expand Up @@ -199,11 +207,11 @@ const updateUserPreferences = async (userId: string, updates: SlackPreferences):


const encrypt = (text: string): string => {
return ENCRYPTION.encrypt(text)
return getEncryption().encrypt(text)
}

const decrypt = (encryptedText: string): string => {
return ENCRYPTION.decrypt(encryptedText)
return getEncryption().decrypt(encryptedText)
}

const getDecryptedToken = (encryptedToken: string): string => {
Expand Down
39 changes: 18 additions & 21 deletions services/__tests__/LicenseService.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test'
import { describe, it, expect, afterEach, mock, spyOn } from 'bun:test'
import { LicenseService } from "../LicenseService"
import { LicenseRepo, type License } from "../../repos/License"

// Mock the LicenseRepo module
mock.module("../../repos/License", () => ({
LicenseRepo: {
getActiveLicenseByUserId: mock(),
createLicense: mock()
}
}))
// Use spies rather than mock.module: Bun applies mock.module to every test file
// in the run, so a module-level stub here would leak into later files.

const mockLicense = {

id: 'license-123',
user_id: 'test-user-id',
license_type: 'free_trial',
status: 'active',
expiration_date: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000)
} as License

describe('LicenseService', () => {
beforeEach(() => {
// Reset all mocks before each test
afterEach(() => {
// Restore all spies so LicenseRepo is untouched for other test files
mock.restore()
})

describe('startFreeTrial', () => {
it('should throw error when user already has an active license', async () => {
// Mock getActiveLicenseByUserId to return an existing license
LicenseRepo.getActiveLicenseByUserId = mock(() => Promise.resolve(mockLicense))
it('should throw error when user already has a license', async () => {
const getLicenseByUserId = spyOn(LicenseRepo, 'getLicenseByUserId').mockResolvedValue(mockLicense)

await expect(LicenseService.startFreeTrial('test-user-id')).rejects.toThrow('User already has a license')
expect(LicenseRepo.getActiveLicenseByUserId).toHaveBeenCalledWith('test-user-id')

expect(getLicenseByUserId).toHaveBeenCalledWith('test-user-id')
})

it('should start a free trial when user has no existing license', async () => {
LicenseRepo.getActiveLicenseByUserId = mock(() => Promise.resolve(null))
LicenseRepo.createLicense = mock(() => Promise.resolve(mockLicense))
const getLicenseByUserId = spyOn(LicenseRepo, 'getLicenseByUserId').mockResolvedValue(null)
const createLicense = spyOn(LicenseRepo, 'createLicense').mockResolvedValue(mockLicense)

const result = await LicenseService.startFreeTrial('test-user-id')

expect(result).toBeDefined()
expect(LicenseRepo.getActiveLicenseByUserId).toHaveBeenCalledWith('test-user-id')
expect(LicenseRepo.createLicense).toHaveBeenCalledWith({
expect(getLicenseByUserId).toHaveBeenCalledWith('test-user-id')
expect(createLicense).toHaveBeenCalledWith({
user_id: 'test-user-id',
license_type: 'free_trial',
expiration_date: expect.any(Date)
})
})
})
})
})
36 changes: 18 additions & 18 deletions tests/services/WebhookService.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { beforeEach, describe, expect, test, mock } from 'bun:test'
import { afterEach, beforeEach, describe, expect, test, mock, spyOn } from 'bun:test'
import { WebhookService } from '../../services/WebhookService.js'
import { LicenseRepo } from '../../repos/License.js'
import type Stripe from 'stripe'

mock.module('../../repos/License.js', () => ({
LicenseRepo: {
createLicense: mock(() => Promise.resolve({ id: 'license-123' })),
updateLicenseByStripePaymentId: mock(() => Promise.resolve({ id: 'license-123' })),
getFreeTrialLicenseByUserId: mock(() => Promise.resolve(null)),
getExistingSubscriptionLicenseByUserId: mock(() => Promise.resolve(null)),
updateLicense: mock(() => Promise.resolve({ id: 'trial-license-456' }))
}
}))
// Use spies rather than mock.module: Bun applies mock.module to every test file
// in the run, so a module-level stub here would leak into later files.

let defaultMockSession = {
id: 'cs_test_b17yWHosFRVEk62aEegAVsREUlB415PcJqoS1B2ZNEGnVMgVm0Bfpnyz1C',
Expand All @@ -33,15 +26,18 @@ let defaultMockSession = {

describe('WebhookService', () => {
beforeEach(() => {
// Clear call history for all mocks
(LicenseRepo.createLicense as any).mockClear();
(LicenseRepo.updateLicense as any).mockClear();
(LicenseRepo.updateLicenseByStripePaymentId as any).mockClear();
(LicenseRepo.getFreeTrialLicenseByUserId as any).mockClear();
(LicenseRepo.getExistingSubscriptionLicenseByUserId as any).mockClear();
spyOn(LicenseRepo, 'createLicense').mockResolvedValue({ id: 'license-123' } as any)
spyOn(LicenseRepo, 'updateLicense').mockResolvedValue({ id: 'trial-license-456' } as any)
spyOn(LicenseRepo, 'getFreeTrialLicenseByUserId').mockResolvedValue(null)
spyOn(LicenseRepo, 'getExistingSubscriptionLicenseByUserId').mockResolvedValue(null)
process.env.STRIPE_SANDBOX = 'true'
})

afterEach(() => {
// Restore all spies so LicenseRepo is untouched for other test files
mock.restore()
})

describe('handleCheckoutSessionCompleted', () => {
test('should create license for valid checkout session with metadata', async () => {

Expand All @@ -53,7 +49,8 @@ describe('WebhookService', () => {
license_type: 'subscription',
purchase_date: expect.any(Date),
stripe_customer_id: 'cus_Sxs0V3mlJjxzg7',
stripe_payment_id: 'sub_1S1wGyAuvxsld26UyyEWlujr'
stripe_payment_id: 'sub_1S1wGyAuvxsld26UyyEWlujr',
expiration_date: undefined
})
})

Expand Down Expand Up @@ -109,6 +106,7 @@ describe('WebhookService', () => {
status: 'active',
license_type: 'subscription',
stripe_payment_id: 'sub_1S1wGyAuvxsld26UyyEWlujr',
expiration_date: null,
updated_at: expect.any(Date)
})

Expand All @@ -131,6 +129,7 @@ describe('WebhookService', () => {
expect(LicenseRepo.updateLicense).toHaveBeenCalledWith('expired-license-456', {
status: 'active',
stripe_payment_id: 'sub_1S1wGyAuvxsld26UyyEWlujr',
expiration_date: null,
updated_at: expect.any(Date)
})

Expand Down Expand Up @@ -180,7 +179,8 @@ describe('WebhookService', () => {
license_type: 'subscription',
purchase_date: expect.any(Date),
stripe_customer_id: 'cus_DifferentCustomer',
stripe_payment_id: 'sub_DifferentSubscription'
stripe_payment_id: 'sub_DifferentSubscription',
expiration_date: undefined
})
})
})
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/discordProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ describe('DiscordNotificationProvider', () => {
expect(callArgs).toBeDefined()
const body = JSON.parse(callArgs![1].body)

expect(body.embeds[0].title).toBe('📊 Weekly Report')
expect(body.embeds[0].title).toBe('📊 Weekly Report Reminder')
expect(body.embeds[0].color).toBe(0x9932CC) // Purple
})
})
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/notificationEngine.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test'
import { describe, it, expect, beforeEach, afterAll, mock, spyOn } from 'bun:test'
import { NotificationEngine } from '../../services/NotificationEngine'
import * as RealDiscordNotificationProvider from '../../services/providers/DiscordNotificationProvider'
import * as RealNotificationService from '../../services/NotificationService'

// Bun applies mock.module to every test file in the run, not just this one.
// Snapshot the real exports (static imports are evaluated before the mocks
// below) and put them back when this file finishes so later files get the
// real modules.
const realDiscordNotificationProvider = { ...RealDiscordNotificationProvider }
const realNotificationService = { ...RealNotificationService }

afterAll(() => {
mock.module('../../services/providers/DiscordNotificationProvider', () => realDiscordNotificationProvider)
mock.module('../../services/NotificationService', () => realNotificationService)
})
import type {
NotificationConfig,
NotificationProvider,
Expand Down
19 changes: 18 additions & 1 deletion tests/unit/processNotificationsWithIdempotency.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test'
import { describe, it, expect, beforeEach, afterAll, mock, spyOn } from 'bun:test'
import { processNotificationsWithIdempotency } from '../../services/JobProcessors'
import type { NewUserRecord, InactiveUserRecord } from '../../types/notifications'
import * as RealNotificationEngine from '../../services/NotificationEngine'
import * as RealUserNotificationsRepo from '../../repos/UserNotifications'
import * as RealNotificationsConfig from '../../config/notifications'

// Bun applies mock.module to every test file in the run, not just this one.
// Snapshot the real exports (static imports are evaluated before the mocks
// below) and put them back when this file finishes so later files get the
// real modules.
const realNotificationEngine = { ...RealNotificationEngine }
const realUserNotificationsRepo = { ...RealUserNotificationsRepo }
const realNotificationsConfig = { ...RealNotificationsConfig }

afterAll(() => {
mock.module('../../services/NotificationEngine', () => realNotificationEngine)
mock.module('../../repos/UserNotifications', () => realUserNotificationsRepo)
mock.module('../../config/notifications', () => realNotificationsConfig)
})

// Mock dependencies directly
const mockNotificationEngine = {
Expand Down
Loading