All files / src normalize.ts

100% Statements 14/14
100% Branches 10/10
100% Functions 5/5
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64                                          9x 8x                             6x 5x 3x   2x     5x       3x 3x           24x         22x 22x    
import type { HttpScenario } from '@ethercalc/shared/oracle-scenarios';
 
/**
 * Per-scenario post-record normalization hooks. Applied after the
 * recorder writes the raw response into the scenario's `expect`
 * field, before the JSON artifact is persisted.
 *
 * We keep this map inside the oracle-harness (rather than widening
 * `HttpScenario` in `@ethercalc/shared`) because the hooks are oracle-
 * replay machinery that never needs to travel to the client or the
 * worker. Scenarios identify themselves by `scenario.name`.
 *
 * The typical reason to register a hook: an endpoint that embeds a
 * UUID/random value in a header or body — e.g. `/_new` returning a
 * different `Location: /<uuid>` every time. Rewrite the volatile
 * value to a `re:` pattern that matches any legitimate output.
 */
export type NormalizeHook = (scenario: HttpScenario) => HttpScenario;
 
/** Rewrite a header field in-place on a recorded scenario. */
export function overrideHeader(scenario: HttpScenario, name: string, value: string): HttpScenario {
  if (!scenario.expect) return scenario;
  return {
    ...scenario,
    expect: {
      ...scenario.expect,
      headers: { ...scenario.expect.headers, [name.toLowerCase()]: value },
    },
  };
}
 
/**
 * Normalize `Content-Length` to a regex (any positive integer) when the
 * body is matcher-ignored — we're not asserting on the body anyway,
 * and its exact length varies with the volatile payload.
 */
export function relaxContentLength(scenario: HttpScenario): HttpScenario {
  if (!scenario.expect) return scenario;
  if ('content-length' in scenario.expect.headers) {
    return overrideHeader(scenario, 'content-length', 're:^\\d+$');
  }
  return scenario;
}
 
export const NORMALIZERS: Readonly<Record<string, NormalizeHook>> = {
  // `/_new` 302s to `/<12-char-base36-uuid>` (see `new-room` in main.ls).
  // The Location and body both embed the random id; relax both.
  'misc/get-new-redirect': (scenario) => {
    const withLocation = overrideHeader(scenario, 'location', 're:^/[a-z0-9]{12}$');
    return relaxContentLength(withLocation);
  },
};
 
/** Look up a normalizer by scenario name; return `null` if none registered. */
export function getNormalizer(name: string): NormalizeHook | null {
  return NORMALIZERS[name] ?? null;
}
 
/** Apply the registered normalizer if any; otherwise return scenario unchanged. */
export function applyNormalizer(scenario: HttpScenario): HttpScenario {
  const hook = getNormalizer(scenario.name);
  return hook ? hook(scenario) : scenario;
}