Add integration tests for members list, move, auto-numbers, primary member, isMinor flag

This commit is contained in:
Ryan Moon
2026-03-28 09:18:32 -05:00
parent 8ea3b8dffb
commit 727275af59

View 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()
})
})
})