alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
435 lines (362 loc) • 12.1 kB
text/typescript
import { Alepha, z } from "alepha";
import { $action, AlephaServer, ServerProvider } from "alepha/server";
import { describe, test } from "vitest";
import { multipartOptions } from "../providers/ServerMultipartProvider.ts";
class App {
upload = $action({
schema: {
body: z.object({
file: z.file(),
}),
response: z.text(),
},
handler: ({ body }) => {
return `${body.file.name} (${body.file.size}b, ${body.file.type})`;
},
});
}
describe("ServerMultipartProvider", () => {
test("should upload file via HTTP", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
await alepha.start();
const file = new File(["test content"], "test.txt", { type: "text/plain" });
const body = new FormData();
body.append("file", file);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body },
);
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("test.txt (12b, text/plain)");
});
test("should upload file locally via run()", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
await alepha.start();
const file = new File(["test content"], "test.txt", { type: "text/plain" });
const result = await alepha.inject(App).upload.run({
body: { file },
});
expect(result).toBe("test.txt (12b, text/plain)");
});
test("should read file content via text()", async ({ expect }) => {
class TextApp {
upload = $action({
schema: {
body: z.object({ file: z.file() }),
response: z.text(),
},
handler: async ({ body }) => body.file.text(),
});
}
const alepha = Alepha.create().with(AlephaServer).with(TextApp);
await alepha.start();
const file = new File(["hello world"], "hi.txt", { type: "text/plain" });
const formData = new FormData();
formData.append("file", file);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(await resp.text()).toBe("hello world");
});
test("should read file content via arrayBuffer()", async ({ expect }) => {
class BufferApp {
upload = $action({
schema: {
body: z.object({ file: z.file() }),
response: z.text(),
},
handler: async ({ body }) => {
const ab = await body.file.arrayBuffer();
return new TextDecoder().decode(ab);
},
});
}
const alepha = Alepha.create().with(AlephaServer).with(BufferApp);
await alepha.start();
const file = new File(["buffer test"], "buf.txt", { type: "text/plain" });
const result = await alepha.inject(BufferApp).upload.run({
body: { file },
});
expect(result).toBe("buffer test");
});
test("should handle binary file uploads", async ({ expect }) => {
class BinaryApp {
upload = $action({
schema: {
body: z.object({ file: z.file() }),
response: z.text(),
},
handler: async ({ body }) => {
const ab = await body.file.arrayBuffer();
const bytes = new Uint8Array(ab);
return `${body.file.name}: ${bytes.length}b, ${body.file.type}`;
},
});
}
const alepha = Alepha.create().with(AlephaServer).with(BinaryApp);
await alepha.start();
const binary = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a]);
const file = new File([binary], "image.png", { type: "image/png" });
const result = await alepha.inject(BinaryApp).upload.run({
body: { file },
});
expect(result).toBe("image.png: 6b, image/png");
});
test("should handle empty file", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
await alepha.start();
const file = new File([], "empty.txt", { type: "text/plain" });
const result = await alepha.inject(App).upload.run({
body: { file },
});
expect(result).toBe("empty.txt (0b, text/plain)");
});
test("should handle mixed file and text fields", async ({ expect }) => {
class MixedApp {
upload = $action({
schema: {
body: z.object({
name: z.text(),
description: z.text(),
file: z.file(),
}),
response: z.text(),
},
handler: ({ body }) =>
`${body.name}: ${body.description} (${body.file.size}b)`,
});
}
const alepha = Alepha.create().with(AlephaServer).with(MixedApp);
await alepha.start();
const file = new File(["content"], "doc.txt", { type: "text/plain" });
const result = await alepha.inject(MixedApp).upload.run({
body: {
name: "Test Document",
description: "A test file upload",
file,
},
});
expect(result).toBe("Test Document: A test file upload (7b)");
});
test("should not have filepath (no filesystem)", async ({ expect }) => {
class PathApp {
upload = $action({
schema: {
body: z.object({ file: z.file() }),
response: z.text(),
},
handler: ({ body }) => String(body.file.filepath ?? "none"),
});
}
const alepha = Alepha.create().with(AlephaServer).with(PathApp);
await alepha.start();
const file = new File(["data"], "f.txt", { type: "text/plain" });
const result = await alepha.inject(PathApp).upload.run({
body: { file },
});
expect(result).toBe("none");
});
});
describe("ServerMultipartProvider - Size Limits", () => {
test("should reject file exceeding individual file size limit", async ({
expect,
}) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
alepha.store.mut(multipartOptions, () => ({
limit: 10_000_000,
fileLimit: 100,
fileCount: 10,
}));
await alepha.start();
const file = new File(["x".repeat(200)], "large.txt", {
type: "text/plain",
});
const formData = new FormData();
formData.append("file", file);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(resp.status).toBe(413);
const body = await resp.json();
expect(body.message).toMatch(/exceeds size limit/i);
});
test("should reject request exceeding total size limit", async ({
expect,
}) => {
class MultiFileApp {
upload = $action({
schema: {
body: z.object({
file1: z.file(),
file2: z.file(),
}),
response: z.text(),
},
handler: () => "ok",
});
}
const alepha = Alepha.create().with(AlephaServer).with(MultiFileApp);
alepha.store.mut(multipartOptions, () => ({
limit: 150,
fileLimit: 100,
fileCount: 10,
}));
await alepha.start();
const file1 = new File(["x".repeat(80)], "f1.txt", {
type: "text/plain",
});
const file2 = new File(["y".repeat(80)], "f2.txt", {
type: "text/plain",
});
const formData = new FormData();
formData.append("file1", file1);
formData.append("file2", file2);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(resp.status).toBe(413);
const body = await resp.json();
expect(body.message).toMatch(/size limit exceeded/i);
});
test("should reject request with too many files", async ({ expect }) => {
class ManyFilesApp {
upload = $action({
schema: {
body: z.object({
file1: z.file(),
file2: z.file(),
file3: z.file(),
}),
response: z.text(),
},
handler: () => "ok",
});
}
const alepha = Alepha.create().with(AlephaServer).with(ManyFilesApp);
alepha.store.mut(multipartOptions, () => ({
limit: 10_000_000,
fileLimit: 5_000_000,
fileCount: 2,
}));
await alepha.start();
const formData = new FormData();
formData.append("file1", new File(["a"], "f1.txt", { type: "text/plain" }));
formData.append("file2", new File(["b"], "f2.txt", { type: "text/plain" }));
formData.append("file3", new File(["c"], "f3.txt", { type: "text/plain" }));
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(resp.status).toBe(413);
const body = await resp.json();
expect(body.message).toMatch(/Too many files/i);
});
test("should accept file exactly at the size limit", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
alepha.store.mut(multipartOptions, () => ({
limit: 10_000_000,
fileLimit: 100,
fileCount: 10,
}));
await alepha.start();
const file = new File(["x".repeat(100)], "exact.txt", {
type: "text/plain",
});
const result = await alepha.inject(App).upload.run({
body: { file },
});
expect(result).toBe("exact.txt (100b, text/plain)");
});
test("should reject file just over the size limit", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
alepha.store.mut(multipartOptions, () => ({
limit: 10_000_000,
fileLimit: 100,
fileCount: 10,
}));
await alepha.start();
const file = new File(["x".repeat(101)], "over.txt", {
type: "text/plain",
});
const formData = new FormData();
formData.append("file", file);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(resp.status).toBe(413);
const body = await resp.json();
expect(body.message).toMatch(/exceeds size limit/i);
});
test("should reject via content-length pre-check", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
alepha.store.mut(multipartOptions, () => ({
limit: 50,
fileLimit: 5_000_000,
fileCount: 10,
}));
await alepha.start();
const file = new File(["x".repeat(1000)], "big.txt", {
type: "text/plain",
});
const formData = new FormData();
formData.append("file", file);
const resp = await fetch(
`${alepha.inject(ServerProvider).hostname}/api/upload`,
{ method: "POST", body: formData },
);
expect(resp.status).toBe(413);
const body = await resp.json();
expect(body.message).toMatch(/size limit exceeded/i);
});
test("should count only file fields toward file count limit", async ({
expect,
}) => {
class TextAndFilesApp {
upload = $action({
schema: {
body: z.object({
field1: z.text(),
field2: z.text(),
field3: z.text(),
file1: z.file(),
file2: z.file(),
}),
response: z.text(),
},
handler: () => "success",
});
}
const alepha = Alepha.create().with(AlephaServer).with(TextAndFilesApp);
alepha.store.mut(multipartOptions, () => ({
limit: 10_000_000,
fileLimit: 5_000_000,
fileCount: 2,
}));
await alepha.start();
const result = await alepha.inject(TextAndFilesApp).upload.run({
body: {
field1: "a",
field2: "b",
field3: "c",
file1: new File(["x"], "f1.txt", { type: "text/plain" }),
file2: new File(["y"], "f2.txt", { type: "text/plain" }),
},
});
expect(result).toBe("success");
});
test("should use default limits", async ({ expect }) => {
const alepha = Alepha.create().with(AlephaServer).with(App);
await alepha.start();
const file = new File(["x".repeat(1000)], "normal.txt", {
type: "text/plain",
});
const result = await alepha.inject(App).upload.run({
body: { file },
});
expect(result).toBe("normal.txt (1000b, text/plain)");
});
});