From 323e64eab340c2004b28b27b8828f80a05367a57 Mon Sep 17 00:00:00 2001 From: Gabriel Tozatti Date: Wed, 11 Mar 2026 14:12:36 -0300 Subject: [PATCH] feat: add unit tests for gaslessUtxos collection --- packages/worker/package.json | 2 + .../worker/src/tests/gaslessUtxos.test.ts | 360 ++++++++++++++++++ .../src/tests/utils/gaslessTestEnvironment.ts | 46 +++ 3 files changed, 408 insertions(+) create mode 100644 packages/worker/src/tests/gaslessUtxos.test.ts create mode 100644 packages/worker/src/tests/utils/gaslessTestEnvironment.ts diff --git a/packages/worker/package.json b/packages/worker/package.json index c96a69480..b87ed9ba6 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -45,9 +45,11 @@ "eslint-plugin-prettier": "3.3.1", "husky": "5.2.0", "lint-staged": "10.5.4", + "mongodb-memory-server": "^11.0.1", "prettier": "2.2.1", "pretty-quick": "3.1.0", "ts-node-dev": "2.0.0", + "tsconfig-paths": "^4.2.0", "tscpaths": "0.0.9" } } diff --git a/packages/worker/src/tests/gaslessUtxos.test.ts b/packages/worker/src/tests/gaslessUtxos.test.ts new file mode 100644 index 000000000..63dcd11da --- /dev/null +++ b/packages/worker/src/tests/gaslessUtxos.test.ts @@ -0,0 +1,360 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { gaslessUtxosCollection } from "@/queues/gaslessUtxos"; +import { GaslessTestEnvironment } from "@/tests/utils/gaslessTestEnvironment"; + +test("gaslessUtxos", async (t) => { + const env = new GaslessTestEnvironment(); + + await env.init(); + + t.after(async () => { + await env.close(); + }); + + await t.test("findAvailable()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test("should return only available UTXOs", async () => { + await env.seed([ + { utxoId: "utxo-1", status: "available" }, + { utxoId: "utxo-2", status: "reserved" }, + { utxoId: "utxo-3", status: "spent" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const result = await utxos.findAvailable(); + + assert.equal(result.length, 1); + assert.equal(result[0].utxoId, "utxo-1"); + }); + + await t.test( + "should return empty array when no UTXOs available", + async () => { + await env.seed([ + { utxoId: "utxo-1", status: "reserved" }, + { utxoId: "utxo-2", status: "spent" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const result = await utxos.findAvailable(); + + assert.equal(result.length, 0); + } + ); + }); + + await t.test("reserve()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test("should reserve an available UTXO", async () => { + await env.seed([{ utxoId: "utxo-1", status: "available" }]); + + const utxos = gaslessUtxosCollection(env.collection); + const reserved = await utxos.reserve({ reservedBy: "reservation-123" }); + + assert.ok(reserved); + assert.equal(reserved.utxoId, "utxo-1"); + assert.equal(reserved.status, "reserved"); + assert.equal(reserved.reservedBy, "reservation-123"); + assert.ok(reserved.reservedAt instanceof Date); + }); + + await t.test("should return null when no UTXOs available", async () => { + await env.seed([{ utxoId: "utxo-1", status: "spent" }]); + + const utxos = gaslessUtxosCollection(env.collection); + const reserved = await utxos.reserve({ reservedBy: "reservation-123" }); + + assert.equal(reserved, null); + }); + + await t.test("should not reserve an already reserved UTXO", async () => { + await env.seed([ + { utxoId: "utxo-1", status: "reserved", reservedBy: "reservation-abc" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const reserved = await utxos.reserve({ reservedBy: "reservation-xyz" }); + + assert.equal(reserved, null); + }); + + await t.test( + "should handle race condition — only one reservation wins", + async () => { + await env.seed([{ utxoId: "utxo-1", status: "available" }]); + + const utxos = gaslessUtxosCollection(env.collection); + + const [first, second] = await Promise.all([ + utxos.reserve({ reservedBy: "reservation-A" }), + utxos.reserve({ reservedBy: "reservation-B" }), + ]); + + const winners = [first, second].filter(Boolean); + const losers = [first, second].filter((r) => r === null); + + assert.equal(winners.length, 1); + assert.equal(losers.length, 1); + + assert.equal(winners[0]!.status, "reserved"); + } + ); + + await t.test( + "should handle race condition with multiple UTXOs", + async () => { + await env.seed([ + { utxoId: "utxo-1", status: "available" }, + { utxoId: "utxo-2", status: "available" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + + const results = await Promise.all([ + utxos.reserve({ reservedBy: "reservation-A" }), + utxos.reserve({ reservedBy: "reservation-B" }), + utxos.reserve({ reservedBy: "reservation-C" }), + ]); + + const winners = results.filter(Boolean); + const losers = results.filter((r) => r === null); + + assert.equal(winners.length, 2); + assert.equal(losers.length, 1); + } + ); + }); + + await t.test("release()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test( + "should release a reserved UTXO back to available", + async () => { + await env.seed([ + { + utxoId: "utxo-1", + status: "reserved", + reservedBy: "reservation-123", + reservedAt: new Date(), + }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.release("utxo-1"); + + assert.ok(released); + assert.equal(released.status, "available"); + assert.equal(released.reservedBy, undefined); + assert.equal(released.reservedAt, undefined); + } + ); + + await t.test("should return null when UTXO is not reserved", async () => { + await env.seed([{ utxoId: "utxo-1", status: "available" }]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.release("utxo-1"); + + assert.equal(released, null); + }); + + await t.test("should return null when UTXO does not exist", async () => { + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.release("utxo-nonexistent"); + + assert.equal(released, null); + }); + }); + + await t.test("markSpent()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test("should mark a reserved UTXO as spent", async () => { + await env.seed([ + { + utxoId: "utxo-1", + status: "reserved", + reservedBy: "reservation-123", + reservedAt: new Date(), + }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const spent = await utxos.markSpent("utxo-1", "0xspent-tx-hash"); + + assert.ok(spent); + assert.equal(spent.status, "spent"); + assert.equal(spent.spentTxHash, "0xspent-tx-hash"); + assert.equal(spent.reservedBy, undefined); + assert.equal(spent.reservedAt, undefined); + }); + + await t.test("should return null when UTXO is not reserved", async () => { + await env.seed([{ utxoId: "utxo-1", status: "available" }]); + + const utxos = gaslessUtxosCollection(env.collection); + const spent = await utxos.markSpent("utxo-1", "0xspent-tx-hash"); + + assert.equal(spent, null); + }); + + await t.test("should return null when UTXO does not exist", async () => { + const utxos = gaslessUtxosCollection(env.collection); + const spent = await utxos.markSpent( + "utxo-nonexistent", + "0xspent-tx-hash" + ); + + assert.equal(spent, null); + }); + }); + + await t.test("getStats()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test("should return correct counts per status", async () => { + await env.seed([ + { utxoId: "utxo-1", status: "available" }, + { utxoId: "utxo-2", status: "available" }, + { utxoId: "utxo-3", status: "reserved" }, + { utxoId: "utxo-4", status: "spent" }, + { utxoId: "utxo-5", status: "spent" }, + { utxoId: "utxo-6", status: "spent" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const stats = await utxos.getStats(); + + assert.equal(stats.available, 2); + assert.equal(stats.reserved, 1); + assert.equal(stats.spent, 3); + assert.equal(stats.total, 6); + }); + + await t.test("should return zeros when collection is empty", async () => { + const utxos = gaslessUtxosCollection(env.collection); + const stats = await utxos.getStats(); + + assert.equal(stats.available, 0); + assert.equal(stats.reserved, 0); + assert.equal(stats.spent, 0); + assert.equal(stats.total, 0); + }); + }); + + await t.test("releaseExpired()", async (t) => { + t.afterEach(async () => { + await env.clear(); + }); + + await t.test( + "should release reservations older than 5 minutes", + async () => { + const expiredDate = new Date(Date.now() - 10 * 60 * 1000); + + await env.seed([ + { + utxoId: "utxo-1", + status: "reserved", + reservedBy: "reservation-123", + reservedAt: expiredDate, + }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.releaseExpired(); + + assert.equal(released, 1); + + const doc = await env.collection.findOne({ utxoId: "utxo-1" }); + assert.equal(doc?.status, "available"); + assert.equal(doc?.reservedBy, undefined); + assert.equal(doc?.reservedAt, undefined); + } + ); + + await t.test( + "should not release reservations newer than 5 minutes", + async () => { + const recentDate = new Date(Date.now() - 1 * 60 * 1000); + + await env.seed([ + { + utxoId: "utxo-1", + status: "reserved", + reservedBy: "reservation-123", + reservedAt: recentDate, + }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.releaseExpired(); + + assert.equal(released, 0); + + const doc = await env.collection.findOne({ utxoId: "utxo-1" }); + assert.equal(doc?.status, "reserved"); + } + ); + + await t.test("should release only expired ones when mixed", async () => { + const expiredDate = new Date(Date.now() - 10 * 60 * 1000); + const recentDate = new Date(Date.now() - 1 * 60 * 1000); + + await env.seed([ + { + utxoId: "utxo-1", + status: "reserved", + reservedBy: "r-1", + reservedAt: expiredDate, + }, + { + utxoId: "utxo-2", + status: "reserved", + reservedBy: "r-2", + reservedAt: recentDate, + }, + { + utxoId: "utxo-3", + status: "reserved", + reservedBy: "r-3", + reservedAt: expiredDate, + }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.releaseExpired(); + + assert.equal(released, 2); + + const stillReserved = await env.collection.findOne({ utxoId: "utxo-2" }); + assert.equal(stillReserved?.status, "reserved"); + }); + + await t.test("should return 0 when nothing is expired", async () => { + await env.seed([ + { utxoId: "utxo-1", status: "available" }, + { utxoId: "utxo-2", status: "spent" }, + ]); + + const utxos = gaslessUtxosCollection(env.collection); + const released = await utxos.releaseExpired(); + + assert.equal(released, 0); + }); + }); +}); diff --git a/packages/worker/src/tests/utils/gaslessTestEnvironment.ts b/packages/worker/src/tests/utils/gaslessTestEnvironment.ts new file mode 100644 index 000000000..b538f2a6d --- /dev/null +++ b/packages/worker/src/tests/utils/gaslessTestEnvironment.ts @@ -0,0 +1,46 @@ +import { MongoMemoryServer } from "mongodb-memory-server"; +import { MongoClient, Collection } from "mongodb"; +import { GaslessUtxo } from "@/queues/gaslessUtxos"; +import { COLLECTION_GASLESS_UTXOS } from "@/queues/gaslessUtxos"; + +export class GaslessTestEnvironment { + private mongod!: MongoMemoryServer; + private client!: MongoClient; + collection!: Collection; + + async init(): Promise { + this.mongod = await MongoMemoryServer.create(); + const uri = this.mongod.getUri(); + + this.client = new MongoClient(uri); + await this.client.connect(); + + const db = this.client.db("test"); + this.collection = db.collection(COLLECTION_GASLESS_UTXOS); + } + + async close(): Promise { + await this.client.close(); + await this.mongod.stop(); + } + + async clear(): Promise { + await this.collection.deleteMany({}); + } + + async seed(utxos: Partial[]): Promise { + const docs = utxos.map((u) => ({ + utxoId: u.utxoId ?? "utxo-1", + txId: u.txId ?? "0xabc", + outputIndex: u.outputIndex ?? 0, + amount: u.amount ?? "200000000000000", + status: u.status ?? "available", + createdAt: u.createdAt ?? new Date(), + ...(u.reservedBy && { reservedBy: u.reservedBy }), + ...(u.reservedAt && { reservedAt: u.reservedAt }), + ...(u.spentTxHash && { spentTxHash: u.spentTxHash }), + })) as GaslessUtxo[]; + + await this.collection.insertMany(docs); + } +}