diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/fileUtils.test.ts | 112 | ||||
| -rw-r--r-- | tests/integration.test.ts | 270 | ||||
| -rw-r--r-- | tests/rateLimiter.test.ts | 106 |
3 files changed, 488 insertions, 0 deletions
diff --git a/tests/fileUtils.test.ts b/tests/fileUtils.test.ts new file mode 100644 index 0000000..15bfa3d --- /dev/null +++ b/tests/fileUtils.test.ts @@ -0,0 +1,112 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { + getUniqueFilename, + getMimeType, + isViewableInBrowser, + formatFileSize, + isAllowedFileType, +} from "../fileUtils.ts"; + +Deno.test("getMimeType - returns correct MIME type for images", () => { + assertEquals(getMimeType("photo.jpg"), "image/jpeg"); + assertEquals(getMimeType("image.png"), "image/png"); + assertEquals(getMimeType("graphic.gif"), "image/gif"); + assertEquals(getMimeType("pic.webp"), "image/webp"); +}); + +Deno.test("getMimeType - returns correct MIME type for videos", () => { + assertEquals(getMimeType("video.mp4"), "video/mp4"); + assertEquals(getMimeType("clip.webm"), "video/webm"); + assertEquals(getMimeType("movie.avi"), "video/x-msvideo"); +}); + +Deno.test("getMimeType - returns correct MIME type for documents", () => { + assertEquals(getMimeType("document.pdf"), "application/pdf"); + assertEquals(getMimeType("readme.txt"), "text/plain"); + assertEquals(getMimeType("archive.zip"), "application/zip"); +}); + +Deno.test("getMimeType - handles uppercase extensions", () => { + assertEquals(getMimeType("PHOTO.JPG"), "image/jpeg"); + assertEquals(getMimeType("Video.MP4"), "video/mp4"); +}); + +Deno.test("getMimeType - returns octet-stream for unknown types", () => { + assertEquals(getMimeType("file.xyz"), "application/octet-stream"); + assertEquals(getMimeType("noextension"), "application/octet-stream"); +}); + +Deno.test("isViewableInBrowser - images are viewable", () => { + assertEquals(isViewableInBrowser("image/jpeg"), true); + assertEquals(isViewableInBrowser("image/png"), true); + assertEquals(isViewableInBrowser("image/gif"), true); +}); + +Deno.test("isViewableInBrowser - videos are viewable", () => { + assertEquals(isViewableInBrowser("video/mp4"), true); + assertEquals(isViewableInBrowser("video/webm"), true); +}); + +Deno.test("isViewableInBrowser - PDFs and text are viewable", () => { + assertEquals(isViewableInBrowser("application/pdf"), true); + assertEquals(isViewableInBrowser("text/plain"), true); +}); + +Deno.test("isViewableInBrowser - archives are not viewable", () => { + assertEquals(isViewableInBrowser("application/zip"), false); + assertEquals(isViewableInBrowser("application/octet-stream"), false); +}); + +Deno.test("formatFileSize - formats bytes correctly", () => { + assertEquals(formatFileSize(0), "0 Bytes"); + assertEquals(formatFileSize(500), "500 Bytes"); + assertEquals(formatFileSize(1024), "1 KB"); + assertEquals(formatFileSize(1536), "1.5 KB"); + assertEquals(formatFileSize(1048576), "1 MB"); + assertEquals(formatFileSize(1073741824), "1 GB"); +}); + +Deno.test("isAllowedFileType - allows configured types", () => { + assertEquals(isAllowedFileType("image/jpeg"), true); + assertEquals(isAllowedFileType("application/pdf"), true); +}); + +Deno.test("getUniqueFilename - returns same name if file doesn't exist", async () => { + const testDir = await Deno.makeTempDir(); + + try { + // Save original config + const { config } = await import("../config.ts"); + const originalUploadDir = config.uploadDir; + config.uploadDir = testDir; + + const filename = await getUniqueFilename("test.txt"); + assertEquals(filename, "test.txt"); + + // Restore + config.uploadDir = originalUploadDir; + } finally { + await Deno.remove(testDir, { recursive: true }); + } +}); + +Deno.test("getUniqueFilename - appends epoch if file exists", async () => { + const testDir = await Deno.makeTempDir(); + + try { + // Create a test file + await Deno.writeTextFile(`${testDir}/existing.txt`, "test"); + + const { config } = await import("../config.ts"); + const originalUploadDir = config.uploadDir; + config.uploadDir = testDir; + + const filename = await getUniqueFilename("existing.txt"); + assertEquals(filename.startsWith("existing_"), true); + assertEquals(filename.endsWith(".txt"), true); + + config.uploadDir = originalUploadDir; + } finally { + await Deno.remove(testDir, { recursive: true }); + } +}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts new file mode 100644 index 0000000..666115a --- /dev/null +++ b/tests/integration.test.ts @@ -0,0 +1,270 @@ +import { assertEquals, assertExists } from "@std/assert"; + +const BASE_URL = "http://localhost:8000"; + +// Helper to check if server is running +async function isServerRunning(): Promise<boolean> { + try { + const response = await fetch(BASE_URL); + // Consume the response body to prevent resource leak + await response.body?.cancel(); + return response.ok; + } catch { + return false; + } +} + +Deno.test({ + name: "GET / - returns upload page", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const response = await fetch(BASE_URL); + assertEquals(response.status, 200); + assertEquals(response.headers.get("content-type"), "text/html"); + + const html = await response.text(); + assertExists(html.includes("File Upload")); + }, +}); + +Deno.test({ + name: "GET /browse - returns browse page", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const response = await fetch(`${BASE_URL}/browse`); + assertEquals(response.status, 200); + assertEquals(response.headers.get("content-type"), "text/html"); + + const html = await response.text(); + assertExists(html.includes("Browse Files")); + }, +}); + +Deno.test({ + name: "GET /gallery - returns gallery page", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const response = await fetch(`${BASE_URL}/gallery`); + assertEquals(response.status, 200); + assertEquals(response.headers.get("content-type"), "text/html"); + + const html = await response.text(); + assertExists(html.includes("Gallery")); + }, +}); + +Deno.test({ + name: "POST /upload - uploads a file", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const formData = new FormData(); + const testFile = new File(["test content"], "test.txt", { type: "text/plain" }); + formData.append("files", testFile); + + const response = await fetch(`${BASE_URL}/upload`, { + method: "POST", + body: formData, + }); + + if (response.status === 429) { + console.log("⚠️ Rate limit exceeded, skipping test"); + await response.body?.cancel(); + return; + } + + assertEquals(response.status, 200); + const data = await response.json(); + assertExists(data.files); + assertEquals(Array.isArray(data.files), true); + assertEquals(data.files.length, 1); + }, +}); + +Deno.test({ + name: "POST /upload - rejects file exceeding size limit", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + // Create a file larger than the configured max size (512MB) + // For testing, we'll just check if the validation logic works + const formData = new FormData(); + + // Create a large blob (10MB for testing purposes) + const largeContent = new Uint8Array(10 * 1024 * 1024); + const largeFile = new File([largeContent], "large.bin", { type: "application/octet-stream" }); + formData.append("files", largeFile); + + const response = await fetch(`${BASE_URL}/upload`, { + method: "POST", + body: formData, + }); + + if (response.status === 429) { + console.log("⚠️ Rate limit exceeded, skipping test"); + await response.body?.cancel(); + return; + } + + // Should succeed because 10MB < 512MB limit + assertEquals(response.status, 200); + await response.body?.cancel(); + }, +}); + +Deno.test({ + name: "GET /download/:filename - downloads uploaded file", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + // First upload a file + const formData = new FormData(); + const testContent = "download test content"; + const testFile = new File([testContent], "downloadtest.txt", { type: "text/plain" }); + formData.append("files", testFile); + + const uploadResponse = await fetch(`${BASE_URL}/upload`, { + method: "POST", + body: formData, + }); + + if (uploadResponse.status === 429) { + console.log("⚠️ Rate limit exceeded, skipping test"); + await uploadResponse.body?.cancel(); + return; + } + + const uploadData = await uploadResponse.json(); + const filename = uploadData.files[0]; + + // Now download it + const downloadResponse = await fetch(`${BASE_URL}/download/${encodeURIComponent(filename)}`); + assertEquals(downloadResponse.status, 200); + + const content = await downloadResponse.text(); + assertEquals(content, testContent); + }, +}); + +Deno.test({ + name: "GET /download/:filename - handles spaces in filename", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + // Upload a file with spaces + const formData = new FormData(); + const testContent = "space test content"; + const testFile = new File([testContent], "test file with spaces.txt", { type: "text/plain" }); + formData.append("files", testFile); + + const uploadResponse = await fetch(`${BASE_URL}/upload`, { + method: "POST", + body: formData, + }); + + if (uploadResponse.status === 429) { + console.log("⚠️ Rate limit exceeded, skipping test"); + await uploadResponse.body?.cancel(); + return; + } + + const uploadData = await uploadResponse.json(); + const filename = uploadData.files[0]; + + // Download with encoded filename + const downloadResponse = await fetch(`${BASE_URL}/download/${encodeURIComponent(filename)}`); + assertEquals(downloadResponse.status, 200); + + const content = await downloadResponse.text(); + assertEquals(content, testContent); + }, +}); + +Deno.test({ + name: "GET /download/nonexistent - returns 404", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const response = await fetch(`${BASE_URL}/download/nonexistent.txt`); + assertEquals(response.status, 404); + // Consume response body to prevent leak + await response.body?.cancel(); + }, +}); + +Deno.test({ + name: "POST /upload-chunk - uploads file in chunks", + async fn() { + if (!await isServerRunning()) { + console.log("⚠️ Server not running, skipping integration test"); + return; + } + + const content = "chunked upload test content"; + const filename = "chunked-test.txt"; + const blob = new Blob([content]); + const chunkSize = 10; // Small chunk size for testing + const chunks = Math.ceil(blob.size / chunkSize); + + // Upload chunks + for (let i = 0; i < chunks; i++) { + const start = i * chunkSize; + const end = Math.min(start + chunkSize, blob.size); + const chunk = blob.slice(start, end); + + const formData = new FormData(); + formData.append("chunk", new File([chunk], "chunk")); + formData.append("filename", filename); + formData.append("chunkIndex", String(i)); + formData.append("totalChunks", String(chunks)); + + const response = await fetch(`${BASE_URL}/upload-chunk`, { + method: "POST", + body: formData, + }); + + if (response.status === 429) { + console.log("⚠️ Rate limit exceeded, skipping test"); + await response.body?.cancel(); + return; + } + + assertEquals(response.status, 200); + await response.body?.cancel(); + } + + // Verify the file was assembled correctly + const downloadResponse = await fetch(`${BASE_URL}/download/${encodeURIComponent(filename)}`); + assertEquals(downloadResponse.status, 200); + + const downloadedContent = await downloadResponse.text(); + assertEquals(downloadedContent, content); + }, +}); diff --git a/tests/rateLimiter.test.ts b/tests/rateLimiter.test.ts new file mode 100644 index 0000000..2645deb --- /dev/null +++ b/tests/rateLimiter.test.ts @@ -0,0 +1,106 @@ +import { assertEquals } from "@std/assert"; +import { config } from "../config.ts"; + +Deno.test("RateLimiter - allows first request", () => { + // Create a new rate limiter instance for testing + class TestRateLimiter { + private storage = new Map<string, { count: number; resetTime: number }>(); + + isRateLimited(ip: string): boolean { + const now = Date.now(); + const entry = this.storage.get(ip); + + if (!entry || now > entry.resetTime) { + this.storage.set(ip, { + count: 1, + resetTime: now + config.rateLimit.windowMs, + }); + return false; + } + + if (entry.count >= config.rateLimit.maxUploads) { + return true; + } + + entry.count++; + return false; + } + } + + const limiter = new TestRateLimiter(); + assertEquals(limiter.isRateLimited("127.0.0.1"), false); +}); + +Deno.test("RateLimiter - allows requests within limit", () => { + class TestRateLimiter { + private storage = new Map<string, { count: number; resetTime: number }>(); + + isRateLimited(ip: string): boolean { + const now = Date.now(); + const entry = this.storage.get(ip); + + if (!entry || now > entry.resetTime) { + this.storage.set(ip, { + count: 1, + resetTime: now + config.rateLimit.windowMs, + }); + return false; + } + + if (entry.count >= config.rateLimit.maxUploads) { + return true; + } + + entry.count++; + return false; + } + } + + const limiter = new TestRateLimiter(); + const ip = "192.168.1.1"; + + // Make requests up to the limit + for (let i = 0; i < config.rateLimit.maxUploads; i++) { + assertEquals(limiter.isRateLimited(ip), false, `Request ${i + 1} should be allowed`); + } + + // Next request should be blocked + assertEquals(limiter.isRateLimited(ip), true, "Request beyond limit should be blocked"); +}); + +Deno.test("RateLimiter - different IPs have separate limits", () => { + class TestRateLimiter { + private storage = new Map<string, { count: number; resetTime: number }>(); + + isRateLimited(ip: string): boolean { + const now = Date.now(); + const entry = this.storage.get(ip); + + if (!entry || now > entry.resetTime) { + this.storage.set(ip, { + count: 1, + resetTime: now + config.rateLimit.windowMs, + }); + return false; + } + + if (entry.count >= config.rateLimit.maxUploads) { + return true; + } + + entry.count++; + return false; + } + } + + const limiter = new TestRateLimiter(); + + // Max out first IP + for (let i = 0; i < config.rateLimit.maxUploads; i++) { + limiter.isRateLimited("10.0.0.1"); + } + assertEquals(limiter.isRateLimited("10.0.0.1"), true); + + // Second IP should still be allowed + assertEquals(limiter.isRateLimited("10.0.0.2"), false); +}); |
