deno.land / std@0.224.0 / http / file_server_test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.import { assert, assertAlmostEquals, assertEquals, assertFalse, assertMatch, assertStringIncludes,} from "../assert/mod.ts";import { stub } from "../testing/mock.ts";import { serveDir, type ServeDirOptions, serveFile } from "./file_server.ts";import { calculate } from "./etag.ts";import { basename, dirname, fromFileUrl, join, resolve, toFileUrl,} from "../path/mod.ts";import { VERSION } from "../version.ts";import { MINUTE } from "../datetime/constants.ts";import { getAvailablePort } from "../net/get_available_port.ts";import { concat } from "../bytes/concat.ts";
const moduleDir = dirname(fromFileUrl(import.meta.url));const testdataDir = resolve(moduleDir, "testdata");const serveDirOptions: ServeDirOptions = { quiet: true, fsRoot: testdataDir, showDirListing: true, showDotfiles: true, enableCors: true,};
const TEST_FILE_PATH = join(testdataDir, "test_file.txt");const TEST_FILE_STAT = await Deno.stat(TEST_FILE_PATH);const TEST_FILE_SIZE = TEST_FILE_STAT.size;const TEST_FILE_ETAG = await calculate(TEST_FILE_STAT) as string;const TEST_FILE_LAST_MODIFIED = TEST_FILE_STAT.mtime instanceof Date ? new Date(TEST_FILE_STAT.mtime).toUTCString() : "";const TEST_FILE_TEXT = await Deno.readTextFile(TEST_FILE_PATH);
/* HTTP GET request allowing arbitrary paths */async function fetchExactPath( hostname: string, port: number, path: string,): Promise<Response> { const encoder = new TextEncoder(); const decoder = new TextDecoder(); const conn = await Deno.connect({ hostname, port }); await conn.write(encoder.encode("GET " + path + " HTTP/1.1\r\n\r\n")); let currentResult = ""; let contentLength = -1; let startOfBody = -1; for await (const chunk of conn.readable) { currentResult += decoder.decode(chunk); if (contentLength === -1) { const match = /^content-length: (.*)$/m.exec(currentResult); if (match && match[1]) { contentLength = Number(match[1]); } } if (startOfBody === -1) { const ind = currentResult.indexOf("\r\n\r\n"); if (ind !== -1) { startOfBody = ind + 4; } } if (startOfBody !== -1 && contentLength !== -1) { const byteLen = encoder.encode(currentResult).length; if (byteLen >= contentLength + startOfBody) { break; } } } const status = /^HTTP\/1.1 (...)/.exec(currentResult); let statusCode = 0; if (status && status[1]) { statusCode = Number(status[1]); }
const body = currentResult.slice(startOfBody); const headersStr = currentResult.slice(0, startOfBody); const headersReg = /^(.*): (.*)$/mg; const headersObj: { [i: string]: string } = {}; let match = headersReg.exec(headersStr); while (match !== null) { if (match[1] && match[2]) { headersObj[match[1]] = match[2]; } match = headersReg.exec(headersStr); } return new Response(body, { status: statusCode, headers: new Headers(headersObj), });}
Deno.test("serveDir() sets last-modified header", async () => { const req = new Request("http://localhost/test_file.txt"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel(); const lastModifiedHeader = res.headers.get("last-modified") as string; const lastModifiedTime = Date.parse(lastModifiedHeader); const expectedTime = TEST_FILE_STAT.mtime instanceof Date ? TEST_FILE_STAT.mtime.getTime() : Number.NaN;
assertAlmostEquals(lastModifiedTime, expectedTime, 5 * MINUTE);});
Deno.test("serveDir() sets date header", async () => { const req = new Request("http://localhost/test_file.txt"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel(); const dateHeader = res.headers.get("date") as string; const date = Date.parse(dateHeader); const expectedTime = TEST_FILE_STAT.atime && TEST_FILE_STAT.atime instanceof Date ? TEST_FILE_STAT.atime.getTime() : Number.NaN;
assertAlmostEquals(date, expectedTime, 5 * MINUTE);});
Deno.test("serveDir()", async () => { const req = new Request("http://localhost/hello.html"); const res = await serveDir(req, serveDirOptions); const downloadedFile = await res.text(); const localFile = await Deno.readTextFile(join(testdataDir, "hello.html"));
assertEquals(res.status, 200); assertEquals(downloadedFile, localFile); assertEquals(res.headers.get("content-type"), "text/html; charset=UTF-8");});
Deno.test("serveDir() with hash symbol in filename", async () => { const filePath = join(testdataDir, "file#2.txt"); const text = "Plain text"; await Deno.writeTextFile(filePath, text);
const req = new Request("http://localhost/file%232.txt"); const res = await serveDir(req, serveDirOptions); const downloadedFile = await res.text();
assertEquals(res.status, 200); assertEquals( res.headers.get("content-type"), "text/plain; charset=UTF-8", ); assertEquals(downloadedFile, text);
await Deno.remove(filePath);});
Deno.test("serveDir() with space in filename", async () => { const filePath = join(testdataDir, "test file.txt"); const text = "Plain text"; await Deno.writeTextFile(filePath, text);
const req = new Request("http://localhost/test%20file.txt"); const res = await serveDir(req, serveDirOptions); const downloadedFile = await res.text();
assertEquals(res.status, 200); assertEquals( res.headers.get("content-type"), "text/plain; charset=UTF-8", ); assertEquals(downloadedFile, text);
await Deno.remove(filePath);});
Deno.test("serveDir() serves directory index", async () => { const filePath = join(testdataDir, "%25A.txt"); await Deno.writeTextFile(filePath, "25A");
const req = new Request("http://localhost/"); const res = await serveDir(req, serveDirOptions); const page = await res.text();
assertEquals(res.status, 200); assertStringIncludes(page, '<a href="/hello.html">hello.html</a>'); assertStringIncludes(page, '<a href="/tls/">tls/</a>'); assertStringIncludes(page, "%2525A.txt"); // `Deno.FileInfo` is not completely compatible with Windows yet // TODO(bartlomieju): `mode` should work correctly in the future. // Correct this test case accordingly. if (Deno.build.os === "windows") { assertMatch(page, /<td class="mode">(\s)*\(unknown mode\)(\s)*<\/td>/); } else { assertMatch(page, /<td class="mode">(\s)*[a-zA-Z- ]{14}(\s)*<\/td>/); }
await Deno.remove(filePath);});
Deno.test("serveDir() serves directory index with file containing space in the filename", async () => { const filePath = join(testdataDir, "test file.txt"); await Deno.writeTextFile(filePath, "25A");
const req = new Request("http://localhost/"); const res = await serveDir(req, serveDirOptions); const page = await res.text();
assertEquals(res.status, 200); assertStringIncludes(page, '<a href="/hello.html">hello.html</a>'); assertStringIncludes(page, '<a href="/tls/">tls/</a>'); assertStringIncludes(page, "test%20file.txt"); // `Deno.FileInfo` is not completely compatible with Windows yet // TODO(bartlomieju): `mode` should work correctly in the future. // Correct this test case accordingly. if (Deno.build.os === "windows") { assertMatch(page, /<td class="mode">(\s)*\(unknown mode\)(\s)*<\/td>/); } else { assertMatch(page, /<td class="mode">(\s)*[a-zA-Z- ]{14}(\s)*<\/td>/); }
await Deno.remove(filePath);});
Deno.test("serveDir() returns a response even if fileinfo is inaccessible", async () => { // Note: Deno.stat for windows system files may be rejected with os error 32. // Mock Deno.stat to test that the dirlisting page can be generated // even if the fileInfo for a particular file cannot be obtained.
// Assuming that fileInfo of `test_file.txt` cannot be accessible using denoStatStub = stub(Deno, "stat", (path): Promise<Deno.FileInfo> => { if (path.toString().includes("test_file.txt")) { return Promise.reject(new Error("__stubed_error__")); } return denoStatStub.original.call(Deno, path); }); const req = new Request("http://localhost/"); const res = await serveDir(req, serveDirOptions); const page = await res.text();
assertEquals(res.status, 200); assertStringIncludes(page, "/test_file.txt");});
Deno.test("serveDir() handles not found files", async () => { const req = new Request("http://localhost/badfile.txt"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.status, 404);});
Deno.test("serveDir() traverses path correctly", async () => { const req = new Request("http://localhost/../../../../../../../.."); const res = await serveDir(req, serveDirOptions); const page = await res.text();
assertEquals(res.status, 200); assertStringIncludes(page, "hello.html");});
Deno.test("serveDir() traverses path", async () => { const controller = new AbortController(); const port = 4507; const server = Deno.serve( { port, signal: controller.signal }, async (req) => await serveDir(req, serveDirOptions), );
const res1 = await fetchExactPath("127.0.0.1", port, "../../../.."); await res1.body?.cancel();
assertEquals(res1.status, 400);
const res2 = await fetchExactPath( "127.0.0.1", port, "http://localhost/../../../..", ); const page = await res2.text();
assertEquals(res2.status, 200); assertStringIncludes(page, "hello.html");
controller.abort(); await server.finished;});
Deno.test("serveDir() traverses encoded URI path", async () => { const req = new Request( "http://localhost/%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..", ); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.status, 301); assertEquals(res.headers.get("location"), "http://localhost/");});
Deno.test("serveDir() serves unusual filename", async () => { const filePath = join(testdataDir, "%"); using _file = await Deno.create(filePath);
const req1 = new Request("http://localhost/%25"); const res1 = await serveDir(req1, serveDirOptions); await res1.body?.cancel();
assertEquals(res1.status, 200); assert(res1.headers.has("access-control-allow-origin")); assert(res1.headers.has("access-control-allow-headers"));
const req2 = new Request("http://localhost/test_file.txt"); const res2 = await serveDir(req2, serveDirOptions); await res2.body?.cancel();
assertEquals(res2.status, 200); assert(res2.headers.has("access-control-allow-origin")); assert(res2.headers.has("access-control-allow-headers"));
await Deno.remove(filePath);});
Deno.test("serveDir() supports CORS", async () => { const req1 = new Request("http://localhost/"); const res1 = await serveDir(req1, serveDirOptions); await res1.body?.cancel();
assertEquals(res1.status, 200); assert(res1.headers.has("access-control-allow-origin")); assert(res1.headers.has("access-control-allow-headers"));
const req2 = new Request("http://localhost/hello.html"); const res2 = await serveDir(req2, serveDirOptions); await res2.body?.cancel();
assertEquals(res2.status, 200); assert(res2.headers.has("access-control-allow-origin")); assert(res2.headers.has("access-control-allow-headers"));});
Deno.test("serveDir() script prints help", async () => { const command = new Deno.Command(Deno.execPath(), { args: [ "run", "--no-check", "--quiet", "--no-lock", "--config", "deno.json", "http/file_server.ts", "--help", ], }); const { stdout } = await command.output(); const output = new TextDecoder().decode(stdout); assert(output.includes(`Deno File Server ${VERSION}`));});
Deno.test("serveDir() script prints version", async () => { const command = new Deno.Command(Deno.execPath(), { args: [ "run", "--no-check", "--quiet", "--no-lock", "--config", "deno.json", "http/file_server.ts", "--version", ], }); const { stdout } = await command.output(); const output = new TextDecoder().decode(stdout); assert(output.includes(`Deno File Server ${VERSION}`));});
Deno.test("serveDir() ignores query params", async () => { const req = new Request("http://localhost/hello.html?key=value"); const res = await serveDir(req, serveDirOptions); const downloadedFile = await res.text(); const localFile = await Deno.readTextFile(join(testdataDir, "hello.html"));
assertEquals(res.status, 200); assertEquals(downloadedFile, localFile);});
Deno.test("serveDir() script fails with partial TLS args", async () => { const command = new Deno.Command(Deno.execPath(), { args: [ "run", "--no-check", "--quiet", "--allow-read", "--allow-net", "--no-lock", "--config", "deno.json", "http/file_server.ts", ".", "--host", "localhost", "--cert", "./testdata/tls/localhost.crt", "-p", `4578`, ], stderr: "null", }); const { stdout, success } = await command.output(); assertFalse(success); assertStringIncludes( new TextDecoder().decode(stdout), "--key and --cert are required for TLS", );});
Deno.test("serveDir() doesn't show directory listings", async () => { const req = new Request("http://localhost/"); const res = await serveDir(req, { ...serveDirOptions, showDirListing: false, }); await res.body?.cancel();
assertEquals(res.status, 404);});
Deno.test("serveDir() doesn't show dotfiles", async () => { const req1 = new Request("http://localhost/"); const res1 = await serveDir(req1, { ...serveDirOptions, showDotfiles: false, }); const page1 = await res1.text();
assert(!page1.includes(".dotfile"));
const req2 = new Request("http://localhost/.dotfile"); const res2 = await serveDir(req2, { ...serveDirOptions, showDotfiles: false, }); const body = await res2.text();
assertEquals(body, "dotfile");});
Deno.test("serveDir() shows .. if it makes sense", async () => { const req1 = new Request("http://localhost/"); const res1 = await serveDir(req1, serveDirOptions); const page1 = await res1.text();
assert(!page1.includes("../")); assertStringIncludes(page1, "tls/");
const req2 = new Request("http://localhost/tls/"); const res2 = await serveDir(req2, serveDirOptions); const page2 = await res2.text();
assertStringIncludes(page2, "../");});
Deno.test("serveDir() handles range request (bytes=0-0)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=0-0" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals(text, "L");});
Deno.test("serveDir() handles range request (bytes=0-100)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=0-100" }, }); const res = await serveDir(req, serveDirOptions);
assertEquals( res.headers.get("content-range"), `bytes 0-100/${TEST_FILE_SIZE}`, ); assertEquals(res.status, 206); assertEquals((await res.arrayBuffer()).byteLength, 101);});
Deno.test("serveDir() handles range request (bytes=300-)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=300-" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals( res.headers.get("content-range"), `bytes 300-${TEST_FILE_SIZE - 1}/${TEST_FILE_SIZE}`, ); assertEquals(text, TEST_FILE_TEXT.substring(300));});
Deno.test("serveDir() handles range request (bytes=-200)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=-200" }, }); const res = await serveDir(req, serveDirOptions);
assertEquals(await res.text(), TEST_FILE_TEXT.slice(-200)); assertEquals( res.headers.get("content-range"), `bytes ${TEST_FILE_SIZE - 200}-${TEST_FILE_SIZE - 1}/${TEST_FILE_SIZE}`, ); assertEquals(res.status, 206); assertEquals(res.statusText, "Partial Content");});
Deno.test("serveDir() clamps ranges that are too large (bytes=0-999999999)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=0-999999999" }, }); const res = await serveDir(req, serveDirOptions);
assertEquals(await res.text(), TEST_FILE_TEXT); assertEquals( res.headers.get("content-range"), `bytes 0-${TEST_FILE_SIZE - 1}/${TEST_FILE_SIZE}`, ); assertEquals(res.status, 206); assertEquals(res.statusText, "Partial Content");});
Deno.test("serveDir() clamps ranges that are too large (bytes=-999999999)", async () => { const req = new Request("http://localhost/test_file.txt", { // This means the last 999999999 bytes. It is too big and should be clamped. headers: { range: "bytes=-999999999" }, }); const res = await serveDir(req, serveDirOptions);
assertEquals(await res.text(), TEST_FILE_TEXT); assertEquals( res.headers.get("content-range"), `bytes 0-${TEST_FILE_SIZE - 1}/${TEST_FILE_SIZE}`, ); assertEquals(res.status, 206); assertEquals(res.statusText, "Partial Content");});
Deno.test("serveDir() handles bad range request (bytes=500-200)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=500-200" }, }); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("content-range"), `bytes */${TEST_FILE_SIZE}`); assertEquals(res.status, 416); assertEquals(res.statusText, "Range Not Satisfiable");});
Deno.test("serveDir() handles bad range request (bytes=99999-999999)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=99999-999999" }, }); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("content-range"), `bytes */${TEST_FILE_SIZE}`); assertEquals(res.status, 416); assertEquals(res.statusText, "Range Not Satisfiable");});
Deno.test("serveDir() handles bad range request (bytes=99999)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=99999-" }, }); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("content-range"), `bytes */${TEST_FILE_SIZE}`); assertEquals(res.status, 416); assertEquals(res.statusText, "Range Not Satisfiable");});
Deno.test("serveDir() ignores bad range request (bytes=100)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=100" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals(text, TEST_FILE_TEXT); assertEquals(res.status, 200); assertEquals(res.statusText, "OK");});
Deno.test("serveDir() ignores bad range request (bytes=a-b)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=a-b" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals(text, TEST_FILE_TEXT); assertEquals(res.status, 200); assertEquals(res.statusText, "OK");});
Deno.test("serveDir() ignores bad multi-range request (bytes=0-10, 20-30)", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { range: "bytes=0-10, 20-30" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals(text, TEST_FILE_TEXT); assertEquals(res.status, 200); assertEquals(res.statusText, "OK");});
Deno.test("serveFile() serves ok response for empty file range request", async () => { const req = new Request("http://localhost/test_empty_file.txt", { headers: { range: "bytes=0-10, 20-30" }, }); const res = await serveDir(req, serveDirOptions); const text = await res.text();
assertEquals(text, ""); assertEquals(res.status, 200); assertEquals(res.statusText, "OK");});
Deno.test("serveDir() sets accept-ranges header to bytes for directory listing", async () => { const req = new Request("http://localhost/"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("accept-ranges"), "bytes");});
Deno.test("serveDir() sets accept-ranges header to bytes for file response", async () => { const req = new Request("http://localhost/test_file.txt"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("accept-ranges"), "bytes");});
Deno.test("serveDir() sets headers if provided as arguments", async () => { const req = new Request("http://localhost/test_file.txt"); const res = await serveDir(req, { ...serveDirOptions, headers: ["cache-control:max-age=100", "x-custom-header:hi"], }); await res.body?.cancel();
assertEquals(res.headers.get("cache-control"), "max-age=100"); assertEquals(res.headers.get("x-custom-header"), "hi");});
Deno.test("serveDir() sets etag header", async () => { const req = new Request("http://localhost/test_file.txt"); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.headers.get("etag"), TEST_FILE_ETAG);});
Deno.test("serveDir() serves empty HTTP 304 response for if-none-match request of unmodified file", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { "if-none-match": TEST_FILE_ETAG }, }); const res = await serveDir(req, serveDirOptions);
assertEquals(await res.text(), ""); assertEquals(res.status, 304); assertEquals(res.statusText, "Not Modified");});
Deno.test("serveDir() serves HTTP 304 response for if-modified-since request of unmodified file", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { "if-modified-since": TEST_FILE_LAST_MODIFIED }, }); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.status, 304); assertEquals(res.statusText, "Not Modified");});
/** * When used in combination with If-None-Match, If-Modified-Since is ignored. * If etag doesn't match, don't return 304 even if if-modified-since is a valid * value. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since} */Deno.test( "serveDir() only uses if-none-match header if if-non-match and if-modified-since headers are provided", async () => { const req = new Request("http://localhost/test_file.txt", { headers: { "if-none-match": "not match etag", "if-modified-since": TEST_FILE_LAST_MODIFIED, }, }); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.status, 200); assertEquals(res.statusText, "OK"); },);
Deno.test("serveFile() serves test file", async () => { const req = new Request("http://localhost/testdata/test_file.txt"); const res = await serveFile(req, TEST_FILE_PATH);
assertEquals(res.status, 200); assertEquals(await res.text(), TEST_FILE_TEXT);});
Deno.test("serveFile() handles file not found", async () => { const req = new Request("http://localhost/testdata/non_existent.txt"); const testdataPath = join(testdataDir, "non_existent.txt"); const res = await serveFile(req, testdataPath); await res.body?.cancel();
assertEquals(res.status, 404); assertEquals(res.statusText, "Not Found");});
Deno.test("serveFile() serves HTTP 404 when the path is a directory", async () => { const req = new Request("http://localhost/testdata/"); const res = await serveFile(req, testdataDir); await res.body?.cancel();
assertEquals(res.status, 404); assertEquals(res.statusText, "Not Found");});
Deno.test("serveFile() handles bad range request (bytes=200-500)", async () => { const req = new Request("http://localhost/testdata/test_file.txt", { headers: { range: "bytes=200-500" }, }); const res = await serveFile(req, TEST_FILE_PATH);
assertEquals(res.status, 206); assertEquals((await res.arrayBuffer()).byteLength, 301);});
Deno.test("serveFile() handles bad range request (bytes=500-200)", async () => { const req = new Request("http://localhost/testdata/test_file.txt", { headers: { range: "bytes=500-200" }, }); const res = await serveFile(req, TEST_FILE_PATH); await res.body?.cancel();
assertEquals(res.status, 416);});
Deno.test("serveFile() serves HTTP 304 response for if-modified-since request of unmodified file", async () => { const req = new Request("http://localhost/testdata/test_file.txt", { headers: { "if-none-match": TEST_FILE_ETAG }, }); const res = await serveFile(req, TEST_FILE_PATH);
assertEquals(res.status, 304); assertEquals(res.statusText, "Not Modified");});
/** * When used in combination with If-None-Match, If-Modified-Since is ignored. * If etag doesn't match, don't return 304 even if if-modified-since is a valid * value. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since} */Deno.test("serveFile() only uses if-none-match header if if-non-match and if-modified-since headers are provided", async () => { const req = new Request("http://localhost/testdata/test_file.txt", { headers: { "if-none-match": "not match etag", "if-modified-since": TEST_FILE_LAST_MODIFIED, }, }); const res = await serveFile(req, TEST_FILE_PATH); await res.body?.cancel();
assertEquals(res.status, 200); assertEquals(res.statusText, "OK");});
Deno.test("serveFile() etag value falls back to DENO_DEPLOYMENT_ID if fileInfo.mtime is not available", async () => { const DENO_DEPLOYMENT_ID = "__THIS_IS_DENO_DEPLOYMENT_ID__"; const hashedDenoDeploymentId = await calculate(DENO_DEPLOYMENT_ID, { weak: true, }); // deno-fmt-ignore const code = ` import { serveFile } from "${import.meta.resolve("./file_server.ts")}"; import { fromFileUrl } from "${import.meta.resolve("../path/mod.ts")}"; import { assertEquals } from "${import.meta.resolve("../assert/assert_equals.ts")}"; const testdataPath = "${toFileUrl(join(testdataDir, "test_file.txt"))}"; const fileInfo = await Deno.stat(new URL(testdataPath)); fileInfo.mtime = null; const req = new Request("http://localhost/testdata/test_file.txt"); const res = await serveFile(req, fromFileUrl(testdataPath), { fileInfo }); assertEquals(res.headers.get("etag"), \`${hashedDenoDeploymentId}\`); `; const command = new Deno.Command(Deno.execPath(), { args: ["eval", "--no-lock", code], stdout: "null", stderr: "null", env: { DENO_DEPLOYMENT_ID }, }); const { success } = await command.output(); assert(success);});
Deno.test("serveDir() without options serves files in current directory", async () => { const req = new Request("http://localhost/http/testdata/hello.html"); const res = await serveDir(req);
assertEquals(res.status, 200); assertStringIncludes(await res.text(), "Hello World");});
Deno.test("serveDir() with fsRoot and urlRoot option serves files in given directory", async () => { const req = new Request( "http://localhost/my-static-root/testdata/hello.html", ); const res = await serveDir(req, { fsRoot: "http", urlRoot: "my-static-root", });
assertEquals(res.status, 200); assertStringIncludes(await res.text(), "Hello World");});
Deno.test("serveDir() serves index.html when showIndex is true", async () => { const url = "http://localhost/http/testdata/subdir-with-index/"; const expectedText = "This is subdir-with-index/index.html"; { const res = await serveDir(new Request(url), { showIndex: true }); assertEquals(res.status, 200); assertStringIncludes(await res.text(), expectedText); }
{ // showIndex is true by default const res = await serveDir(new Request(url)); assertEquals(res.status, 200); assertStringIncludes(await res.text(), expectedText); }});
Deno.test("serveDir() doesn't serve index.html when showIndex is false", async () => { const req = new Request( "http://localhost/http/testdata/subdir-with-index/", ); const res = await serveDir(req, { showIndex: false });
assertEquals(res.status, 404);});
Deno.test( "serveDir() redirects a directory URL not ending with a slash if it has an index", async () => { const url = "http://localhost/http/testdata/subdir-with-index"; const res = await serveDir(new Request(url), { showIndex: true });
assertEquals(res.status, 301); assertEquals( res.headers.get("Location"), "http://localhost/http/testdata/subdir-with-index/", ); },);
Deno.test("serveDir() redirects a directory URL not ending with a slash correctly even with a query string", async () => { const url = "http://localhost/http/testdata/subdir-with-index?test"; const res = await serveDir(new Request(url), { showIndex: true });
assertEquals(res.status, 301); assertEquals( res.headers.get("Location"), "http://localhost/http/testdata/subdir-with-index/?test", );});
Deno.test("serveDir() redirects a file URL ending with a slash correctly even with a query string", async () => { const url = "http://localhost/http/testdata/test_file.txt/?test"; const res = await serveDir(new Request(url), { showIndex: true });
assertEquals(res.status, 301); assertEquals( res.headers.get("Location"), "http://localhost/http/testdata/test_file.txt?test", );});
Deno.test("serveDir() redirects non-canonical URLs", async () => { const url = "http://localhost/http/testdata//////test_file.txt/////?test"; const res = await serveDir(new Request(url), { showIndex: true });
assertEquals(res.status, 301); assertEquals( res.headers.get("Location"), "http://localhost/http/testdata/test_file.txt/?test", );});
Deno.test("serveDir() serves HTTP 304 for if-none-match requests with W/-prefixed etag", async () => { const testurl = "http://localhost/desktop.ini"; const fileurl = new URL("./testdata/desktop.ini", import.meta.url); const req1 = new Request(testurl, { headers: { "accept-encoding": "gzip, deflate, br" }, }); const res1 = await serveDir(req1, serveDirOptions); const etag = res1.headers.get("etag");
assertEquals(res1.status, 200); assertEquals(res1.statusText, "OK"); assertEquals(await Deno.readTextFile(fileurl), await res1.text()); assert(typeof etag === "string"); assert(etag.length > 0); assert(etag.startsWith("W/"));
const req2 = new Request(testurl, { headers: { "if-none-match": etag }, }); const res2 = await serveDir(req2, serveDirOptions);
assertEquals(res2.status, 304); assertEquals(res2.statusText, "Not Modified"); assertEquals("", await res2.text()); assert( etag === res2.headers.get("etag") || etag === "W/" + res2.headers.get("etag"), );});
Deno.test("serveDir() resolves path correctly on Windows", { ignore: Deno.build.os !== "windows",}, async () => { const req = new Request("http://localhost/"); const res = await serveDir(req, { ...serveDirOptions, fsRoot: "c:/" }); await res.body?.cancel();
assertEquals(res.status, 200);});
Deno.test( "serveDir() resolves empty sub-directory without asking for current directory read permissions on Windows", { ignore: Deno.build.os !== "windows", permissions: { read: [`${moduleDir}/testdata`], write: true, }, }, async () => { const tempDir = await Deno.makeTempDir({ dir: `${moduleDir}/testdata`, }); const req = new Request(`http://localhost/${basename(tempDir)}/`); const res = await serveDir(req, serveDirOptions); await res.body?.cancel();
assertEquals(res.status, 200);
await Deno.remove(tempDir); },);
Deno.test("file_server prints local and network urls", async () => { const port = await getAvailablePort(); const process = spawnDeno([ "--allow-net", "--allow-read", "--allow-sys=networkInterfaces", "http/file_server.ts", "--port", `${port}`, ]); const output = await readUntilMatch(process.stdout, "Network:"); const networkAdress = Deno.networkInterfaces().find((i) => i.family === "IPv4" && !i.address.startsWith("127") )?.address; assertEquals( output, `Listening on:\n- Local: http://localhost:${port}\n- Network: http://${networkAdress}:${port}\n`, ); process.stdout.cancel(); process.stderr.cancel(); process.kill(); await process.status;});
Deno.test("file_server prints only local address on Deploy", async () => { const port = await getAvailablePort(); const process = spawnDeno([ "--allow-net", "--allow-read", "--allow-sys=networkInterfaces", "--allow-env=DENO_DEPLOYMENT_ID", "http/file_server.ts", "--port", `${port}`, ], { env: { DENO_DEPLOYMENT_ID: "abcdef", }, }); const output = await readUntilMatch(process.stdout, "Local:"); console.log(output); assertEquals( output, `Listening on:\n- Local: http://localhost:${port}\n`, ); process.stdout.cancel(); process.stderr.cancel(); process.kill(); await process.status;});
/** Spawn deno child process with the options convenient for testing */function spawnDeno(args: string[], opts?: Deno.CommandOptions) { const cmd = new Deno.Command(Deno.execPath(), { args: [ "run", "--no-lock", "--quiet", "--config", "deno.json", ...args, ], stdout: "piped", stderr: "piped", ...opts, }); return cmd.spawn();}
async function readUntilMatch( source: ReadableStream, match: string,) { const reader = source.getReader(); let buf = new Uint8Array(0); const dec = new TextDecoder(); while (!dec.decode(buf).includes(match)) { const { value } = await reader.read(); if (!value) { break; } buf = concat([buf, value]); } reader.releaseLock(); return dec.decode(buf);}
Version Info