import type { FlowcoreEventSchema } from "@flowcore/sdk-transformer-core"
import type { Static } from "@sinclair/typebox"
import Elysia from "elysia"
import { v4 as uuid } from "uuid"
export type WebhookTestFixtureServer = Elysia<"">
export type WebhookTestFixtureOptions = {
export type WebhookTestFixtureEndpoint = {
export class WebhookTestFixture {
private readonly app = new Elysia()
private readonly redirects: WebhookTestFixtureEndpoint[] = []
constructor(private readonly options: WebhookTestFixtureOptions) {
this.options = { ...{ secured: false }, ...options }
public addEndpoint(flowType: string, eventType: string, redirectTo?: string): this {
redirectTo: redirectTo ?? `http://localhost:${this.options.transformerPort}/transformers/${flowType}`,
this.app.get("/health", () => "ok")
for (const redirect of this.redirects) {
`/event/${this.options.tenant}/${this.options.dataCore}/${redirect.flowType}/${redirect.eventType}`,
let metadata: Record<string, string> | undefined
if (req.headers["x-flowcore-metadata-json"]) {
metadata = JSON.parse(Buffer.from(req.headers["x-flowcore-metadata-json"], "base64").toString("utf-8"))
const event: Static<typeof FlowcoreEventSchema> = {
aggregator: redirect.flowType,
eventType: redirect.eventType,
validTime: new Date().toISOString(),
metadata: metadata ?? {},
if (this.options.secured) {
if (!metadata?.["audit/user-id"]) {
`Missing audit/user-id in metadata when secured webhook is expected when sending to: /event/${this.options.tenant}/${this.options.dataCore}/${redirect.flowType}/${redirect.eventType}`,
throw new Error("Missing audit/user-id in metadata when secured webhook is expected")
if (metadata["audit/user-id"] && !metadata?.["audit/on-behalf-of"]) {
`Missing audit/on-behalf-of in metadata when secured webhook is expected when sending, as system, to: /event/${this.options.tenant}/${this.options.dataCore}/${redirect.flowType}/${redirect.eventType}`,
throw new Error("Missing audit/on-behalf-of in metadata when secured webhook is expected")
const response = await fetch(`${redirect.redirectTo}`, {
body: JSON.stringify(event),
"Content-Type": "application/json",
"x-secret": this.options.secret,
if (![200, 201].includes(response.status)) {
`Received non-success status code: ${response.status} (${response.statusText}) with body ${await response.text()}`,
console.log(`Error ${redirect.flowType}/${redirect.eventType}: ${error}`)
return this.app.listen(this.options.port)