Initial Commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { createHttpStorage } from "@suaveplan/storage-http";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "@suaveplan/testing/runner";
|
||||
import type { StorageLike } from "@suaveplan/types";
|
||||
import { createStorageUriResolver } from "../../src/index.js";
|
||||
import type { UriPlugin } from "../../src/types/plugin.types.js";
|
||||
|
||||
function fileUriPlugin(adapter: StorageLike, rootDir: string): UriPlugin {
|
||||
return {
|
||||
scheme: "file",
|
||||
adapter,
|
||||
parse(uri) {
|
||||
// file:///abs/path → key relative to rootDir.
|
||||
const prefix = "file://";
|
||||
const rest = uri.startsWith(prefix)
|
||||
? uri.slice(prefix.length)
|
||||
: uri;
|
||||
const abs = rest.startsWith("/") ? rest : `/${rest}`;
|
||||
const root = rootDir.endsWith("/") ? rootDir : `${rootDir}/`;
|
||||
const key = abs.startsWith(root)
|
||||
? abs.slice(root.length)
|
||||
: abs.replace(/^\//, "");
|
||||
return { key };
|
||||
},
|
||||
build(key) {
|
||||
const root = rootDir.endsWith("/") ? rootDir : `${rootDir}/`;
|
||||
return `file://${root}${key}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function httpsUriPlugin(adapter: StorageLike): UriPlugin {
|
||||
return {
|
||||
scheme: "https",
|
||||
adapter,
|
||||
parse(uri) {
|
||||
const url = new URL(uri);
|
||||
return {
|
||||
key: url.pathname.replace(/^\//, ""),
|
||||
meta: { host: url.host },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("cross-scheme integration", () => {
|
||||
let root: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), "storage-uri-int-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("composes a resolver with real storage-file and storage-http adapters", {
|
||||
timeout: 15000,
|
||||
}, async () => {
|
||||
const fileAdapter = createNodeFsStorage({ rootDir: root });
|
||||
|
||||
// In-process fetch stub serving a single payload at a known URL.
|
||||
const payload = new TextEncoder().encode("from-http");
|
||||
const httpAdapter = createHttpStorage({
|
||||
baseUrl: "https://origin.test/",
|
||||
fetch: async (input: unknown) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input instanceof Request
|
||||
? input.url
|
||||
: String((input as { url?: string }).url ?? input);
|
||||
if (url.endsWith("/poster.bin")) {
|
||||
return new Response(payload, { status: 200 });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const resolver = createStorageUriResolver([
|
||||
fileUriPlugin(fileAdapter, root),
|
||||
httpsUriPlugin(httpAdapter),
|
||||
]);
|
||||
|
||||
// HTTP read.
|
||||
const httpBytes = await resolver.read("https://origin.test/poster.bin");
|
||||
expect(new TextDecoder().decode(httpBytes)).toBe("from-http");
|
||||
|
||||
// File round-trip via resolver.
|
||||
const fileUri = `file://${root}/cache/copy.bin`;
|
||||
await resolver.write(fileUri, httpBytes);
|
||||
const back = await resolver.read(fileUri);
|
||||
expect(new TextDecoder().decode(back)).toBe("from-http");
|
||||
|
||||
expect(await resolver.exists(fileUri)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
// Polyfills for test environment — none required; Node 18+ provides
|
||||
// `URL`, `AbortController`, `ReadableStream`, and `Response` natively.
|
||||
// File exists so vitest has a stable setup entry and to mirror sibling
|
||||
// storage packages.
|
||||
export {};
|
||||
@@ -0,0 +1,54 @@
|
||||
// Test-only helpers used by the resolver test suite. Coverage excluded via
|
||||
// vitest config's `*.test.ts` filter — this file is `.ts` rather than
|
||||
// `.test.ts` so it can be imported from multiple test files.
|
||||
|
||||
import type { StorageLike } from "@suaveplan/types";
|
||||
|
||||
/**
|
||||
* Minimal in-memory `StorageLike` for resolver dispatch tests. NOT a mock —
|
||||
* it's a real implementation of the structural contract, written by hand
|
||||
* because rule 2 forbids `vi.mock`. Test files attach this to a
|
||||
* `UriPlugin` to exercise scheme dispatch.
|
||||
*/
|
||||
export class MemoryStorageLike implements StorageLike {
|
||||
public readonly store = new Map<string, Uint8Array>();
|
||||
public lastSignal: AbortSignal | undefined;
|
||||
public getCalls = 0;
|
||||
public setCalls = 0;
|
||||
public deleteCalls = 0;
|
||||
public existsCalls = 0;
|
||||
public listCalls = 0;
|
||||
|
||||
public async get(key: string): Promise<Uint8Array | undefined> {
|
||||
this.getCalls += 1;
|
||||
return this.store.get(key);
|
||||
}
|
||||
public async set(key: string, value: Uint8Array): Promise<void> {
|
||||
this.setCalls += 1;
|
||||
this.store.set(key, value);
|
||||
}
|
||||
public async delete(key: string): Promise<boolean> {
|
||||
this.deleteCalls += 1;
|
||||
return this.store.delete(key);
|
||||
}
|
||||
public async exists(key: string): Promise<boolean> {
|
||||
this.existsCalls += 1;
|
||||
return this.store.has(key);
|
||||
}
|
||||
public async list(options?: { prefix?: string; limit?: number }): Promise<{
|
||||
keys: string[];
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
continuationToken?: string | undefined;
|
||||
}> {
|
||||
this.listCalls += 1;
|
||||
const prefix = options?.prefix ?? "";
|
||||
const all = [...this.store.keys()].filter((k) => k.startsWith(prefix));
|
||||
const limit = options?.limit ?? all.length;
|
||||
const keys = all.slice(0, limit);
|
||||
return { keys, total: all.length, hasMore: keys.length < all.length };
|
||||
}
|
||||
public async clear(): Promise<void> {
|
||||
this.store.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user