Move tests to __tests__ folders, add unit tests for shared utils and services
Restructure tests into __tests__/ directories at package root so they can be excluded from production builds. Add unit tests for dates, currency, lookup service, payment method default logic, and tax exemption state transitions.
This commit is contained in:
240
packages/backend/__tests__/services/account.service.test.ts
Normal file
240
packages/backend/__tests__/services/account.service.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { createTestApp, cleanDb, seedTestCompany, registerAndLogin, TEST_COMPANY_ID } from '../../src/test/helpers.js'
|
||||
import { AccountService, PaymentMethodService, TaxExemptionService } from '../../src/services/account.service.js'
|
||||
|
||||
describe('PaymentMethodService', () => {
|
||||
let app: FastifyInstance
|
||||
let accountId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `pm-svc-${Date.now()}@test.com` })
|
||||
|
||||
const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'PM Test' })
|
||||
accountId = account.id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('creates a payment method', async () => {
|
||||
const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_abc',
|
||||
cardBrand: 'visa',
|
||||
lastFour: '4242',
|
||||
expMonth: 12,
|
||||
expYear: 2027,
|
||||
isDefault: false,
|
||||
})
|
||||
expect(pm.processorPaymentMethodId).toBe('pm_abc')
|
||||
expect(pm.isDefault).toBe(false)
|
||||
})
|
||||
|
||||
it('unsets old default when creating a new default', async () => {
|
||||
const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_first',
|
||||
isDefault: true,
|
||||
})
|
||||
expect(first.isDefault).toBe(true)
|
||||
|
||||
const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_second',
|
||||
isDefault: true,
|
||||
})
|
||||
expect(second.isDefault).toBe(true)
|
||||
|
||||
// First should no longer be default
|
||||
const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id)
|
||||
expect(firstRefreshed!.isDefault).toBe(false)
|
||||
})
|
||||
|
||||
it('unsets old default when updating to default', async () => {
|
||||
const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_1',
|
||||
isDefault: true,
|
||||
})
|
||||
|
||||
const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_2',
|
||||
isDefault: false,
|
||||
})
|
||||
|
||||
await PaymentMethodService.update(app.db, TEST_COMPANY_ID, second.id, { isDefault: true })
|
||||
|
||||
const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id)
|
||||
expect(firstRefreshed!.isDefault).toBe(false)
|
||||
|
||||
const secondRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, second.id)
|
||||
expect(secondRefreshed!.isDefault).toBe(true)
|
||||
})
|
||||
|
||||
it('does not affect other accounts when setting default', async () => {
|
||||
const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' })
|
||||
|
||||
const otherPm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId: otherAccount.id,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_other',
|
||||
isDefault: true,
|
||||
})
|
||||
|
||||
// Create default on our account — should not touch the other account's default
|
||||
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_ours',
|
||||
isDefault: true,
|
||||
})
|
||||
|
||||
const otherRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, otherPm.id)
|
||||
expect(otherRefreshed!.isDefault).toBe(true)
|
||||
})
|
||||
|
||||
it('lists only methods for the requested account', async () => {
|
||||
const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' })
|
||||
|
||||
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_a',
|
||||
isDefault: false,
|
||||
})
|
||||
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId: otherAccount.id,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_b',
|
||||
isDefault: false,
|
||||
})
|
||||
|
||||
const methods = await PaymentMethodService.listByAccount(app.db, TEST_COMPANY_ID, accountId)
|
||||
expect(methods.length).toBe(1)
|
||||
expect(methods[0].processorPaymentMethodId).toBe('pm_a')
|
||||
})
|
||||
|
||||
it('deletes a payment method', async () => {
|
||||
const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_del',
|
||||
isDefault: false,
|
||||
})
|
||||
|
||||
const deleted = await PaymentMethodService.delete(app.db, TEST_COMPANY_ID, pm.id)
|
||||
expect(deleted).not.toBeNull()
|
||||
|
||||
const fetched = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, pm.id)
|
||||
expect(fetched).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TaxExemptionService', () => {
|
||||
let app: FastifyInstance
|
||||
let accountId: string
|
||||
let userId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `tax-svc-${Date.now()}@test.com` })
|
||||
userId = (auth.user as { id: string }).id
|
||||
|
||||
const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Tax Test' })
|
||||
accountId = account.id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('creates an exemption in pending status', async () => {
|
||||
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'TX-001',
|
||||
certificateType: 'resale',
|
||||
issuingState: 'TX',
|
||||
})
|
||||
expect(exemption.status).toBe('pending')
|
||||
expect(exemption.certificateNumber).toBe('TX-001')
|
||||
expect(exemption.approvedBy).toBeNull()
|
||||
})
|
||||
|
||||
it('approves a pending exemption', async () => {
|
||||
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'TX-002',
|
||||
})
|
||||
|
||||
const approved = await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId)
|
||||
expect(approved!.status).toBe('approved')
|
||||
expect(approved!.approvedBy).toBe(userId)
|
||||
expect(approved!.approvedAt).toBeTruthy()
|
||||
})
|
||||
|
||||
it('revokes an exemption with reason', async () => {
|
||||
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'TX-003',
|
||||
})
|
||||
await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId)
|
||||
|
||||
const revoked = await TaxExemptionService.revoke(app.db, TEST_COMPANY_ID, exemption.id, userId, 'Expired')
|
||||
expect(revoked!.status).toBe('none')
|
||||
expect(revoked!.revokedBy).toBe(userId)
|
||||
expect(revoked!.revokedReason).toBe('Expired')
|
||||
expect(revoked!.revokedAt).toBeTruthy()
|
||||
})
|
||||
|
||||
it('lists exemptions for an account', async () => {
|
||||
await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'CERT-A',
|
||||
})
|
||||
await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'CERT-B',
|
||||
})
|
||||
|
||||
const list = await TaxExemptionService.listByAccount(app.db, TEST_COMPANY_ID, accountId)
|
||||
expect(list.length).toBe(2)
|
||||
})
|
||||
|
||||
it('updates exemption details', async () => {
|
||||
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
|
||||
accountId,
|
||||
certificateNumber: 'OLD',
|
||||
})
|
||||
|
||||
const updated = await TaxExemptionService.update(app.db, TEST_COMPANY_ID, exemption.id, {
|
||||
certificateNumber: 'NEW',
|
||||
issuingState: 'CA',
|
||||
})
|
||||
expect(updated!.certificateNumber).toBe('NEW')
|
||||
expect(updated!.issuingState).toBe('CA')
|
||||
})
|
||||
|
||||
it('returns null for nonexistent exemption', async () => {
|
||||
const result = await TaxExemptionService.getById(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
152
packages/backend/__tests__/services/lookup.service.test.ts
Normal file
152
packages/backend/__tests__/services/lookup.service.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../src/test/helpers.js'
|
||||
import { UnitStatusService, ItemConditionService } from '../../src/services/lookup.service.js'
|
||||
|
||||
describe('UnitStatusService', () => {
|
||||
let app: FastifyInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
describe('seedForCompany', () => {
|
||||
it('seeds system statuses on first call', async () => {
|
||||
const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID)
|
||||
expect(statuses.length).toBeGreaterThanOrEqual(8)
|
||||
expect(statuses.every((s) => s.isSystem)).toBe(true)
|
||||
})
|
||||
|
||||
it('is idempotent — second seed does not duplicate', async () => {
|
||||
await UnitStatusService.seedForCompany(app.db, TEST_COMPANY_ID)
|
||||
const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID)
|
||||
const slugs = statuses.map((s) => s.slug)
|
||||
const uniqueSlugs = new Set(slugs)
|
||||
expect(slugs.length).toBe(uniqueSlugs.size)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getBySlug', () => {
|
||||
it('returns a system status by slug', async () => {
|
||||
const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available')
|
||||
expect(status).not.toBeNull()
|
||||
expect(status!.slug).toBe('available')
|
||||
expect(status!.isSystem).toBe(true)
|
||||
})
|
||||
|
||||
it('returns null for nonexistent slug', async () => {
|
||||
const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'nonexistent')
|
||||
expect(status).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateSlug', () => {
|
||||
it('returns true for valid active slug', async () => {
|
||||
expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'sold')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for nonexistent slug', async () => {
|
||||
expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'bogus')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
it('creates a custom (non-system) status', async () => {
|
||||
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
|
||||
name: 'In Transit',
|
||||
slug: 'in_transit',
|
||||
description: 'Being shipped between locations',
|
||||
sortOrder: 99,
|
||||
})
|
||||
expect(custom.slug).toBe('in_transit')
|
||||
expect(custom.isSystem).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
it('throws when deleting a system status', async () => {
|
||||
const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available')
|
||||
expect(system).not.toBeNull()
|
||||
expect(() => UnitStatusService.delete(app.db, TEST_COMPANY_ID, system!.id)).toThrow('system')
|
||||
})
|
||||
|
||||
it('deletes a custom status', async () => {
|
||||
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
|
||||
name: 'Temp',
|
||||
slug: 'temp',
|
||||
sortOrder: 0,
|
||||
})
|
||||
const deleted = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, custom.id)
|
||||
expect(deleted).not.toBeNull()
|
||||
expect(deleted!.slug).toBe('temp')
|
||||
})
|
||||
|
||||
it('returns null for nonexistent id', async () => {
|
||||
const result = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('throws when deactivating a system status', async () => {
|
||||
const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'sold')
|
||||
expect(() => UnitStatusService.update(app.db, TEST_COMPANY_ID, system!.id, { isActive: false })).toThrow('system')
|
||||
})
|
||||
|
||||
it('allows renaming a custom status', async () => {
|
||||
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
|
||||
name: 'Old Name',
|
||||
slug: 'custom_rename',
|
||||
sortOrder: 0,
|
||||
})
|
||||
const updated = await UnitStatusService.update(app.db, TEST_COMPANY_ID, custom.id, { name: 'New Name' })
|
||||
expect(updated!.name).toBe('New Name')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ItemConditionService', () => {
|
||||
let app: FastifyInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('seeds system conditions', async () => {
|
||||
const conditions = await ItemConditionService.list(app.db, TEST_COMPANY_ID)
|
||||
const slugs = conditions.map((c) => c.slug)
|
||||
expect(slugs).toContain('new')
|
||||
expect(slugs).toContain('excellent')
|
||||
expect(slugs).toContain('good')
|
||||
expect(slugs).toContain('fair')
|
||||
expect(slugs).toContain('poor')
|
||||
})
|
||||
|
||||
it('creates a custom condition', async () => {
|
||||
const custom = await ItemConditionService.create(app.db, TEST_COMPANY_ID, {
|
||||
name: 'Refurbished',
|
||||
slug: 'refurbished',
|
||||
sortOrder: 10,
|
||||
})
|
||||
expect(custom.isSystem).toBe(false)
|
||||
expect(await ItemConditionService.validateSlug(app.db, TEST_COMPANY_ID, 'refurbished')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user