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
97 changes: 97 additions & 0 deletions app/api/governance-indexer/[...path]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import { GET } from "./route";

function context(path: string[]) {
return { params: Promise.resolve({ path }) };
}

describe("governance indexer proxy", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});

it("returns 503 when the indexer URL is not configured", async () => {
vi.stubEnv("GOVERNANCE_INDEXER_URL", "");

const response = await GET(
new Request(
"https://example.test/api/governance-indexer/api/tally/stats"
),
context(["api", "tally", "stats"])
);

expect(response.status).toBe(503);
await expect(response.json()).resolves.toEqual({
error: "Governance indexer is not configured.",
});
});

it("proxies the encoded path and query string to the configured upstream", async () => {
vi.stubEnv("GOVERNANCE_INDEXER_URL", "https://indexer.example.test/");
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
new Response(JSON.stringify({ ok: true }), {
status: 200,
statusText: "OK",
})
);

const response = await GET(
new Request(
"https://example.test/api/governance-indexer/api/tally/delegates?query=dao&limit=10"
),
context(["api", "tally", "delegates"])
);

expect(response.status).toBe(200);
expect(String(fetchMock.mock.calls[0][0])).toBe(
"https://indexer.example.test/api/tally/delegates?query=dao&limit=10"
);
expect(fetchMock.mock.calls[0][1]).toMatchObject({
method: "GET",
headers: { accept: "application/json" },
});
expect(response.headers.get("content-type")).toBe("application/json");
expect(response.headers.get("cache-control")).toBe(
"public, s-maxage=30, stale-while-revalidate=300"
);
});

it("forwards upstream status codes", async () => {
vi.stubEnv("GOVERNANCE_INDEXER_URL", "https://indexer.example.test");
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
new Response(JSON.stringify({ error: "missing" }), {
status: 404,
statusText: "Not Found",
})
);

const response = await GET(
new Request(
"https://example.test/api/governance-indexer/api/tally/missing"
),
context(["api", "tally", "missing"])
);

expect(response.status).toBe(404);
expect(response.statusText).toBe("Not Found");
});

it("returns 502 for upstream failures", async () => {
vi.stubEnv("GOVERNANCE_INDEXER_URL", "https://indexer.example.test");
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("offline"));

const response = await GET(
new Request(
"https://example.test/api/governance-indexer/api/tally/stats"
),
context(["api", "tally", "stats"])
);

expect(response.status).toBe(502);
await expect(response.json()).resolves.toEqual({
error: "Indexer upstream error.",
});
});
});
60 changes: 60 additions & 0 deletions app/api/governance-indexer/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export const dynamic = "force-dynamic";

const FETCH_TIMEOUT_MS = 10_000;

type RouteContext = {
params: Promise<{ path: string[] }>;
};

function getIndexerUrl(): string | null {
// eslint-disable-next-line no-process-env
const value = process.env.GOVERNANCE_INDEXER_URL?.trim();
return value ? value.replace(/\/+$/, "") : null;
}

export async function GET(
request: Request,
context: RouteContext
): Promise<Response> {
const { path } = await context.params;
const indexerUrl = getIndexerUrl();
if (!indexerUrl) {
return Response.json(
{ error: "Governance indexer is not configured." },
{ status: 503 }
);
}

const inboundUrl = new URL(request.url);
const encodedPath = path.map(encodeURIComponent).join("/");
const upstreamUrl = new URL(`${indexerUrl}/${encodedPath}`);
upstreamUrl.search = inboundUrl.search;

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

try {
const upstream = await fetch(upstreamUrl, {
method: "GET",
signal: controller.signal,
headers: { accept: "application/json" },
});

const headers = new Headers();
headers.set("content-type", "application/json");
headers.set(
"cache-control",
"public, s-maxage=30, stale-while-revalidate=300"
);

return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers,
});
} catch {
return Response.json({ error: "Indexer upstream error." }, { status: 502 });
} finally {
clearTimeout(timer);
}
}
162 changes: 0 additions & 162 deletions app/tally-data/tally-zero.sqlite/route.test.ts

This file was deleted.

Loading