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
4 changes: 2 additions & 2 deletions src/scripts/replay-bridge-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ const main = async () => {
: undefined

for await (const event of listAllEvents({
start_date: args.start,
end_date: args.end,
start: args.start,
end: args.end,
event_type: bridgeFilter,
})) {
const replayEventObject: BridgeReplayEventEnvelope = {
Expand Down
16 changes: 11 additions & 5 deletions src/services/bridge/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,6 @@ export interface BridgeWebhookEvent {
}

export interface ListEventsParams {
start_date?: string
end_date?: string
event_type?: string
after?: string
page_size?: number
Expand Down Expand Up @@ -563,16 +561,24 @@ export class BridgeClient {
export default new BridgeClient()

export async function* listAllEvents(
params?: Omit<ListEventsParams, "after">,
params?: Omit<ListEventsParams, "after"> & { start?: string; end?: string },
): AsyncGenerator<BridgeWebhookEvent> {
const client = new BridgeClient()
const startMs = params?.start ? new Date(params.start).getTime() : -Infinity
const endMs = params?.end ? new Date(params.end).getTime() : Infinity

// Strip start/end — Bridge /webhook_events only supports cursor params; filter locally.
const { start: _s, end: _e, ...apiParams } = params ?? {}

let cursor: string | undefined
do {
const page = await client.listEvents({ ...params, after: cursor, page_size: 100 })
const page = await client.listEvents({ ...apiParams, after: cursor, page_size: 100 })

for (const event of page.data) {
yield event
const eventMs = new Date(event.created_at).getTime()
if (eventMs >= startMs && eventMs <= endMs) {
yield event
}
}

cursor = page.has_more ? page.cursor : undefined
Expand Down
36 changes: 20 additions & 16 deletions test/flash/unit/services/bridge/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,30 +119,34 @@ describe("listAllEvents", () => {
)
})

it("forwards start_date, end_date and event_type filters to every page", async () => {
listEventsSpy
.mockResolvedValueOnce({ data: [makeEvent("e1")], has_more: true, cursor: "c1" })
.mockResolvedValueOnce({
data: [makeEvent("e2")],
has_more: false,
cursor: undefined,
})
it("filters events locally by start/end window and does not forward start/end to Bridge API", async () => {
const inWindow = makeEvent("e1") // created_at: "2026-05-01T10:00:00Z" — inside window
const tooEarly = { ...makeEvent("e2"), created_at: "2026-04-30T23:59:59Z" }

const params = {
start_date: "2026-05-01T00:00:00Z",
end_date: "2026-05-02T00:00:00Z",
event_type: "transfer.completed",
}
listEventsSpy
.mockResolvedValueOnce({ data: [inWindow, tooEarly], has_more: true, cursor: "c1" })
.mockResolvedValueOnce({ data: [makeEvent("e3")], has_more: false, cursor: undefined })

const drained: BridgeWebhookEvent[] = []
for await (const event of listAllEvents(params)) {
for await (const event of listAllEvents({
start: "2026-05-01T00:00:00Z",
end: "2026-05-02T00:00:00Z",
event_type: "transfer.completed",
})) {
drained.push(event)
}

expect(drained).toHaveLength(2)
// e2 is before the window start — only e1 and e3 pass through
expect(drained.map((e) => e.id)).toEqual(["e1", "e3"])
expect(listEventsSpy).toHaveBeenCalledTimes(2)
for (const call of listEventsSpy.mock.calls) {
expect(call[0]).toMatchObject(params)
// start/end must NOT be sent to Bridge — it only understands cursor params
expect(call[0]).not.toHaveProperty("start")
expect(call[0]).not.toHaveProperty("end")
expect(call[0]).not.toHaveProperty("start_date")
expect(call[0]).not.toHaveProperty("end_date")
// event_type is still forwarded (mapped to category inside listEvents)
expect(call[0]).toMatchObject({ event_type: "transfer.completed" })
}
})

Expand Down
26 changes: 13 additions & 13 deletions test/flash/unit/services/bridge/webhook-server/replay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jest.mock("@services/logger", () => ({
}))

jest.mock("@services/mongoose/bridge-replay-log", () => ({
createBridgeReplayLog: jest.fn(),
createBridgeReplay: jest.fn(),
}))

jest.mock("@services/bridge/webhook-server/routes/deposit", () => ({
Expand Down Expand Up @@ -173,7 +173,7 @@ describe("replayHandler", () => {
]

beforeEach(() => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-001" })
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-001" })
})

it("routes outbound withdrawal payment_processed replay to transfer handler", async () => {
Expand All @@ -182,7 +182,7 @@ describe("replayHandler", () => {
;(res.json as jest.Mock)({ status: "success" })
return Promise.resolve(res)
})
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-wd-001" })
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-wd-001" })

const res = makeRes()
await replayHandler(
Expand Down Expand Up @@ -227,7 +227,7 @@ describe("replayHandler", () => {

describe("dry_run mode", () => {
it("returns 200 without calling any handler", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({
id: "log-dry-001",
})

Expand All @@ -241,20 +241,20 @@ describe("replayHandler", () => {
})

it("persists a dry-run log entry with httpStatus 0", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({
id: "log-dry-002",
})

const res = makeRes()
await replayHandler(makeReq({ ...BASE_BODY, dry_run: true }), res)

expect(ReplayLog.createBridgeReplayLog).toHaveBeenCalledWith(
expect(ReplayLog.createBridgeReplay).toHaveBeenCalledWith(
expect.objectContaining({ httpStatus: 0, dryRun: true }),
)
})

it("returns 500 when dry-run log creation fails", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue(
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue(
new Error("db error"),
)

Expand All @@ -277,7 +277,7 @@ describe("replayHandler", () => {
})

it("returns the handler's status code and response body", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({
id: "log-live-001",
})

Expand All @@ -290,14 +290,14 @@ describe("replayHandler", () => {
})

it("persists a replay log with triage context (operator + time window)", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({
id: "log-live-002",
})

const res = makeRes()
await replayHandler(makeReq(BASE_BODY), res)

expect(ReplayLog.createBridgeReplayLog).toHaveBeenCalledWith(
expect(ReplayLog.createBridgeReplay).toHaveBeenCalledWith(
expect.objectContaining({
operator: "ops@example.com",
timeWindowStart: new Date("2026-05-01T00:00:00Z"),
Expand All @@ -310,7 +310,7 @@ describe("replayHandler", () => {
})

it("includes log_id in the response so ops can trace the replay", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({
id: "log-trace-007",
})

Expand All @@ -322,7 +322,7 @@ describe("replayHandler", () => {
})

it("returns 500 when log creation fails after a successful handler run", async () => {
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue(
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue(
new Error("mongo down"),
)

Expand All @@ -340,7 +340,7 @@ describe("replayHandler", () => {
return Promise.resolve(res)
},
)
;(ReplayLog.createBridgeReplayLog as jest.Mock).mockResolvedValue({ id: "log-4xx" })
;(ReplayLog.createBridgeReplay as jest.Mock).mockResolvedValue({ id: "log-4xx" })

const res = makeRes()
await replayHandler(makeReq(BASE_BODY), res)
Expand Down