Remove DB-dependent unit tests, use api-tests for integration testing

All tests in __tests__/ were hitting the database directly. Integration
testing is handled by the api-tests/ suite instead.
This commit is contained in:
Ryan Moon
2026-03-30 08:52:01 -05:00
parent 9400828f62
commit 701e15ea6d
13 changed files with 0 additions and 2607 deletions

View File

@@ -1,491 +0,0 @@
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')
})
})
})

View File

@@ -1,326 +0,0 @@
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.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)
})
})
})

View File

@@ -1,166 +0,0 @@
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'
describe('Auth routes', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
})
afterAll(async () => {
await app.close()
})
describe('POST /v1/auth/register', () => {
it('creates a user and returns token', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'staff@musicstore.com',
password: 'securepassword',
firstName: 'Jane',
lastName: 'Doe',
role: 'staff',
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.user.email).toBe('staff@musicstore.com')
expect(body.user.firstName).toBe('Jane')
expect(body.user.role).toBe('staff')
expect(body.token).toBeDefined()
expect(body.user.passwordHash).toBeUndefined()
})
it('rejects duplicate email within same company', async () => {
await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'dupe@test.com',
password: 'password123',
firstName: 'First',
lastName: 'User',
},
})
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'dupe@test.com',
password: 'password456',
firstName: 'Second',
lastName: 'User',
},
})
expect(response.statusCode).toBe(409)
})
it('rejects invalid email', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'not-an-email',
password: 'password123',
firstName: 'Bad',
lastName: 'Email',
},
})
expect(response.statusCode).toBe(400)
})
it('rejects short password', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'short@test.com',
password: '123',
firstName: 'Short',
lastName: 'Pass',
},
})
expect(response.statusCode).toBe(400)
})
})
describe('POST /v1/auth/login', () => {
beforeEach(async () => {
await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'correctpassword',
firstName: 'Login',
lastName: 'User',
},
})
})
it('returns token with valid credentials', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'correctpassword',
},
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.token).toBeDefined()
expect(body.user.email).toBe('login@test.com')
})
it('rejects wrong password', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'wrongpassword',
},
})
expect(response.statusCode).toBe(401)
})
it('rejects nonexistent email', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'nobody@test.com',
password: 'whatever',
},
})
expect(response.statusCode).toBe(401)
})
})
})

View File

@@ -1,30 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp } from '../../../src/test/helpers.js'
describe('GET /v1/health', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
afterAll(async () => {
await app.close()
})
it('returns ok when db and redis are connected', async () => {
const response = await app.inject({
method: 'GET',
url: '/v1/health',
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.status).toBe('ok')
expect(body.db).toBe('connected')
expect(body.redis).toBe('connected')
expect(body.timestamp).toBeDefined()
})
})

View File

