diff --git a/backend/src/api/controllers/archived-vaults.test.ts b/backend/src/api/controllers/archived-vaults.test.ts index 7e3a62a..7f661f4 100644 --- a/backend/src/api/controllers/archived-vaults.test.ts +++ b/backend/src/api/controllers/archived-vaults.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import type { Request, Response, NextFunction } from "express"; import { getArchivedVaults } from "./admin.js"; -import * as vault from "../../services/vault.js"; vi.mock("../../services/vault.js", () => ({ VaultService: vi.fn().mockImplementation(() => ({ @@ -82,8 +81,9 @@ describe("getArchivedVaults (#675)", () => { }, ]; - const mockVaultService = new vault.VaultService(); - vi.mocked(mockVaultService.listArchivedVaults).mockResolvedValue(mockVaults); + const { VaultService } = await import("../../services/vault.js"); + const mockListArchivedVaults = vi.fn().mockResolvedValue(mockVaults); + vi.mocked(VaultService).mockReturnValue({ listArchivedVaults: mockListArchivedVaults }); await getArchivedVaults(mockReq as Request, mockRes as Response, mockNext); @@ -91,8 +91,9 @@ describe("getArchivedVaults (#675)", () => { }); it("returns empty array when no archived vaults exist", async () => { - const mockVaultService = new vault.VaultService(); - vi.mocked(mockVaultService.listArchivedVaults).mockResolvedValue([]); + const { VaultService } = await import("../../services/vault.js"); + const mockListArchivedVaults = vi.fn().mockResolvedValue([]); + vi.mocked(VaultService).mockReturnValue({ listArchivedVaults: mockListArchivedVaults }); await getArchivedVaults(mockReq as Request, mockRes as Response, mockNext); @@ -101,8 +102,9 @@ describe("getArchivedVaults (#675)", () => { it("calls next on error", async () => { const error = new Error("Database error"); - const mockVaultService = new vault.VaultService(); - vi.mocked(mockVaultService.listArchivedVaults).mockRejectedValue(error); + const { VaultService } = await import("../../services/vault.js"); + const mockListArchivedVaults = vi.fn().mockRejectedValue(error); + vi.mocked(VaultService).mockReturnValue({ listArchivedVaults: mockListArchivedVaults }); await getArchivedVaults(mockReq as Request, mockRes as Response, mockNext); diff --git a/backend/src/api/controllers/consistency-total-supply.test.ts b/backend/src/api/controllers/consistency-total-supply.test.ts index 5b320f6..f1716b7 100644 --- a/backend/src/api/controllers/consistency-total-supply.test.ts +++ b/backend/src/api/controllers/consistency-total-supply.test.ts @@ -45,8 +45,7 @@ describe("getTotalSupplyConsistency (#673)", () => { const { VaultService } = await import("../../services/vault.js"); const { readTotalSupply } = await import("../../services/stellar.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue({ + const mockGetVault = vi.fn().mockResolvedValue({ id: 1, contractId: CONTRACT_ID, factoryId: null, @@ -72,6 +71,7 @@ describe("getTotalSupplyConsistency (#673)", () => { createdAt: new Date(), updatedAt: new Date(), }); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVault }); vi.mocked(readTotalSupply).mockResolvedValue(5000n); @@ -91,8 +91,7 @@ describe("getTotalSupplyConsistency (#673)", () => { const { VaultService } = await import("../../services/vault.js"); const { readTotalSupply } = await import("../../services/stellar.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue({ + const mockGetVault2 = vi.fn().mockResolvedValue({ id: 1, contractId: CONTRACT_ID, factoryId: null, @@ -118,6 +117,7 @@ describe("getTotalSupplyConsistency (#673)", () => { createdAt: new Date(), updatedAt: new Date(), }); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVault2 }); vi.mocked(readTotalSupply).mockResolvedValue(5000n); @@ -137,8 +137,7 @@ describe("getTotalSupplyConsistency (#673)", () => { const { VaultService } = await import("../../services/vault.js"); const { readTotalSupply } = await import("../../services/stellar.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue({ + const mockGetVault3 = vi.fn().mockResolvedValue({ id: 1, contractId: CONTRACT_ID, factoryId: null, @@ -165,6 +164,8 @@ describe("getTotalSupplyConsistency (#673)", () => { updatedAt: new Date(), }); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVault3 }); + vi.mocked(readTotalSupply).mockResolvedValue(5000n); await getTotalSupplyConsistency(mockReq as Request, mockRes as Response, mockNext); @@ -183,8 +184,7 @@ describe("getTotalSupplyConsistency (#673)", () => { const { VaultService } = await import("../../services/vault.js"); const { readTotalSupply } = await import("../../services/stellar.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue({ + const mockGetVault4 = vi.fn().mockResolvedValue({ id: 1, contractId: CONTRACT_ID, factoryId: null, @@ -211,6 +211,8 @@ describe("getTotalSupplyConsistency (#673)", () => { updatedAt: new Date(), }); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVault4 }); + vi.mocked(readTotalSupply).mockResolvedValue(5100n); await getTotalSupplyConsistency(mockReq as Request, mockRes as Response, mockNext); @@ -236,8 +238,8 @@ describe("getTotalSupplyConsistency (#673)", () => { mockReq.query = { contractId: CONTRACT_ID }; const { VaultService } = await import("../../services/vault.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue(null); + const mockGetVaultNull = vi.fn().mockResolvedValue(null); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVaultNull }); await getTotalSupplyConsistency(mockReq as Request, mockRes as Response, mockNext); @@ -254,8 +256,7 @@ describe("getTotalSupplyConsistency (#673)", () => { const { VaultService } = await import("../../services/vault.js"); const { readTotalSupply } = await import("../../services/stellar.js"); - const mockVaultService = new VaultService(); - vi.mocked(mockVaultService.getVault).mockResolvedValue({ + const mockGetVault5 = vi.fn().mockResolvedValue({ id: 1, contractId: CONTRACT_ID, factoryId: null, @@ -282,6 +283,7 @@ describe("getTotalSupplyConsistency (#673)", () => { updatedAt: new Date(), }); + vi.mocked(VaultService).mockReturnValue({ getVault: mockGetVault5 }); vi.mocked(readTotalSupply).mockRejectedValue(new Error("RPC timeout")); await getTotalSupplyConsistency(mockReq as Request, mockRes as Response, mockNext); diff --git a/backend/src/api/controllers/webhooks.test.ts b/backend/src/api/controllers/webhooks.test.ts index 08e0452..cc34d00 100644 --- a/backend/src/api/controllers/webhooks.test.ts +++ b/backend/src/api/controllers/webhooks.test.ts @@ -1,6 +1,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("../../db/index.js", () => ({ query: vi.fn() })); +vi.mock("../../services/notifications.js", () => ({ + validateWebhookUrl: vi.fn().mockResolvedValue(undefined), + NotificationService: vi.fn().mockImplementation(() => ({})), +})); async function getTestContext() { const { query } = await import("../../db/index.js"); @@ -42,6 +46,7 @@ describe("Webhook Controller", () => { url: "https://example.com/hook", events: ["deposit"], active: true, + consecutiveFailures: 0, createdAt: row.created_at, }); }); diff --git a/backend/src/deposit-indexing.e2e.test.ts b/backend/src/deposit-indexing.e2e.test.ts index 94707c1..2b2f5d4 100644 --- a/backend/src/deposit-indexing.e2e.test.ts +++ b/backend/src/deposit-indexing.e2e.test.ts @@ -11,7 +11,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("./db/index.js", () => ({ query: vi.fn().mockResolvedValue([]) })); vi.mock("./logger.js", () => ({ - logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), child: vi.fn().mockReturnThis() }, })); vi.mock("./services/stellar.js", () => ({ getSorobanRpc: vi.fn() })); vi.mock("./services/vault.js", () => ({ diff --git a/backend/src/services/indexer-lag-alert.test.ts b/backend/src/services/indexer-lag-alert.test.ts index ffa1267..a297ca9 100644 --- a/backend/src/services/indexer-lag-alert.test.ts +++ b/backend/src/services/indexer-lag-alert.test.ts @@ -35,13 +35,13 @@ describe("Indexer lag alert (#672)", () => { getEvents: ReturnType; }; - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks(); mockServer = { getLatestLedger: vi.fn(), getEvents: vi.fn().mockResolvedValue({ events: [], latestLedger: 1000 }), }; - const { getSorobanRpc } = require("./stellar.js"); + const { getSorobanRpc } = await import("./stellar.js"); (getSorobanRpc as any).mockReturnValue(mockServer); indexer = new Indexer(); indexer["lastLedger"] = 900; @@ -75,11 +75,11 @@ describe("Indexer lag alert (#672)", () => { it("logs error on every tick when lagging", async () => { mockServer.getLatestLedger.mockResolvedValue({ sequence: 1050 }); - indexer["lastLedger"] = 900; - await indexer.tick(); - await indexer.tick(); - await indexer.tick(); + for (let i = 0; i < 3; i++) { + indexer["lastLedger"] = 900; + await indexer.tick(); + } expect(logger.error).toHaveBeenCalledTimes(3); }); diff --git a/backend/src/services/indexer.ts b/backend/src/services/indexer.ts index d49102d..aed47ec 100644 --- a/backend/src/services/indexer.ts +++ b/backend/src/services/indexer.ts @@ -1028,44 +1028,6 @@ export class Indexer { ); } - private async handleOperatorAdded( - contractId: string, - ev: { operator: string; timestamp: bigint }, - ): Promise { - const vaultRow = await query<{ id: number }>( - "SELECT id FROM vaults WHERE contract_id = $1", - [contractId], - ); - if (vaultRow.length === 0) return; - const vaultId = vaultRow[0].id; - const assignedAt = new Date(Number(ev.timestamp) * 1000); - await query( - `INSERT INTO vault_operators (vault_id, address, active, assigned_at, updated_at) - VALUES ($1, $2, TRUE, $3, NOW()) - ON CONFLICT (vault_id, address) DO UPDATE SET active = TRUE, updated_at = NOW()`, - [vaultId, ev.operator, assignedAt], - ); - logger.info({ contractId, operator: ev.operator }, "Processed op_add event"); - } - - private async handleOperatorRemoved( - contractId: string, - ev: { operator: string }, - ): Promise { - const vaultRow = await query<{ id: number }>( - "SELECT id FROM vaults WHERE contract_id = $1", - [contractId], - ); - if (vaultRow.length === 0) return; - const vaultId = vaultRow[0].id; - await query( - `UPDATE vault_operators SET active = FALSE, updated_at = NOW() - WHERE vault_id = $1 AND address = $2`, - [vaultId, ev.operator], - ); - logger.info({ contractId, operator: ev.operator }, "Processed op_rem event"); - } - private async handleZkmeVerifierUpdated( contractId: string, ev: { newVerifier: string }, diff --git a/backend/src/services/redemption-queue.test.ts b/backend/src/services/redemption-queue.test.ts index 8a93fac..3f2ad93 100644 --- a/backend/src/services/redemption-queue.test.ts +++ b/backend/src/services/redemption-queue.test.ts @@ -1,15 +1,15 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -vi.mock("../../db/index.js", () => ({ query: vi.fn() })); -vi.mock("../../logger.js", () => ({ +vi.mock("../db/index.js", () => ({ query: vi.fn() })); +vi.mock("../logger.js", () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); -vi.mock("../../services/stellar.js", () => ({ getSorobanRpc: vi.fn() })); -vi.mock("../../services/notifications.js", () => ({ NotificationService: vi.fn().mockImplementation(() => ({})) })); +vi.mock("./stellar.js", () => ({ getSorobanRpc: vi.fn() })); +vi.mock("./notifications.js", () => ({ NotificationService: vi.fn().mockImplementation(() => ({})) })); import { xdr } from "@stellar/stellar-sdk"; -import { VaultService } from "../../services/vault.js"; -import { Indexer, parseRequestEarlyRedemptionEvent } from "../../services/indexer.js"; +import { VaultService } from "./vault.js"; +import { Indexer, parseRequestEarlyRedemptionEvent } from "./indexer.js"; const VAULT_CONTRACT = "CDLZFC3SYJYHZDQA6M57EYUC2XBDA6LQF3M6KFRDZ7TXJYJL2K3B"; const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"; @@ -25,7 +25,7 @@ describe("VaultService - getRedemptionQueue", () => { }); it("returns an empty array when vault has no unprocessed requests", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); query.mockResolvedValue([]); const queue = await service.getRedemptionQueue(VAULT_CONTRACT); @@ -37,7 +37,7 @@ describe("VaultService - getRedemptionQueue", () => { }); it("returns unprocessed requests ordered by request_time ASC", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const now = new Date(); const earlier = new Date(now.getTime() - 60000); const latest = new Date(now.getTime() + 60000); @@ -73,7 +73,7 @@ describe("VaultService - getRedemptionQueue", () => { }); it("excludes processed requests from the queue", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); query.mockResolvedValue([ { id: 1, @@ -93,7 +93,7 @@ describe("VaultService - getRedemptionQueue", () => { }); it("returns correctly mapped field names (userAddress not user_address)", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); query.mockResolvedValue([ { id: 42, @@ -118,14 +118,11 @@ describe("parseRequestEarlyRedemptionEvent", () => { const mockEvent = { topic: [ xdr.ScVal.scvSymbol("erq_req"), - xdr.ScVal.scvAddress(xdr.Address.typeAccount(new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 42)))), + xdr.ScVal.scvAddress(new xdr.ScAddress(xdr.ScAddressType.scAddressTypeAccount(), new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 42)))), ], value: xdr.ScVal.scvVec([ xdr.ScVal.scvU32(1), - xdr.ScVal.scvU128(xdr.Uint128Parts.fromXDRObject({ - lo: xdr.Uint64.fromString("1000"), - hi: xdr.Uint64.fromString("0"), - })), + xdr.ScVal.scvU128(new xdr.UInt128Parts({ lo: xdr.Uint64.fromString("1000"), hi: xdr.Uint64.fromString("0") })), xdr.ScVal.scvU64(xdr.Uint64.fromString("1609459200")), ]), }; @@ -154,7 +151,7 @@ describe("parseRequestEarlyRedemptionEvent", () => { const mockEvent = { topic: [ xdr.ScVal.scvSymbol("deposit"), - xdr.ScVal.scvAddress(xdr.Address.typeAccount(new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 42)))), + xdr.ScVal.scvAddress(new xdr.ScAddress(xdr.ScAddressType.scAddressTypeAccount(), new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 42)))), ], value: xdr.ScVal.scvVoid(), }; @@ -180,14 +177,14 @@ describe("Indexer - request_early_redemption handler", () => { }); it("inserts new redemption request when event is processed", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); - // First call: lookup vault - // Second call: insert redemption request + // processEvent first checks for duplicates, then vault, then inserts query + .mockResolvedValueOnce([]) // check existing event (no duplicate) .mockResolvedValueOnce([{ id: 10 }]) // vault lookup .mockResolvedValueOnce([]) // insert redemption request - .mockResolvedValueOnce([]) // check existing event + .mockResolvedValueOnce([]) // queue position count .mockResolvedValueOnce([{ id: 0 }]); // indexed_events insert const mockEvent = { @@ -198,14 +195,11 @@ describe("Indexer - request_early_redemption handler", () => { txHash: "hash123", topic: [ xdr.ScVal.scvSymbol("request_early_redemption"), - xdr.ScVal.scvAddress(xdr.Address.typeAccount(new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 99)))), + xdr.ScVal.scvAddress(new xdr.ScAddress(xdr.ScAddressType.scAddressTypeAccount(), new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 99)))), ], value: xdr.ScVal.scvVec([ xdr.ScVal.scvU32(1), - xdr.ScVal.scvU128(xdr.Uint128Parts.fromXDRObject({ - lo: xdr.Uint64.fromString("500"), - hi: xdr.Uint64.fromString("0"), - })), + xdr.ScVal.scvU128(new xdr.UInt128Parts({ lo: xdr.Uint64.fromString("500"), hi: xdr.Uint64.fromString("0") })), xdr.ScVal.scvU64(xdr.Uint64.fromString("1609459200")), ]), }; @@ -219,8 +213,8 @@ describe("Indexer - request_early_redemption handler", () => { }); it("skips insertion when vault not found", async () => { - const { query } = await import("../../db/index.js"); - const { logger } = await import("../../logger.js"); + const { query } = await import("../db/index.js"); + const { logger } = await import("../logger.js"); // Vault not found query.mockResolvedValueOnce([]); @@ -233,14 +227,11 @@ describe("Indexer - request_early_redemption handler", () => { txHash: "hash456", topic: [ xdr.ScVal.scvSymbol("request_early_redemption"), - xdr.ScVal.scvAddress(xdr.Address.typeAccount(new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 88)))), + xdr.ScVal.scvAddress(new xdr.ScAddress(xdr.ScAddressType.scAddressTypeAccount(), new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 88)))), ], value: xdr.ScVal.scvVec([ xdr.ScVal.scvU32(2), - xdr.ScVal.scvU128(xdr.Uint128Parts.fromXDRObject({ - lo: xdr.Uint64.fromString("100"), - hi: xdr.Uint64.fromString("0"), - })), + xdr.ScVal.scvU128(new xdr.UInt128Parts({ lo: xdr.Uint64.fromString("100"), hi: xdr.Uint64.fromString("0") })), xdr.ScVal.scvU64(xdr.Uint64.fromString("1609459200")), ]), }; @@ -251,14 +242,15 @@ describe("Indexer - request_early_redemption handler", () => { expect(logger.warn).toHaveBeenCalled(); }); - it("handles duplicate events idempotently (ON CONFLICT DO NOTHING)", async () => { - const { query } = await import("../../db/index.js"); + it("handles duplicate events idempotently (ON CONFLICT DO UPDATE)", async () => { + const { query } = await import("../db/index.js"); - // First call: vault lookup + // processEvent checks for duplicate first, then vault, then inserts query + .mockResolvedValueOnce([]) // check existing event (no duplicate) .mockResolvedValueOnce([{ id: 10 }]) // vault lookup .mockResolvedValueOnce([]) // insert (should do nothing on duplicate) - .mockResolvedValueOnce([]) // check existing event (empty, so not skipped at start) + .mockResolvedValueOnce([]) // queue position count .mockResolvedValueOnce([{ id: 0 }]); // indexed_events insert const mockEvent = { @@ -269,14 +261,11 @@ describe("Indexer - request_early_redemption handler", () => { txHash: "hash789", topic: [ xdr.ScVal.scvSymbol("request_early_redemption"), - xdr.ScVal.scvAddress(xdr.Address.typeAccount(new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 77)))), + xdr.ScVal.scvAddress(new xdr.ScAddress(xdr.ScAddressType.scAddressTypeAccount(), new xdr.PublicKey.publicKeyTypeEd25519(Buffer.alloc(32, 77)))), ], value: xdr.ScVal.scvVec([ xdr.ScVal.scvU32(3), - xdr.ScVal.scvU128(xdr.Uint128Parts.fromXDRObject({ - lo: xdr.Uint64.fromString("250"), - hi: xdr.Uint64.fromString("0"), - })), + xdr.ScVal.scvU128(new xdr.UInt128Parts({ lo: xdr.Uint64.fromString("250"), hi: xdr.Uint64.fromString("0") })), xdr.ScVal.scvU64(xdr.Uint64.fromString("1609459200")), ]), }; @@ -287,7 +276,7 @@ describe("Indexer - request_early_redemption handler", () => { const calls = (query as any).mock.calls; const insertCall = calls.find((c: any) => c[0].includes("ON CONFLICT")); expect(insertCall).toBeDefined(); - expect(insertCall[0]).toContain("DO NOTHING"); + expect(insertCall[0]).toContain("DO UPDATE"); }); }); @@ -302,7 +291,7 @@ describe("Redemption Queue - edge cases", () => { }); it("handles very large share amounts", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const largeAmount = "99999999999999999999999999.99"; query.mockResolvedValue([ @@ -319,7 +308,7 @@ describe("Redemption Queue - edge cases", () => { }); it("handles multiple requests from same user in same vault", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const baseTime = new Date(); query.mockResolvedValue([ @@ -344,7 +333,7 @@ describe("Redemption Queue - edge cases", () => { }); it("correctly filters by vault contract ID", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const otherVault = "COTHER123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; query.mockResolvedValue([]); @@ -369,7 +358,7 @@ describe("Redemption Queue - happy path scenarios", () => { }); it("single unprocessed request appears in queue", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const now = new Date(); query.mockResolvedValue([ @@ -390,7 +379,7 @@ describe("Redemption Queue - happy path scenarios", () => { }); it("multiple requests are ordered by request_time ASC", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const early = new Date("2024-01-01"); const mid = new Date("2024-01-02"); const late = new Date("2024-01-03"); @@ -407,7 +396,7 @@ describe("Redemption Queue - happy path scenarios", () => { }); it("returns all fields for each request", async () => { - const { query } = await import("../../db/index.js"); + const { query } = await import("../db/index.js"); const now = new Date(); query.mockResolvedValue([