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
101 changes: 101 additions & 0 deletions tests/unit/config/route-body-limits.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { ROUTE_BODY_LIMITS } from "../../../src/config/route-body-limits.js";

/**
* #605: per-route body size limits and their wiring into the routes that
* reference them.
*
* "Route matching" is verified by grepping the route modules that import
* ROUTE_BODY_LIMITS for the exact key each route is supposed to be using,
* rather than booting the whole Fastify app — the config module has no
* knowledge of routes itself (it's the routes that reach into it), so this
* is the narrowest check that still exercises the real wiring.
*/
function readSource(relativePath: string): string {
const url = new URL(`../../../${relativePath}`, import.meta.url);
return readFileSync(fileURLToPath(url), "utf8");
}

const KB = 1024;
const MB = 1024 * KB;

describe("ROUTE_BODY_LIMITS (#605)", () => {
describe("limit values", () => {
it("sets profileUpdate to 10KB", () => {
expect(ROUTE_BODY_LIMITS.profileUpdate).toBe(10 * KB);
});

it("sets courseImport to 5MB", () => {
expect(ROUTE_BODY_LIMITS.courseImport).toBe(5 * MB);
});

it("sets quizAuthoring to 100KB", () => {
expect(ROUTE_BODY_LIMITS.quizAuthoring).toBe(100 * KB);
});

it("sets webhookCreate to 10KB", () => {
expect(ROUTE_BODY_LIMITS.webhookCreate).toBe(10 * KB);
});

it("orders limits from smallest to largest as profileUpdate/webhookCreate <= quizAuthoring <= courseImport", () => {
expect(ROUTE_BODY_LIMITS.profileUpdate).toBeLessThanOrEqual(ROUTE_BODY_LIMITS.quizAuthoring);
expect(ROUTE_BODY_LIMITS.webhookCreate).toBeLessThanOrEqual(ROUTE_BODY_LIMITS.quizAuthoring);
expect(ROUTE_BODY_LIMITS.quizAuthoring).toBeLessThanOrEqual(ROUTE_BODY_LIMITS.courseImport);
});

it("exposes only known, positive numeric limits", () => {
const values = Object.values(ROUTE_BODY_LIMITS);
expect(values.length).toBeGreaterThan(0);
for (const value of values) {
expect(typeof value).toBe("number");
expect(value).toBeGreaterThan(0);
}
});

it("is readonly at the type level (as const) so limits can't drift at runtime", () => {
// `as const` only enforces this at compile time; this asserts the
// object itself isn't frozen either way (documents current behavior)
// without depending on Object.isFrozen, which would be a false
// negative here since the source doesn't call Object.freeze.
expect(ROUTE_BODY_LIMITS).toEqual({
profileUpdate: 10 * KB,
courseImport: 5 * MB,
quizAuthoring: 100 * KB,
webhookCreate: 10 * KB,
});
});
});

describe("route matching", () => {
it("wires profileUpdate into the user profile update route", () => {
const source = readSource("src/modules/users/user.routes.ts");
expect(source).toContain("ROUTE_BODY_LIMITS.profileUpdate");
});

it("wires webhookCreate into the admin webhook creation route", () => {
const source = readSource("src/modules/admin/webhook.routes.ts");
expect(source).toContain("ROUTE_BODY_LIMITS.webhookCreate");
});

it("wires quizAuthoring into the admin quiz authoring routes", () => {
const source = readSource("src/modules/courses/admin-course.routes.ts");
const matches = source.match(/ROUTE_BODY_LIMITS\.quizAuthoring/g) ?? [];
expect(matches.length).toBeGreaterThan(0);
});

it("does not reference courseImport from any route yet (no current consumer)", () => {
// Documents the current state rather than asserting a requirement:
// if a future route starts using courseImport, this test should be
// updated alongside it rather than silently going stale.
const userRoutes = readSource("src/modules/users/user.routes.ts");
const webhookRoutes = readSource("src/modules/admin/webhook.routes.ts");
const courseRoutes = readSource("src/modules/courses/admin-course.routes.ts");
const anyReferencesCourseImport = [userRoutes, webhookRoutes, courseRoutes].some((src) =>
src.includes("ROUTE_BODY_LIMITS.courseImport"),
);
expect(anyReferencesCourseImport).toBe(false);
});
});
});
145 changes: 145 additions & 0 deletions tests/unit/config/stellar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import * as StellarSdk from "@stellar/stellar-sdk";