@@ -1,193 +0,0 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
} from '../../../src/test/helpers.js'
describe('Category 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()
})
it('creates a category', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Guitars', description: 'All guitars', sortOrder: 1 },
})
expect(response.statusCode).toBe(201)
expect(response.json().name).toBe('Guitars')
expect(response.json().sortOrder).toBe(1)
})
it('creates a child category', async () => {
const parentRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Instruments' },
})
const parentId = parentRes.json().id
const childRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Brass', parentId },
})
expect(childRes.statusCode).toBe(201)
expect(childRes.json().parentId).toBe(parentId)
})
it('lists categories sorted by sortOrder', async () => {
await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Zzz Last', sortOrder: 99 },
})
await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Aaa First', sortOrder: 1 },
})
const response = await app.inject({
method: 'GET',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.data.length).toBe(2)
expect(body.data[0].name).toBe('Aaa First')
})
it('soft-deletes a category', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'To Delete' },
})
const delRes = await app.inject({
method: 'DELETE',
url: `/v1/categories/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(delRes.json().isActive).toBe(false)
const listRes = await app.inject({
method: 'GET',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
})
expect(listRes.json().data.length).toBe(0)
})
})
describe('Supplier 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: `supplier-${Date.now()}@test.com` })
token = auth.token
})
afterAll(async () => {
await app.close()
})
it('creates a supplier', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'RS Musical',
contactName: 'Bob Smith',
email: 'bob@rsmusical.com',
paymentTerms: 'Net 30',
},
})
expect(response.statusCode).toBe(201)
expect(response.json().name).toBe('RS Musical')
expect(response.json().paymentTerms).toBe('Net 30')
})
it('searches suppliers by name', async () => {
await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: "Ferree's Tools" },
})
await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Allied Supply' },
})
const response = await app.inject({
method: 'GET',
url: '/v1/suppliers?q=ferree',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(1)
expect(response.json().data[0].name).toBe("Ferree's Tools")
})
it('updates a supplier', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Old Name' },
})
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/suppliers/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
payload: { name: 'New Name', paymentTerms: 'COD' },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().name).toBe('New Name')
expect(updateRes.json().paymentTerms).toBe('COD')
})
})

View File

@@ -1,360 +0,0 @@
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()
})
})
describe('Account inheritance', () => {
it('inherits email, phone from account when not provided', async () => {
const acctRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Email Family', email: 'family@test.com', phone: '555-1234' },
})
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${acctRes.json().id}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Child', lastName: 'One' },
})
expect(res.statusCode).toBe(201)
expect(res.json().email).toBe('family@test.com')
expect(res.json().phone).toBe('555-1234')
})
it('uses member email when provided instead of account', async () => {
const acctRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Override Family', email: 'family@test.com' },
})
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${acctRes.json().id}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Adult', lastName: 'Own', email: 'own@test.com' },
})
expect(res.statusCode).toBe(201)
expect(res.json().email).toBe('own@test.com')
})
it('inherits address from account when not provided', async () => {
const acctRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Address Family',
address: { street: '123 Main St', city: 'Austin', state: 'TX', zip: '78701' },
},
})
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${acctRes.json().id}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Kid', lastName: 'Home' },
})
expect(res.statusCode).toBe(201)
expect(res.json().address.city).toBe('Austin')
expect(res.json().address.state).toBe('TX')
})
})
describe('State normalization', () => {
it('normalizes state name to code on account', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'State Test', address: { state: 'texas' } },
})
expect(res.statusCode).toBe(201)
expect(res.json().address.state).toBe('TX')
})
it('normalizes full state name to code', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'State Name Test', address: { state: 'California' } },
})
expect(res.statusCode).toBe(201)
expect(res.json().address.state).toBe('CA')
})
it('leaves unrecognized state codes as-is', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Foreign Test', address: { state: 'ON' } },
})
expect(res.statusCode).toBe(201)
expect(res.json().address.state).toBe('ON')
})
})
})

View File

@@ -1,222 +0,0 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
} from '../../../src/test/helpers.js'
describe('Product 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()
})
it('creates a non-serialized product', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Guitar Strings - D\'Addario EJ16',
sku: 'STR-DAD-EJ16',
upc: '019954121266',
brand: "D'Addario",
price: 6.99,
qtyOnHand: 24,
qtyReorderPoint: 10,
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.name).toBe('Guitar Strings - D\'Addario EJ16')
expect(body.isSerialized).toBe(false)
expect(body.qtyOnHand).toBe(24)
})
it('creates a serialized product', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Yamaha YTR-2330 Trumpet',
sku: 'BRASS-YAM-2330',
brand: 'Yamaha',
model: 'YTR-2330',
isSerialized: true,
price: 1299.99,
minPrice: 1100.00,
},
})
expect(response.statusCode).toBe(201)
expect(response.json().isSerialized).toBe(true)
})
it('searches by name, sku, and brand', async () => {
await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Fender Stratocaster', brand: 'Fender', sku: 'GTR-FND-STRAT' },
})
await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Gibson Les Paul', brand: 'Gibson', sku: 'GTR-GIB-LP' },
})
const byName = await app.inject({
method: 'GET',
url: '/v1/products?q=stratocaster',
headers: { authorization: `Bearer ${token}` },
})
expect(byName.json().data.length).toBe(1)
const bySku = await app.inject({
method: 'GET',
url: '/v1/products?q=GTR-GIB',
headers: { authorization: `Bearer ${token}` },
})
expect(bySku.json().data.length).toBe(1)
const byBrand = await app.inject({
method: 'GET',
url: '/v1/products?q=fender',
headers: { authorization: `Bearer ${token}` },
})
expect(byBrand.json().data.length).toBe(1)
})
it('logs price change on update', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Test Item', price: 10.00 },
})
const id = createRes.json().id
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/products/${id}`,
headers: { authorization: `Bearer ${token}` },
payload: { price: 12.50 },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().price).toBe('12.50')
})
})
describe('Inventory unit routes', () => {
let app: FastifyInstance
let token: string
let productId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `unit-${Date.now()}@test.com` })
token = auth.token
const productRes = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Yamaha Trumpet', isSerialized: true, price: 1200 },
})
productId = productRes.json().id
})
afterAll(async () => {
await app.close()
})
it('creates an inventory unit with serial number', async () => {
const response = await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: {
serialNumber: 'YAM-2024-001234',
condition: 'new',
purchaseDate: '2024-09-15',
purchaseCost: 650.00,
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.serialNumber).toBe('YAM-2024-001234')
expect(body.condition).toBe('new')
expect(body.status).toBe('available')
expect(body.purchaseCost).toBe('650.00')
})
it('lists units for a product', async () => {
await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-001' },
})
await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-002' },
})
const response = await app.inject({
method: 'GET',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(2)
})
it('updates unit status and condition', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-UPDATE' },
})
const unitId = createRes.json().id
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/units/${unitId}`,
headers: { authorization: `Bearer ${token}` },
payload: { status: 'rented', condition: 'good' },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().status).toBe('rented')
expect(updateRes.json().condition).toBe('good')
})
})

View File

@@ -1,294 +0,0 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp, cleanDb, seedTestCompany, registerAndLogin } from '../../../src/test/helpers.js'
describe('WebDAV', () => {
let app: FastifyInstance
let basicAuth: string
beforeAll(async () => {
app = await createTestApp()
})
afterAll(async () => {
await app.close()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const { user } = await registerAndLogin(app, {
email: 'webdav@forte.dev',
password: 'webdavpass1234',
})
basicAuth = 'Basic ' + Buffer.from('webdav@forte.dev:webdavpass1234').toString('base64')
})
describe('OPTIONS', () => {
it('returns DAV headers on root', async () => {
const res = await app.inject({ method: 'OPTIONS', url: '/webdav/' })
expect(res.statusCode).toBe(200)
expect(res.headers['dav']).toContain('1')
expect(res.headers['allow']).toContain('PROPFIND')
expect(res.headers['allow']).toContain('GET')
expect(res.headers['allow']).toContain('PUT')
})
it('returns DAV headers on wildcard path', async () => {
const res = await app.inject({ method: 'OPTIONS', url: '/webdav/some/path' })
expect(res.statusCode).toBe(200)
expect(res.headers['dav']).toContain('1')
})
})
describe('Authentication', () => {
it('returns 401 without credentials', async () => {
const res = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
})
expect(res.statusCode).toBe(401)
expect(res.headers['www-authenticate']).toContain('Basic')
})
it('returns 401 with wrong password', async () => {
const res = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
headers: { authorization: 'Basic ' + Buffer.from('webdav@forte.dev:wrongpass').toString('base64') },
})
expect(res.statusCode).toBe(401)
})
it('succeeds with correct credentials', async () => {
const res = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
headers: { authorization: basicAuth, depth: '0' },
})
expect(res.statusCode).toBe(207)
})
})
describe('PROPFIND', () => {
it('lists root with depth 0', async () => {
const res = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
headers: { authorization: basicAuth, depth: '0' },
})
expect(res.statusCode).toBe(207)
expect(res.headers['content-type']).toContain('application/xml')
expect(res.body).toContain('<D:multistatus')
expect(res.body).toContain('<D:collection/')
})
it('lists root folders with depth 1', async () => {
// Create a folder first via MKCOL
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Test%20Folder',
headers: { authorization: basicAuth },
})
const res = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
headers: { authorization: basicAuth, depth: '1' },
})
expect(res.statusCode).toBe(207)
expect(res.body).toContain('Test Folder')
})
})
describe('MKCOL', () => {
it('creates a folder', async () => {
const res = await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Documents',
headers: { authorization: basicAuth },
})
expect(res.statusCode).toBe(201)
// Verify it appears in PROPFIND
const listing = await app.inject({
method: 'PROPFIND' as any,
url: '/webdav/',
headers: { authorization: basicAuth, depth: '1' },
})
expect(listing.body).toContain('Documents')
})
it('returns 405 if folder already exists', async () => {
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Documents',
headers: { authorization: basicAuth },
})
const res = await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Documents',
headers: { authorization: basicAuth },
})
expect(res.statusCode).toBe(405)
})
})
describe('PUT / GET / DELETE', () => {
it('uploads, downloads, and deletes a file', async () => {
// Create parent folder
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Uploads',
headers: { authorization: basicAuth },
})
// PUT a file
const fileContent = 'Hello, WebDAV!'
const putRes = await app.inject({
method: 'PUT',
url: '/webdav/Uploads/test.txt',
headers: {
authorization: basicAuth,
'content-type': 'text/plain',
},
body: fileContent,
})
expect(putRes.statusCode).toBe(201)
expect(putRes.headers['etag']).toBeDefined()
// GET the file
const getRes = await app.inject({
method: 'GET',
url: '/webdav/Uploads/test.txt',
headers: { authorization: basicAuth },
})
expect(getRes.statusCode).toBe(200)
expect(getRes.body).toBe(fileContent)
expect(getRes.headers['content-type']).toContain('text/plain')
// DELETE the file
const delRes = await app.inject({
method: 'DELETE',
url: '/webdav/Uploads/test.txt',
headers: { authorization: basicAuth },
})
expect(delRes.statusCode).toBe(204)
// Verify it's gone
const getRes2 = await app.inject({
method: 'GET',
url: '/webdav/Uploads/test.txt',
headers: { authorization: basicAuth },
})
expect(getRes2.statusCode).toBe(404)
})
it('overwrites an existing file with PUT', async () => {
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/Overwrite',
headers: { authorization: basicAuth },
})
// Upload original
await app.inject({
method: 'PUT',
url: '/webdav/Overwrite/doc.txt',
headers: { authorization: basicAuth, 'content-type': 'text/plain' },
body: 'version 1',
})
// Overwrite
const putRes = await app.inject({
method: 'PUT',
url: '/webdav/Overwrite/doc.txt',
headers: { authorization: basicAuth, 'content-type': 'text/plain' },
body: 'version 2',
})
expect(putRes.statusCode).toBe(204)
// Verify new content
const getRes = await app.inject({
method: 'GET',
url: '/webdav/Overwrite/doc.txt',
headers: { authorization: basicAuth },
})
expect(getRes.body).toBe('version 2')
})
})
describe('DELETE folder', () => {
it('deletes a folder', async () => {
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/ToDelete',
headers: { authorization: basicAuth },
})
const res = await app.inject({
method: 'DELETE',
url: '/webdav/ToDelete',
headers: { authorization: basicAuth },
})
expect(res.statusCode).toBe(204)
})
})
describe('LOCK / UNLOCK', () => {
it('returns a lock token', async () => {
const res = await app.inject({
method: 'LOCK' as any,
url: '/webdav/some-resource',
headers: { authorization: basicAuth },
})
expect(res.statusCode).toBe(200)
expect(res.headers['lock-token']).toContain('opaquelocktoken:')
expect(res.body).toContain('<D:lockdiscovery')
})
it('unlocks a resource', async () => {
const lockRes = await app.inject({
method: 'LOCK' as any,
url: '/webdav/some-resource',
headers: { authorization: basicAuth },
})
const lockToken = lockRes.headers['lock-token'] as string
const unlockRes = await app.inject({
method: 'UNLOCK' as any,
url: '/webdav/some-resource',
headers: {
authorization: basicAuth,
'lock-token': lockToken,
},
})
expect(unlockRes.statusCode).toBe(204)
})
})
describe('HEAD', () => {
it('returns headers for a file', async () => {
await app.inject({
method: 'MKCOL' as any,
url: '/webdav/HeadTest',
headers: { authorization: basicAuth },
})
await app.inject({
method: 'PUT',
url: '/webdav/HeadTest/file.txt',
headers: { authorization: basicAuth, 'content-type': 'text/plain' },
body: 'test content',
})
const res = await app.inject({
method: 'HEAD',
url: '/webdav/HeadTest/file.txt',
headers: { authorization: basicAuth },
})
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/plain')
expect(res.headers['etag']).toBeDefined()
})
})
})

View File

@@ -1,240 +0,0 @@
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()
})
})

View File

@@ -1,152 +0,0 @@
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)
})
})