alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
194 lines (152 loc) • 6.79 kB
text/typescript
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { brotliCompressSync, gzipSync } from "node:zlib";
import { Alepha } from "alepha";
import { AlephaServer, ServerProvider } from "alepha/server";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import {
$serve,
AlephaServerStatic,
type ServePrimitiveOptions,
} from "../index.ts";
// --- Test Setup: Create a temporary directory for static files ---
const tempTestDir = join(tmpdir(), `alepha-static-test-${Date.now()}`);
const tempWeirdFileName = "weird file 02020&&&&&éééé";
beforeAll(async () => {
await mkdir(tempTestDir, { recursive: true });
// Create some test files
await writeFile(join(tempTestDir, "index.html"), "<h1>Hello World</h1>");
await writeFile(join(tempTestDir, "style.css"), "body { color: red; }");
await writeFile(join(tempTestDir, "script.js"), "console.log('test');");
await writeFile(join(tempTestDir, ".secret"), "should-not-be-served");
await writeFile(join(tempTestDir, tempWeirdFileName), "ok");
// Create pre-compressed versions
const cssContent = "body { color: blue; }";
await writeFile(join(tempTestDir, "compressed.css"), cssContent);
await writeFile(join(tempTestDir, "compressed.css.gz"), gzipSync(cssContent));
await writeFile(
join(tempTestDir, "compressed.css.br"),
brotliCompressSync(cssContent),
);
});
afterAll(async () => {
// Clean up the temporary directory
await rm(tempTestDir, { recursive: true, force: true });
});
// --- Test Suite ---
describe("alepha/server/static", () => {
const setupServer = async (serveOptions: ServePrimitiveOptions) => {
class TestApp {
staticContent = $serve({ root: tempTestDir, ...serveOptions });
}
const alepha = Alepha.create({ env: { LOG_LEVEL: "error" } })
.with(AlephaServer)
.with(AlephaServerStatic)
.with(TestApp);
await alepha.start();
const server = alepha.inject(ServerProvider);
return {
hostname: server.hostname,
};
};
test("should serve a basic static file with correct content-type", async () => {
const { hostname } = await setupServer({});
const response = await fetch(`${hostname}/style.css`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/css");
expect(await response.text()).toBe("body { color: red; }");
});
test("should serve a file with invalid character", async () => {
const { hostname } = await setupServer({});
const r1 = await fetch(`${hostname}/${tempWeirdFileName}`);
expect(r1.status).toBe(200);
expect(await r1.text()).toBe("ok");
const r2 = await fetch(`${hostname}/${encodeURI(tempWeirdFileName)}`);
expect(r2.status).toBe(200);
expect(await r2.text()).toBe("ok");
});
test("should serve index.html for root path", async () => {
const { hostname } = await setupServer({});
const response = await fetch(`${hostname}/`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
expect(await response.text()).toBe("<h1>Hello World</h1>");
});
test("should handle ETag and Last-Modified headers for caching", async () => {
const { hostname } = await setupServer({});
const initialResponse = await fetch(`${hostname}/script.js`);
const etag = initialResponse.headers.get("etag");
const lastModified = initialResponse.headers.get("last-modified");
expect(etag).toBeDefined();
expect(lastModified).toBeDefined();
// Second request with caching headers
const cachedResponse = await fetch(`${hostname}/script.js`, {
headers: {
"if-none-match": etag!,
"if-modified-since": lastModified!,
},
});
expect(cachedResponse.status).toBe(304); // Not Modified
expect(await cachedResponse.text()).toBe(""); // Body should be empty
});
test("should serve pre-compressed .gz file if accepted", async () => {
const { hostname } = await setupServer({});
const response = await fetch(`${hostname}/compressed.css`, {
headers: { "Accept-Encoding": "gzip, deflate, br" },
});
expect(response.status).toBe(200);
expect(response.headers.get("content-encoding")).toBe("br");
expect(response.headers.get("content-type")).toBe("text/css");
// The fetched content will be automatically decompressed by fetch
expect(await response.text()).toBe("body { color: blue; }");
});
test("should serve pre-compressed .br file if accepted and preferred", async () => {
const { hostname } = await setupServer({});
// Brotli is generally preferred by servers if available
const response = await fetch(`${hostname}/compressed.css`, {
headers: { "Accept-Encoding": "br, gzip" },
});
expect(response.status).toBe(200);
expect(response.headers.get("content-encoding")).toBe("br");
expect(await response.text()).toBe("body { color: blue; }");
});
test("should not serve dotfiles by default", async () => {
const { hostname } = await setupServer({});
const response = await fetch(`${hostname}/.secret`);
expect(response.status).toBe(404);
});
test("should serve dotfiles if ignoreDotEnvFiles is false", async () => {
const { hostname } = await setupServer({ ignoreDotEnvFiles: false });
const response = await fetch(`${hostname}/.secret`);
expect(response.status).toBe(200);
expect(await response.text()).toBe("should-not-be-served");
});
test("should use historyApiFallback for SPA routing", async () => {
const { hostname } = await setupServer({ historyApiFallback: true });
// A path that doesn't correspond to a real file
const response = await fetch(`${hostname}/some/deep/spa/route`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/html");
expect(await response.text()).toBe("<h1>Hello World</h1>");
// Should still not fallback for paths that look like files
const fileResponse = await fetch(`${hostname}/non-existent/style.css`);
expect(fileResponse.status).toBe(404);
});
test("should apply Cache-Control headers for configured file types", async () => {
const { hostname } = await setupServer({
cacheControl: {
fileTypes: [".css"],
maxAge: [1, "day"],
immutable: true,
},
});
const cssResponse = await fetch(`${hostname}/style.css`);
expect(cssResponse.headers.get("cache-control")).toBe(
"public, max-age=86400, immutable",
);
// JS file should not have the header
const jsResponse = await fetch(`${hostname}/script.js`);
expect(jsResponse.headers.get("cache-control")).toBeNull();
});
});