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:
327
packages/backend/__tests__/routes/v1/accounts.test.ts
Normal file
327
packages/backend/__tests__/routes/v1/accounts.test.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
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('Account 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)
|
||||
token = auth.token
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
describe('POST /v1/accounts', () => {
|
||||
it('creates an account', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
name: 'Smith Family',
|
||||
email: 'smith@example.com',
|
||||
phone: '512-555-1234',
|
||||
billingMode: 'consolidated',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(201)
|
||||
const body = response.json()
|
||||
expect(body.name).toBe('Smith Family')
|
||||
expect(body.email).toBe('smith@example.com')
|
||||
expect(body.companyId).toBe(TEST_COMPANY_ID)
|
||||
expect(body.billingMode).toBe('consolidated')
|
||||
})
|
||||
|
||||
it('rejects missing name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { email: 'noname@test.com' },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /v1/accounts', () => {
|
||||
it('lists accounts for the company', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Account One' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Account Two' },
|
||||
})
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
const body = response.json()
|
||||
expect(body.data.length).toBe(2)
|
||||
expect(body.pagination.total).toBe(2)
|
||||
expect(body.pagination.page).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /v1/accounts?q=', () => {
|
||||
it('searches by name', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Johnson Family', phone: '555-9999' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Williams Music' },
|
||||
})
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts?q=johnson',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
const body = response.json()
|
||||
expect(body.data.length).toBe(1)
|
||||
expect(body.data[0].name).toBe('Johnson Family')
|
||||
})
|
||||
|
||||
it('searches by phone', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Phone Test', phone: '512-867-5309' },
|
||||
})
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts?q=867-5309',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.json().data.length).toBe(1)
|
||||
})
|
||||
|
||||
it('paginates results', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: `Account ${i}` },
|
||||
})
|
||||
}
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts?page=1&limit=2',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
const body = response.json()
|
||||
expect(body.data.length).toBe(2)
|
||||
expect(body.pagination.total).toBe(5)
|
||||
expect(body.pagination.totalPages).toBe(3)
|
||||
expect(body.pagination.page).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /v1/accounts/:id', () => {
|
||||
it('updates an account', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Original Name' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/v1/accounts/${id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Updated Name' },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.json().name).toBe('Updated Name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /v1/accounts/:id', () => {
|
||||
it('soft-deletes an account', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'To Delete' },
|
||||
})
|
||||
const id = createRes.json().id
|
||||
|
||||
const delRes = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/v1/accounts/${id}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(delRes.statusCode).toBe(200)
|
||||
expect(delRes.json().isActive).toBe(false)
|
||||
|
||||
// Should not appear in list
|
||||
const listRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(listRes.json().data.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Member 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: `member-test-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
|
||||
const accountRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Test Family' },
|
||||
})
|
||||
accountId = accountRes.json().id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
describe('POST /v1/accounts/:accountId/members', () => {
|
||||
it('creates a member', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
firstName: 'Emma',
|
||||
lastName: 'Chen',
|
||||
dateOfBirth: '2015-03-15',
|
||||
email: 'emma@test.com',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(201)
|
||||
const body = response.json()
|
||||
expect(body.firstName).toBe('Emma')
|
||||
expect(body.lastName).toBe('Chen')
|
||||
expect(body.isMinor).toBe(true)
|
||||
expect(body.accountId).toBe(accountId)
|
||||
})
|
||||
|
||||
it('marks adult members as not minor', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
firstName: 'John',
|
||||
lastName: 'Chen',
|
||||
dateOfBirth: '1985-06-20',
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(201)
|
||||
expect(response.json().isMinor).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /v1/accounts/:accountId/members', () => {
|
||||
it('lists members for an account', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Child', lastName: 'One' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Child', lastName: 'Two' },
|
||||
})
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.json().data.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /members/:id', () => {
|
||||
it('updates a member and recalculates isMinor', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Kid', lastName: 'Test', dateOfBirth: '2015-01-01' },
|
||||
})
|
||||
const memberId = createRes.json().id
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/v1/members/${memberId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { dateOfBirth: '1980-01-01' },
|
||||
})
|
||||
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.json().isMinor).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user