spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { readFileSync } from "node:fs";2import { join } from "node:path";3import type { RawObservation } from "@market-atlas/market-model";4import { NormalizedObservationSchema } from "@market-atlas/market-model";5import { HttpClient } from "./http.js";6import { RateLimiter } from "./ratelimit.js";7import type { ConnectorContext, ConnectorDefinition, ConnectorLogger, NormalizedBatch } from "./types.js";8import { ManagedWebSocket, type ManagedWebSocketOptions } from "./ws.js";910/** Load a fixture file (JSON parsed when the extension is .json, raw text otherwise). */11export function loadFixture(dir: string, name: string): unknown {12 const text = readFileSync(join(dir, name), "utf8");13 return name.endsWith(".json") ? JSON.parse(text) : text;14}1516export function rawFromFixture(def: ConnectorDefinition, kind: string, payload: unknown, receivedAt = 1_789_200_000_000): RawObservation {17 return { connectorId: def.metadata.id, sourceId: def.metadata.sourceId, kind, payload, receivedAt };18}1920/** Runs normalize() on a payload and validates every observation against the canonical schema. */21export function normalizeFixture(def: ConnectorDefinition, kind: string, payload: unknown): NormalizedBatch {22 const batch = def.normalize(rawFromFixture(def, kind, payload));23 for (const o of batch.observations) {24 const res = NormalizedObservationSchema.safeParse(o);25 if (!res.success) throw new Error(`invalid observation from ${def.metadata.id}: ${res.error.message}`);26 }27 return batch;28}2930const silentLogger: ConnectorLogger = { debug() {}, info() {}, warn() {}, error() {} };3132/** Offline connector context for tests: no network unless a fetchImpl is provided. */33export function makeTestContext(34 def: ConnectorDefinition,35 opts: { fetchImpl?: typeof fetch; marketOpen?: boolean; secrets?: Record<string, string>; symbols?: string[] } = {},36): ConnectorContext & { emitted: RawObservation[]; errors: unknown[]; sockets: ManagedWebSocket[] } {37 const emitted: RawObservation[] = [];38 const errors: unknown[] = [];39 const sockets: ManagedWebSocket[] = [];40 const store = new Map<string, unknown>();41 const http = new HttpClient({42 userAgent: "MarketAtlas-test/0.1",43 limiter: new RateLimiter({ ratePerSec: 1000 }),44 fetchImpl:45 opts.fetchImpl ??46 (async () => {47 throw new Error("network disabled in tests");48 }),49 });50 return {51 connectorId: def.metadata.id,52 logger: silentLogger,53 http,54 state: {55 async get(k) {56 return store.get(k) as never;57 },58 async set(k, v) {59 store.set(k, v);60 },61 },62 emit(r) {63 emitted.push(...(Array.isArray(r) ? r : [r]));64 },65 openWebSocket(url: string, o: ManagedWebSocketOptions) {66 const s = new ManagedWebSocket(url, o);67 sockets.push(s);68 return s;69 },70 isMarketOpen: () => opts.marketOpen ?? true,71 watchedSymbols: () => opts.symbols ?? def.defaultSymbols ?? [],72 reportError: (e) => errors.push(e),73 secret: (n) => opts.secrets?.[n],74 now: () => Date.now(),75 emitted,76 errors,77 sockets,78 };79}8081/** Build a fake fetch that serves canned responses by URL substring. */82export function fakeFetch(routes: Record<string, string | { status?: number; body: string; headers?: Record<string, string> }>): typeof fetch {83 return (async (input: string | URL | Request) => {84 const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;85 const key = Object.keys(routes).find((k) => url.includes(k));86 if (!key) return new Response("not found", { status: 404 });87 const r = routes[key]!;88 const spec = typeof r === "string" ? { body: r } : r;89 return new Response(spec.body, { status: spec.status ?? 200, headers: spec.headers ?? {} });90 }) as typeof fetch;91}92