feat: init

This commit is contained in:
montaghanmy
2023-03-23 11:03:16 +01:00
commit 10fe6f78d1
11518 changed files with 509786 additions and 0 deletions
@@ -0,0 +1,116 @@
import { describe, expect, it, jest } from "@jest/globals";
import { CreateResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeCreated } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeCreated decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
done();
});
it("should call the original method send back original result and emit event", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<CreateResult<string>> {
return new CreateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("created", {
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
type: "string",
context: undefined,
operation: "create",
raw: undefined,
} as CreateResult<string>,
});
emitSpy.mockRestore();
done();
});
it("should emit event with path computed from function", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeCreated<string>(input => [
{ room: ResourcePath.get(`/foo/bar/${input}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<CreateResult<string>> {
return new CreateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("created", {
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "create",
type: "string",
raw: undefined,
} as CreateResult<string>,
});
emitSpy.mockRestore();
done();
});
});
@@ -0,0 +1,116 @@
import { describe, expect, it, jest } from "@jest/globals";
import { DeleteResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeDeleted } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeDeleted decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
done();
});
it("should call the original method send back original result and emit event", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<DeleteResult<string>> {
return new DeleteResult<string>("string", input.split("").reverse().join(""), true);
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("deleted", {
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "delete",
deleted: true,
type: "string",
} as DeleteResult<string>,
});
emitSpy.mockRestore();
done();
});
it("should emit event with path computed from function", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeDeleted(result => [
{ room: ResourcePath.get(`/foo/bar/${result}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<DeleteResult<string>> {
return new DeleteResult<string>("string", input.split("").reverse().join(""), true);
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("deleted", {
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
entity: "oloy",
type: "string",
result: {
entity: "oloy",
context: undefined,
operation: "delete",
deleted: true,
type: "string",
} as DeleteResult<string>,
});
emitSpy.mockRestore();
done();
});
});
@@ -0,0 +1,118 @@
import { describe, expect, it, jest } from "@jest/globals";
import { UpdateResult } from "../../../../../../../src/core/platform/framework/api/crud-service";
import { RealtimeUpdated } from "../../../../../../../src/core/platform/framework/decorators";
import { websocketEventBus } from "../../../../../../../src/core/platform/services/realtime/bus";
import { ResourcePath } from "../../../../../../../src/core/platform/services/realtime/types";
describe("The RealtimeUpdated decorator", () => {
it("should call the original method send back original result but do not emit event if result type is wrong", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated({ room: "/foo/bar" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reverseMeBaby(input: string): Promise<string> {
return Promise.resolve(input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(0);
emitSpy.mockRestore();
done();
});
it("should call the original method send back original result and emit event", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated({ room: "/foo/bar", path: "/foo/bar/baz" })
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<UpdateResult<string>> {
return new UpdateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("updated", {
entity: "oloy",
room: {
name: "default",
path: ["/foo/bar"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
type: "string",
result: {
type: "string",
entity: "oloy",
affected: undefined,
context: undefined,
operation: "update",
raw: undefined,
} as UpdateResult<string>,
});
emitSpy.mockRestore();
done();
});
it("should emit event with path computed from function", async done => {
const emitSpy = jest.spyOn(websocketEventBus, "emit");
class TestMe {
@RealtimeUpdated(result => [
{ room: ResourcePath.get(`/foo/bar/${result}`), path: "/foo/bar/baz" },
])
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async reverseMeBaby(input: string): Promise<UpdateResult<string>> {
return new UpdateResult<string>("string", input.split("").reverse().join(""));
}
}
const test = new TestMe();
const originalSpy = jest.spyOn(test, "reverseMeBaby");
const result = await test.reverseMeBaby("yolo");
expect(result.entity).toEqual("oloy");
expect(originalSpy).toHaveBeenCalledTimes(1);
expect(originalSpy).toHaveBeenCalledWith("yolo");
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith("updated", {
entity: "oloy",
room: {
name: "default",
path: ["/foo/bar/oloy"],
} as ResourcePath,
resourcePath: "/foo/bar/baz",
type: "string",
result: {
type: "string",
context: undefined,
operation: "update",
entity: "oloy",
affected: undefined,
raw: undefined,
} as UpdateResult<string>,
});
emitSpy.mockRestore();
done();
});
});
@@ -0,0 +1,181 @@
import "reflect-metadata";
import { describe, expect, it } from "@jest/globals";
import {
buildSelectQuery,
buildComparison,
buildIn,
} from "../../../../../../../../../src/core/platform/services/database/services/orm/connectors/cassandra/query-builder";
import { ChannelMemberNotificationPreference } from "../../../../../../../../../src/services/notifications/entities/channel-member-notification-preferences";
describe("The QueryBuilder module", () => {
describe("The buildSelectQuery function", () => {
it("should build a valid query from primary key parameters", () => {
const filters = {
company_id: "comp1",
channel_id: "chan1",
};
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
ChannelMemberNotificationPreference,
filters,
{},
{ keyspace: "twake" },
);
expect(result).toEqual(
"SELECT * FROM twake.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;",
);
});
it("should build a valid query from primary key parameters and comparison", () => {
const filters = {
company_id: "comp1",
channel_id: "chan1",
};
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
ChannelMemberNotificationPreference,
filters,
{
$lt: [["last_read", 1000]],
},
{ keyspace: "twake" },
);
expect(result).toEqual(
"SELECT * FROM twake.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND last_read < 1000;",
);
});
it("should build IN query from array parameters", () => {
const filters = {
company_id: "comp1",
channel_id: "chan1",
user_id: ["u1", "u2", "u3"],
};
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
ChannelMemberNotificationPreference,
filters,
{},
{ keyspace: "twake" },
);
expect(result).toEqual(
"SELECT * FROM twake.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1 AND user_id IN (u1,u2,u3);",
);
});
it("should not build IN query from array parameters when array is empty", () => {
const filters = {
company_id: "comp1",
channel_id: "chan1",
user_id: [],
};
const result = buildSelectQuery<ChannelMemberNotificationPreference>(
ChannelMemberNotificationPreference,
filters,
{},
{ keyspace: "twake" },
);
expect(result).toEqual(
"SELECT * FROM twake.channel_members_notification_preferences WHERE company_id = comp1 AND channel_id = chan1;",
);
});
});
describe("The buildComparison function", () => {
it("should create a valid < string", () => {
expect(
buildComparison({
$lt: [["foo", 1]],
}),
).toContain("foo < 1");
const result = buildComparison({
$lt: [
["foo", 1],
["bar", 2],
],
});
expect(result).toContain("foo < 1");
expect(result).toContain("bar < 2");
});
it("should create a valid <= string", () => {
expect(
buildComparison({
$lte: [["foo", 1]],
}),
).toContain("foo <= 1");
const result = buildComparison({
$lte: [
["foo", 1],
["bar", 2],
],
});
expect(result).toContain("foo <= 1");
expect(result).toContain("bar <= 2");
});
it("should create a valid > string", () => {
expect(
buildComparison({
$gt: [["foo", 1]],
}),
).toContain("foo > 1");
const result = buildComparison({
$gt: [
["foo", 1],
["bar", 2],
],
});
expect(result).toContain("foo > 1");
expect(result).toContain("bar > 2");
});
it("should create a valid >= string", () => {
expect(
buildComparison({
$gte: [["foo", 1]],
}),
).toContain("foo >= 1");
const result = buildComparison({
$gte: [
["foo", 1],
["bar", 2],
],
});
expect(result).toContain("foo >= 1");
expect(result).toContain("bar >= 2");
});
it("should combine conditions", () => {
const result = buildComparison({
$gt: [["foo", 1]],
$gte: [["bar", 2]],
$lt: [["baz", 3]],
$lte: [["qix", 4]],
});
expect(result).toContain("foo > 1");
expect(result).toContain("bar >= 2");
expect(result).toContain("baz < 3");
expect(result).toContain("qix <= 4");
});
});
describe("The buildIn function", () => {
it("should create a id IN (ids) string", () => {
expect(
buildIn({
$in: [["id", ["1", "2", "3"]]],
}),
).toContain("id IN (1,2,3)");
});
});
});
@@ -0,0 +1,31 @@
import "reflect-metadata";
import { describe, expect, it } from "@jest/globals";
import {
fromMongoDbOrderable,
toMongoDbOrderable,
} from "../../../../../../src/core/platform/services/database/services/orm/utils";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import _ from "lodash";
import { v1 as uuidv1 } from "uuid";
describe("The MongoDb to Orderable module", () => {
describe("The to/from orderable function", () => {
it("should be unique", () => {
const uuid1 = toMongoDbOrderable(uuidv1());
const uuid2 = toMongoDbOrderable(uuidv1());
const uuid3 = toMongoDbOrderable(uuidv1());
const uuid4 = toMongoDbOrderable(uuidv1());
expect(_.uniq([uuid1, uuid2, uuid3, uuid4]).length).toBe(4);
});
it("should convert both ways", () => {
const uuid = uuidv1();
const orderable = toMongoDbOrderable(uuid);
expect(fromMongoDbOrderable(orderable)).toBe(uuid);
expect(orderable).toBe(toMongoDbOrderable(fromMongoDbOrderable(orderable)));
});
});
});
@@ -0,0 +1,31 @@
import { describe, expect, it } from "@jest/globals";
import { decrypt } from "../../../../../src/core/crypto/index";
import v2 from "../../../../../src/core/crypto/v2";
import v1 from "../../../../../src/core/crypto/v1";
import legacy from "../../../../../src/core/crypto/legacy";
describe("Encryption", () => {
const encryptionKey = "a7c06651a7c063bb3e90c0c9a17eab88ab8977665127196a";
describe("The encrypt/decrypt functions", () => {
it("should successfully describe legacy encrypted values", async () => {
const legacyEncrypted = "encrypted_DwMLnKhuFbIanqBJPA5rcw==";
expect(legacy.decrypt(legacyEncrypted, encryptionKey).data).toBe("My company");
expect(decrypt(legacyEncrypted, encryptionKey).data).toBe("My company");
});
it("should successfully describe all versions", async () => {
const myData = { key: "some data" };
const v1Encrypted = v1.encrypt(myData, encryptionKey);
const v2Encrypted = v2.encrypt(myData, encryptionKey);
expect(v1.decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData);
expect(v2.decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData);
expect(decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData);
expect(decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData);
});
});
});
@@ -0,0 +1,305 @@
import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals";
import {
IncomingMessageQueueMessage,
MessageQueueHandler,
MessageQueueServiceAPI,
MessageQueueServiceProcessor,
} from "../../../../../../src/core/platform/services/message-queue/api";
describe("The MessageQueueServiceProcessor class", () => {
let pubsubService: MessageQueueServiceAPI;
let subscribe;
let publish;
let topic;
let errorTopic;
let outTopic;
beforeEach(() => {
topic = "inputtopic";
errorTopic = "errorTopic";
outTopic = "outTopic";
subscribe = jest.fn();
publish = jest.fn();
pubsubService = {
publish,
subscribe,
} as unknown as MessageQueueServiceAPI;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
describe("The init function", () => {
it("should not subscribe when topics is not defined", async () => {
subscribe.mockResolvedValue(true);
const processor = new MessageQueueServiceProcessor(
{} as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
expect(subscribe).not.toHaveBeenCalled;
});
it("should not subscribe when topics.in is not defined", async () => {
subscribe.mockResolvedValue(true);
const processor = new MessageQueueServiceProcessor(
{
topics: {},
} as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
expect(subscribe).not.toHaveBeenCalled;
});
it("should subscribe when topics.in is defined", async () => {
subscribe.mockResolvedValue(true);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
},
} as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
expect(subscribe).toHaveBeenCalledTimes(1);
expect(subscribe).toHaveBeenCalledWith(topic, expect.any(Function), undefined);
});
});
describe("The function handling incoming messages", () => {
describe("When handler.validate is defined", () => {
it("should not process data when data is not valid", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
const validate = jest.fn().mockReturnValue(false);
const process = jest.fn().mockReturnValue(true);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
},
validate,
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(validate).toBeCalledTimes(1);
expect(validate).toBeCalledWith(message.data);
expect(process).not.toHaveBeenCalled;
});
it("should process data when data is valid", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
const validate = jest.fn().mockReturnValue(true);
const process = jest.fn().mockReturnValue(true);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
},
validate,
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(validate).toBeCalledTimes(1);
expect(validate).toBeCalledWith(message.data);
expect(process).toHaveBeenCalled;
});
});
describe("When processing message", () => {
it("should not publish error when topics.error is not defined", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
const process = jest.fn().mockRejectedValue(new Error("I failed to process"));
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).not.toBeCalled;
});
it("should publish error when topics.error is defined", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
const process = jest.fn().mockRejectedValue(new Error("I failed to process"));
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
error: errorTopic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).toBeCalledTimes(1);
expect(publish).toBeCalledWith(errorTopic, expect.anything());
});
it("should not publish when processing does not return result", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const process = jest.fn().mockResolvedValue(null);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
out: outTopic,
error: errorTopic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).not.toBeCalled;
});
it("should not publish when out topic is not defined", async () => {
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const process = jest.fn().mockResolvedValue(true);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
error: errorTopic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).not.toBeCalled;
});
it("should publish when out topic is defined and process returns result", async () => {
const result = "processing result";
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const process = jest.fn().mockResolvedValue(result);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
error: errorTopic,
out: outTopic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).toBeCalledTimes(1);
expect(publish).toBeCalledWith(
outTopic,
expect.objectContaining({
data: result,
}),
);
});
it("should publish error when result can not be published", async () => {
publish.mockRejectedValue(new Error("I failed to publish"));
const result = "processing result";
const message = {
data: "foo",
} as IncomingMessageQueueMessage<string>;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const process = jest.fn().mockResolvedValue(result);
const processor = new MessageQueueServiceProcessor(
{
topics: {
in: topic,
error: errorTopic,
out: outTopic,
},
process,
} as unknown as MessageQueueHandler<unknown, unknown>,
pubsubService,
);
await processor.init();
const processMessage = subscribe.mock.calls[0][1];
await processMessage(message);
expect(process).toBeCalledTimes(1);
expect(process).toHaveBeenCalledWith(message.data);
expect(publish).toBeCalledTimes(2);
expect(publish.mock.calls[0][0]).toEqual(outTopic);
expect(publish.mock.calls[1][0]).toEqual(errorTopic);
});
});
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, it } from "@jest/globals";
import * as pickUtils from "../../src/utils/pick";
describe("The pick utils", () => {
describe("The pick function", () => {
it("should return an object wich contains only the defined properties", () => {
class TestClass {
keep: string;
me: string;
skip: string;
}
const keysToKeep = ["keep", "me"] as const;
const object = { keep: "foo", me: "bar", skip: "baz" } as TestClass;
const result = pickUtils.pick(object, ...keysToKeep);
expect(result).toEqual({ keep: "foo", me: "bar" });
expect(result).not.toContain("skip");
});
it("should return an empty object when input is empty", () => {
class TestClass {
keep: string;
me: string;
skip: string;
}
const keysToKeep = [] as const;
const object = { keep: "foo", me: "bar", skip: "baz" } as TestClass;
const result = pickUtils.pick(object, ...keysToKeep);
expect(result).toEqual({});
});
});
});
@@ -0,0 +1,504 @@
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import {
ListResult,
OperationType,
SaveResult,
} from "../../../../../../../../src/core/platform/framework/api/crud-service";
import { ChannelMemberNotificationLevel } from "../../../../../../../../src/services/channels/types";
import { MessageNotification } from "../../../../../../../../src/services/messages/types";
import {
ChannelMemberNotificationPreference,
ChannelThreadUsers,
} from "../../../../../../../../src/services/notifications/entities";
import { ChannelType } from "../../../../../../../../src/utils/types";
import gr from "../../../../../../../../src/services/global-resolver";
import { NewChannelMessageProcessor } from "../../../../../../../../src/services/notifications/services/engine/processors/new-channel-message";
describe("The NewChannelMessageProcessor class", () => {
let channel_id, company_id, workspace_id, thread_id;
let service: any;
let processor: NewChannelMessageProcessor;
let getUsersInThread;
let getChannelPreferencesForUsers;
beforeEach(() => {
channel_id = "channel_id";
company_id = "company_id";
workspace_id = "workspace_id";
getUsersInThread = jest.fn();
getChannelPreferencesForUsers = jest.fn();
setPreferences();
setUsersInThread();
service = {
channelThreads: {
bulkSave: jest
.fn()
.mockResolvedValue(
new SaveResult<ChannelThreadUsers[]>("thread", [], OperationType.CREATE) as never,
),
getUsersInThread,
},
channelPreferences: {
getChannelPreferencesForUsers,
},
};
gr.services = {
notifications: service,
} as any;
processor = new NewChannelMessageProcessor();
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
function getMessage(newThread: boolean = false): MessageNotification {
const message = {
company_id,
workspace_id,
channel_id,
id: "id",
thread_id: "thread_id",
sender: "sender",
mentions: {
users: [],
},
} as MessageNotification;
if (newThread) {
// message is new thread when thread_id is not defined
message.thread_id = undefined;
}
return message;
}
function setPreferences(preferences: ChannelMemberNotificationPreference[] = []): void {
getChannelPreferencesForUsers.mockResolvedValue(
new ListResult<ChannelMemberNotificationPreference>("preferences", preferences),
);
}
function setUsersInThread(users: string[] = []) {
getUsersInThread.mockResolvedValue(
new ListResult<ChannelThreadUsers>(
"users",
users.map(
user_id =>
({
company_id,
channel_id,
workspace_id,
thread_id,
user_id,
} as ChannelThreadUsers),
),
),
);
}
describe("The process method", () => {
describe("When message is a new thread", () => {
function setNewThreadPreferences() {
setPreferences([
{
channel_id,
company_id,
user_id: "1",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.ALL,
},
{
channel_id,
company_id,
user_id: "2",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.MENTIONS,
},
{
channel_id,
company_id,
user_id: "3",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.ME,
},
{
channel_id,
company_id,
user_id: "4",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.NONE,
},
]);
}
it("should return undefined when there is no one to notify", async done => {
const message = getMessage(true);
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result).toBeUndefined();
done();
});
it("without mentions, should get the preferences for all channel members and return only the ones who want to be notified", async done => {
const message = getMessage(true);
setNewThreadPreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result.mentions.users).toEqual(["1"]);
done();
});
it("with @all mention, should get the preferences for all channel members and return only the ones who want to be notified", async done => {
const message = getMessage(true);
message.mentions.specials = ["all"];
setNewThreadPreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result.mentions.users).toEqual(["1", "2"]);
done();
});
it("with @here mentions, should get the preferences for all channel members and return only the ones who want to be notified", async done => {
const message = getMessage(true);
message.mentions.specials = ["here"];
setNewThreadPreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result.mentions.users).toEqual(["1", "2"]);
done();
});
it("with @user mentions, should get the preferences for all channel members and return only the ones who want to be notified", async done => {
const message = getMessage(true);
message.mentions.users = ["1", "2", "3", "4"];
setNewThreadPreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "1",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "2",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "3",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "4",
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result.mentions.users).toEqual(["1", "2", "3"]);
done();
});
it("When message is a direct message", async done => {
const message = getMessage(true);
message.workspace_id = ChannelType.DIRECT;
message.mentions.users = ["1", "2", "3", "4"];
setNewThreadPreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: message.sender,
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "1",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "2",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "3",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.id,
user_id: "4",
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).not.toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledWith({
company_id: message.company_id,
channel_id: message.channel_id,
});
expect(result.mentions.users).toEqual(["1", "2", "3"]);
done();
});
});
describe("When message is a response to a thread", () => {
function setThreadResponsePreferences() {
setPreferences([
{
channel_id,
company_id,
user_id: "1",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.ALL,
},
{
channel_id,
company_id,
user_id: "2",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.MENTIONS,
},
{
channel_id,
company_id,
user_id: "3",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.ME,
},
{
channel_id,
company_id,
user_id: "4",
last_read: Date.now(),
preferences: ChannelMemberNotificationLevel.NONE,
},
]);
}
it("should return undefined when there is no one to notify", async done => {
const message = getMessage();
setUsersInThread(["1", "2", "3", "4"]);
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(result).toBeUndefined();
done();
});
it("without mentions, should get the preferences for all members involved and return only the ones with preference !== NONE", async done => {
const message = getMessage();
setUsersInThread(["1", "2", "3", "4"]);
setThreadResponsePreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: message.sender,
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(result.mentions.users).toEqual(["1", "2", "3"]);
done();
});
it("with @user mentions, should get the preferences for all members involved and return only the ones who want to be notified", async done => {
const message = getMessage();
message.mentions.users = ["1", "2", "3", "4"];
setUsersInThread(["1", "2", "3", "4"]);
setThreadResponsePreferences();
const result = await processor.process(message);
expect(service.channelThreads.bulkSave).toBeCalledTimes(1);
expect(service.channelThreads.bulkSave).toBeCalledWith(
[
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: message.sender,
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: "1",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: "2",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: "3",
},
{
company_id: message.company_id,
channel_id: message.channel_id,
thread_id: message.thread_id,
user_id: "4",
},
],
undefined,
);
expect(service.channelThreads.getUsersInThread).toBeCalled;
expect(service.channelPreferences.getChannelPreferencesForUsers).toBeCalledTimes(1);
expect(result.mentions.users).toEqual(["1", "2", "3"]);
done();
});
});
});
});
@@ -0,0 +1,244 @@
import "reflect-metadata";
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import {
ListResult,
OperationType,
SaveResult,
} from "../../../../../../../../src/core/platform/framework/api/crud-service";
import { MessageQueueServiceAPI } from "../../../../../../../../src/core/platform/services/message-queue/api";
import { ChannelMemberNotificationLevel } from "../../../../../../../../src/services/channels/types";
import {
ChannelMemberNotificationPreference,
ChannelThreadUsers,
} from "../../../../../../../../src/services/notifications/entities";
import {
TYPE as UserNotificationBadgeType,
UserNotificationBadge,
} from "../../../../../../../../src/services/notifications/entities/user-notification-badges";
import { MentionNotification } from "../../../../../../../../src/services/notifications/types";
import { uniqueId } from "lodash";
import { PushNotificationToUsersMessageProcessor } from "../../../../../../../../src/services/notifications/services/engine/processors/push-to-users";
import gr from "../../../../../../../../src/services/global-resolver";
describe("The PushNotificationToUsersMessageProcessor class", () => {
let channel_id, company_id, workspace_id, thread_id;
let pubsubService: MessageQueueServiceAPI;
let processor: PushNotificationToUsersMessageProcessor;
let getUsersInThread;
let getChannelPreferencesForUsers;
let saveBadge;
beforeEach(async () => {
channel_id = "channel_id";
company_id = "company_id";
workspace_id = "workspace_id";
thread_id: "thread_id";
getUsersInThread = jest.fn();
getChannelPreferencesForUsers = jest.fn();
saveBadge = jest.fn();
setPreferences();
setUsersInThread();
pubsubService = {
publish: jest.fn(),
} as unknown as MessageQueueServiceAPI;
//
const service = {
badges: {
save: saveBadge,
},
channelPreferences: {
getChannelPreferencesForUsers,
},
};
gr.platformServices = {
messageQueue: pubsubService,
} as any;
gr.services = {
notifications: service,
} as any;
processor = new PushNotificationToUsersMessageProcessor();
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
function getMessage(newThread: boolean = false): MentionNotification {
const message = {
company_id,
workspace_id,
channel_id,
message_id: "id",
creation_date: Date.now(),
thread_id,
mentions: {
users: [],
},
} as MentionNotification;
if (newThread) {
// message is new thread when thread_id is not defined
message.thread_id = undefined;
}
return message;
}
function setPreferences(preferences: ChannelMemberNotificationPreference[] = []): void {
getChannelPreferencesForUsers.mockResolvedValue(
new ListResult<ChannelMemberNotificationPreference>("preferences", preferences),
);
}
function setBadgeResults(badges: UserNotificationBadge[] = []): void {
badges.forEach(badge =>
saveBadge.mockResolvedValueOnce(
new SaveResult(UserNotificationBadgeType, badge, OperationType.CREATE),
),
);
}
function setUsersInThread(users: string[] = []) {
getUsersInThread.mockResolvedValue(
new ListResult<ChannelThreadUsers>(
"users",
users.map(
user_id =>
({
company_id,
channel_id,
workspace_id,
thread_id,
user_id,
} as ChannelThreadUsers),
),
),
);
}
describe("The process method", () => {
it("will fail when channel_id is not defined", async () => {
const message = getMessage();
delete message.channel_id;
await expect(processor.process(message)).rejects.toThrowError("Missing required fields");
});
it("will fail when company_id is not defined", async () => {
const message = getMessage();
delete message.company_id;
await expect(processor.process(message)).rejects.toThrowError("Missing required fields");
});
it("will fail when workspace_id is not defined", async () => {
const message = getMessage();
delete message.workspace_id;
await expect(processor.process(message)).rejects.toThrowError("Missing required fields");
});
it("will fail when creation_date is not defined", async () => {
const message = getMessage();
delete message.creation_date;
await expect(processor.process(message)).rejects.toThrowError("Missing required fields");
});
it("will do nothing when mentions is not defined", async done => {
const message = getMessage();
delete message.mentions;
await processor.process(message);
expect(gr.services.notifications.channelPreferences.getChannelPreferencesForUsers).not
.toBeCalled;
done();
});
it("will do nothing when mentions.users is not defined", async done => {
const message = getMessage();
message.mentions = { users: undefined };
await processor.process(message);
expect(gr.services.notifications.channelPreferences.getChannelPreferencesForUsers).not
.toBeCalled;
done();
});
it("will do nothing when mentions.users is empty", async done => {
const message = getMessage();
message.mentions = { users: [] };
await processor.process(message);
expect(gr.services.notifications.channelPreferences.getChannelPreferencesForUsers).not
.toBeCalled;
done();
});
it("will keep users who did not read the channel yet", async done => {
const message = getMessage();
setPreferences([
{
channel_id,
company_id,
last_read: Date.now(),
user_id: "1",
preferences: ChannelMemberNotificationLevel.ALL,
},
{
channel_id,
company_id,
last_read: Date.now(),
user_id: "3",
preferences: ChannelMemberNotificationLevel.ALL,
},
]);
setBadgeResults([
{
channel_id,
company_id,
thread_id,
user_id: "1",
workspace_id,
message_id: message.message_id,
id: uniqueId(),
},
{
channel_id,
company_id,
thread_id,
user_id: "3",
workspace_id,
message_id: message.message_id,
id: uniqueId(),
},
]);
const lessThan = message.creation_date;
const users = ["1", "2", "3", "4"];
const channel = {
channel_id,
company_id,
};
message.mentions = { users };
await processor.process(message);
expect(getChannelPreferencesForUsers).toBeCalledWith(channel, users, { lessThan }, undefined);
expect(gr.services.notifications.badges.save).toBeCalledTimes(2);
expect(pubsubService.publish).toBeCalledTimes(2);
done();
});
});
});
@@ -0,0 +1,176 @@
import { describe, expect, it, jest, beforeEach, afterEach, afterAll } from "@jest/globals";
import { generateVideoPreview } from "../../../../../../../src/services/previews/services/files/processing/video";
import ffmpeg, { ffprobe } from "fluent-ffmpeg";
import { cleanFiles, getTmpFile } from "../../../../../../../src/utils/files";
import fs from "fs";
jest.mock("fluent-ffmpeg");
jest.mock("../../../../../../../src/utils/files");
const ffmpegMock = {
screenshot: jest.fn().mockReturnValue({
on: function (e, cb) {
cb();
return this;
},
}),
};
beforeEach(() => {
jest.spyOn(fs, "statSync").mockReturnValue({ size: 1 } as any);
(ffmpeg as any).mockImplementation(() => ffmpegMock);
(cleanFiles as any).mockImplementation(() => jest.fn());
(getTmpFile as any).mockImplementation(() => "/tmp/file");
(ffprobe as any).mockImplementation((i, cb) => {
cb(null, {
streams: [
{
width: 320,
height: 240,
},
],
});
});
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
describe("the generateVideoPreview function", () => {
it("should return a promise", () => {
const result = generateVideoPreview([]);
expect(result).toBeInstanceOf(Promise);
});
it("should attempt to create a screenshot using ffmpeg", async () => {
await generateVideoPreview(["/foo/bar"]);
expect(ffmpeg as any).toBeCalledWith("/foo/bar");
});
it("should call ffmpeg screenshot method with the original video dimentions", async () => {
(getTmpFile as any).mockImplementation(() => "/tmp/some-random-file");
(ffprobe as any).mockImplementation((i, cb) => {
cb(null, {
streams: [
{
width: 500,
height: 500,
},
],
});
});
await generateVideoPreview(["/foo/bar"]);
expect(ffmpegMock.screenshot).toBeCalledWith({
count: 1,
filename: "some-random-file.jpg",
folder: "/tmp",
timemarks: ["0"],
size: "500x500",
});
});
it("should attempt to generate a temporary file", async () => {
await generateVideoPreview(["/foo/bar"]);
expect(getTmpFile).toBeCalled();
});
it("should attempt to clean up the temporary file in case of errors", async () => {
const expectedError = new Error("failed to generate video preview: Error: foo");
(ffmpeg as any).mockImplementation(() => {
throw new Error("foo");
});
(ffprobe as any).mockImplementation((i, cb) => {
cb(null, {
streams: [
{
width: 500,
height: 500,
},
],
});
});
await expect(generateVideoPreview(["/foo/bar"])).rejects.toThrow(expectedError);
expect(cleanFiles).toBeCalledWith(["/tmp/file.jpg"]);
});
it("should return the thumbnail information", async () => {
const result = await generateVideoPreview(["/foo/bar"]);
expect(result).toEqual([
{
width: 320,
height: 240,
type: "image/jpg",
size: 1,
path: "/tmp/file.jpg",
},
]);
});
it("should generate thumbnails for multiple files", async () => {
const result = await generateVideoPreview(["/foo/bar", "/foo/baz", "/foo/tar"]);
expect(result).toHaveLength(3);
});
it("should use ffprobe to get the video dimensions", async () => {
await generateVideoPreview(["/foo/bar"]);
expect(ffprobe).toBeCalledWith("/foo/bar", expect.any(Function));
});
it("should use 1080p as the maximum video dimensions", async () => {
(ffprobe as any).mockImplementation((i, cb) => {
cb(null, {
streams: [
{
width: 5000,
height: 5000,
},
],
});
});
const result = await generateVideoPreview(["/foo/bar"]);
expect(result).toEqual([
{
width: 1920,
height: 1080,
type: "image/jpg",
size: 1,
path: "/tmp/file.jpg",
},
]);
});
it("should use 320x240 as the minimum video dimensions", async () => {
(ffprobe as any).mockImplementation((i, cb) => {
cb(null, {
streams: [
{
width: 320,
height: 180,
},
],
});
});
const result = await generateVideoPreview(["/foo/bar"]);
expect(result).toEqual([
{
width: 320,
height: 240,
type: "image/jpg",
size: 1,
path: "/tmp/file.jpg",
},
]);
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it, jest, beforeEach, afterEach, afterAll } from "@jest/globals";
import { generateImageUrlPreview } from "../../../../../../../src/services/previews/services/links/processing/image";
import getFavicons from "get-website-favicon";
import imageProbe from "probe-image-size";
jest.mock("get-website-favicon");
jest.mock("probe-image-size");
beforeEach(() => {
(imageProbe as any).mockImplementation(() => ({
width: 320,
height: 240,
}));
(getFavicons as any).mockImplementation(() => ({
icons: [
{
src: "http://foo.bar/favicon.ico",
},
],
}));
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
describe("the generateImageUrlPreview function", () => {
it("should return a promise", () => {
const result = generateImageUrlPreview("http://foo.bar");
expect(result).toBeInstanceOf(Promise);
});
it("should generate a preview for a given image url", async () => {
const result = await generateImageUrlPreview("https://foo.bar/image.jpg");
expect(result).toEqual({
title: "image.jpg", // title should be the file name
domain: "foo.bar",
favicon: "http://foo.bar/favicon.ico",
url: "https://foo.bar/image.jpg",
img_height: 240,
img_width: 320,
description: null,
img: "https://foo.bar/image.jpg",
});
});
it("should return nothing in case of error", async () => {
(imageProbe as any).mockImplementation(() => {
throw new Error("failed to probe image");
});
const result = await generateImageUrlPreview("https://foo.bar/image.jpg");
expect(result).toBeUndefined();
});
});
@@ -0,0 +1,186 @@
import { describe, expect, it, jest, beforeEach, afterEach, afterAll } from "@jest/globals";
import { generateLinkPreview } from "../../../../../../../src/services/previews/services/links/processing/link";
import { parser } from "html-metadata-parser";
import getFavicons from "get-website-favicon";
import imageProbe from "probe-image-size";
jest.mock("html-metadata-parser");
jest.mock("get-website-favicon");
jest.mock("probe-image-size");
beforeEach(() => {
(imageProbe as any).mockImplementation(() => ({
width: 320,
height: 240,
}));
(getFavicons as any).mockImplementation(() => ({
icons: [
{
src: "http://foo.bar/favicon.ico",
},
],
}));
(parser as any).mockImplementation(() => ({
og: {
title: "Foo",
description: "Bar",
image: "http://foo.bar/image.jpg",
},
meta: {
title: "Foo",
description: "Bar",
image: "http://foo.bar/image1.jpg",
},
images: ["http://foo.bar/image2.jpg"],
}));
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
describe("the generateLinkPreview service", () => {
it("should return a promise", () => {
const result = generateLinkPreview("http://foo.bar");
expect(result).toBeInstanceOf(Promise);
});
it("should return a promise that resolves to a preview", async () => {
const result = await generateLinkPreview("https://foo.bar");
expect(result).toEqual({
title: "Foo",
description: "Bar",
img: "http://foo.bar/image.jpg",
favicon: "http://foo.bar/favicon.ico",
img_width: 320,
img_height: 240,
domain: "foo.bar",
url: "https://foo.bar",
});
});
it("should return a promise that resolves to undefined if no previews are found", async () => {
(parser as any).mockImplementation(() => {
throw new Error("failed to parse");
});
(getFavicons as any).mockImplementation(() => []);
(imageProbe as any).mockImplementation(() => ({}));
const result = await generateLinkPreview("https://foo.bar");
expect(result).toBeUndefined();
});
it("should use og information as first choice", async () => {
(parser as any).mockImplementation(() => ({
og: {
title: "test",
description: "test",
image: "http://foo.bar/test.jpg",
},
meta: {
title: "test2",
description: "test2",
image: "http://foo.bar/test2.jpg",
},
images: ["http://foo.bar/test3.jpg"],
}));
const result = await generateLinkPreview("https://foo.bar");
expect(result).toEqual({
title: "test",
description: "test",
img: "http://foo.bar/test.jpg",
favicon: "http://foo.bar/favicon.ico",
img_width: 320,
img_height: 240,
domain: "foo.bar",
url: "https://foo.bar",
});
});
it("should use meta information as second choice", async () => {
(parser as any).mockImplementation(() => ({
meta: {
title: "test2",
description: "test2",
image: "http://foo.bar/test2.jpg",
},
images: [],
}));
const result = await generateLinkPreview("https://foo.bar");
expect(result).toEqual({
title: "test2",
description: "test2",
img: "http://foo.bar/test2.jpg",
favicon: "http://foo.bar/favicon.ico",
img_width: 320,
img_height: 240,
domain: "foo.bar",
url: "https://foo.bar",
});
});
it("should use the first image found in the url when none are present in the og or meta information", async () => {
(parser as any).mockImplementation(() => ({
og: {
title: "test",
description: "test",
},
meta: {
title: "test2",
description: "test2",
},
images: ["http://foo.bar/test3.jpg", "http://foo.bar/test4.jpg"],
}));
const result = await generateLinkPreview("https://foo.bar");
expect(result).toEqual({
title: "test",
description: "test",
img: "http://foo.bar/test3.jpg",
favicon: "http://foo.bar/favicon.ico",
img_width: 320,
img_height: 240,
domain: "foo.bar",
url: "https://foo.bar",
});
});
it("shouldn't attempt to probe for image size when none are found", async () => {
(parser as any).mockImplementation(() => ({
og: {
title: "test",
description: "test",
},
meta: {
title: "test2",
description: "test2",
},
images: [],
}));
await generateLinkPreview("https://foo.bar");
expect(imageProbe).not.toHaveBeenCalled();
});
it("should strip www from the domain", async () => {
const result = await generateLinkPreview("https://www.foo.bar");
expect(result).toEqual({
title: "Foo",
description: "Bar",
img: "http://foo.bar/image.jpg",
favicon: "http://foo.bar/favicon.ico",
img_width: 320,
img_height: 240,
domain: "foo.bar",
url: "https://www.foo.bar",
});
});
});
@@ -0,0 +1,109 @@
import {
describe,
expect,
it,
jest,
beforeEach,
afterEach,
afterAll,
beforeAll,
} from "@jest/globals";
import { LinkPreviewProcessService } from "../../../../../../../src/services/previews/services/links/processing/service";
import { generateLinkPreview } from "../../../../../../../src/services/previews/services/links/processing/link";
import { generateImageUrlPreview } from "../../../../../../../src/services/previews/services/links/processing/image";
import axios from "axios";
jest.mock("axios");
jest.mock("../../../../../../../src/services/previews/services/links/processing/link");
jest.mock("../../../../../../../src/services/previews/services/links/processing/image");
let service: LinkPreviewProcessService;
beforeEach(async () => {
(axios as any).mockImplementation(() => ({
headers: {
"content-type": "text/html",
},
}));
(generateLinkPreview as any).mockImplementation(() => ({
title: "Foo",
description: "Bar",
img: "http://foo.bar/image.jpg",
favicon: "http://foo.bar/favicon.ico",
}));
(generateImageUrlPreview as any).mockImplementation(() => ({
title: "image.jpg",
description: null,
img: "http://foo.bar/image.jpg",
favicon: "http://foo.bar/favicon.ico",
}));
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
beforeAll(async () => {
service = await new LinkPreviewProcessService().init();
});
describe("the LinkPreviewProcessService service", () => {
it("should return a promise", () => {
const result = service.generatePreviews(["http://foo.bar"]);
expect(result).toBeInstanceOf(Promise);
});
it("should call generateLinkPreview when it's a html page", async () => {
(axios as any).mockImplementation(() => ({
headers: {
"content-type": "text/html",
},
}));
await service.generatePreviews(["http://foo.bar"]);
expect(generateLinkPreview).toHaveBeenCalled();
});
it("should call generateImageUrlPreview when it's an image", async () => {
(axios as any).mockImplementation(() => ({
headers: {
"content-type": "image/jpeg",
},
}));
await service.generatePreviews(["http://foo.bar/image.jpg"]);
expect(generateImageUrlPreview).toHaveBeenCalled();
});
it("should skip if the url content is not supported", async () => {
(axios as any).mockImplementation(() => ({
headers: {
"content-type": "application/json",
},
}));
await service.generatePreviews(["http://foo.bar"]);
expect(generateLinkPreview).not.toHaveBeenCalled();
expect(generateImageUrlPreview).not.toHaveBeenCalled();
});
// should filter null values
it("should filter null values", async () => {
(axios as any).mockImplementation(() => ({
headers: {
"content-type": "text/html",
},
}));
(generateLinkPreview as any).mockImplementation(() => null);
const result = await service.generatePreviews(["http://foo.bar", "http://foo.xyz"]);
expect(result).toEqual([]);
});
});