diff --git a/.gitignore b/.gitignore index 0966b99..8abffbc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ pnpm-debug.log* .vscode/ *.swp *.swo + +# Test coverage +coverage/ diff --git a/src/pages/api/transactions/[id]/index.ts b/src/pages/api/transactions/[id]/index.ts index dc9665b..9d50928 100644 --- a/src/pages/api/transactions/[id]/index.ts +++ b/src/pages/api/transactions/[id]/index.ts @@ -27,27 +27,46 @@ export const PUT: APIRoute = async ({ request, params }) => { } const oldTransaction = transactions[transactionIndex]; - const account = accounts.find((a) => a.id === oldTransaction.accountId); - if (!account) { + // Get the old account first + const oldAccount = accounts.find((a) => a.id === oldTransaction.accountId); + if (!oldAccount) { return new Response(JSON.stringify({ error: "Account not found" }), { status: 404, headers: { "Content-Type": "application/json" }, }); } - // Revert old transaction amount from account balance - account.balance -= oldTransaction.amount; + // If account is changing, validate new account exists + let newAccount = oldAccount; + if (updates.accountId && updates.accountId !== oldTransaction.accountId) { + newAccount = accounts.find((a) => a.id === updates.accountId); + if (!newAccount) { + return new Response(JSON.stringify({ error: "Account not found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + } - // Update transaction with new data + // First, remove the old transaction's effect on the old account + oldAccount.balance -= oldTransaction.amount; + + // Create updated transaction const updatedTransaction: Transaction = { ...oldTransaction, ...updates, id: id, // Ensure ID doesn't change }; - // Add new amount to account balance - account.balance += updatedTransaction.amount; + // Then add the new transaction's effect to the appropriate account + if (newAccount === oldAccount) { + // If same account, just add the new amount + oldAccount.balance += updatedTransaction.amount; + } else { + // If different account, add to the new account + newAccount.balance += updatedTransaction.amount; + } // Update transaction in array transactions[transactionIndex] = updatedTransaction; diff --git a/src/test/transactions.test.ts b/src/test/transactions.test.ts index e427683..1bbf9bc 100644 --- a/src/test/transactions.test.ts +++ b/src/test/transactions.test.ts @@ -5,6 +5,7 @@ import { DELETE as deleteTransaction, } from "../pages/api/transactions/[id]/index"; import { accounts, transactions } from "../data/store"; +import type { Transaction } from "../types"; describe("Transactions API", () => { describe("POST /api/transactions", () => { @@ -52,6 +53,43 @@ describe("Transactions API", () => { expect(response.status).toBe(400); expect(error).toHaveProperty("error", "Missing required fields"); }); + + it("should reject transaction with invalid account", async () => { + const invalidTransaction = { + accountId: "999", // Non-existent account + date: "2025-04-24", + description: "Invalid Account Test", + amount: 100, + }; + + const response = await createTransaction({ + request: new Request("http://localhost:4321/api/transactions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(invalidTransaction), + }), + }); + + 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 response = await createTransaction({ + request: new Request("http://localhost:4321/api/transactions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "invalid json", + }), + }); + + const error = await response.json(); + + expect(response.status).toBe(400); + expect(error).toHaveProperty("error", "Invalid request body"); + }); }); describe("PUT /api/transactions/:id", () => { @@ -82,6 +120,22 @@ describe("Transactions API", () => { ); }); + it("should reject update with invalid request body", async () => { + const response = await updateTransaction({ + params: { id: "1" }, + request: new Request("http://localhost:4321/api/transactions/1", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: "invalid json", + }), + }); + + 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 response = await updateTransaction({ params: { id: "999" }, @@ -97,6 +151,122 @@ describe("Transactions API", () => { 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 response = await updateTransaction({ + params: { id: "1" }, + request: new Request("http://localhost:4321/api/transactions/1", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amount: -100 }), + }), + }); + + 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"); + + // Update transaction to move it to a different account + const response = await updateTransaction({ + params: { id: "1" }, + 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 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 response = await updateTransaction({ + params: {}, + request: new Request("http://localhost:4321/api/transactions/undefined", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ description: "Test" }), + }), + }); + + 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 response = await updateTransaction({ + params: { id: "1" }, + request: new Request("http://localhost:4321/api/transactions/1", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amount: -100 }), + }), + }); + + 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 response = await updateTransaction({ + params: { id: "1" }, + 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 error = await response.json(); + + expect(response.status).toBe(404); + expect(error).toHaveProperty("error", "Account not found"); + }); }); describe("DELETE /api/transactions/:id", () => { @@ -114,6 +284,17 @@ describe("Transactions API", () => { expect(accounts[0].balance).toBe(initialBalance - transactionAmount); }); + it("should reject delete without transaction ID", async () => { + const response = await deleteTransaction({ + params: {}, + }); + + 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({ params: { id: "999" }, @@ -124,5 +305,26 @@ describe("Transactions API", () => { 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({ + params: { id: "test-delete" }, + }); + + const error = await response.json(); + + expect(response.status).toBe(404); + expect(error).toHaveProperty("error", "Account not found"); + }); }); });