Ctrl K
ComunidadGuías…

test/shared/booking-intent.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { assertArrayIncludes } from "@std/assert";
import { expect } from "@std/expect";
import { describe, it as test } from "@std/testing/bdd";
import * as v from "valibot";
import {
  BookingIntentSchema,
  type BookingItem,
} from "#shared/booking-intent.ts";

/** A minimal booking line that satisfies every schema rule; spread and override
 * one field per case to probe a single boundary in isolation. Every line is
 * checked the way production checks one — as the single line of a whole
 * booking — so these cases exercise the real parser, not a piece of it. */
const validItem: BookingItem = { e: 1, p: 0, q: 1 };

/** The smallest booking a payment can carry. */
const validIntent = {
  address: "",
  date: null,
  email: "buyer@example.com",
  items: [validItem],
  modifiers: [],
  name: "Buyer",
  phone: "",
  special_instructions: "",
};

const oneLineBooking = (item: Record<string, unknown>) => ({
  ...validIntent,
  items: [item],
});
const accepts = (item: Record<string, unknown>) =>
  expect(v.is(BookingIntentSchema, oneLineBooking(item))).toBe(true);
const rejects = (item: Record<string, unknown>) =>
  expect(v.is(BookingIntentSchema, oneLineBooking(item))).toBe(false);

describe("booking line validation", () => {
  test("accepts a minimal signed line", () => {
    accepts(validItem);
  });

  test("accepts the optional edge tag with both kinds", () => {
    accepts({ ...validItem, k: "p", r: 1 });
    accepts({ ...validItem, k: "g", r: 2 });
  });

  test("the edge tag is a pair: k and r are both present or both absent", () => {
    accepts(validItem); // neither
    rejects({ ...validItem, k: "p" }); // k without r
    rejects({ ...validItem, r: 1 }); // r without k
  });

  test("the paired-edge-tag rejection carries its explanatory message", () => {
    const result = v.safeParse(
      BookingIntentSchema,
      oneLineBooking({ ...validItem, k: "p" }),
    );
    expect(result.success).toBe(false);
    assertArrayIncludes(result.issues?.map((issue) => issue.message) ?? [], [
      "edge tag k and r must both be present or both absent",
    ]);
  });

  test("the listing id (e) must be a positive integer", () => {
    accepts({ ...validItem, e: 1 });
    rejects({ ...validItem, e: 0 });
    rejects({ ...validItem, e: -1 });
    rejects({ ...validItem, e: 1.5 });
  });

  test("the quantity (q) must be a non-negative integer", () => {
    // A signed line may deliberately carry quantity 0 (an admin sentinel or a
    // refunded/deleted-listing placeholder), preserved rather than coerced to 1.
    accepts({ ...validItem, q: 1 });
    accepts({ ...validItem, q: 0 });
    rejects({ ...validItem, q: -1 });
    rejects({ ...validItem, q: 1.5 });
  });

  test("the line total (p) is a signed integer of minor units", () => {
    accepts({ ...validItem, p: -250 });
    accepts({ ...validItem, p: 0 });
    // p is unitPrice * quantity, both integer minor units — a fraction is corrupt.
    rejects({ ...validItem, p: 12.5 });
    rejects({ ...validItem, p: Number.POSITIVE_INFINITY });
    rejects({ ...validItem, p: Number.NaN });
  });

  test("the edge kind (k) only accepts the two literals", () => {
    accepts({ ...validItem, k: "p", r: 1 });
    accepts({ ...validItem, k: "g", r: 1 });
    rejects({ ...validItem, k: "x", r: 1 });
  });

  test("the group id (r) must be a positive integer when present", () => {
    accepts({ ...validItem, k: "p", r: 1 });
    rejects({ ...validItem, k: "p", r: 0 });
    rejects({ ...validItem, k: "p", r: 1.5 });
  });
});

describe("how many lines a booking may carry", () => {
  test("accepts several valid lines", () => {
    expect(
      v.is(BookingIntentSchema, {
        ...validIntent,
        items: [validItem, { ...validItem, e: 2 }],
      }),
    ).toBe(true);
  });

  test("refuses a booking with no lines at all", () => {
    expect(v.is(BookingIntentSchema, { ...validIntent, items: [] })).toBe(
      false,
    );
  });

  test("refuses every line when one of them is bad", () => {
    expect(
      v.is(BookingIntentSchema, {
        ...validIntent,
        items: [validItem, { ...validItem, e: 0 }],
      }),
    ).toBe(false);
  });
});

const acceptsIntent = (extra: Record<string, unknown>) =>
  expect(v.is(BookingIntentSchema, { ...validIntent, ...extra })).toBe(true);
const rejectsIntent = (extra: Record<string, unknown>) =>
  expect(v.is(BookingIntentSchema, { ...validIntent, ...extra })).toBe(false);

describe("what a booking a payment carries may be", () => {
  test("accepts the smallest booking", () => {
    acceptsIntent({});
  });

  // Answers are filed under a listing id. A key in any other shape matches no
  // listing, so those answers would be dropped without a word after paying.
  for (const [name, key, allowed] of [
    ["a listing id", "12", true],
    ["a listing id with a leading zero", "012", false],
    ["a name instead of an id", "listing-12", false],
    ["nothing at all", "", false],
    ["a listing id with spaces around it", " 12 ", false],
  ] as const) {
    test(`${allowed ? "accepts" : "refuses"} answers filed under ${name}`, () => {
      const check = allowed ? acceptsIntent : rejectsIntent;
      check({ listingAnswerIds: { [key]: [1] } });
      check({ listingTextAnswerIds: { [key]: [{ q: 1, s: 1 }] } });
    });
  }

  // A free-text answer whose stored text cannot be found is the one thing a
  // booking forgives: the buyer has already paid, so that single answer is
  // dropped and the rest of the order goes through.
  for (const [name, stringId] of [
    ["was lost on the way back", undefined],
    ["is not an id we could have stored", "not-a-string-id"],
    ["counts from nothing", 0],
    ["is part of a number", 1.5],
  ] as const) {
    test(`forgets a free-text answer whose stored text ${name}`, () => {
      const parsed = v.parse(BookingIntentSchema, {
        ...validIntent,
        listingTextAnswerIds: { "12": [{ q: 1, s: stringId }] },
      });

      expect(parsed.listingTextAnswerIds).toEqual({ "12": [{ q: 1 }] });
    });
  }

  test("keeps a free-text answer that still knows its stored text", () => {
    const parsed = v.parse(BookingIntentSchema, {
      ...validIntent,
      listingTextAnswerIds: { "12": [{ q: 1, s: 5 }] },
    });

    expect(parsed.listingTextAnswerIds).toEqual({ "12": [{ q: 1, s: 5 }] });
  });

  test("accepts paying off a balance with the one line it owes", () => {
    acceptsIntent({ balanceAttendeeId: 1 });
  });

  test("refuses paying off a balance with something added on top", () => {
    // Settling clears the line's own amount, so an add-on would be charged for
    // and then left out of the money record entirely.
    rejectsIntent({ balanceAttendeeId: 1, modifiers: [{ i: 3, q: 1 }] });
  });

  test("refuses paying off a balance that carries a second line", () => {
    // Only the balance line is settled, so a second line would be charged for
    // and then neither booked nor given back.
    rejectsIntent({ balanceAttendeeId: 1, items: [validItem, validItem] });
  });

  test("refuses a deposit nobody can read", () => {
    rejectsIntent({ reservationAmount: "half of it" });
  });

  test("accepts a deposit written as a share of the price", () => {
    acceptsIntent({ reservationAmount: "10%" });
  });
});