Преглед на файлове

feat(tests): add unit tests for storageErrors and cursor functionality

Code_Uwe преди 2 седмици
родител
ревизия
db216fb536
променени са 2 файла, в които са добавени 161 реда и са изтрити 0 реда
  1. 71 0
      lib/api/storageErrors.test.js
  2. 90 0
      lib/search/cursor.test.js

+ 71 - 0
lib/api/storageErrors.test.js

@@ -0,0 +1,71 @@
+/* @vitest-environment node */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+vi.mock("node:fs/promises", () => ({
+	default: {
+		access: vi.fn(),
+	},
+}));
+
+import fs from "node:fs/promises";
+import { ApiError } from "./errors.js";
+import { isFsNotFoundError, mapStorageReadError } from "./storageErrors.js";
+
+describe("lib/api/storageErrors", () => {
+	const ORIGINAL = process.env.NAS_ROOT_PATH;
+
+	beforeEach(() => {
+		vi.clearAllMocks();
+		process.env.NAS_ROOT_PATH = "/mnt/niederlassungen";
+	});
+
+	afterEach(() => {
+		process.env.NAS_ROOT_PATH = ORIGINAL;
+	});
+
+	it("isFsNotFoundError detects ENOENT/ENOTDIR", () => {
+		expect(isFsNotFoundError({ code: "ENOENT" })).toBe(true);
+		expect(isFsNotFoundError({ code: "ENOTDIR" })).toBe(true);
+		expect(isFsNotFoundError({ code: "EACCES" })).toBe(false);
+		expect(isFsNotFoundError(null)).toBe(false);
+	});
+
+	it("maps ENOENT to 404 when NAS root is accessible", async () => {
+		fs.access.mockResolvedValue(undefined);
+
+		const err = Object.assign(new Error("nope"), { code: "ENOENT" });
+		const apiErr = await mapStorageReadError(err, {
+			details: { branch: "NL01" },
+		});
+
+		expect(apiErr).toBeInstanceOf(ApiError);
+		expect(apiErr.status).toBe(404);
+		expect(apiErr.code).toBe("FS_NOT_FOUND");
+		expect(apiErr.details).toEqual({ branch: "NL01" });
+	});
+
+	it("maps ENOENT to 500 when NAS root is NOT accessible", async () => {
+		fs.access.mockRejectedValue(new Error("no access"));
+
+		const err = Object.assign(new Error("nope"), { code: "ENOENT" });
+		const apiErr = await mapStorageReadError(err, {
+			details: { branch: "NL01" },
+		});
+
+		expect(apiErr).toBeInstanceOf(ApiError);
+		expect(apiErr.status).toBe(500);
+		expect(apiErr.code).toBe("FS_STORAGE_ERROR");
+	});
+
+	it("maps non-notfound filesystem errors to 500", async () => {
+		fs.access.mockResolvedValue(undefined);
+
+		const err = Object.assign(new Error("boom"), { code: "EACCES" });
+		const apiErr = await mapStorageReadError(err, { details: { x: 1 } });
+
+		expect(apiErr).toBeInstanceOf(ApiError);
+		expect(apiErr.status).toBe(500);
+		expect(apiErr.code).toBe("FS_STORAGE_ERROR");
+	});
+});

+ 90 - 0
lib/search/cursor.test.js

@@ -0,0 +1,90 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import { ApiError } from "@/lib/api/errors";
+import { encodeCursor, decodeCursor } from "./cursor.js";
+
+describe("lib/search/cursor", () => {
+	it("decodeCursor(null) returns the default cursor", () => {
+		expect(decodeCursor(null)).toEqual({
+			v: 1,
+			mode: "sync",
+			offset: 0,
+			contextId: null,
+		});
+	});
+
+	it('decodeCursor("") treats empty string like missing cursor (default)', () => {
+		expect(decodeCursor("")).toEqual({
+			v: 1,
+			mode: "sync",
+			offset: 0,
+			contextId: null,
+		});
+	});
+
+	it("encodeCursor + decodeCursor roundtrip works", () => {
+		const encoded = encodeCursor({ v: 1, mode: "sync", offset: 10 });
+		expect(typeof encoded).toBe("string");
+		expect(encoded.length).toBeGreaterThan(5);
+
+		const decoded = decodeCursor(encoded);
+		expect(decoded).toEqual({
+			v: 1,
+			mode: "sync",
+			offset: 10,
+			contextId: null,
+		});
+	});
+
+	it("keeps contextId when provided", () => {
+		const encoded = encodeCursor({
+			v: 1,
+			mode: "sync",
+			offset: 5,
+			contextId: "ctx-123",
+		});
+
+		expect(decodeCursor(encoded)).toEqual({
+			v: 1,
+			mode: "sync",
+			offset: 5,
+			contextId: "ctx-123",
+		});
+	});
+
+	it("encodeCursor rejects invalid payloads", () => {
+		expect(() => encodeCursor(null)).toThrow(ApiError);
+		expect(() => encodeCursor({})).toThrow(ApiError);
+		expect(() => encodeCursor({ offset: -1 })).toThrow(ApiError);
+	});
+
+	it("decodeCursor rejects whitespace-only cursor", () => {
+		expect(() => decodeCursor("   ")).toThrow(ApiError);
+	});
+
+	it("decodeCursor rejects non-JSON payloads (deterministic)", () => {
+		// Using a valid base64url string that decodes to non-JSON ensures deterministic failure:
+		// JSON.parse("not-json") must throw => decodeCursor must throw ApiError.
+		const bad = Buffer.from("not-json", "utf8").toString("base64url");
+		expect(() => decodeCursor(bad)).toThrow(ApiError);
+	});
+
+	it("decodeCursor rejects wrong version", () => {
+		const bad = Buffer.from(
+			JSON.stringify({ v: 2, mode: "sync", offset: 0 }),
+			"utf8"
+		).toString("base64url");
+
+		expect(() => decodeCursor(bad)).toThrow(ApiError);
+	});
+
+	it("decodeCursor rejects negative offsets", () => {
+		const bad = Buffer.from(
+			JSON.stringify({ v: 1, mode: "sync", offset: -1 }),
+			"utf8"
+		).toString("base64url");
+
+		expect(() => decodeCursor(bad)).toThrow(ApiError);
+	});
+});