feat: add integration test for economy transfer flow
Tests the full transfer cycle against a real database: debit/credit, transaction records, insufficient funds rejection, self-transfer rejection, non-positive amounts, and sequential transfers. Uses *.integration.test.ts convention — excluded from default test runs, included with --integration flag in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,10 @@ import { economyService } from "./economy.service";
|
|||||||
const SENDER_ID = "999000000000000001";
|
const SENDER_ID = "999000000000000001";
|
||||||
const RECEIVER_ID = "999000000000000002";
|
const RECEIVER_ID = "999000000000000002";
|
||||||
|
|
||||||
|
// This test requires a real database connection.
|
||||||
|
// It is excluded from `bun test` by default (*.integration.test.ts pattern)
|
||||||
|
// and only runs in CI with `--integration` flag, which provides PostgreSQL.
|
||||||
|
|
||||||
async function cleanupTestUsers() {
|
async function cleanupTestUsers() {
|
||||||
// transactions.userId has onDelete: 'cascade', so deleting users removes their transactions
|
// transactions.userId has onDelete: 'cascade', so deleting users removes their transactions
|
||||||
await DrizzleClient.delete(users).where(eq(users.id, BigInt(SENDER_ID)));
|
await DrizzleClient.delete(users).where(eq(users.id, BigInt(SENDER_ID)));
|
||||||
@@ -77,16 +81,26 @@ describe("Economy Integration Tests", () => {
|
|||||||
expect(receiverTxs[0]!.type).toBe("TRANSFER_IN");
|
expect(receiverTxs[0]!.type).toBe("TRANSFER_IN");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject a transfer when sender has insufficient funds", async () => {
|
it("should reject transfer with insufficient funds and leave balances unchanged", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
economyService.transfer(SENDER_ID, RECEIVER_ID, 2000n)
|
economyService.transfer(SENDER_ID, RECEIVER_ID, 2000n)
|
||||||
).rejects.toThrow("Insufficient funds");
|
).rejects.toThrow("Insufficient funds");
|
||||||
|
|
||||||
// Verify balances are unchanged
|
// Verify balances unchanged
|
||||||
const sender = await DrizzleClient.query.users.findFirst({
|
const sender = await DrizzleClient.query.users.findFirst({
|
||||||
where: eq(users.id, BigInt(SENDER_ID)),
|
where: eq(users.id, BigInt(SENDER_ID)),
|
||||||
});
|
});
|
||||||
|
const receiver = await DrizzleClient.query.users.findFirst({
|
||||||
|
where: eq(users.id, BigInt(RECEIVER_ID)),
|
||||||
|
});
|
||||||
expect(sender!.balance).toBe(1000n);
|
expect(sender!.balance).toBe(1000n);
|
||||||
|
expect(receiver!.balance).toBe(500n);
|
||||||
|
|
||||||
|
// Verify no transaction records were created
|
||||||
|
const senderTxs = await DrizzleClient.query.transactions.findMany({
|
||||||
|
where: eq(transactions.userId, BigInt(SENDER_ID)),
|
||||||
|
});
|
||||||
|
expect(senderTxs.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reject a self-transfer", async () => {
|
it("should reject a self-transfer", async () => {
|
||||||
@@ -122,30 +136,5 @@ describe("Economy Integration Tests", () => {
|
|||||||
// Receiver: 500 + 100 + 200 - 50 = 750
|
// Receiver: 500 + 100 + 200 - 50 = 750
|
||||||
expect(receiver!.balance).toBe(750n);
|
expect(receiver!.balance).toBe(750n);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should roll back the entire transfer if an error occurs mid-transaction", async () => {
|
|
||||||
// Attempting to transfer more than balance ensures the transaction aborts
|
|
||||||
// after the sender check but before any SQL update is committed
|
|
||||||
const senderBefore = await DrizzleClient.query.users.findFirst({
|
|
||||||
where: eq(users.id, BigInt(SENDER_ID)),
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
economyService.transfer(SENDER_ID, RECEIVER_ID, 9999n)
|
|
||||||
).rejects.toThrow("Insufficient funds");
|
|
||||||
|
|
||||||
const senderAfter = await DrizzleClient.query.users.findFirst({
|
|
||||||
where: eq(users.id, BigInt(SENDER_ID)),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Balance must be unchanged — no partial debit
|
|
||||||
expect(senderAfter!.balance).toBe(senderBefore!.balance);
|
|
||||||
|
|
||||||
// No transaction records should have been written
|
|
||||||
const senderTxs = await DrizzleClient.query.transactions.findMany({
|
|
||||||
where: eq(transactions.userId, BigInt(SENDER_ID)),
|
|
||||||
});
|
|
||||||
expect(senderTxs.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user