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:
491
packages/backend/__tests__/routes/v1/accounts-extended.test.ts
Normal file
491
packages/backend/__tests__/routes/v1/accounts-extended.test.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
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'
|
||||
|
||||
describe('Processor link routes', () => {
|
||||
let app: FastifyInstance
|
||||
let token: string
|
||||
let accountId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `pl-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Link Test Account' },
|
||||
})
|
||||
accountId = res.json().id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('creates a processor link', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorCustomerId: 'cus_test123' },
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().processor).toBe('stripe')
|
||||
expect(res.json().processorCustomerId).toBe('cus_test123')
|
||||
expect(res.json().accountId).toBe(accountId)
|
||||
})
|
||||
|
||||
it('lists processor links for an account', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorCustomerId: 'cus_1' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'global_payments', processorCustomerId: 'gp_1' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().data.length).toBe(2)
|
||||
})
|
||||
|
||||
it('updates a processor link', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorCustomerId: 'cus_1' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/v1/processor-links/${id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { isActive: false },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().isActive).toBe(false)
|
||||
})
|
||||
|
||||
it('deletes a processor link', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/processor-links`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorCustomerId: 'cus_del' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
const delRes = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/processor-links/${id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(delRes.statusCode).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Payment method routes', () => {
|
||||
let app: FastifyInstance
|
||||
let token: string
|
||||
let accountId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `pm-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Payment Test Account' },
|
||||
})
|
||||
accountId = res.json().id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('creates a payment method', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
processor: 'stripe',
|
||||
processorPaymentMethodId: 'pm_test123',
|
||||
cardBrand: 'visa',
|
||||
lastFour: '4242',
|
||||
expMonth: 12,
|
||||
expYear: 2027,
|
||||
isDefault: true,
|
||||
},
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().cardBrand).toBe('visa')
|
||||
expect(res.json().lastFour).toBe('4242')
|
||||
expect(res.json().isDefault).toBe(true)
|
||||
})
|
||||
|
||||
it('lists payment methods for an account', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_1', lastFour: '1111' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_2', lastFour: '2222' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().data.length).toBe(2)
|
||||
})
|
||||
|
||||
it('sets new default and unsets old default', async () => {
|
||||
const first = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_1', isDefault: true },
|
||||
})
|
||||
const firstId = first.json().id
|
||||
|
||||
const second = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_2', isDefault: true },
|
||||
})
|
||||
expect(second.json().isDefault).toBe(true)
|
||||
|
||||
// First should no longer be default
|
||||
const getFirst = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/payment-methods/${firstId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(getFirst.json().isDefault).toBe(false)
|
||||
})
|
||||
|
||||
it('deletes a payment method', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/payment-methods`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_del' },
|
||||
})
|
||||
|
||||
const delRes = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/payment-methods/${createRes.json().id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(delRes.statusCode).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tax exemption routes', () => {
|
||||
let app: FastifyInstance
|
||||
let token: string
|
||||
let accountId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `tax-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Tax Test Account' },
|
||||
})
|
||||
accountId = res.json().id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('creates a tax exemption', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
certificateNumber: 'TX-12345',
|
||||
certificateType: 'resale',
|
||||
issuingState: 'TX',
|
||||
expiresAt: '2027-12-31',
|
||||
},
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().certificateNumber).toBe('TX-12345')
|
||||
expect(res.json().status).toBe('pending')
|
||||
expect(res.json().issuingState).toBe('TX')
|
||||
})
|
||||
|
||||
it('lists tax exemptions for an account', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'CERT-1' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().data.length).toBe(1)
|
||||
})
|
||||
|
||||
it('approves a tax exemption', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'CERT-APPROVE' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/tax-exemptions/${id}/approve`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().status).toBe('approved')
|
||||
expect(res.json().approvedBy).toBeTruthy()
|
||||
expect(res.json().approvedAt).toBeTruthy()
|
||||
})
|
||||
|
||||
it('revokes a tax exemption with reason', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'CERT-REVOKE' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
// Approve first
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/tax-exemptions/${id}/approve`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/tax-exemptions/${id}/revoke`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { reason: 'Certificate expired' },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().status).toBe('none')
|
||||
expect(res.json().revokedReason).toBe('Certificate expired')
|
||||
})
|
||||
|
||||
it('rejects revoke without reason', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'CERT-NO-REASON' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/tax-exemptions/${createRes.json().id}/revoke`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {},
|
||||
})
|
||||
expect(res.statusCode).toBe(400)
|
||||
})
|
||||
|
||||
it('updates a tax exemption', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/tax-exemptions`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'OLD-CERT' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/v1/tax-exemptions/${createRes.json().id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { certificateNumber: 'NEW-CERT', issuingState: 'CA' },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().certificateNumber).toBe('NEW-CERT')
|
||||
expect(res.json().issuingState).toBe('CA')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lookup table routes', () => {
|
||||
let app: FastifyInstance
|
||||
let token: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDb(app)
|
||||
await seedTestCompany(app)
|
||||
const auth = await registerAndLogin(app, { email: `lookup-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
describe('Unit statuses', () => {
|
||||
it('lists system-seeded unit statuses', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/unit-statuses',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
const slugs = res.json().data.map((s: { slug: string }) => s.slug)
|
||||
expect(slugs).toContain('available')
|
||||
expect(slugs).toContain('sold')
|
||||
expect(slugs).toContain('on_trial')
|
||||
expect(slugs).toContain('layaway')
|
||||
expect(slugs).toContain('lost')
|
||||
})
|
||||
|
||||
it('creates a custom status', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/unit-statuses',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'On Display', slug: 'on_display', description: 'Showroom floor' },
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().slug).toBe('on_display')
|
||||
expect(res.json().isSystem).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects duplicate slug', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/unit-statuses',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Available Copy', slug: 'available' },
|
||||
})
|
||||
expect(res.statusCode).toBe(409)
|
||||
})
|
||||
|
||||
it('blocks deleting a system status', async () => {
|
||||
const list = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/unit-statuses',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
const systemStatus = list.json().data.find((s: { isSystem: boolean }) => s.isSystem)
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/unit-statuses/${systemStatus.id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(403)
|
||||
})
|
||||
|
||||
it('allows deleting a custom status', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/unit-statuses',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Temp', slug: 'temp_status' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/unit-statuses/${createRes.json().id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Item conditions', () => {
|
||||
it('lists system-seeded item conditions', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/item-conditions',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(res.statusCode).toBe(200)
|
||||
const slugs = res.json().data.map((c: { slug: string }) => 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 res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/item-conditions',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Refurbished', slug: 'refurbished', description: 'Professionally restored' },
|
||||
})
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().slug).toBe('refurbished')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user