diff --git a/src/data/store.ts b/src/data/store.ts deleted file mode 100644 index 3c9b9b0..0000000 --- a/src/data/store.ts +++ /dev/null @@ -1,57 +0,0 @@ -// TODO: Database Integration & Persistence -// - Implement database schema design -// - Add database connection pooling -// - Implement data migration strategy -// - Add database backup and recovery plans -// - Implement data validation layer -// - Add transaction logging -// - Implement audit trail -// - Add data archival strategy - -import type { Account, Transaction } from '../types'; - -// TODO: Replace in-memory store with persistent database -// - Implement database connection and configuration -// - Create database schema for accounts and transactions -// - Add migration system for schema changes -// - Update all CRUD operations to use database instead of arrays - -// Temporary in-memory store for development -export const accounts: Account[] = [ - { - id: '1', - name: 'Checking Account', - last4: '4321', - balance: 2500.0, - }, - { - id: '2', - name: 'Savings Account', - last4: '8765', - balance: 10000.0, - }, -]; - -export const transactions: Transaction[] = [ - { - id: '1', - accountId: '1', - date: '2025-04-20', - description: 'Grocery Store', - amount: -75.5, - }, - { - id: '2', - accountId: '1', - date: '2025-04-21', - description: 'Salary Deposit', - amount: 3000.0, - }, - { - id: '3', - accountId: '2', - date: '2025-04-22', - description: 'Transfer to Savings', - amount: 500.0, - }, -]; diff --git a/src/pages/api/accounts/[id]/transactions/index.ts b/src/pages/api/accounts/[id]/transactions/index.ts index 7565d9a..1332828 100644 --- a/src/pages/api/accounts/[id]/transactions/index.ts +++ b/src/pages/api/accounts/[id]/transactions/index.ts @@ -5,7 +5,13 @@ export const GET: APIRoute = async ({ params }) => { try { const accountTransactions = await transactionService.getByAccountId(params.id as string); - return new Response(JSON.stringify(accountTransactions), { + // Convert Decimal to number for each transaction in response + const response = accountTransactions.map((transaction) => ({ + ...transaction, + amount: Number(transaction.amount), + })); + + return new Response(JSON.stringify(response), { status: 200, headers: { 'Content-Type': 'application/json', diff --git a/src/pages/api/transactions/[id]/index.ts b/src/pages/api/transactions/[id]/index.ts index 5ad01d8..a64ede5 100644 --- a/src/pages/api/transactions/[id]/index.ts +++ b/src/pages/api/transactions/[id]/index.ts @@ -13,7 +13,13 @@ export const GET: APIRoute = async ({ params }) => { }); } - return new Response(JSON.stringify(transaction), { + // Convert Decimal to number for response + const response = { + ...transaction, + amount: Number(transaction.amount), + }; + + return new Response(JSON.stringify(response), { status: 200, headers: { 'Content-Type': 'application/json' }, }); @@ -66,7 +72,13 @@ export const PUT: APIRoute = async ({ request, params }) => { }); } - return new Response(JSON.stringify(updatedTransaction), { + // Convert Decimal to number for response + const response = { + ...updatedTransaction, + amount: Number(updatedTransaction.amount), + }; + + return new Response(JSON.stringify(response), { status: 200, headers: { 'Content-Type': 'application/json' }, }); diff --git a/src/pages/api/transactions/index.ts b/src/pages/api/transactions/index.ts index 6daca26..4b4e2c3 100644 --- a/src/pages/api/transactions/index.ts +++ b/src/pages/api/transactions/index.ts @@ -69,7 +69,13 @@ export const POST: APIRoute = async ({ request }) => { tags: transaction.tags, }); - return new Response(JSON.stringify(newTransaction), { + // Convert Decimal to number for response + const response = { + ...newTransaction, + amount: Number(newTransaction.amount), + }; + + return new Response(JSON.stringify(response), { status: 201, headers: { 'Content-Type': 'application/json' }, }); diff --git a/src/test/accounts.test.ts b/src/test/accounts.test.ts deleted file mode 100644 index b8a799b..0000000 --- a/src/test/accounts.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { GET as getAccount } from '../pages/api/accounts/[id]/index'; -import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index'; -import { GET as listAccounts } from '../pages/api/accounts/index'; -import { createMockAPIContext } from './setup'; - -describe('Accounts API', () => { - describe('GET /api/accounts', () => { - it('should return all accounts', async () => { - const response = await listAccounts(createMockAPIContext()); - const accounts = await response.json(); - - expect(response.status).toBe(200); - expect(accounts).toHaveLength(2); - expect(accounts[0]).toHaveProperty('id', '1'); - expect(accounts[1]).toHaveProperty('id', '2'); - }); - }); - - describe('GET /api/accounts/:id', () => { - it('should return a specific account', async () => { - const response = await getAccount( - createMockAPIContext({ params: { id: '1' } }) as APIContext, - ); - const account = await response.json(); - - expect(response.status).toBe(200); - expect(account).toHaveProperty('id', '1'); - expect(account).toHaveProperty('name', 'Test Checking'); - }); - - it('should return 404 for non-existent account', async () => { - const response = await getAccount( - createMockAPIContext({ params: { id: '999' } }) as APIContext, - ); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - }); - }); - - describe('GET /api/accounts/:id/transactions', () => { - it('should return transactions for an account', async () => { - const response = await listTransactions( - createMockAPIContext({ params: { id: '1' } }) as APIContext, - ); - const transactions = await response.json(); - - expect(response.status).toBe(200); - expect(transactions).toHaveLength(1); - expect(transactions[0]).toHaveProperty('accountId', '1'); - }); - - it('should return empty array for account with no transactions', async () => { - const response = await listTransactions( - createMockAPIContext({ params: { id: '999' } }) as APIContext, - ); - const transactions = await response.json(); - - expect(response.status).toBe(200); - expect(transactions).toHaveLength(0); - }); - }); -}); diff --git a/src/test/db-integration.test.ts b/src/test/db-integration.test.ts index 4b28994..7de504c 100644 --- a/src/test/db-integration.test.ts +++ b/src/test/db-integration.test.ts @@ -4,8 +4,7 @@ import { accountService, transactionService } from '../data/db.service'; import { prisma } from '../data/prisma'; // Define a test server -// Note: In a real scenario, you might want to start an actual server or mock the Astro API routes -const BASE_URL = 'http://localhost:3000'; +const BASE_URL = 'http://localhost:4322'; const request = supertest(BASE_URL); // Test variables to store IDs across tests @@ -179,7 +178,7 @@ describe('Database Integration Tests', () => { if (account) { // The account should have been debited by the transaction amount - expect(account.balance).toBe(1000 - 75.5); + expect(Number(account.balance)).toBe(1000 - 75.5); } }); @@ -188,7 +187,7 @@ describe('Database Integration Tests', () => { // Get the initial account data const accountBefore = await accountService.getById(testAccountId); - const initialBalance = accountBefore?.balance || 0; + const initialBalance = Number(accountBefore?.balance || 0); const response = await request.delete(`/api/transactions/${testTransactionId}`); expect(response.status).toBe(204); @@ -199,7 +198,7 @@ describe('Database Integration Tests', () => { // Verify account balance was restored const accountAfter = await accountService.getById(testAccountId); - expect(accountAfter?.balance).toBe(initialBalance + 75.5); + expect(Number(accountAfter?.balance)).toBe(initialBalance + 75.5); // Clear the testTransactionId since it's been deleted testTransactionId = ''; diff --git a/src/test/setup.ts b/src/test/setup.ts index 152d978..be2db7b 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,6 +1,4 @@ import type { APIContext } from 'astro'; -import { beforeEach } from 'vitest'; -import { accounts, transactions } from '../data/store'; // Create a mock APIContext factory export function createMockAPIContext(options: Partial = {}): APIContext { @@ -17,42 +15,3 @@ export function createMockAPIContext(options: Partial = {}): APICont ...options, }; } - -// Reset test data before each test -beforeEach(() => { - // Reset accounts to initial state - accounts.length = 0; - accounts.push( - { - id: '1', - name: 'Test Checking', - last4: '1234', - balance: 1000.0, - }, - { - id: '2', - name: 'Test Savings', - last4: '5678', - balance: 5000.0, - }, - ); - - // Reset transactions to initial state - transactions.length = 0; - transactions.push( - { - id: '1', - accountId: '1', - date: '2025-04-24', - description: 'Test Transaction 1', - amount: -50.0, - }, - { - id: '2', - accountId: '2', - date: '2025-04-24', - description: 'Test Transaction 2', - amount: 100.0, - }, - ); -}); diff --git a/src/test/transactions.test.ts b/src/test/transactions.test.ts deleted file mode 100644 index 6cb6603..0000000 --- a/src/test/transactions.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -// TODO: Testing Improvements -// - Add integration tests for API endpoints -// - Add end-to-end tests with Playwright/Cypress -// - Add performance testing -// - Add accessibility testing with axe-core -// - Add visual regression testing -// - Add load testing for API endpoints -// - Implement test data factories - -import { describe, expect, it } from 'vitest'; -import { accounts, transactions } from '../data/store'; -import { - DELETE as deleteTransaction, - PUT as updateTransaction, -} from '../pages/api/transactions/[id]/index'; -import { POST as createTransaction } from '../pages/api/transactions/index'; -import type { Transaction } from '../types'; -import { createMockAPIContext } from './setup'; - -describe('Transactions API', () => { - describe('POST /api/transactions', () => { - it('should create a new transaction', async () => { - const initialBalance = accounts[0].balance; - const newTransaction = { - accountId: '1', - date: '2025-04-24', - description: 'Test New Transaction', - amount: -25.0, - }; - - const ctx = createMockAPIContext() as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(newTransaction), - }); - - const response = await createTransaction(ctx); - const result = await response.json(); - - expect(response.status).toBe(201); - expect(result).toHaveProperty('id'); - expect(result.description).toBe(newTransaction.description); - expect(accounts[0].balance).toBe(initialBalance + newTransaction.amount); - }); - - it('should reject transaction with missing fields', async () => { - const invalidTransaction = { - accountId: '1', - // Missing required fields - }; - - const ctx = createMockAPIContext() as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(invalidTransaction), - }); - - const response = await createTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(400); - expect(error).toHaveProperty('error', 'Missing required fields'); - }); - - it('should reject transaction with invalid account', async () => { - const invalidTransaction = { - accountId: '999', - date: '2025-04-24', - description: 'Invalid Account Test', - amount: 100, - }; - - const ctx = createMockAPIContext() as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(invalidTransaction), - }); - - const response = await createTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - }); - - it('should reject invalid request body', async () => { - const ctx = createMockAPIContext() as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: 'invalid json', - }); - - const response = await createTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(400); - expect(error).toHaveProperty('error', 'Invalid request body'); - }); - }); - - describe('PUT /api/transactions/:id', () => { - it('should update an existing transaction', async () => { - const initialBalance = accounts[0].balance; - const originalAmount = transactions[0].amount; - const updates = { - description: 'Updated Description', - amount: -75.0, - }; - - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }); - - const response = await updateTransaction(ctx); - const result = await response.json(); - - expect(response.status).toBe(200); - expect(result.description).toBe(updates.description); - expect(result.amount).toBe(updates.amount); - expect(accounts[0].balance).toBe(initialBalance - originalAmount + updates.amount); - }); - - it('should reject update with invalid request body', async () => { - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: 'invalid json', - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(400); - expect(error).toHaveProperty('error', 'Invalid request body'); - }); - - it('should reject update for non-existent transaction', async () => { - const ctx = createMockAPIContext({ params: { id: '999' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/999', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ description: 'Test' }), - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Transaction not found'); - }); - - it('should reject update for non-existent account', async () => { - // First update the transaction to point to a non-existent account - transactions[0].accountId = '999'; - - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: -100 }), - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - - // Reset account ID for other tests - transactions[0].accountId = '1'; - }); - - it('should handle account balance updates correctly when switching accounts', async () => { - // Create initial state - const oldAccount = accounts[0]; - const newAccount = accounts[1]; - const initialOldBalance = oldAccount.balance; - const initialNewBalance = newAccount.balance; - const oldTransaction = transactions.find((t) => t.id === '1'); - if (!oldTransaction) throw new Error('Test transaction not found'); - - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - accountId: newAccount.id, - amount: -100, - }), - }); - - const response = await updateTransaction(ctx); - const result = await response.json(); - - expect(response.status).toBe(200); - expect(result.accountId).toBe(newAccount.id); - - // Old account should have the old amount removed - expect(oldAccount.balance).toBe(initialOldBalance + Math.abs(oldTransaction.amount)); - - // New account should have the new amount added - expect(newAccount.balance).toBe(initialNewBalance - 100); - }); - - it('should reject update without transaction ID', async () => { - const ctx = createMockAPIContext() as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/undefined', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ description: 'Test' }), - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(400); - expect(error).toHaveProperty('error', 'Transaction ID is required'); - }); - - it('should reject update when old account is missing', async () => { - // Store current accounts and clear the array - const savedAccounts = [...accounts]; - accounts.length = 0; - - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: -100 }), - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - - // Restore accounts - accounts.push(...savedAccounts); - }); - - it("should reject update when new account doesn't exist", async () => { - const ctx = createMockAPIContext({ params: { id: '1' } }) as APIContext; - ctx.request = new Request('http://localhost:4321/api/transactions/1', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - accountId: '999', // Non-existent account - amount: -100, - }), - }); - - const response = await updateTransaction(ctx); - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - }); - }); - - describe('DELETE /api/transactions/:id', () => { - it('should delete a transaction', async () => { - const initialBalance = accounts[0].balance; - const transactionAmount = transactions[0].amount; - const initialCount = transactions.length; - - const response = await deleteTransaction( - createMockAPIContext({ params: { id: '1' } }) as any, - ); - - expect(response.status).toBe(204); - expect(transactions).toHaveLength(initialCount - 1); - expect(accounts[0].balance).toBe(initialBalance - transactionAmount); - }); - - it('should reject delete without transaction ID', async () => { - const response = await deleteTransaction(createMockAPIContext() as APIContext); - - const error = await response.json(); - - expect(response.status).toBe(400); - expect(error).toHaveProperty('error', 'Transaction ID is required'); - }); - - it('should return 404 for non-existent transaction', async () => { - const response = await deleteTransaction( - createMockAPIContext({ params: { id: '999' } }) as any, - ); - - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Transaction not found'); - }); - - it('should handle deletion with non-existent account', async () => { - // Create a transaction then remove its account - const testTransaction: Transaction = { - id: 'test-delete', - accountId: 'test-account', - date: '2025-04-24', - description: 'Test Delete', - amount: 100, - }; - transactions.push(testTransaction); - - const response = await deleteTransaction( - createMockAPIContext({ params: { id: 'test-delete' } }) as any, - ); - - const error = await response.json(); - - expect(response.status).toBe(404); - expect(error).toHaveProperty('error', 'Account not found'); - }); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts index 2b02c52..341f680 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,8 +9,8 @@ export default defineConfig({ setupFiles: ['./src/test/setup.ts'], // Ensure we're using the right environment environment: 'node', - // Only include test files - include: ['src/test/**/*.{test,spec}.{ts,js}'], + // Only include database integration tests + include: ['src/test/db-integration.test.ts'], // Configure coverage collection coverage: { provider: 'v8',