lokalise-file-exchange
Version:
Exchange translation files with Lokalise TMS
275 lines (233 loc) • 6.94 kB
text/typescript
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { Writable } from "node:stream";
import { LokaliseError } from "../../../lib/errors/LokaliseError.js";
import { FakeLokaliseDownload } from "../../fixtures/fake_classes/FakeLokaliseDownload.js";
import {
MockAgent,
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
setGlobalDispatcher,
vi,
} from "../../setup.js";
import type { Interceptable } from "../../setup.js";
type MockWriteStream = fs.WriteStream & {
path: string;
bytesWritten: number;
pending: boolean;
};
describe("LokaliseDownload: downloadZip()", () => {
const projectId = "803826145ba90b42d5d860.46800099";
const apiKey = process.env.API_KEY as string;
let downloader: FakeLokaliseDownload;
let mockAgent: MockAgent;
let mockPool: Interceptable;
beforeAll(() => {
mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
mockAgent.disableNetConnect();
});
afterAll(() => {
mockAgent.close();
});
beforeEach(() => {
downloader = new FakeLokaliseDownload({ apiKey }, { projectId });
mockPool = mockAgent.get("https://example.com");
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Success Cases", () => {
it("should successfully download a ZIP file", async () => {
const mockZipContent = "Mock ZIP file content";
const mockTempPath = "/mock/temp/lokalise-translations.zip";
vi.spyOn(path, "join").mockReturnValue(mockTempPath);
vi.spyOn(fs, "createWriteStream").mockImplementation(
(filePath): fs.WriteStream => {
const { Writable } = require("node:stream");
const stream = new Writable({
write(_chunk, _encoding, callback) {
callback(); // Simulate writing to the stream
},
});
const mockStream: MockWriteStream = Object.assign(stream, {
path: filePath,
bytesWritten: 0,
pending: false,
});
return mockStream;
},
);
mockPool
.intercept({
path: "/download.zip",
method: "GET",
})
.reply(200, mockZipContent, {
headers: { "Content-Type": "application/zip" },
});
const zipPath = await downloader.downloadZip(
"https://example.com/download.zip",
);
expect(zipPath).toBe(mockTempPath);
expect(path.join).toHaveBeenCalledWith(
os.tmpdir(),
expect.stringMatching(/^lokalise-translations-.*\.zip$/),
);
expect(fs.createWriteStream).toHaveBeenCalledWith(mockTempPath);
});
});
describe("Error Cases", () => {
it("should throw an error if the response is not OK", async () => {
mockPool
.intercept({
path: "/download.zip",
method: "GET",
})
.reply(404, "Not Found");
await expect(
downloader.downloadZip("https://example.com/download.zip"),
).rejects.toThrow(
new LokaliseError("Failed to download ZIP file: Not Found (404)"),
);
});
it("should throw an error if the response body is null", async () => {
// Save the original fetch
const originalFetch = global.fetch;
// Mock fetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
body: null,
});
await expect(
downloader.downloadZip("https://example.com/download.zip"),
).rejects.toThrow(
new LokaliseError(
"Response body is null. Cannot download ZIP file from URL: https://example.com/download.zip",
),
);
// Restore the original fetch
global.fetch = originalFetch;
});
it("should throw an error for a malformed URL", async () => {
await expect(downloader.downloadZip("htp://invalid-url")).rejects.toThrow(
"Invalid URL: htp://invalid-url",
);
});
it("should throw an error if the stream fails during download", async () => {
const mockTempPath = "/mock/temp/lokalise-translations.zip";
vi.spyOn(path, "join").mockReturnValue(mockTempPath);
vi.spyOn(fs, "createWriteStream").mockImplementation(
(filePath: fs.PathLike) => {
// Ensure filePath is strictly of type string or Buffer
const normalizedPath =
typeof filePath === "string" ? filePath : filePath.toString();
class MockWriteStream extends Writable {
path: string | Buffer;
bytesWritten: number;
pending: boolean;
constructor(path: string | Buffer) {
super();
this.path = path;
this.bytesWritten = 0;
this.pending = false;
}
close() {
// Simulate close functionality
}
_write(
_chunk: string,
_encoding: string,
callback: (error?: Error | null) => void,
) {
// Simulate a stream write error
callback(new Error("Stream write error"));
}
}
return new MockWriteStream(normalizedPath);
},
);
mockPool
.intercept({
path: "/download.zip",
method: "GET",
})
.reply(200, "Mock ZIP content");
await expect(
downloader.downloadZip("https://example.com/download.zip"),
).rejects.toThrow("Stream write error");
});
it("should throw a timeout error if the download takes too long", async () => {
vi.spyOn(global, "fetch").mockImplementation(
() =>
new Promise((_, reject) => {
setTimeout(
() =>
reject(
new DOMException("The operation was aborted.", "AbortError"),
),
5,
);
}),
);
await expect(
downloader.downloadZip("https://example.com/download.zip", 1),
).rejects.toThrow(
new LokaliseError("Request timed out after 1ms", 408, {
reason: "timeout",
}),
);
});
});
describe("Edge Cases", () => {
it("should handle a slow response gracefully", async () => {
const mockZipContent = "Mock ZIP file content";
const mockTempPath = "/mock/temp/lokalise-translations.zip";
vi.spyOn(path, "join").mockReturnValue(mockTempPath);
vi.spyOn(fs, "createWriteStream").mockImplementation(
(filePath: fs.PathLike) => {
// Ensure filePath is strictly of type string or Buffer
const normalizedPath =
typeof filePath === "string" ? filePath : filePath.toString();
class MockWriteStream extends Writable {
path: string | Buffer;
bytesWritten: number;
pending: boolean;
constructor(path: string | Buffer) {
super();
this.path = path;
this.bytesWritten = 0;
this.pending = false;
}
close() {
// Simulate closing the stream
}
}
// Simulate slow writes
const slowStream = new MockWriteStream(normalizedPath);
slowStream._write = (_chunk, _encoding, callback) => {
setTimeout(callback, 100); // Simulate a slow write
};
return slowStream as fs.WriteStream;
},
);
mockPool
.intercept({
path: "/download.zip",
method: "GET",
})
.reply(200, mockZipContent);
await expect(
downloader.downloadZip("https://example.com/download.zip"),
).resolves.not.toThrow();
});
});
});