Skip to content
Open
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
151 changes: 151 additions & 0 deletions __tests__/api/stellar/activity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { NextResponse } from "next/server"
import { beforeEach, describe, expect, it, vi } from "vitest"

const { requireAuthenticatedUser, finalizeAuthenticatedResponse, getStellarConfig, find, loadAccount } = vi.hoisted(() => ({
requireAuthenticatedUser: vi.fn(),
finalizeAuthenticatedResponse: vi.fn(async (response: unknown) => response),
getStellarConfig: vi.fn(),
find: vi.fn(),
loadAccount: vi.fn(),
}))

vi.mock("@/lib/api/route-guard", () => ({ requireAuthenticatedUser, finalizeAuthenticatedResponse }))
vi.mock("@/lib/dbConnect", () => ({ default: vi.fn() }))
vi.mock("@/lib/stellar/config", () => ({ getStellarConfig }))
vi.mock("@/lib/stellar/client", () => ({
getStellarClient: vi.fn(() => ({
horizon: {
loadAccount,
},
})),
}))

vi.mock("@/models/StellarIndexedEvent", () => ({
default: {
find,
},
}))

import { GET } from "@/app/api/stellar/activity/route"

function buildRequest() {
return new Request("http://localhost/api/stellar/activity", {
method: "GET",
})
}

function buildUser(overrides: Record<string, unknown> = {}): Record<string, any> {
return {
_id: "user-1",
name: "Test User",
email: "test@example.com",
role: "investor",
stellarPublicKey: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H",
...overrides,
}
}

describe("GET /api/stellar/activity", () => {
beforeEach(() => {
requireAuthenticatedUser.mockReset()
finalizeAuthenticatedResponse.mockClear()
getStellarConfig.mockReset()
find.mockReset()
loadAccount.mockReset()
})

it("returns 401/error if not authenticated", async () => {
requireAuthenticatedUser.mockResolvedValue({
response: NextResponse.json({ message: "Unauthorized" }, { status: 401 }),
})

const response = await GET(buildRequest())
expect(response.status).toBe(401)

Check failure on line 63 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
})

it("returns mock data when mock mode is enabled", async () => {
const user = buildUser()
requireAuthenticatedUser.mockResolvedValue({ user })
getStellarConfig.mockReturnValue({
mock: true,
network: "testnet",
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
contractId: "C123",
issuerPublicKey: "GD123",
})

const response = await GET(buildRequest())
const payload = await response.json()

Check failure on line 79 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.

expect(response.status).toBe(200)

Check failure on line 81 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
expect(payload.mock).toBe(true)
expect(payload.balances.length).toBeGreaterThan(0)
expect(payload.activities.length).toBeGreaterThan(0)
expect(loadAccount).not.toHaveBeenCalled()
})

it("returns empty balances/activities if live mode but no public key is linked", async () => {
const user = buildUser({ stellarPublicKey: null })
requireAuthenticatedUser.mockResolvedValue({ user })
getStellarConfig.mockReturnValue({
mock: false,
network: "testnet",
})

const response = await GET(buildRequest())
const payload = await response.json()

Check failure on line 97 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.

expect(response.status).toBe(200)

Check failure on line 99 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
expect(payload.balances).toEqual([])
expect(payload.activities).toEqual([])
})

it("fetches live balances and database events when live mode and key is linked", async () => {
const user = buildUser()
requireAuthenticatedUser.mockResolvedValue({ user })
getStellarConfig.mockReturnValue({
mock: false,
network: "testnet",
})

loadAccount.mockResolvedValue({
balances: [
{ asset_type: "native", balance: "150.00" },
{ asset_type: "credit_alphanum4", asset_code: "USDC", balance: "20.00", asset_issuer: "GDUSDC" },
],
})

find.mockReturnValue({
sort: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue({
lean: vi.fn().mockResolvedValue([
{
_id: "tx-1",
chainMoveRecordType: "repayment",
eventType: "payment",
amount: "100.00",
asset: "CMOVE",
sourceAccount: user.stellarPublicKey,
destinationAccount: "GBX",
createdAt: new Date(),
raw: { transaction_hash: "hash-1" },
},
]),
}),
}),
})

const response = await GET(buildRequest())
const payload = await response.json()

Check failure on line 140 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.

expect(response.status).toBe(200)

Check failure on line 142 in __tests__/api/stellar/activity.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
expect(loadAccount).toHaveBeenCalledWith(user.stellarPublicKey)
expect(payload.balances).toEqual([
{ asset: "XLM", balance: "150.00", type: "native", issuer: null },
{ asset: "USDC", balance: "20.00", type: "credit_alphanum4", issuer: "GDUSDC" },
])
expect(payload.activities[0].id).toBe("tx-1")
expect(payload.activities[0].title).toBe("Repayment Settlement")
})
})
74 changes: 74 additions & 0 deletions __tests__/api/stellar/sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextResponse } from "next/server"
import { beforeEach, describe, expect, it, vi } from "vitest"

