Add integration tests for members list, move, auto-numbers, primary member, isMinor flag
This commit is contained in:
258
packages/backend/__tests__/routes/v1/members.test.ts
Normal file
258
packages/backend/__tests__/routes/v1/members.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
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('Top-level 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: `members-${Date.now()}@test.com` })
|
||||
token = auth.token
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Test Family' },
|
||||
})
|
||||
accountId = res.json().id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
describe('GET /v1/members', () => {
|
||||
it('lists all members across accounts', async () => {
|
||||
const account2Res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Other Family' },
|
||||
})
|
||||
const account2Id = account2Res.json().id
|
||||
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Alice', lastName: 'Smith' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${account2Id}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Bob', lastName: 'Jones' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/members',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(200)
|
||||
const body = res.json()
|
||||
expect(body.data.length).toBe(2)
|
||||
expect(body.data[0].accountName).toBeTruthy()
|
||||
})
|
||||
|
||||
it('searches members by name', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Alice', lastName: 'Smith' },
|
||||
})
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Bob', lastName: 'Jones' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/members?q=alice',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().data.length).toBe(1)
|
||||
expect(res.json().data[0].firstName).toBe('Alice')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auto-generated numbers', () => {
|
||||
it('generates account number on create', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Number Test' },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(201)
|
||||
const body = res.json()
|
||||
expect(body.accountNumber).toBeTruthy()
|
||||
expect(body.accountNumber.length).toBe(6)
|
||||
})
|
||||
|
||||
it('generates member number on create', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Test', lastName: 'Member' },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().memberNumber).toBeTruthy()
|
||||
expect(res.json().memberNumber.length).toBe(6)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Primary member', () => {
|
||||
it('auto-sets primary member on first member create', async () => {
|
||||
const memberRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Primary', lastName: 'Person' },
|
||||
})
|
||||
const memberId = memberRes.json().id
|
||||
|
||||
const accountRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(accountRes.json().primaryMemberId).toBe(memberId)
|
||||
})
|
||||
|
||||
it('does not overwrite primary on second member', async () => {
|
||||
const first = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'First', lastName: 'Person' },
|
||||
})
|
||||
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Second', lastName: 'Person' },
|
||||
})
|
||||
|
||||
const accountRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/v1/accounts/${accountId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(accountRes.json().primaryMemberId).toBe(first.json().id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /v1/members/:id/move', () => {
|
||||
it('moves member to existing account', async () => {
|
||||
const memberRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Mover', lastName: 'Person' },
|
||||
})
|
||||
const memberId = memberRes.json().id
|
||||
|
||||
const newAccountRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/v1/accounts',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { name: 'Target Account' },
|
||||
})
|
||||
const targetId = newAccountRes.json().id
|
||||
|
||||
const moveRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/members/${memberId}/move`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { accountId: targetId },
|
||||
})
|
||||
|
||||
expect(moveRes.statusCode).toBe(200)
|
||||
expect(moveRes.json().accountId).toBe(targetId)
|
||||
})
|
||||
|
||||
it('moves member to new account when no accountId given', async () => {
|
||||
const memberRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Solo', lastName: 'Flyer' },
|
||||
})
|
||||
const memberId = memberRes.json().id
|
||||
|
||||
const moveRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/members/${memberId}/move`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {},
|
||||
})
|
||||
|
||||
expect(moveRes.statusCode).toBe(200)
|
||||
expect(moveRes.json().accountId).not.toBe(accountId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Account search includes members', () => {
|
||||
it('finds account by member name', async () => {
|
||||
await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'UniqueChildName', lastName: 'Smith' },
|
||||
})
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/v1/accounts?q=UniqueChildName',
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json().data.length).toBe(1)
|
||||
expect(res.json().data[0].id).toBe(accountId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isMinor flag', () => {
|
||||
it('accepts explicit isMinor without DOB', async () => {
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/v1/accounts/${accountId}/members`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: { firstName: 'Kid', lastName: 'NoDate', isMinor: true },
|
||||
})
|
||||
|
||||
expect(res.statusCode).toBe(201)
|
||||
expect(res.json().isMinor).toBe(true)
|
||||
expect(res.json().dateOfBirth).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user