164 lines
5.0 KiB
JavaScript
164 lines
5.0 KiB
JavaScript
jest.mock("axios", () => ({
|
|
get: jest.fn(),
|
|
}));
|
|
jest.mock("../../utils/logger", () => ({
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
}));
|
|
|
|
const fs = require("fs");
|
|
const fsp = require("fs/promises");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { Readable } = require("stream");
|
|
const axios = require("axios");
|
|
const archive = require("./index");
|
|
|
|
function setNested(target, dottedKey, value) {
|
|
const keys = dottedKey.split(".");
|
|
let current = target;
|
|
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
current[keys[index]] = current[keys[index]] || {};
|
|
current = current[keys[index]];
|
|
}
|
|
current[keys[keys.length - 1]] = value;
|
|
}
|
|
|
|
function createDatabase(document) {
|
|
const collection = {
|
|
findOne: jest.fn(async (query) => {
|
|
if (query._id !== undefined && query._id !== document._id) return null;
|
|
return document;
|
|
}),
|
|
updateOne: jest.fn(async (_query, update) => {
|
|
Object.entries(update.$set || {}).forEach(([key, value]) => {
|
|
setNested(document, key, value);
|
|
});
|
|
Object.entries(update.$inc || {}).forEach(([key, value]) => {
|
|
const current = key.split(".").reduce((result, part) => result[part], document);
|
|
setNested(document, key, current + value);
|
|
});
|
|
return { modifiedCount: 1 };
|
|
}),
|
|
};
|
|
return {
|
|
collection: jest.fn(() => collection),
|
|
};
|
|
}
|
|
|
|
describe("IM image archive", () => {
|
|
let temporaryRoot;
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
temporaryRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "im-image-archive-"));
|
|
process.env.CONFIG_IM_IMAGE_ROOT = temporaryRoot;
|
|
delete process.env.CONFIG_IM_IMAGE_ALLOWED_HOSTS;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
delete process.env.CONFIG_IM_IMAGE_ROOT;
|
|
await fsp.rm(temporaryRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
test("prepares the original image from a Tencent callback", () => {
|
|
const message = {
|
|
MsgBody: [
|
|
{
|
|
MsgType: "TIMImageElem",
|
|
MsgContent: {
|
|
UUID: "image-uuid",
|
|
ImageInfoArray: [
|
|
{ Type: 3, URL: "https://example.my-imcloud.com/thumb" },
|
|
{ Type: 1, URL: "https://example.my-imcloud.com/original", Size: 42 },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = archive.prepareMessageForArchive(message);
|
|
|
|
expect(result.imageArchive).toMatchObject({
|
|
status: "pending",
|
|
sourceUUID: "image-uuid",
|
|
sourceType: 1,
|
|
sourceURL: "https://example.my-imcloud.com/original",
|
|
expectedSize: 42,
|
|
});
|
|
});
|
|
|
|
test("rejects non-Tencent and non-HTTPS source URLs", () => {
|
|
expect(archive._test.isAllowedSourceUrl("https://a.my-imcloud.com/image")).toBe(true);
|
|
expect(archive._test.isAllowedSourceUrl("http://a.my-imcloud.com/image")).toBe(false);
|
|
expect(archive._test.isAllowedSourceUrl("https://my-imcloud.com.example.com/image")).toBe(false);
|
|
});
|
|
|
|
test("uses the same ../private-files rule in source and bundled runtime", () => {
|
|
expect(
|
|
archive._test.resolveDefaultImageRoot(
|
|
path.join("/workspace", "hn-hlw-service", "hlw", "im-image-archive"),
|
|
false
|
|
)
|
|
).toBe(path.resolve("/workspace", "private-files", "im-chat"));
|
|
expect(
|
|
archive._test.resolveDefaultImageRoot("/opt/apps/hnhlw", true)
|
|
).toBe(path.resolve("/opt/apps", "private-files", "im-chat"));
|
|
});
|
|
|
|
test("creates and validates the configured storage directory", async () => {
|
|
const configuredRoot = path.join(temporaryRoot, "nested", "im-chat");
|
|
process.env.CONFIG_IM_IMAGE_ROOT = configuredRoot;
|
|
|
|
await expect(archive.initializeStorage()).resolves.toBe(
|
|
path.resolve(configuredRoot)
|
|
);
|
|
await expect(fsp.access(configuredRoot, fs.constants.W_OK)).resolves.toBe(
|
|
undefined
|
|
);
|
|
});
|
|
|
|
test("downloads an image and marks the database record successful", async () => {
|
|
const jpeg = Buffer.concat([
|
|
Buffer.from([0xff, 0xd8, 0xff, 0xe0]),
|
|
Buffer.alloc(64, 1),
|
|
]);
|
|
axios.get.mockResolvedValue({
|
|
headers: { "content-length": String(jpeg.length) },
|
|
data: Readable.from([jpeg]),
|
|
});
|
|
const message = archive.prepareMessageForArchive({
|
|
_id: "document-1",
|
|
MsgId: "message-1",
|
|
MsgTime: 1786013773,
|
|
MsgBody: [
|
|
{
|
|
MsgType: "TIMImageElem",
|
|
MsgContent: {
|
|
UUID: "image-uuid",
|
|
ImageInfoArray: [
|
|
{
|
|
Type: 1,
|
|
URL: "https://a.my-imcloud.com/original",
|
|
Size: jpeg.length,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const db = createDatabase(message);
|
|
|
|
await archive._test.archiveDocument(db, message._id);
|
|
|
|
expect(message.imageArchive.status).toBe("success");
|
|
expect(message.imageArchive.mimeType).toBe("image/jpeg");
|
|
expect(message.imageArchive.size).toBe(jpeg.length);
|
|
expect(message.imageArchive.relativePath).toMatch(/\.jpg$/);
|
|
expect(
|
|
fs.existsSync(path.resolve(temporaryRoot, message.imageArchive.relativePath))
|
|
).toBe(true);
|
|
});
|
|
});
|