/**
* #604: Stellar SDK configuration — network passphrase, server
* construction, and platform keypair loading.
*
* src/config/stellar.ts reads the memoized `config` singleton from
* src/config/index.ts, which itself reads process.env once at import time.
* Each case resets the module registry and re-imports both modules with a
* fresh env, following the same pattern as tests/unit/config/cors-origins.test.ts.
*/
async function loadStellarConfig(env: Record<string, string | undefined>) {
vi.resetModules();
for (const [key, value] of Object.entries(env)) {
if (value === undefined) {
vi.stubEnv(key, "");
delete process.env[key];
} else {
vi.stubEnv(key, value);
}
}
return import("../../../src/config/stellar.js");
}

// A valid Stellar secret key (not tied to any funded account; used only to
// exercise Keypair.fromSecret's checksum validation in tests).
const VALID_SECRET = "SABFGDEMCBXDUQKC575F2W5QSJ2CLYUPQVCQC4GNSONWXRTWE4ZM3S7Q";

describe("Stellar config (#604)", () => {
beforeEach(() => {
vi.unstubAllEnvs();
});

afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});

describe("getNetworkPassphrase", () => {
it("returns the testnet passphrase when STELLAR_NETWORK is testnet", async () => {
const { getNetworkPassphrase } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_NETWORK: "testnet",
});

expect(getNetworkPassphrase()).toBe(StellarSdk.Networks.TESTNET);
});

it("returns the public/mainnet passphrase when STELLAR_NETWORK is mainnet", async () => {
const { getNetworkPassphrase } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_NETWORK: "mainnet",
});

expect(getNetworkPassphrase()).toBe(StellarSdk.Networks.PUBLIC);
});

it("defaults to the testnet passphrase when STELLAR_NETWORK is unset", async () => {
const { getNetworkPassphrase } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_NETWORK: undefined,
});

expect(getNetworkPassphrase()).toBe(StellarSdk.Networks.TESTNET);
});
});

describe("getHorizonServer", () => {
it("constructs a Horizon server pointed at the configured URL", async () => {
const { getHorizonServer } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org",
});

const server = getHorizonServer();
expect(server).toBeInstanceOf(StellarSdk.Horizon.Server);
expect(server.serverURL.toString()).toBe("https://horizon-testnet.stellar.org/");
});

it("returns a new Horizon server instance on each call", async () => {
const { getHorizonServer } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org",
});

expect(getHorizonServer()).not.toBe(getHorizonServer());
});
});

describe("getSorobanServer", () => {
it("constructs a Soroban RPC server pointed at the configured URL", async () => {
const { getSorobanServer } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_SOROBAN_RPC_URL: "https://soroban-testnet.stellar.org",
});

const server = getSorobanServer();
expect(server).toBeInstanceOf(StellarSdk.rpc.Server);
});

it("returns a new Soroban server instance on each call", async () => {
const { getSorobanServer } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_SOROBAN_RPC_URL: "https://soroban-testnet.stellar.org",
});

expect(getSorobanServer()).not.toBe(getSorobanServer());
});
});

describe("getPlatformKeypair", () => {
it("loads a Keypair from STELLAR_PLATFORM_SECRET", async () => {
const { getPlatformKeypair } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_PLATFORM_SECRET: VALID_SECRET,
});

const keypair = getPlatformKeypair();
expect(keypair).toBeInstanceOf(StellarSdk.Keypair);
expect(keypair.secret()).toBe(VALID_SECRET);
});

it("caches the keypair as a singleton across calls", async () => {
const { getPlatformKeypair } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_PLATFORM_SECRET: VALID_SECRET,
});

const first = getPlatformKeypair();
const second = getPlatformKeypair();
expect(first).toBe(second);
});

it("derives the expected public key from the secret", async () => {
const { getPlatformKeypair } = await loadStellarConfig({
NODE_ENV: "test",
STELLAR_PLATFORM_SECRET: VALID_SECRET,
});

const expected = StellarSdk.Keypair.fromSecret(VALID_SECRET).publicKey();
expect(getPlatformKeypair().publicKey()).toBe(expected);
});
});
});