feat: init
This commit is contained in:
+116
@@ -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();
|
||||
});
|
||||
});
|
||||
+116
@@ -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();
|
||||
});
|
||||
});
|
||||
+118
@@ -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();
|
||||
});
|
||||
});
|
||||
+181
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+305
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
+504
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+244
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+176
@@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+61
@@ -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();
|
||||
});
|
||||
});
|
||||
+186
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
+109
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user