164 lines
5.0 KiB
JavaScript
Raw Normal View History

2026-08-25 17:03:56 +08:00
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"));
});
2026-08-25 18:43:04 +08:00
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
);
});
2026-08-25 17:03:56 +08:00
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);
});
});