246 lines
8.1 KiB
TypeScript
246 lines
8.1 KiB
TypeScript
import { SuaveplanError } from "@suaveplan/error";
|
|
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
asDrive,
|
|
FakeDrive,
|
|
FakeDriveError,
|
|
} from "../../tests/setup/fake-drive.js";
|
|
import { DRIVE_URI_CODES, DriveUriError } from "../error-codes/error-codes.js";
|
|
import {
|
|
DriveStorageLikeAdapter,
|
|
isDriveNotFound,
|
|
} from "./drive-storage-like.js";
|
|
|
|
class CustomNotInDriveError extends SuaveplanError<string> {}
|
|
|
|
describe("isDriveNotFound", () => {
|
|
it("matches any SuaveplanError whose code ends in .NOT_FOUND", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
isDriveNotFound(new CustomNotInDriveError("DRIVE.NOT_FOUND", "x"))
|
|
).toBe(true);
|
|
expect(
|
|
isDriveNotFound(
|
|
new CustomNotInDriveError("SOMETHING_ELSE.NOT_FOUND", "x")
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false for non-SuaveplanError values", { timeout: 5000 }, () => {
|
|
expect(isDriveNotFound(undefined)).toBe(false);
|
|
expect(isDriveNotFound(null)).toBe(false);
|
|
expect(isDriveNotFound("string")).toBe(false);
|
|
expect(isDriveNotFound(new Error("plain"))).toBe(false);
|
|
});
|
|
|
|
it("walks the cause chain", { timeout: 5000 }, () => {
|
|
const inner = new FakeDriveError("DRIVE.NOT_FOUND", "missing");
|
|
const outer = new Error("wrapped");
|
|
(outer as Error & { cause?: unknown }).cause = inner;
|
|
expect(isDriveNotFound(outer)).toBe(true);
|
|
});
|
|
|
|
it("aborts on a self-referential cause cycle", { timeout: 5000 }, () => {
|
|
const err = new Error("cyclic");
|
|
(err as Error & { cause?: unknown }).cause = err;
|
|
expect(isDriveNotFound(err)).toBe(false);
|
|
});
|
|
|
|
it("returns false when no cause member exists", { timeout: 5000 }, () => {
|
|
expect(isDriveNotFound({ message: "no cause", code: "X" })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("DriveStorageLikeAdapter", () => {
|
|
let fake: FakeDrive;
|
|
let adapter: DriveStorageLikeAdapter;
|
|
|
|
beforeEach(() => {
|
|
fake = new FakeDrive({
|
|
contents: new Map([["a.bin", new Uint8Array([1, 2, 3])]]),
|
|
});
|
|
adapter = new DriveStorageLikeAdapter(asDrive(fake));
|
|
});
|
|
|
|
it("returns undefined for Drive not-found errors", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const result = await adapter.get("missing.bin");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("returns the bytes on a hit", { timeout: 5000 }, async () => {
|
|
const bytes = await adapter.get("a.bin");
|
|
expect(bytes).toEqual(new Uint8Array([1, 2, 3]));
|
|
});
|
|
|
|
it("re-throws non-not-found Drive errors unchanged", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const failure = new FakeDriveError(
|
|
"DRIVE.PLUGIN_FAILED",
|
|
"plugin crashed"
|
|
);
|
|
const bad = new FakeDrive({ readError: () => failure });
|
|
const a = new DriveStorageLikeAdapter(asDrive(bad));
|
|
await expect(a.get("x")).rejects.toBe(failure);
|
|
});
|
|
|
|
it("forwards set, delete, exists, list", { timeout: 5000 }, async () => {
|
|
await adapter.set("b.bin", new Uint8Array([9]));
|
|
expect(fake.calls.write).toHaveLength(1);
|
|
|
|
expect(await adapter.exists("a.bin")).toBe(true);
|
|
expect(await adapter.exists("missing")).toBe(false);
|
|
|
|
const removed = await adapter.delete("b.bin");
|
|
expect(removed).toBe(true);
|
|
|
|
const listed = await adapter.list({ prefix: "a" });
|
|
expect(listed.keys).toContain("a.bin");
|
|
expect(listed.hasMore).toBe(false);
|
|
expect(typeof listed.total).toBe("number");
|
|
});
|
|
|
|
it("list with no options defaults prefix/limit to undefined", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const result = await adapter.list();
|
|
expect(Array.isArray(result.keys)).toBe(true);
|
|
});
|
|
|
|
it("list forwards options.limit to the drive", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const result = await adapter.list({ prefix: "a", limit: 10 });
|
|
expect(Array.isArray(result.keys)).toBe(true);
|
|
});
|
|
|
|
it("list copes with a drive returning entries-shape instead of keys-shape", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const odd = {
|
|
async read(): Promise<Uint8Array> {
|
|
return new Uint8Array();
|
|
},
|
|
async write(): Promise<void> {},
|
|
async delete(): Promise<boolean> {
|
|
return false;
|
|
},
|
|
async stat(): Promise<unknown | null> {
|
|
return null;
|
|
},
|
|
async list(): Promise<unknown> {
|
|
return {
|
|
entries: [{ path: "p1" }, { path: "p2" }],
|
|
};
|
|
},
|
|
createReadStream(): ReadableStream<Uint8Array> {
|
|
return new ReadableStream();
|
|
},
|
|
};
|
|
const a = new DriveStorageLikeAdapter(
|
|
odd as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
|
);
|
|
const r = await a.list();
|
|
expect(r.keys).toEqual(["p1", "p2"]);
|
|
expect(r.total).toBe(2);
|
|
});
|
|
|
|
it("list copes with a drive returning neither shape", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const oddler = {
|
|
async read(): Promise<Uint8Array> {
|
|
return new Uint8Array();
|
|
},
|
|
async write(): Promise<void> {},
|
|
async delete(): Promise<boolean> {
|
|
return false;
|
|
},
|
|
async stat(): Promise<unknown | null> {
|
|
return null;
|
|
},
|
|
async list(): Promise<unknown> {
|
|
return undefined;
|
|
},
|
|
createReadStream(): ReadableStream<Uint8Array> {
|
|
return new ReadableStream();
|
|
},
|
|
};
|
|
const a = new DriveStorageLikeAdapter(
|
|
oddler as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
|
);
|
|
const r = await a.list();
|
|
expect(r.keys).toEqual([]);
|
|
expect(r.total).toBe(0);
|
|
expect(r.hasMore).toBe(false);
|
|
});
|
|
|
|
it("list forwards continuationToken when the drive returns one", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const cursored = {
|
|
async read(): Promise<Uint8Array> {
|
|
return new Uint8Array();
|
|
},
|
|
async write(): Promise<void> {},
|
|
async delete(): Promise<boolean> {
|
|
return false;
|
|
},
|
|
async stat(): Promise<unknown | null> {
|
|
return null;
|
|
},
|
|
async list(): Promise<unknown> {
|
|
return {
|
|
keys: ["a", "b"],
|
|
total: 99,
|
|
hasMore: true,
|
|
continuationToken: "next-page",
|
|
};
|
|
},
|
|
createReadStream(): ReadableStream<Uint8Array> {
|
|
return new ReadableStream();
|
|
},
|
|
};
|
|
const a = new DriveStorageLikeAdapter(
|
|
cursored as unknown as Parameters<typeof DriveStorageLikeAdapter>[0]
|
|
);
|
|
const r = await a.list();
|
|
expect(r.continuationToken).toBe("next-page");
|
|
expect(r.hasMore).toBe(true);
|
|
expect(r.total).toBe(99);
|
|
});
|
|
|
|
it("clear is unsupported and names the alternative", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
await expect(adapter.clear()).rejects.toBeInstanceOf(SuaveplanError);
|
|
try {
|
|
await adapter.clear();
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DriveUriError);
|
|
expect((err as DriveUriError).code).toBe(
|
|
DRIVE_URI_CODES.WRITE_UNSUPPORTED
|
|
);
|
|
expect((err as DriveUriError).message).toContain(
|
|
"Drive.delete(path)"
|
|
);
|
|
}
|
|
});
|
|
|
|
it("createReadStream forwards to Drive.createReadStream", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const stream = adapter.createReadStream("a.bin");
|
|
expect(fake.calls.createReadStream).toHaveLength(1);
|
|
// Drain it
|
|
const reader = stream.getReader();
|
|
const { value } = await reader.read();
|
|
expect(value).toEqual(new Uint8Array([1, 2, 3]));
|
|
await reader.read(); // close
|
|
reader.releaseLock();
|
|
});
|
|
});
|