feat(tests): add unit tests for accounts and transactions APIs

- Updated package.json to include Vitest for testing and added necessary devDependencies.
- Created accounts.test.ts to test the accounts API endpoints for listing and retrieving accounts.
- Implemented setup.ts to reset test data before each test run.
- Developed transactions.test.ts to cover creating, updating, and deleting transactions through the API.
- Added vitest.config.ts for configuring Vitest with appropriate settings and coverage options.
This commit is contained in:
GitHub Copilot
2025-04-24 08:52:48 -04:00
parent bb6bd75434
commit 99b70b519b
6 changed files with 2726 additions and 2 deletions

2470
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,17 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"astro": "^5.7.5"
},
"devDependencies": {
"@types/supertest": "^2.0.12",
"@vitest/coverage-v8": "^0.34.6",
"supertest": "^6.3.3",
"vitest": "^0.34.3"
}
}

56
src/test/accounts.test.ts Normal file
View File

@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { GET as listAccounts } from "../pages/api/accounts/index";
import { GET as getAccount } from "../pages/api/accounts/[id]/index";
import { GET as listTransactions } from "../pages/api/accounts/[id]/transactions/index";
describe("Accounts API", () => {
describe("GET /api/accounts", () => {
it("should return all accounts", async () => {
const response = await listAccounts();
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({ params: { id: "1" } });
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({ params: { id: "999" } });
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 a specific account", async () => {
const response = await listTransactions({ params: { id: "1" } });
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({ params: { id: "999" } });
const transactions = await response.json();
expect(response.status).toBe(200);
expect(transactions).toHaveLength(0);
});
});
});

41
src/test/setup.ts Normal file
View File

@@ -0,0 +1,41 @@
import { beforeEach } from "vitest";
import { accounts, transactions } from "../data/store";
// 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,
}
);
});

View File

@@ -0,0 +1,128 @@
import { describe, it, expect } from "vitest";
import { POST as createTransaction } from "../pages/api/transactions/index";
import {
PUT as updateTransaction,
DELETE as deleteTransaction,
} from "../pages/api/transactions/[id]/index";
import { accounts, transactions } from "../data/store";
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 response = await createTransaction({
request: new Request("http://localhost:4321/api/transactions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTransaction),
}),
});
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 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(400);
expect(error).toHaveProperty("error", "Missing required fields");
});
});
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 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(updates),
}),
});
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 for non-existent transaction", async () => {
const response = await updateTransaction({
params: { id: "999" },
request: new Request("http://localhost:4321/api/transactions/999", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ description: "Test" }),
}),
});
const error = await response.json();
expect(response.status).toBe(404);
expect(error).toHaveProperty("error", "Transaction 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({
params: { id: "1" },
});
expect(response.status).toBe(204);
expect(transactions).toHaveLength(initialCount - 1);
expect(accounts[0].balance).toBe(initialBalance - transactionAmount);
});
it("should return 404 for non-existent transaction", async () => {
const response = await deleteTransaction({
params: { id: "999" },
});
const error = await response.json();
expect(response.status).toBe(404);
expect(error).toHaveProperty("error", "Transaction not found");
});
});
});

21
vitest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// Increase timeout for slower CI environments
testTimeout: 10000,
// Use the setup file we created
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}"],
// Configure coverage collection
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "src/test/**/*", "**/*.d.ts"],
},
},
});