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); }); });