lokalise-file-exchange
Version:
Exchange translation files with Lokalise TMS
370 lines (330 loc) • 9.56 kB
text/typescript
import fs from "node:fs";
import path from "node:path";
import type {
DownloadedFileProcessDetails,
FileFormat,
QueuedProcess,
} from "@lokalise/node-api";
import mockFs from "mock-fs";
import { MockAgent, setGlobalDispatcher } from "undici";
import { LokaliseError } from "../../../lib/index.js";
import { FakeLokaliseDownload } from "../../fixtures/fake_classes/FakeLokaliseDownload.js";
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "../../setup.js";
import type { Interceptable } from "../../setup.js";
describe("LokaliseDownload: downloadTranslations()", () => {
const projectId = "803826145ba90b42d5d860.46800099";
const apiKey = process.env.API_KEY as string;
const downloadFileParams = { format: "json" as FileFormat };
const extractParams = { outputDir: "/output/dir" };
let downloader: FakeLokaliseDownload;
const demoZipPath = path.resolve(
__dirname,
"../../fixtures/demo_archive.zip",
);
const mockOutputDir = "/output/dir";
let mockAgent: MockAgent;
let mockPool: Interceptable;
const processId = "74738ff5-5367-5958-9aee-98fffdcd1876";
beforeAll(() => {
mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
mockAgent.disableNetConnect();
});
beforeEach(() => {
downloader = new FakeLokaliseDownload({ apiKey }, { projectId });
mockFs({
[demoZipPath]: fs.readFileSync(demoZipPath),
[mockOutputDir]: {},
});
mockPool = mockAgent.get("https://api.lokalise.com");
});
afterEach(() => {
vi.restoreAllMocks();
mockFs.restore();
});
afterAll(() => {
mockAgent.close();
});
describe("Success Cases", () => {
it("should download, extract, and clean up translations successfully", async () => {
const mockResponse = {
process_id: processId,
};
const fakeDownloadUrl = "https://example.com/fake.zip";
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
mockPool
.intercept({
method: "GET",
path: `/api2/projects/${projectId}/processes/${processId}`,
})
.reply(() => {
return {
statusCode: 200,
data: {
process: {
process_id: processId,
status: "finished",
details: {
download_url: fakeDownloadUrl,
},
},
},
};
})
.times(1);
const unlinkSpy = vi
.spyOn(fs.promises, "unlink")
.mockResolvedValue(undefined);
const downloadZipSpy = vi
.spyOn(downloader, "downloadZip")
.mockResolvedValue(demoZipPath);
await downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 500,
pollMaximumWaitTime: 5000,
bundleDownloadTimeout: 10000,
},
});
expect(fs.existsSync("/output/dir/en/en.json")).toBe(true);
expect(fs.existsSync("/output/dir/fr_CA/no_filename.json")).toBe(true);
const jsonContent = JSON.parse(
fs.readFileSync("/output/dir/fr_FR/fr_FR.json", "utf8"),
);
expect(jsonContent).toEqual({ welcome: "Bienvenue!" });
expect(unlinkSpy).toHaveBeenCalledWith(demoZipPath);
expect(downloadZipSpy).toHaveBeenCalledWith(fakeDownloadUrl, 10000);
});
});
describe("Error Cases", () => {
it("should throw an error if the async download process does not finish in time", async () => {
const mockResponse = {
process_id: processId,
};
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
const incompleteProcess: QueuedProcess = {
process_id: processId,
status: "running",
details: {} as DownloadedFileProcessDetails,
type: "file-import",
message: "",
created_by: "20181",
created_by_email: "bodrovis@protonmail.com",
created_at: "2023-09-19 13:26:18 (Etc/UTC)",
created_at_timestamp: 1695129978,
};
vi.spyOn(downloader, "pollProcesses").mockResolvedValue([
incompleteProcess,
]);
const pollMaximumWaitTime = 100;
await expect(
downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 1,
pollMaximumWaitTime,
},
}),
).rejects.toThrow(
`Download process took too long to finalize; gave up after ${pollMaximumWaitTime}ms`,
);
});
it("should throw an error if async download process finishes without a download URL", async () => {
const mockResponse = {
process_id: processId,
};
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
const finishedNoUrlProcess = {
process_id: processId,
status: "finished",
details: {} as DownloadedFileProcessDetails, // missing download_url
type: "file-import",
message: "",
created_by: "20181",
created_by_email: "bodrovis@protonmail.com",
created_at: "2023-09-19 13:26:18 (Etc/UTC)",
created_at_timestamp: 1695129978,
};
vi.spyOn(downloader, "pollProcesses").mockResolvedValue([
finishedNoUrlProcess,
]);
await expect(
downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 100,
pollMaximumWaitTime: 1000,
},
}),
).rejects.toThrow("Invalid URL: undefined");
});
it("should throw an error if downloadZip fails", async () => {
const mockResponse = {
process_id: processId,
};
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
const finishedProcess = {
process_id: processId,
status: "finished",
details: {
download_url: "https://example.com/fake.zip",
file_size_kb: 1,
total_number_of_keys: 3,
},
type: "file-import",
message: "",
created_by: "20181",
created_by_email: "bodrovis@protonmail.com",
created_at: "2023-09-19 13:26:18 (Etc/UTC)",
created_at_timestamp: 1695129978,
};
vi.spyOn(downloader, "pollProcesses").mockResolvedValue([
finishedProcess,
]);
vi.spyOn(downloader, "downloadZip").mockRejectedValue(
new LokaliseError("Download failed", 500),
);
await expect(
downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 100,
pollMaximumWaitTime: 1000,
},
}),
).rejects.toThrow("Download failed");
});
it("should throw an error if unpackZip fails but still attempt to clean up the ZIP file", async () => {
const mockResponse = {
process_id: processId,
};
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
const finishedProcess = {
process_id: processId,
status: "finished",
details: {
download_url: "https://example.com/fake.zip",
file_size_kb: 1,
total_number_of_keys: 3,
},
type: "file-import",
message: "",
created_by: "20181",
created_by_email: "bodrovis@protonmail.com",
created_at: "2023-09-19 13:26:18 (Etc/UTC)",
created_at_timestamp: 1695129978,
};
vi.spyOn(downloader, "pollProcesses").mockResolvedValue([
finishedProcess,
]);
vi.spyOn(downloader, "downloadZip").mockResolvedValue(demoZipPath);
vi.spyOn(downloader, "unpackZip").mockRejectedValue(
new LokaliseError("Extraction failed", 500),
);
const unlinkSpy = vi
.spyOn(fs.promises, "unlink")
.mockResolvedValue(undefined);
await expect(
downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 100,
pollMaximumWaitTime: 1000,
},
}),
).rejects.toThrow("Extraction failed");
expect(unlinkSpy).toHaveBeenCalledWith(demoZipPath);
});
it("should throw an error if the download URL is invalid", async () => {
const mockResponse = {
process_id: processId,
};
const url = "ftp://example.com/fake.zip";
mockPool
.intercept({
path: `/api2/projects/${projectId}/files/async-download`,
method: "POST",
body: JSON.stringify(downloadFileParams),
})
.reply(200, mockResponse);
const finishedProcess = {
process_id: processId,
status: "finished",
details: {
download_url: url,
file_size_kb: 1,
total_number_of_keys: 3,
},
type: "file-import",
message: "",
created_by: "20181",
created_by_email: "bodrovis@protonmail.com",
created_at: "2023-09-19 13:26:18 (Etc/UTC)",
created_at_timestamp: 1695129978,
};
vi.spyOn(downloader, "pollProcesses").mockResolvedValue([
finishedProcess,
]);
await expect(
downloader.downloadTranslations({
downloadFileParams,
extractParams,
processDownloadFileParams: {
asyncDownload: true,
pollInitialWaitTime: 100,
pollMaximumWaitTime: 1000,
},
}),
).rejects.toThrow(`Invalid URL: ${url}`);
});
});
});