Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 74 additions & 14 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions backend/src/api/controllers/admin.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import supertest from "supertest";
import { createHash } from "crypto";

vi.mock("../../db/index.js", () => ({ query: vi.fn() }));
vi.mock("../../services/indexerSingleton.js", () => ({
indexer: {
isRunning: vi.fn().mockReturnValue(false),
getLastIndexedLedger: vi.fn().mockResolvedValue(0),
getLastTickAt: vi.fn().mockReturnValue(null),
getEventsIndexedCount: vi.fn().mockResolvedValue(0),
queueBackfill: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock("../../services/vault.js", () => ({
VaultService: vi.fn().mockImplementation(() => ({
listArchivedVaults: vi.fn().mockResolvedValue([]),
getVault: vi.fn().mockResolvedValue(null),
})),
}));
vi.mock("../../services/stellar.js", () => ({
readTotalSupply: vi.fn().mockResolvedValue(0n),
}));
vi.mock("pino-http", () => ({ pinoHttp: () => (_req: any, _res: any, next: any) => next() }));

async function getTestContext() {
const { query } = await import("../../db/index.js");
const { getAdminStats } = await import("./admin.js");
return { query: query as ReturnType<typeof vi.fn>, getAdminStats };
}

async function getApp() {
const { createApp } = await import("../../app.js");
return createApp();
}

/** Hash an API key the same way the auth middleware does */
function hashKey(plaintext: string): string {

Check failure on line 38 in backend/src/api/controllers/admin.test.ts

View workflow job for this annotation

GitHub Actions / Backend Lint, Build & Test

'hashKey' is defined but never used. Allowed unused vars must match /^_/u
return createHash("sha256").update(plaintext).digest("hex");
}

describe("Admin Controller", () => {
beforeEach(() => {
vi.clearAllMocks();
});

// ── Unit tests (controller function directly) ─────────────────────────────
describe("getAdminStats", () => {
it("returns vault/user/epoch counts and TVL", async () => {
const { query, getAdminStats } = await getTestContext();
Expand All @@ -33,4 +66,66 @@
expect(res.json).toHaveBeenCalledWith({ vaultCount: 2, userCount: 42, totalValueLocked: "12345", epochCount: 3 });
});
});

// ── Integration tests: GET /api/v1/admin/stats (Issue #692) ──────────────
describe("GET /api/v1/admin/stats", () => {
const VALID_KEY = "test-admin-api-key-12345";

beforeEach(async () => {
const { query } = await import("../../db/index.js");
const mockQuery = query as ReturnType<typeof vi.fn>;
mockQuery.mockReset();
});

it("returns 401 when the Authorization header is missing", async () => {
const app = await getApp();
const res = await supertest(app).get("/api/v1/admin/stats");
expect(res.status).toBe(401);
expect(res.body).toMatchObject({ error: "Unauthorized" });
});

it("returns 403 when the API key is invalid", async () => {
const { query } = await import("../../db/index.js");
const mockQuery = query as ReturnType<typeof vi.fn>;
// auth middleware queries api_keys — return empty = key not found
mockQuery.mockResolvedValue([]);

const app = await getApp();
const res = await supertest(app)
.get("/api/v1/admin/stats")
.set("Authorization", "Bearer not-a-real-key");

expect(res.status).toBe(403);
expect(res.body).toMatchObject({ error: "Forbidden" });
});

it("returns 200 with correct vaultCount and userCount for a valid admin key and seeded DB", async () => {
const { query } = await import("../../db/index.js");
const mockQuery = query as ReturnType<typeof vi.fn>;

// auth middleware: api_keys lookup → match the hashed key
mockQuery.mockResolvedValueOnce([{ id: 1, role: "admin", label: "test" }]);
// getAdminStats: vaultCount
mockQuery.mockResolvedValueOnce([{ count: "3" }]);
// getAdminStats: userCount
mockQuery.mockResolvedValueOnce([{ count: "7" }]);
// getAdminStats: totalValueLocked
mockQuery.mockResolvedValueOnce([{ total: "9999999" }]);
// getAdminStats: epochCount
mockQuery.mockResolvedValueOnce([{ count: "5" }]);

const app = await getApp();
const res = await supertest(app)
.get("/api/v1/admin/stats")
.set("Authorization", `Bearer ${VALID_KEY}`);

expect(res.status).toBe(200);
expect(res.body).toEqual({
vaultCount: 3,
userCount: 7,
totalValueLocked: "9999999",
epochCount: 5,
});
});
});
});
5 changes: 5 additions & 0 deletions backend/src/api/controllers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export async function getUserKycHistory(req: Request, res: Response, next: NextF
});

res.json({ data, total, page, pageSize });
} catch (err) {
next(err);
}
}

export async function getKycBatch(req: Request, res: Response, next: NextFunction) {
try {
const { addresses, vaultId } = req.body as { addresses: string[]; vaultId: string };
Expand Down
20 changes: 0 additions & 20 deletions backend/src/api/controllers/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,26 +402,6 @@ export async function getVaultHolders(req: Request, res: Response, next: NextFun
}
}

/**
* GET /api/v1/vaults/:contractId/operators
*
* Returns the current active operators for a vault.
*/
export async function getVaultOperators(req: Request, res: Response, next: NextFunction) {
try {
const vault = await vaultService.getVault(String(req.params["contractId"]));
if (!vault) {
res.status(404).json({ error: "NotFound", message: "Vault not found" });
return;
}
const operators = await vaultService.listVaultOperators(String(req.params["contractId"]));
setCacheHeaders(res);
res.json(operators);
} catch (err) {
next(err);
}
}

/**
* GET /api/v1/vaults/:contractId/roles
*
Expand Down
Loading
Loading