cursor.test.js 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /* @vitest-environment node */
  2. import { describe, it, expect } from "vitest";
  3. import { ApiError } from "@/lib/api/errors";
  4. import { encodeCursor, decodeCursor } from "./cursor.js";
  5. describe("lib/search/cursor", () => {
  6. it("decodeCursor(null) returns the default cursor", () => {
  7. expect(decodeCursor(null)).toEqual({
  8. v: 1,
  9. mode: "sync",
  10. offset: 0,
  11. contextId: null,
  12. });
  13. });
  14. it('decodeCursor("") treats empty string like missing cursor (default)', () => {
  15. expect(decodeCursor("")).toEqual({
  16. v: 1,
  17. mode: "sync",
  18. offset: 0,
  19. contextId: null,
  20. });
  21. });
  22. it("encodeCursor + decodeCursor roundtrip works", () => {
  23. const encoded = encodeCursor({ v: 1, mode: "sync", offset: 10 });
  24. expect(typeof encoded).toBe("string");
  25. expect(encoded.length).toBeGreaterThan(5);
  26. const decoded = decodeCursor(encoded);
  27. expect(decoded).toEqual({
  28. v: 1,
  29. mode: "sync",
  30. offset: 10,
  31. contextId: null,
  32. });
  33. });
  34. it("keeps contextId when provided", () => {
  35. const encoded = encodeCursor({
  36. v: 1,
  37. mode: "sync",
  38. offset: 5,
  39. contextId: "ctx-123",
  40. });
  41. expect(decodeCursor(encoded)).toEqual({
  42. v: 1,
  43. mode: "sync",
  44. offset: 5,
  45. contextId: "ctx-123",
  46. });
  47. });
  48. it("encodeCursor rejects invalid payloads", () => {
  49. expect(() => encodeCursor(null)).toThrow(ApiError);
  50. expect(() => encodeCursor({})).toThrow(ApiError);
  51. expect(() => encodeCursor({ offset: -1 })).toThrow(ApiError);
  52. });
  53. it("decodeCursor rejects whitespace-only cursor", () => {
  54. expect(() => decodeCursor(" ")).toThrow(ApiError);
  55. });
  56. it("decodeCursor rejects non-JSON payloads (deterministic)", () => {
  57. // Using a valid base64url string that decodes to non-JSON ensures deterministic failure:
  58. // JSON.parse("not-json") must throw => decodeCursor must throw ApiError.
  59. const bad = Buffer.from("not-json", "utf8").toString("base64url");
  60. expect(() => decodeCursor(bad)).toThrow(ApiError);
  61. });
  62. it("decodeCursor rejects wrong version", () => {
  63. const bad = Buffer.from(
  64. JSON.stringify({ v: 2, mode: "sync", offset: 0 }),
  65. "utf8"
  66. ).toString("base64url");
  67. expect(() => decodeCursor(bad)).toThrow(ApiError);
  68. });
  69. it("decodeCursor rejects negative offsets", () => {
  70. const bad = Buffer.from(
  71. JSON.stringify({ v: 1, mode: "sync", offset: -1 }),
  72. "utf8"
  73. ).toString("base64url");
  74. expect(() => decodeCursor(bad)).toThrow(ApiError);
  75. });
  76. });