const { requireAuthenticatedUser, finalizeAuthenticatedResponse, sync } = vi.hoisted(() => ({
requireAuthenticatedUser: vi.fn(),
finalizeAuthenticatedResponse: vi.fn(async (response: unknown) => response),
sync: vi.fn(),
}))

vi.mock("@/lib/api/route-guard", () => ({ requireAuthenticatedUser, finalizeAuthenticatedResponse }))
vi.mock("@/lib/dbConnect", () => ({ default: vi.fn() }))
vi.mock("@/lib/stellar/indexer", () => ({
createStellarIndexer: vi.fn(() => ({
sync,
})),
}))

import { POST } from "@/app/api/admin/stellar/sync/route"

function buildRequest() {
return new Request("http://localhost/api/admin/stellar/sync", {
method: "POST",
})
}

function buildUser(overrides: Record<string, unknown> = {}): Record<string, any> {
return {
_id: "admin-1",
name: "Admin User",
email: "admin@example.com",
role: "admin",
...overrides,
}
}

describe("POST /api/admin/stellar/sync", () => {
beforeEach(() => {
requireAuthenticatedUser.mockReset()
finalizeAuthenticatedResponse.mockClear()
sync.mockReset()
})

it("returns 401/error if not authenticated as admin", async () => {
requireAuthenticatedUser.mockResolvedValue({
response: NextResponse.json({ message: "Unauthorized" }, { status: 401 }),
})

const response = await POST(buildRequest())
expect(response.status).toBe(401)

Check failure on line 49 in __tests__/api/stellar/sync.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
expect(sync).not.toHaveBeenCalled()
})

it("triggers sync and returns metrics when authenticated as admin", async () => {
const user = buildUser()
requireAuthenticatedUser.mockResolvedValue({ user })
sync.mockResolvedValue({
processed: 5,
duplicates: 2,
errors: 0,
lastCursor: "cursor-123",
})

const response = await POST(buildRequest())
const payload = await response.json()

Check failure on line 64 in __tests__/api/stellar/sync.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.

expect(response.status).toBe(200)

Check failure on line 66 in __tests__/api/stellar/sync.test.ts

View workflow job for this annotation

GitHub Actions / Lint and build

'response' is possibly 'undefined'.
expect(sync).toHaveBeenCalledTimes(1)
expect(payload.success).toBe(true)
expect(payload.processed).toBe(5)
expect(payload.duplicates).toBe(2)
expect(payload.errors).toBe(0)
expect(payload.lastCursor).toBe("cursor-123")
})
})
29 changes: 29 additions & 0 deletions app/api/admin/stellar/sync/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server"
import { finalizeAuthenticatedResponse, requireAuthenticatedUser } from "@/lib/api/route-guard"
import dbConnect from "@/lib/dbConnect"
import { createStellarIndexer } from "@/lib/stellar/indexer"

export async function POST(request: Request) {
try {
const auth = await requireAuthenticatedUser(request, ["admin"])
if ("response" in auth) return auth.response

await dbConnect()

const indexer = createStellarIndexer()
const result = await indexer.sync()

const response = NextResponse.json({
success: true,
processed: result.processed,
duplicates: result.duplicates,
errors: result.errors,
lastCursor: result.lastCursor,
})

return finalizeAuthenticatedResponse(response, auth)
} catch (error) {
console.error("STELLAR_SYNC_API_ERROR", error)
return NextResponse.json({ error: "Failed to sync Stellar indexer" }, { status: 500 })
}
}
Loading
Loading