release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
assertUploadedFiles,
|
||||
formatFileSize,
|
||||
uploadAccept,
|
||||
uploaderAttributes,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("uploader helper kit", () => {
|
||||
test("formats sizes and builds uploader attributes", () => {
|
||||
expect(formatFileSize(1024)).toContain("KB");
|
||||
expect(uploadAccept(["image/png", ".jpg"])).toBe("image/png,.jpg");
|
||||
expect(uploaderAttributes({ store: "public", multiple: true })["data-uploader"]).toBe("public");
|
||||
});
|
||||
|
||||
test("asserts uploaded file count and status", () => {
|
||||
const files = [{ name: "a.png", size: 10, type: "image/png", key: "a", url: "/a" }];
|
||||
expect(assertUploadedFiles(files, { min: 1, max: 1 })).toEqual(files);
|
||||
expect(() => assertUploadedFiles([], { min: 1 })).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
createTemporaryObjectCleaner,
|
||||
ffmpegVideoTranscoder,
|
||||
memoryQuotaStore,
|
||||
multipartUpload,
|
||||
postgresQuotaStore,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("quota stores enforce durable byte and object limits", async () => {
|
||||
const quota = memoryQuotaStore();
|
||||
expect(await quota.reserve("tenant", 60, { bytes: 100, objects: 2 })).toBeTrue();
|
||||
expect(await quota.reserve("tenant", 50, { bytes: 100, objects: 2 })).toBeFalse();
|
||||
await quota.release("tenant", 60);
|
||||
expect(await quota.get("tenant")).toMatchObject({ bytes: 0, objects: 0 });
|
||||
const calls: unknown[][] = [];
|
||||
const sql = postgresQuotaStore({
|
||||
async query<T>(_sql: string, parameters?: unknown[]) {
|
||||
calls.push(parameters ?? []);
|
||||
return { rows: [{ owner: "tenant" } as T] };
|
||||
},
|
||||
});
|
||||
expect(await sql.reserve("tenant", 10, { bytes: 20 })).toBeTrue();
|
||||
expect(calls[0]?.[0]).toBe("tenant");
|
||||
});
|
||||
|
||||
test("multipart uploader limits concurrency, completes and aborts failures", async () => {
|
||||
const uploaded: number[] = [];
|
||||
let completed = false;
|
||||
let aborted = false;
|
||||
const client = {
|
||||
async create() {
|
||||
return "upload";
|
||||
},
|
||||
async uploadPart(_id: string, _key: string, part: number) {
|
||||
uploaded.push(part);
|
||||
return `etag-${part}`;
|
||||
},
|
||||
async complete() {
|
||||
completed = true;
|
||||
},
|
||||
async abort() {
|
||||
aborted = true;
|
||||
},
|
||||
};
|
||||
await multipartUpload(
|
||||
client,
|
||||
"video",
|
||||
new Uint8Array(11 * 1024 * 1024),
|
||||
{ contentType: "video/mp4" },
|
||||
{ partBytes: 5 * 1024 * 1024, concurrency: 2 },
|
||||
);
|
||||
expect(uploaded).toEqual([1, 2, 3]);
|
||||
expect(completed).toBeTrue();
|
||||
expect(aborted).toBeFalse();
|
||||
});
|
||||
|
||||
test("temporary cleanup and video transcoding are bounded and injectable", async () => {
|
||||
let now = 10;
|
||||
const deleted: string[] = [];
|
||||
const cleaner = createTemporaryObjectCleaner(
|
||||
{
|
||||
async put() {},
|
||||
async get() {
|
||||
return null;
|
||||
},
|
||||
async delete(key) {
|
||||
deleted.push(key);
|
||||
},
|
||||
publicUrl() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{ now: () => now },
|
||||
);
|
||||
cleaner.track("tmp/a", 5);
|
||||
expect(await cleaner.cleanup()).toBe(0);
|
||||
now = 15;
|
||||
expect(await cleaner.cleanup()).toBe(1);
|
||||
expect(deleted).toEqual(["tmp/a"]);
|
||||
let command: string[] = [];
|
||||
await ffmpegVideoTranscoder({
|
||||
spawn(args) {
|
||||
command = args;
|
||||
return { exited: Promise.resolve(0) };
|
||||
},
|
||||
})("input.mov", "output.mp4", { format: "mp4", width: 1280, videoBitrateKbps: 2000 });
|
||||
expect(command).toContain("scale=1280:-2");
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createResumableUploadManager, memoryResumableSessionStore } from "../src/index.ts";
|
||||
import type { StorageDriver } from "../src/index.ts";
|
||||
|
||||
function driver() {
|
||||
const objects = new Map<string, Uint8Array>();
|
||||
const storage: StorageDriver = {
|
||||
async put(key, data) {
|
||||
objects.set(key, data.slice());
|
||||
},
|
||||
async get(key) {
|
||||
const body = objects.get(key);
|
||||
return body ? { body, contentType: "application/octet-stream", size: body.length } : null;
|
||||
},
|
||||
async delete(key) {
|
||||
objects.delete(key);
|
||||
},
|
||||
publicUrl: () => null,
|
||||
};
|
||||
return { storage, objects };
|
||||
}
|
||||
|
||||
test("resumable uploads accept out-of-order idempotent chunks and assemble once", async () => {
|
||||
const { storage, objects } = driver();
|
||||
const manager = createResumableUploadManager({
|
||||
driver: storage,
|
||||
chunkSize: 3,
|
||||
maxBytes: 20,
|
||||
accept: ["text/plain"],
|
||||
});
|
||||
const session = await manager.create({ name: "hello.txt", type: "text/plain", size: 8 });
|
||||
expect(session.totalChunks).toBe(3);
|
||||
expect((await manager.uploadChunk(session.id, 1, new TextEncoder().encode("lo "))).complete).toBe(
|
||||
false,
|
||||
);
|
||||
await manager.uploadChunk(session.id, 0, new TextEncoder().encode("hel"));
|
||||
await manager.uploadChunk(session.id, 0, new TextEncoder().encode("hel"));
|
||||
const result = await manager.uploadChunk(session.id, 2, new TextEncoder().encode("!!"));
|
||||
expect(result.complete).toBe(true);
|
||||
expect(new TextDecoder().decode(objects.get(result.file!.key))).toBe("hello !!");
|
||||
expect(await manager.status(session.id)).toBeNull();
|
||||
});
|
||||
|
||||
test("resumable uploads enforce checksums, conflicts, limits, expiry, and cancellation", async () => {
|
||||
let now = 0;
|
||||
const { storage } = driver();
|
||||
const sessions = memoryResumableSessionStore();
|
||||
const manager = createResumableUploadManager({
|
||||
driver: storage,
|
||||
sessions,
|
||||
chunkSize: 2,
|
||||
maxBytes: 4,
|
||||
maxSessions: 1,
|
||||
ttlMs: 10,
|
||||
now: () => now,
|
||||
});
|
||||
const session = await manager.create({ name: "data.bin", size: 4 });
|
||||
await expect(manager.create({ name: "other.bin", size: 1 })).rejects.toThrow("too many");
|
||||
await expect(manager.uploadChunk(session.id, 0, new Uint8Array([1, 2]), "bad")).rejects.toThrow(
|
||||
"checksum",
|
||||
);
|
||||
await manager.uploadChunk(session.id, 0, new Uint8Array([1, 2]));
|
||||
await expect(manager.uploadChunk(session.id, 0, new Uint8Array([2, 1]))).rejects.toThrow(
|
||||
"different content",
|
||||
);
|
||||
expect(await manager.cancel(session.id)).toBe(true);
|
||||
const expiring = await manager.create({ name: "expire.bin", size: 2 });
|
||||
now = 11;
|
||||
expect(await manager.prune()).toBe(1);
|
||||
await expect(manager.uploadChunk(expiring.id, 0, new Uint8Array([1, 2]))).rejects.toThrow(
|
||||
"expired",
|
||||
);
|
||||
});
|
||||
@@ -61,3 +61,30 @@ test("signed file tokens reject tampering and expired payloads", async () => {
|
||||
expect(await verifySignedFileToken(`${token}x`, secret, 1_000)).toBeNull();
|
||||
expect(await verifySignedFileToken(token, secret, 2_000)).toBeNull();
|
||||
});
|
||||
|
||||
test("upload scanning rejects unsafe bytes before storage", async () => {
|
||||
const form = new FormData();
|
||||
form.set("file", new File(["virus"], "bad.txt", { type: "text/plain" }));
|
||||
await expect(
|
||||
upload("public", new Request("http://test/upload", { method: "POST", body: form }), {
|
||||
scan: async () => ({ safe: false, scanner: "test-av", reason: "signature" }),
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 422 });
|
||||
expect(await getStore("public").driver.get("bad.txt")).toBeNull();
|
||||
});
|
||||
|
||||
test("post-storage processor failure rolls back the object", async () => {
|
||||
const form = new FormData();
|
||||
form.set("file", new File(["image"], "photo.png", { type: "image/png" }));
|
||||
let key = "";
|
||||
await expect(
|
||||
upload("public", new Request("http://test/upload", { method: "POST", body: form }), {
|
||||
afterStore(file) {
|
||||
key = file.key;
|
||||
throw new Error("transform failed");
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({ status: 422 });
|
||||
expect(key).not.toBe("");
|
||||
expect(await getStore("public").driver.get(key)).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user