Applied formatting fixes to 21 files: - Fixed import ordering - Normalized indentation - Wrapped long lines No logic changes, formatting only.
348 lines
9.4 KiB
TypeScript
348 lines
9.4 KiB
TypeScript
/**
|
|
* Download Tests
|
|
*/
|
|
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import {
|
|
afterAll,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from "vitest";
|
|
import {
|
|
cleanOrphanedTempFiles,
|
|
ensureTempDir,
|
|
getTelegramTempDir,
|
|
streamDownloadToTemp,
|
|
} from "./download.js";
|
|
|
|
// Mock global fetch
|
|
global.fetch = vi.fn();
|
|
|
|
describe("download", () => {
|
|
const testTempDir = path.join(
|
|
os.tmpdir(),
|
|
`warelay-test-${process.pid}-download`,
|
|
);
|
|
|
|
beforeAll(() => {
|
|
process.env.TELEGRAM_TEMP_DIR = testTempDir;
|
|
});
|
|
|
|
afterAll(() => {
|
|
delete process.env.TELEGRAM_TEMP_DIR;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clean test directory before each test
|
|
await fs.rm(testTempDir, { recursive: true, force: true }).catch(() => {});
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("getTelegramTempDir", () => {
|
|
it("returns correct path", () => {
|
|
const dir = getTelegramTempDir();
|
|
expect(dir).toBe(testTempDir);
|
|
expect(path.isAbsolute(dir)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("ensureTempDir", () => {
|
|
it("creates directory if not exists", async () => {
|
|
// Use the real temp dir for this test
|
|
const dir = getTelegramTempDir();
|
|
|
|
// Clean it first
|
|
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
|
|
// Verify it doesn't exist
|
|
const existsBefore = await fs
|
|
.access(dir)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(existsBefore).toBe(false);
|
|
|
|
// Create it
|
|
await ensureTempDir();
|
|
|
|
// Verify it exists
|
|
const existsAfter = await fs
|
|
.access(dir)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(existsAfter).toBe(true);
|
|
});
|
|
|
|
it("succeeds if already exists", async () => {
|
|
const dir = getTelegramTempDir();
|
|
|
|
// Create it twice
|
|
await ensureTempDir();
|
|
await ensureTempDir();
|
|
|
|
// Should not throw
|
|
const exists = await fs
|
|
.access(dir)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(exists).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("streamDownloadToTemp", () => {
|
|
it("downloads small file to temp directory", async () => {
|
|
const testData = Buffer.from("test file content");
|
|
|
|
// Mock fetch response with readable stream
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
headers: {
|
|
get: (name: string) =>
|
|
name === "content-type" ? "text/plain" : null,
|
|
},
|
|
body: Readable.from([testData]),
|
|
} as any);
|
|
|
|
const result = await streamDownloadToTemp(
|
|
"https://example.com/test.txt",
|
|
1024 * 1024, // 1MB max
|
|
);
|
|
|
|
try {
|
|
// Verify file exists and has correct content
|
|
const content = await fs.readFile(result.tempPath);
|
|
expect(content.equals(testData)).toBe(true);
|
|
expect(result.size).toBe(testData.length);
|
|
expect(result.contentType).toBe("text/plain");
|
|
|
|
// Verify path is in temp directory
|
|
expect(result.tempPath.startsWith(testTempDir)).toBe(true);
|
|
expect(path.basename(result.tempPath)).toMatch(/telegram-dl-.*\.tmp$/);
|
|
} finally {
|
|
await result.cleanup();
|
|
}
|
|
});
|
|
|
|
it("throws on HTTP error (ok: false)", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: false,
|
|
statusText: "Not Found",
|
|
} as any);
|
|
|
|
await expect(
|
|
streamDownloadToTemp("https://example.com/missing.txt", 1024),
|
|
).rejects.toThrow(/Failed to download media.*Not Found/);
|
|
});
|
|
|
|
it("throws when size exceeds maxSize", async () => {
|
|
// Create data larger than maxSize
|
|
const largeData = Buffer.alloc(1024 * 10); // 10KB
|
|
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
headers: {
|
|
get: () => null,
|
|
},
|
|
body: Readable.from([largeData]),
|
|
} as any);
|
|
|
|
await expect(
|
|
streamDownloadToTemp(
|
|
"https://example.com/large.bin",
|
|
1024, // Only allow 1KB
|
|
),
|
|
).rejects.toThrow(/Download size.*exceeds maximum/);
|
|
});
|
|
|
|
it("cleans up temp file on error", async () => {
|
|
const largeData = Buffer.alloc(1024 * 10);
|
|
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
headers: {
|
|
get: () => null,
|
|
},
|
|
body: Readable.from([largeData]),
|
|
} as any);
|
|
|
|
let _tempPath: string | undefined;
|
|
try {
|
|
await streamDownloadToTemp("https://example.com/large.bin", 1024);
|
|
} catch {
|
|
// Expected to fail
|
|
}
|
|
|
|
// Give cleanup a moment to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
// Verify no temp files left
|
|
const tempDir = getTelegramTempDir();
|
|
const files = await fs.readdir(tempDir).catch(() => []);
|
|
const tempFiles = files.filter((f) => f.startsWith("telegram-dl-"));
|
|
expect(tempFiles.length).toBe(0);
|
|
});
|
|
|
|
it("cleanup() removes temp file", async () => {
|
|
const testData = Buffer.from("cleanup test");
|
|
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
headers: {
|
|
get: () => null,
|
|
},
|
|
body: Readable.from([testData]),
|
|
} as any);
|
|
|
|
const result = await streamDownloadToTemp(
|
|
"https://example.com/test.txt",
|
|
1024,
|
|
);
|
|
|
|
// Verify file exists
|
|
const existsBefore = await fs
|
|
.access(result.tempPath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(existsBefore).toBe(true);
|
|
|
|
// Cleanup
|
|
await result.cleanup();
|
|
|
|
// Verify file removed
|
|
const existsAfter = await fs
|
|
.access(result.tempPath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(existsAfter).toBe(false);
|
|
});
|
|
|
|
it("handles missing response body (body: null)", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
body: null,
|
|
} as any);
|
|
|
|
await expect(
|
|
streamDownloadToTemp("https://example.com/empty.txt", 1024),
|
|
).rejects.toThrow(/No response body/);
|
|
});
|
|
});
|
|
|
|
describe("cleanOrphanedTempFiles", () => {
|
|
it("removes old files (>TTL)", async () => {
|
|
const tempDir = getTelegramTempDir();
|
|
await ensureTempDir();
|
|
|
|
// Create a temp file
|
|
const oldFile = path.join(tempDir, "telegram-dl-old.tmp");
|
|
await fs.writeFile(oldFile, "old content");
|
|
|
|
// Mock stat to return old mtime
|
|
const statSpy = vi.spyOn(fs, "stat");
|
|
const oldDate = Date.now() - 2 * 60 * 60 * 1000; // 2 hours ago
|
|
statSpy.mockResolvedValue({
|
|
mtimeMs: oldDate,
|
|
} as any);
|
|
|
|
// Clean with 1 hour TTL
|
|
await cleanOrphanedTempFiles(60 * 60 * 1000);
|
|
|
|
// File should be removed
|
|
const exists = await fs
|
|
.access(oldFile)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(exists).toBe(false);
|
|
|
|
statSpy.mockRestore();
|
|
});
|
|
|
|
it("preserves recent files", async () => {
|
|
const tempDir = getTelegramTempDir();
|
|
await ensureTempDir();
|
|
|
|
// Create a recent file
|
|
const recentFile = path.join(tempDir, "telegram-dl-recent.tmp");
|
|
await fs.writeFile(recentFile, "recent content");
|
|
|
|
// Mock stat to return recent mtime
|
|
const statSpy = vi.spyOn(fs, "stat");
|
|
const recentDate = Date.now() - 10 * 60 * 1000; // 10 minutes ago
|
|
statSpy.mockResolvedValue({
|
|
mtimeMs: recentDate,
|
|
} as any);
|
|
|
|
// Clean with 1 hour TTL
|
|
await cleanOrphanedTempFiles(60 * 60 * 1000);
|
|
|
|
// File should still exist
|
|
const exists = await fs
|
|
.access(recentFile)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
expect(exists).toBe(true);
|
|
|
|
// Clean up
|
|
await fs.rm(recentFile, { force: true });
|
|
statSpy.mockRestore();
|
|
});
|
|
|
|
it("handles missing temp directory", async () => {
|
|
const tempDir = getTelegramTempDir();
|
|
|
|
// Delete temp directory
|
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
|
|
// Should not throw
|
|
await expect(cleanOrphanedTempFiles()).resolves.not.toThrow();
|
|
});
|
|
|
|
it("continues on individual file errors", async () => {
|
|
const tempDir = getTelegramTempDir();
|
|
await ensureTempDir();
|
|
|
|
// Create multiple files
|
|
const file1 = path.join(tempDir, "telegram-dl-1.tmp");
|
|
const file2 = path.join(tempDir, "telegram-dl-2.tmp");
|
|
await fs.writeFile(file1, "content 1");
|
|
await fs.writeFile(file2, "content 2");
|
|
|
|
// Mock stat to return old dates
|
|
const statSpy = vi.spyOn(fs, "stat");
|
|
const oldDate = Date.now() - 2 * 60 * 60 * 1000;
|
|
statSpy.mockResolvedValue({
|
|
mtimeMs: oldDate,
|
|
} as any);
|
|
|
|
// Mock rm to fail on first file
|
|
const rmSpy = vi.spyOn(fs, "rm");
|
|
let callCount = 0;
|
|
rmSpy.mockImplementation((async (filePath: string, options?: any) => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
throw new Error("Permission denied");
|
|
}
|
|
// Use actual implementation for other calls
|
|
return rmSpy.getMockImplementation
|
|
? Promise.resolve()
|
|
: fs.rm(filePath, options);
|
|
}) as any);
|
|
|
|
// Should not throw, continues processing
|
|
await expect(cleanOrphanedTempFiles()).resolves.not.toThrow();
|
|
|
|
// Clean up
|
|
await fs.rm(file1, { force: true }).catch(() => {});
|
|
await fs.rm(file2, { force: true }).catch(() => {});
|
|
statSpy.mockRestore();
|
|
rmSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|