diff --git a/packages/backend/__tests__/routes/v1/accounts-extended.test.ts b/packages/backend/__tests__/routes/v1/accounts-extended.test.ts deleted file mode 100644 index e57e0b9..0000000 --- a/packages/backend/__tests__/routes/v1/accounts-extended.test.ts +++ /dev/null @@ -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') - }) - }) -}) diff --git a/packages/backend/__tests__/routes/v1/accounts.test.ts b/packages/backend/__tests__/routes/v1/accounts.test.ts deleted file mode 100644 index 978a005..0000000 --- a/packages/backend/__tests__/routes/v1/accounts.test.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/packages/backend/__tests__/routes/v1/auth.test.ts b/packages/backend/__tests__/routes/v1/auth.test.ts deleted file mode 100644 index faddfc1..0000000 --- a/packages/backend/__tests__/routes/v1/auth.test.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/packages/backend/__tests__/routes/v1/health.test.ts b/packages/backend/__tests__/routes/v1/health.test.ts deleted file mode 100644 index a74d370..0000000 --- a/packages/backend/__tests__/routes/v1/health.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/packages/backend/__tests__/routes/v1/inventory.test.ts b/packages/backend/__tests__/routes/v1/inventory.test.ts deleted file mode 100644 index 695ce8d..0000000 --- a/packages/backend/__tests__/routes/v1/inventory.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/packages/backend/__tests__/routes/v1/members.test.ts b/packages/backend/__tests__/routes/v1/members.test.ts deleted file mode 100644 index 6124dc1..0000000 --- a/packages/backend/__tests__/routes/v1/members.test.ts +++ /dev/null @@ -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') - }) - }) -}) diff --git a/packages/backend/__tests__/routes/v1/products.test.ts b/packages/backend/__tests__/routes/v1/products.test.ts deleted file mode 100644 index 1372241..0000000 --- a/packages/backend/__tests__/routes/v1/products.test.ts +++ /dev/null @@ -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') - }) -}) \ No newline at end of file diff --git a/packages/backend/__tests__/routes/webdav/webdav.test.ts b/packages/backend/__tests__/routes/webdav/webdav.test.ts deleted file mode 100644 index 361bf5d..0000000 --- a/packages/backend/__tests__/routes/webdav/webdav.test.ts +++ /dev/null @@ -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(' { - // 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(' { - 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() - }) - }) -}) diff --git a/packages/backend/__tests__/services/account.service.test.ts b/packages/backend/__tests__/services/account.service.test.ts deleted file mode 100644 index 4dea6b9..0000000 --- a/packages/backend/__tests__/services/account.service.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/packages/backend/__tests__/services/lookup.service.test.ts b/packages/backend/__tests__/services/lookup.service.test.ts deleted file mode 100644 index d25d593..0000000 --- a/packages/backend/__tests__/services/lookup.service.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/backend/bunfig.toml b/packages/backend/bunfig.toml deleted file mode 100644 index d72e93a..0000000 --- a/packages/backend/bunfig.toml +++ /dev/null @@ -1,3 +0,0 @@ -[test] -preload = ["./src/test/setup.ts"] -timeout = 15000 diff --git a/packages/backend/src/test/helpers.ts b/packages/backend/src/test/helpers.ts deleted file mode 100644 index 57a4f7d..0000000 --- a/packages/backend/src/test/helpers.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { FastifyInstance } from 'fastify' -import { buildApp } from '../main.js' -import { sql, eq } from 'drizzle-orm' -import { companies, locations } from '../db/schema/stores.js' -import { UnitStatusService, ItemConditionService } from '../services/lookup.service.js' -import { RbacService } from '../services/rbac.service.js' -import { roles } from '../db/schema/rbac.js' -import { users } from '../db/schema/users.js' - -export const TEST_COMPANY_ID = '00000000-0000-0000-0000-000000000099' -export const TEST_LOCATION_ID = '00000000-0000-0000-0000-000000000099' - -export async function createTestApp(): Promise { - const app = await buildApp() - await app.ready() - return app -} - -export async function cleanDb(app: FastifyInstance): Promise { - await app.db.execute(sql` - DO $$ DECLARE - r RECORD; - BEGIN - SET client_min_messages TO WARNING; - FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP - EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; - END LOOP; - RESET client_min_messages; - END $$ - `) -} - -export async function seedTestCompany(app: FastifyInstance): Promise { - await app.db.insert(companies).values({ - id: TEST_COMPANY_ID, - name: 'Test Music Co.', - timezone: 'America/Chicago', - }) - await app.db.insert(locations).values({ - id: TEST_LOCATION_ID, - name: 'Test Location', - }) - - await UnitStatusService.seedDefaults(app.db) - await ItemConditionService.seedDefaults(app.db) - - // Seed RBAC permissions and default roles - await RbacService.seedPermissions(app.db) - await RbacService.seedDefaultRoles(app.db) -} - -export async function registerAndLogin( - app: FastifyInstance, - overrides: { - email?: string - password?: string - firstName?: string - lastName?: string - role?: string - } = {}, -): Promise<{ token: string; user: Record }> { - const response = await app.inject({ - method: 'POST', - url: '/v1/auth/register', - headers: { 'x-company-id': TEST_COMPANY_ID }, - payload: { - email: overrides.email ?? 'test@forte.dev', - password: overrides.password ?? 'testpassword1234', - firstName: overrides.firstName ?? 'Test', - lastName: overrides.lastName ?? 'User', - role: overrides.role ?? 'admin', - }, - }) - - const body = response.json() - - // Assign the admin role to the test user so they have all permissions - if (body.user?.id) { - const [adminRole] = await app.db - .select() - .from(roles) - .where(eq(roles.slug, 'admin')) - .limit(1) - - if (adminRole) { - await RbacService.assignRole(app.db, body.user.id, adminRole.id) - } - - // Re-login to get a fresh token (permissions are loaded on authenticate) - const loginRes = await app.inject({ - method: 'POST', - url: '/v1/auth/login', - payload: { - email: overrides.email ?? 'test@forte.dev', - password: overrides.password ?? 'testpassword1234', - }, - }) - const loginBody = loginRes.json() - return { token: loginBody.token, user: loginBody.user } - } - - return { token: body.token, user: body.user } -} diff --git a/packages/backend/src/test/setup.ts b/packages/backend/src/test/setup.ts deleted file mode 100644 index e7f69b2..0000000 --- a/packages/backend/src/test/setup.ts +++ /dev/null @@ -1,27 +0,0 @@ -import postgres from 'postgres' - -const TEST_DB_URL = - process.env.DATABASE_URL?.replace(/\/forte$/, '/forte_test') ?? - 'postgresql://forte:forte@localhost:5432/forte_test' - -// Override DATABASE_URL for all tests to use forte_test -process.env.DATABASE_URL = TEST_DB_URL -process.env.NODE_ENV = 'test' -process.env.LOG_LEVEL = 'silent' -process.env.JWT_SECRET = 'test-secret-for-jwt-signing' - -/** - * Ensure the forte_test database exists before tests run. - */ -async function ensureTestDb() { - const adminUrl = TEST_DB_URL.replace(/\/forte_test$/, '/postgres') - const sql = postgres(adminUrl) - - const result = await sql`SELECT 1 FROM pg_database WHERE datname = 'forte_test'` - if (result.length === 0) { - await sql`CREATE DATABASE forte_test` - } - await sql.end() -} - -await ensureTestDb()