| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- import { describe, expect, test } from "bun:test"
- import { Effect, Exit } from "effect"
- import { existsSync, readdirSync, writeFileSync } from "node:fs"
- import type { Interaction } from "../src/cassette/model"
- import { HttpRecorder } from "../src"
- import { Service, hasCassetteSync, memory } from "../src/cassette/store"
- import { cassetteLayer } from "../src/http/recorder"
- import { failureText, post, readCassette, runFileCassette, seedCassetteDirectory, tempDirectory } from "./support"
- describe("cassette", () => {
- test("UnsafeCassetteError fails the request when a recording would write a known secret", async () => {
- using server = Bun.serve({
- port: 0,
- fetch: () => new Response("Bearer abcdefghijklmnopqrstuvwxyz1234"),
- })
- const url = `http://127.0.0.1:${server.port}/leaky`
- using directory = tempDirectory("http-recorder-unsafe-")
- const exit = await Effect.runPromise(
- Effect.exit(
- post(url, { ok: true }).pipe(
- Effect.provide(
- cassetteLayer("unsafe-record", {
- directory: directory.path,
- mode: "record",
- }),
- ),
- ),
- ),
- )
- expect(Exit.isFailure(exit)).toBe(true)
- expect(failureText(exit)).toContain("contains possible secrets")
- expect(existsSync(`${directory.path}/unsafe-record.json`)).toBe(false)
- })
- test("failed memory appends leave cassette state unchanged", async () => {
- await Effect.runPromise(
- Effect.gen(function* () {
- const cassette = yield* Service
- const interaction: Interaction = {
- transport: "http",
- request: {
- method: "GET",
- url: "https://example.test",
- headers: {},
- body: "",
- },
- response: { status: 200, headers: {}, body: "safe" },
- }
- yield* cassette.append("transactional", interaction)
- yield* cassette
- .append("transactional", {
- ...interaction,
- response: {
- ...interaction.response,
- body: "Bearer abcdefghijklmnopqrstuvwxyz1234",
- },
- })
- .pipe(Effect.flip)
- expect(yield* cassette.read("transactional")).toEqual([interaction])
- }).pipe(Effect.provide(memory())),
- )
- })
- test("concurrent file appends preserve every interaction", async () => {
- using directory = tempDirectory("http-recorder-concurrent-")
- await runFileCassette(
- directory.path,
- Effect.gen(function* () {
- const cassette = yield* Service
- yield* Effect.forEach(
- Array.from({ length: 20 }, (_, index) => index),
- (index) =>
- cassette.append("concurrent", {
- transport: "http",
- request: {
- method: "GET",
- url: `https://example.test/${index}`,
- headers: {},
- body: "",
- },
- response: { status: 200, headers: {}, body: String(index) },
- }),
- { concurrency: "unbounded" },
- )
- }),
- )
- const cassette = readCassette(`${directory.path}/concurrent.json`)
- expect(cassette.interactions).toHaveLength(20)
- expect(readdirSync(directory.path).filter((file) => file.endsWith(".tmp"))).toEqual([])
- })
- test("generated metadata cannot be overridden", async () => {
- using directory = tempDirectory("http-recorder-metadata-")
- await runFileCassette(
- directory.path,
- Effect.gen(function* () {
- const cassette = yield* Service
- yield* cassette.append(
- "metadata",
- {
- transport: "http",
- request: { method: "GET", url: "https://example.test", headers: {}, body: "" },
- response: { status: 200, headers: {}, body: "safe" },
- },
- { name: "wrong", recordedAt: "wrong" },
- )
- }),
- )
- const cassette = readCassette(`${directory.path}/metadata.json`)
- expect(cassette.metadata?.name).toBe("metadata")
- expect(cassette.metadata?.recordedAt).not.toBe("wrong")
- })
- test("reports malformed cassettes as invalid", async () => {
- using directory = tempDirectory("http-recorder-invalid-")
- writeFileSync(`${directory.path}/invalid.json`, "{not-json")
- const error = await runFileCassette(
- directory.path,
- Effect.gen(function* () {
- const cassette = yield* Service
- return yield* cassette.read("invalid").pipe(Effect.flip)
- }),
- )
- expect(error._tag).toBe("InvalidCassetteError")
- })
- test("rejects cassette paths outside the recordings directory", () => {
- using directory = tempDirectory("http-recorder-path-")
- expect(() => hasCassetteSync("../outside", { directory: directory.path })).toThrow("Invalid cassette name")
- expect(() => hasCassetteSync("C:\\outside", { directory: directory.path })).toThrow("Invalid cassette name")
- })
- test("public cassette lifecycle helpers check and remove a recording", async () => {
- using directory = tempDirectory("http-recorder-lifecycle-")
- const options = { directory: directory.path }
- expect(HttpRecorder.hasCassetteSync("nested/example", options)).toBe(false)
- await seedCassetteDirectory(directory.path, "nested/example", [
- {
- transport: "http",
- request: { method: "GET", url: "https://example.test", headers: {}, body: "" },
- response: { status: 200, headers: {}, body: "safe" },
- },
- ])
- expect(HttpRecorder.hasCassetteSync("nested/example", options)).toBe(true)
- HttpRecorder.removeCassetteSync("nested/example", options)
- expect(HttpRecorder.hasCassetteSync("nested/example", options)).toBe(false)
- expect(() => HttpRecorder.removeCassetteSync("nested/example", options)).not.toThrow()
- expect(() => HttpRecorder.removeCassetteSync("../outside", options)).toThrow("Invalid cassette name")
- })
- test("Cassette.list enumerates recorded cassette names", async () => {
- using directory = tempDirectory("http-recorder-list-")
- await seedCassetteDirectory(directory.path, "alpha/one", [
- {
- transport: "http",
- request: {
- method: "GET",
- url: "https://x.test/a",
- headers: {},
- body: "",
- },
- response: { status: 200, headers: {}, body: "a" },
- },
- ])
- await seedCassetteDirectory(directory.path, "beta", [
- {
- transport: "http",
- request: {
- method: "GET",
- url: "https://x.test/b",
- headers: {},
- body: "",
- },
- response: { status: 200, headers: {}, body: "b" },
- },
- ])
- const names = await runFileCassette(
- directory.path,
- Effect.gen(function* () {
- const cassette = yield* Service
- return yield* cassette.list()
- }),
- )
- expect(names).toEqual(["alpha/one", "beta"])
- })
- })
|