snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
153
apps/meteor/tests/unit/app/apps/server/codecs/enums.spec.ts
Normal file
153
apps/meteor/tests/unit/app/apps/server/codecs/enums.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
|
||||
import { SettingType } from '@rocket.chat/apps-engine/definition/settings';
|
||||
import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
RoomTypeCodec,
|
||||
SettingTypeCodec,
|
||||
UserStatusConnectionCodec,
|
||||
UserTypeCodec,
|
||||
} from '../../../../../../app/apps/server/converters/codecs/enums';
|
||||
|
||||
/*
|
||||
* These are copies of the legacy private helpers the codecs replace. They act as the oracle:
|
||||
* `decode` must produce identical output for every input the converters could see.
|
||||
*/
|
||||
|
||||
function legacyUserType(type: string | undefined): string {
|
||||
switch (type) {
|
||||
case 'user':
|
||||
return UserType.USER;
|
||||
case 'bot':
|
||||
return UserType.BOT;
|
||||
case 'app':
|
||||
return UserType.APP;
|
||||
case '':
|
||||
case undefined:
|
||||
return UserType.UNKNOWN;
|
||||
default:
|
||||
return type.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
function legacyStatusConnection(status: string | undefined): string {
|
||||
switch (status) {
|
||||
case 'offline':
|
||||
return UserStatusConnection.OFFLINE;
|
||||
case 'online':
|
||||
return UserStatusConnection.ONLINE;
|
||||
case 'away':
|
||||
return UserStatusConnection.AWAY;
|
||||
case 'busy':
|
||||
return UserStatusConnection.BUSY;
|
||||
case undefined:
|
||||
return UserStatusConnection.UNDEFINED;
|
||||
default:
|
||||
return !status ? UserStatusConnection.OFFLINE : status.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
function legacyRoomType(typeChar: string): string {
|
||||
switch (typeChar) {
|
||||
case 'c':
|
||||
return RoomType.CHANNEL;
|
||||
case 'p':
|
||||
return RoomType.PRIVATE_GROUP;
|
||||
case 'd':
|
||||
return RoomType.DIRECT_MESSAGE;
|
||||
case 'l':
|
||||
return RoomType.LIVE_CHAT;
|
||||
default:
|
||||
return typeChar;
|
||||
}
|
||||
}
|
||||
|
||||
function legacySettingType(type: string): string {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return SettingType.BOOLEAN;
|
||||
case 'code':
|
||||
return SettingType.CODE;
|
||||
case 'color':
|
||||
return SettingType.COLOR;
|
||||
case 'font':
|
||||
return SettingType.FONT;
|
||||
case 'int':
|
||||
return SettingType.NUMBER;
|
||||
case 'select':
|
||||
return SettingType.SELECT;
|
||||
case 'string':
|
||||
return SettingType.STRING;
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
describe('apps converter enum codecs', () => {
|
||||
describe('UserTypeCodec', () => {
|
||||
const inputs = ['user', 'bot', 'app', '', undefined, 'newfangled'];
|
||||
|
||||
it('decode matches the legacy _convertUserTypeToEnum for every input', () => {
|
||||
for (const input of inputs) {
|
||||
expect(z.decode(UserTypeCodec, input)).to.equal(legacyUserType(input));
|
||||
}
|
||||
});
|
||||
|
||||
it('encode passes the Apps-Engine value straight through', () => {
|
||||
for (const value of Object.values(UserType)) {
|
||||
expect(z.encode(UserTypeCodec, value)).to.equal(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserStatusConnectionCodec', () => {
|
||||
const inputs = ['offline', 'online', 'away', 'busy', '', undefined, 'weird'];
|
||||
|
||||
it('decode matches the legacy _convertStatusConnectionToEnum for every input', () => {
|
||||
for (const input of inputs) {
|
||||
expect(z.decode(UserStatusConnectionCodec, input)).to.equal(legacyStatusConnection(input));
|
||||
}
|
||||
});
|
||||
|
||||
it('encode passes the Apps-Engine value straight through', () => {
|
||||
for (const value of Object.values(UserStatusConnection)) {
|
||||
expect(z.encode(UserStatusConnectionCodec, value)).to.equal(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('RoomTypeCodec', () => {
|
||||
const inputs = ['c', 'p', 'd', 'l', 'v'];
|
||||
|
||||
it('decode matches the legacy _convertTypeToApp for every input', () => {
|
||||
for (const input of inputs) {
|
||||
expect(z.decode(RoomTypeCodec, input)).to.equal(legacyRoomType(input));
|
||||
}
|
||||
});
|
||||
|
||||
it('encode passes the Apps-Engine value straight through', () => {
|
||||
for (const value of Object.values(RoomType)) {
|
||||
expect(z.encode(RoomTypeCodec, value)).to.equal(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('SettingTypeCodec', () => {
|
||||
const inputs = ['boolean', 'code', 'color', 'font', 'int', 'select', 'string', 'multiSelect'];
|
||||
|
||||
it('decode matches the legacy _convertTypeToApp for every input', () => {
|
||||
for (const input of inputs) {
|
||||
expect(z.decode(SettingTypeCodec, input)).to.equal(legacySettingType(input));
|
||||
}
|
||||
});
|
||||
|
||||
it('encode passes the Apps-Engine value straight through', () => {
|
||||
for (const value of Object.values(SettingType)) {
|
||||
expect(z.encode(SettingTypeCodec, value)).to.equal(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
238
apps/meteor/tests/unit/app/apps/server/codecs/mappedData.spec.ts
Normal file
238
apps/meteor/tests/unit/app/apps/server/codecs/mappedData.spec.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { createMappedCodec, mappedDecode, mappedDecodeAsync } from '../../../../../../app/apps/server/converters/codecs/mappedData';
|
||||
|
||||
describe('createMappedCodec', () => {
|
||||
const codec = createMappedCodec({
|
||||
id: '_id',
|
||||
name: 'name',
|
||||
scope: 'scope',
|
||||
});
|
||||
|
||||
it('decode renames mapped fields and buckets the rest', () => {
|
||||
const result = z.decode(codec, { _id: 'r1', name: 'admin', scope: 'Users', extra: 1, _updatedAt: 'x' });
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
id: 'r1',
|
||||
name: 'admin',
|
||||
scope: 'Users',
|
||||
_unmappedProperties_: { extra: 1, _updatedAt: 'x' },
|
||||
});
|
||||
});
|
||||
|
||||
it('decode omits mapped targets whose source is undefined but still consumes the source key', () => {
|
||||
const result = z.decode(codec, { _id: 'r1', name: undefined, other: true });
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
id: 'r1',
|
||||
_unmappedProperties_: { other: true },
|
||||
});
|
||||
expect(result).to.not.have.nested.property('_unmappedProperties_.name');
|
||||
});
|
||||
|
||||
it('decode does not mutate the input (deep isolation)', () => {
|
||||
const input = { _id: 'r1', nested: { a: 1 } };
|
||||
const result = z.decode(codec, input) as any;
|
||||
|
||||
result._unmappedProperties_.nested.a = 999;
|
||||
|
||||
expect(input.nested.a).to.equal(1);
|
||||
});
|
||||
|
||||
it('encode applies the inverse rename and merges _unmappedProperties_', () => {
|
||||
const result = z.encode(codec, { id: 'r1', name: 'admin', scope: 'Users', _unmappedProperties_: { extra: 1 } });
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'r1', name: 'admin', scope: 'Users', extra: 1 });
|
||||
});
|
||||
|
||||
it('round-trips decode -> encode back to the original shape', () => {
|
||||
const original = { _id: 'r1', name: 'admin', scope: 'Users', extra: 1 };
|
||||
|
||||
expect(z.encode(codec, z.decode(codec, original))).to.deep.equal(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mappedDecodeAsync', () => {
|
||||
it('handles string renames, sync and async derived fields, and buckets the rest', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'r1', t: 'c', u: { _id: 'u1' }, leftover: 42 },
|
||||
{
|
||||
id: '_id',
|
||||
type: (data) => {
|
||||
const { t } = data;
|
||||
delete data.t;
|
||||
return t === 'c' ? 'channel' : t;
|
||||
},
|
||||
creator: async (data) => {
|
||||
const { u } = data;
|
||||
delete data.u;
|
||||
return u ? { id: u._id } : undefined;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
id: 'r1',
|
||||
type: 'channel',
|
||||
creator: { id: 'u1' },
|
||||
_unmappedProperties_: { leftover: 42 },
|
||||
});
|
||||
});
|
||||
|
||||
it('omits derived fields that return undefined and does not mutate the input', async () => {
|
||||
const input = { _id: 'r1', nested: { a: 1 } };
|
||||
const result = await mappedDecodeAsync(input, {
|
||||
id: '_id',
|
||||
creator: () => undefined,
|
||||
});
|
||||
|
||||
expect(result).to.deep.equal({ id: 'r1', _unmappedProperties_: { nested: { a: 1 } } });
|
||||
expect(result).to.not.have.property('creator');
|
||||
|
||||
(result as any)._unmappedProperties_.nested.a = 999;
|
||||
expect(input.nested.a).to.equal(1);
|
||||
});
|
||||
|
||||
it('maps a nested object recursively, bucketing unmapped fields at each level', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'c1', visitor: { visitorId: 'v1', extra: 'drop' }, spare: true },
|
||||
{
|
||||
_id: '_id',
|
||||
visitor: { from: 'visitor', map: { visitorId: 'visitorId' } },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
_id: 'c1',
|
||||
visitor: { visitorId: 'v1', _unmappedProperties_: { extra: 'drop' } },
|
||||
_unmappedProperties_: { spare: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('omits a nested object whose source is missing without a property access error', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'c1', spare: true },
|
||||
{
|
||||
_id: '_id',
|
||||
lastChat: { from: 'lastChat', map: { _id: '_id', ts: 'ts' } },
|
||||
visitor: { from: 'visitor', map: { visitorId: 'visitorId' } },
|
||||
details: { from: 'details', map: { type: 'type' } },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'c1', _unmappedProperties_: { spare: true } });
|
||||
expect(result).to.not.have.property('lastChat');
|
||||
expect(result).to.not.have.property('visitor');
|
||||
expect(result).to.not.have.property('details');
|
||||
});
|
||||
|
||||
it('omits a nested object whose source is null without a property access error', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'c1', lastChat: null, visitor: null, details: null, spare: true },
|
||||
{
|
||||
_id: '_id',
|
||||
lastChat: { from: 'lastChat', map: { _id: '_id', ts: 'ts' } },
|
||||
visitor: { from: 'visitor', map: { visitorId: 'visitorId' } },
|
||||
details: { from: 'details', map: { type: 'type' } },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'c1', _unmappedProperties_: { spare: true } });
|
||||
expect(result).to.not.have.property('lastChat');
|
||||
expect(result).to.not.have.property('visitor');
|
||||
expect(result).to.not.have.property('details');
|
||||
});
|
||||
|
||||
it('drops the _unmappedProperties_ bucket at the root and every nested level when `dropUnmapped` is set', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'c1', visitor: { visitorId: 'v1', extra: 'drop' }, phones: [{ phoneNumber: '1', extra: 'x' }], spare: true },
|
||||
{
|
||||
_id: '_id',
|
||||
visitor: { from: 'visitor', map: { visitorId: 'visitorId' } },
|
||||
phones: { from: 'phones', list: true, map: { phoneNumber: 'phoneNumber' } },
|
||||
},
|
||||
{ dropUnmapped: true },
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
_id: 'c1',
|
||||
visitor: { visitorId: 'v1' },
|
||||
phones: [{ phoneNumber: '1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('copies a nested source verbatim when the descriptor has no `map`', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ _id: 'c1', details: { type: 'sms', id: 'sms-1' }, spare: true },
|
||||
{
|
||||
_id: '_id',
|
||||
details: { from: 'details' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
_id: 'c1',
|
||||
details: { type: 'sms', id: 'sms-1' },
|
||||
_unmappedProperties_: { spare: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a list of objects via `list` + `map`, bucketing each element', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ phones: [{ phoneNumber: '1', extra: 'x' }, { phoneNumber: '2' }] },
|
||||
{ phones: { from: 'phones', list: true, map: { phoneNumber: 'phoneNumber' } } },
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
phones: [
|
||||
{ phoneNumber: '1', _unmappedProperties_: { extra: 'x' } },
|
||||
{ phoneNumber: '2', _unmappedProperties_: {} },
|
||||
],
|
||||
_unmappedProperties_: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('drops a source property from the bucket when a function deletes it explicitly', async () => {
|
||||
const result = await mappedDecodeAsync(
|
||||
{ firstName: 'John', lastName: 'Doe' },
|
||||
{
|
||||
fullName: (d: Record<string, any>) => {
|
||||
const value = `${d.firstName} ${d.lastName}`;
|
||||
delete d.firstName;
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.deep.equal({ fullName: 'John Doe', _unmappedProperties_: { lastName: 'Doe' } });
|
||||
});
|
||||
|
||||
it('wraps a non-array value into a list when `list` is true', async () => {
|
||||
const result = await mappedDecodeAsync({ tag: 'admin' }, { tags: { from: 'tag', list: true, map: {} } });
|
||||
|
||||
expect(result).to.deep.equal({ tags: ['admin'], _unmappedProperties_: {} });
|
||||
});
|
||||
|
||||
it('keeps every property as unmapped when the map is empty', async () => {
|
||||
const result = await mappedDecodeAsync({ os: 'android', version: '1.9', lan: 'en' }, {});
|
||||
|
||||
expect(result).to.deep.equal({ _unmappedProperties_: { os: 'android', version: '1.9', lan: 'en' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('mappedDecode type constraints', () => {
|
||||
type Source = { _id: string; name: string };
|
||||
|
||||
it('constrains map values to keys of the source type and infers the decoded shape', () => {
|
||||
const decode = mappedDecode<Source>({ id: '_id', label: 'name' });
|
||||
const result = decode({ _id: 'a', name: 'b' });
|
||||
|
||||
// `result` is typed `Decoded<Source, ...>`: renamed targets plus the bucket.
|
||||
expect(result).to.deep.equal({ id: 'a', label: 'b', _unmappedProperties_: {} });
|
||||
|
||||
// @ts-expect-error - 'missing' is not a key of Source, so the field map is rejected.
|
||||
mappedDecode<Source>({ id: 'missing' });
|
||||
});
|
||||
});
|
||||
52
apps/meteor/tests/unit/app/apps/server/codecs/rooms.spec.ts
Normal file
52
apps/meteor/tests/unit/app/apps/server/codecs/rooms.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { expect } from 'chai';
|
||||
import { beforeEach, describe, it } from 'mocha';
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
let visitor: Record<string, unknown>;
|
||||
|
||||
const { appRoomToRocketChat } = proxyquire.noCallThru().load('../../../../../../app/apps/server/converters/codecs/rooms', {
|
||||
'@rocket.chat/models': {
|
||||
LivechatVisitors: {
|
||||
findOneEnabledById: async () => visitor,
|
||||
},
|
||||
LivechatDepartment: { findOneById: async () => undefined },
|
||||
Users: { findOneById: async () => undefined },
|
||||
LivechatContacts: { findOneEnabledById: async () => undefined },
|
||||
},
|
||||
});
|
||||
|
||||
describe('appRoomToRocketChat', () => {
|
||||
beforeEach(() => {
|
||||
visitor = { _id: 'visitor-1', username: 'guest', token: 'tok-1', status: 'online' };
|
||||
});
|
||||
|
||||
it('does not throw when a visitor is present but visitorChannelInfo is missing', async () => {
|
||||
const result = (await appRoomToRocketChat({ id: 'lc-1', type: 'l', visitor: { id: 'visitor-1' } }, false)) as Record<string, any>;
|
||||
|
||||
expect(result.v).to.include({ _id: 'visitor-1' });
|
||||
expect(result.v).to.not.have.property('lastMessageTs');
|
||||
expect(result.v).to.not.have.property('phone');
|
||||
});
|
||||
|
||||
it('includes the activity when the visitor has a non-empty activity array', async () => {
|
||||
visitor.activity = ['2026-09'];
|
||||
|
||||
const result = (await appRoomToRocketChat({ id: 'lc-1', type: 'l', visitor: { id: 'visitor-1' } }, false)) as Record<string, any>;
|
||||
|
||||
expect(result.v).to.have.property('activity').that.deep.equals(['2026-09']);
|
||||
});
|
||||
|
||||
it('omits the activity when the visitor activity is null', async () => {
|
||||
visitor.activity = null;
|
||||
|
||||
const result = (await appRoomToRocketChat({ id: 'lc-1', type: 'l', visitor: { id: 'visitor-1' } }, false)) as Record<string, any>;
|
||||
|
||||
expect(result.v).to.not.have.property('activity');
|
||||
});
|
||||
|
||||
it('omits the activity when the visitor has no activity', async () => {
|
||||
const result = (await appRoomToRocketChat({ id: 'lc-1', type: 'l', visitor: { id: 'visitor-1' } }, false)) as Record<string, any>;
|
||||
|
||||
expect(result.v).to.not.have.property('activity');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { IAppsUser } from '@rocket.chat/apps';
|
||||
import type { ISetting, IUser } from '@rocket.chat/core-typings';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
|
||||
import { AppSettingsConverter } from '../../../../../app/apps/server/converters/settings';
|
||||
import { AppUsersConverter } from '../../../../../app/apps/server/converters/users';
|
||||
|
||||
const orch: any = {};
|
||||
|
||||
const asUser = (value: Record<string, unknown>) => value as unknown as IUser;
|
||||
const asAppUser = (value: Record<string, unknown>) => value as unknown as IAppsUser;
|
||||
const asSetting = (value: Record<string, unknown>) => value as unknown as ISetting;
|
||||
|
||||
describe('apps converters — lenient enum edge cases', () => {
|
||||
describe('AppUsersConverter.convertToApp', () => {
|
||||
const converter = new AppUsersConverter(orch);
|
||||
|
||||
it('maps an undefined statusConnection to UNDEFINED (Livechat guests / rocket.cat)', () => {
|
||||
const result = converter.convertToApp(asUser({ _id: 'u', username: 'a', statusConnection: undefined }));
|
||||
|
||||
expect(result.statusConnection).to.equal('undefined');
|
||||
expect(result.type).to.equal('unknown');
|
||||
});
|
||||
|
||||
it('does not throw and falls back to OFFLINE for a null statusConnection', () => {
|
||||
const result = converter.convertToApp(asUser({ _id: 'u', username: 'a', statusConnection: null }));
|
||||
|
||||
expect(result.statusConnection).to.equal('offline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppUsersConverter.convertToRocketChat', () => {
|
||||
const converter = new AppUsersConverter(orch);
|
||||
|
||||
it('does not throw when the app user has no type/statusConnection and strips empties', () => {
|
||||
const result = converter.convertToRocketChat(asAppUser({ id: 'u', username: 'a' }));
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'u', username: 'a' });
|
||||
});
|
||||
|
||||
it('falls back to the legacy utfOffset property when utcOffset is absent', () => {
|
||||
const result = converter.convertToRocketChat(asAppUser({ id: 'u', username: 'a', utfOffset: -3 }));
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'u', username: 'a', utcOffset: -3 });
|
||||
});
|
||||
|
||||
it('prefers utcOffset over the legacy utfOffset when both are present', () => {
|
||||
const result = converter.convertToRocketChat(asAppUser({ id: 'u', username: 'a', utcOffset: 5, utfOffset: -3 }));
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'u', username: 'a', utcOffset: 5 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppSettingsConverter.convertToApp', () => {
|
||||
const converter = new AppSettingsConverter(orch);
|
||||
|
||||
it('passes an unknown/undefined setting type through without throwing', () => {
|
||||
const result = converter.convertToApp(asSetting({ _id: 's', type: undefined }));
|
||||
|
||||
expect(result.id).to.equal('s');
|
||||
expect(result.type).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
904
apps/meteor/tests/unit/app/apps/server/converters.golden.spec.ts
Normal file
904
apps/meteor/tests/unit/app/apps/server/converters.golden.spec.ts
Normal file
@@ -0,0 +1,904 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
|
||||
import { AppContactsConverter } from '../../../../../app/apps/server/converters/contacts';
|
||||
import { AppDepartmentsConverter } from '../../../../../app/apps/server/converters/departments';
|
||||
import { AppMessagesConverter } from '../../../../../app/apps/server/converters/messages';
|
||||
import { AppRolesConverter } from '../../../../../app/apps/server/converters/roles';
|
||||
import { AppRoomsConverter } from '../../../../../app/apps/server/converters/rooms';
|
||||
import { AppSettingsConverter } from '../../../../../app/apps/server/converters/settings';
|
||||
import { AppThreadsConverter } from '../../../../../app/apps/server/converters/threads';
|
||||
import { AppUploadsConverter } from '../../../../../app/apps/server/converters/uploads';
|
||||
import { AppUsersConverter } from '../../../../../app/apps/server/converters/users';
|
||||
import { AppVideoConferencesConverter } from '../../../../../app/apps/server/converters/videoConferences';
|
||||
import { AppVisitorsConverter } from '../../../../../app/apps/server/converters/visitors';
|
||||
|
||||
/*
|
||||
* Golden snapshots of the converters' deterministic transform methods, captured from the
|
||||
* behaviour that existed before the Zod-codec migration. These lock the RC <-> Apps-Engine field
|
||||
* mapping (including the `_unmappedProperties_` bucket and pass-through rules) so that each phase
|
||||
* of the migration can prove it preserves behaviour exactly.
|
||||
*
|
||||
* Output is compared after `JSON.parse(JSON.stringify(...))` so Date instances are normalised to
|
||||
* ISO strings and `undefined` fields are dropped — matching how these payloads cross the app bridge.
|
||||
*/
|
||||
|
||||
const ts = new Date('2024-01-01T00:00:00.000Z');
|
||||
const updated = new Date('2024-02-02T00:00:00.000Z');
|
||||
|
||||
const json = (value: unknown): unknown => JSON.parse(JSON.stringify(value));
|
||||
|
||||
// A stub orchestrator whose cross-converter lookups resolve deterministically, so the decode
|
||||
// direction of the cross-converter converters (uploads/rooms/messages) can be snapshotted without a
|
||||
// database.
|
||||
const stubOrch: any = {
|
||||
getConverters: () => ({
|
||||
get: (key: string) => ({
|
||||
convertById: async (id: string) => ({ __converted: key, id }),
|
||||
convertToApp: (u: any) => ({ __convertedToApp: key, id: u?._id }),
|
||||
convertByToken: async (token: string) => ({ __converted: key, token }),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
describe('apps converters — golden snapshots (pre-codec behaviour)', () => {
|
||||
describe('AppSettingsConverter', () => {
|
||||
it('convertToApp maps a setting to the Apps-Engine shape', () => {
|
||||
const converter = new AppSettingsConverter(stubOrch);
|
||||
const setting = {
|
||||
_id: 'Some_Setting',
|
||||
type: 'int',
|
||||
packageValue: 10,
|
||||
values: [{ key: 'a', i18nLabel: 'A' }],
|
||||
value: 42,
|
||||
public: true,
|
||||
hidden: false,
|
||||
group: 'General',
|
||||
i18nLabel: 'Some_Setting',
|
||||
i18nDescription: 'Some_Setting_Description',
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
sorter: 5,
|
||||
};
|
||||
|
||||
expect(json(converter.convertToApp(setting as any))).to.deep.equal({
|
||||
id: 'Some_Setting',
|
||||
type: 'int',
|
||||
packageValue: 10,
|
||||
values: [{ key: 'a', i18nLabel: 'A' }],
|
||||
value: 42,
|
||||
public: true,
|
||||
hidden: false,
|
||||
group: 'General',
|
||||
i18nLabel: 'Some_Setting',
|
||||
i18nDescription: 'Some_Setting_Description',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppUsersConverter', () => {
|
||||
const converter = new AppUsersConverter(stubOrch);
|
||||
|
||||
it('convertToApp maps a user to the Apps-Engine shape', () => {
|
||||
const user = {
|
||||
_id: 'user-1',
|
||||
username: 'john.doe',
|
||||
emails: [{ address: 'john@example.com', verified: true }],
|
||||
type: 'bot',
|
||||
active: true,
|
||||
name: 'John Doe',
|
||||
roles: ['admin', 'user'],
|
||||
bio: 'hello',
|
||||
status: 'online',
|
||||
statusText: 'working',
|
||||
statusConnection: 'online',
|
||||
utcOffset: -3,
|
||||
createdAt: ts,
|
||||
_updatedAt: updated,
|
||||
lastLogin: ts,
|
||||
appId: 'app-1',
|
||||
customFields: { foo: 'bar' },
|
||||
federated: false,
|
||||
federation: undefined,
|
||||
freeSwitchExtension: '1001',
|
||||
settings: { preferences: { language: 'en' } },
|
||||
};
|
||||
|
||||
expect(json(converter.convertToApp(user as any))).to.deep.equal({
|
||||
id: 'user-1',
|
||||
username: 'john.doe',
|
||||
emails: [{ address: 'john@example.com', verified: true }],
|
||||
type: 'bot',
|
||||
isEnabled: true,
|
||||
name: 'John Doe',
|
||||
roles: ['admin', 'user'],
|
||||
bio: 'hello',
|
||||
status: 'online',
|
||||
statusText: 'working',
|
||||
statusConnection: 'online',
|
||||
utcOffset: -3,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
lastLoginAt: '2024-01-01T00:00:00.000Z',
|
||||
appId: 'app-1',
|
||||
customFields: { foo: 'bar' },
|
||||
isFederated: false,
|
||||
sipExtension: '1001',
|
||||
settings: { preferences: { language: 'en' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('convertToRocketChat maps an app user back to the RC shape', () => {
|
||||
const appUser = {
|
||||
id: 'user-1',
|
||||
username: 'john.doe',
|
||||
emails: [{ address: 'john@example.com', verified: true }],
|
||||
type: 'bot',
|
||||
isEnabled: true,
|
||||
name: 'John Doe',
|
||||
roles: ['admin', 'user'],
|
||||
bio: 'hello',
|
||||
status: 'online',
|
||||
statusConnection: 'online',
|
||||
utcOffset: -3,
|
||||
createdAt: ts,
|
||||
updatedAt: updated,
|
||||
lastLoginAt: ts,
|
||||
appId: 'app-1',
|
||||
isFederated: false,
|
||||
federation: undefined,
|
||||
sipExtension: '1001',
|
||||
};
|
||||
|
||||
expect(json(converter.convertToRocketChat(appUser as any))).to.deep.equal({
|
||||
_id: 'user-1',
|
||||
username: 'john.doe',
|
||||
emails: [{ address: 'john@example.com', verified: true }],
|
||||
type: 'bot',
|
||||
active: true,
|
||||
name: 'John Doe',
|
||||
roles: ['admin', 'user'],
|
||||
bio: 'hello',
|
||||
status: 'online',
|
||||
statusConnection: 'online',
|
||||
utcOffset: -3,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
_updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
lastLogin: '2024-01-01T00:00:00.000Z',
|
||||
appId: 'app-1',
|
||||
federated: false,
|
||||
freeSwitchExtension: '1001',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppVisitorsConverter', () => {
|
||||
const converter = new AppVisitorsConverter(stubOrch);
|
||||
|
||||
it('convertVisitor maps a visitor and buckets unmapped fields', async () => {
|
||||
const visitor = {
|
||||
_id: 'visitor-1',
|
||||
username: 'guest-1',
|
||||
name: 'Guest One',
|
||||
department: 'dep-1',
|
||||
_updatedAt: updated,
|
||||
token: 'tok-abc',
|
||||
phone: [{ phoneNumber: '+15551234' }],
|
||||
visitorEmails: [{ address: 'guest@example.com' }],
|
||||
livechatData: { note: 'vip' },
|
||||
status: 'online',
|
||||
activity: ['2024-01'],
|
||||
externalIds: ['ext-1'],
|
||||
lastChat: { _id: 'room-1', ts },
|
||||
};
|
||||
|
||||
expect(json(await converter.convertVisitor(visitor as any))).to.deep.equal({
|
||||
id: 'visitor-1',
|
||||
username: 'guest-1',
|
||||
name: 'Guest One',
|
||||
department: 'dep-1',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
token: 'tok-abc',
|
||||
phone: [{ phoneNumber: '+15551234' }],
|
||||
visitorEmails: [{ address: 'guest@example.com' }],
|
||||
livechatData: { note: 'vip' },
|
||||
status: 'online',
|
||||
activity: ['2024-01'],
|
||||
externalIds: ['ext-1'],
|
||||
_unmappedProperties_: { lastChat: { _id: 'room-1', ts: '2024-01-01T00:00:00.000Z' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('convertAppVisitor maps an app visitor back and merges unmapped fields', () => {
|
||||
const appVisitor = {
|
||||
id: 'visitor-1',
|
||||
username: 'guest-1',
|
||||
name: 'Guest One',
|
||||
token: 'tok-abc',
|
||||
phone: [{ phoneNumber: '+15551234' }],
|
||||
livechatData: { note: 'vip' },
|
||||
status: 'online',
|
||||
visitorEmails: [{ address: 'guest@example.com' }],
|
||||
department: 'dep-1',
|
||||
externalIds: ['ext-1'],
|
||||
_unmappedProperties_: { customField: 'x' },
|
||||
};
|
||||
|
||||
expect(json(converter.convertAppVisitor(appVisitor as any))).to.deep.equal({
|
||||
_id: 'visitor-1',
|
||||
username: 'guest-1',
|
||||
name: 'Guest One',
|
||||
token: 'tok-abc',
|
||||
phone: [{ phoneNumber: '+15551234' }],
|
||||
livechatData: { note: 'vip' },
|
||||
status: 'online',
|
||||
visitorEmails: [{ address: 'guest@example.com' }],
|
||||
department: 'dep-1',
|
||||
externalIds: ['ext-1'],
|
||||
customField: 'x',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppDepartmentsConverter', () => {
|
||||
const converter = new AppDepartmentsConverter(stubOrch);
|
||||
|
||||
it('convertDepartment maps a department and buckets unmapped fields', async () => {
|
||||
const department = {
|
||||
_id: 'dep-1',
|
||||
name: 'Support',
|
||||
email: 'support@example.com',
|
||||
_updatedAt: updated,
|
||||
enabled: true,
|
||||
numAgents: 3,
|
||||
showOnOfflineForm: true,
|
||||
description: 'Support dept',
|
||||
offlineMessageChannelName: 'offline',
|
||||
requestTagBeforeClosingChat: true,
|
||||
chatClosingTags: ['done'],
|
||||
abandonedRoomsCloseCustomMessage: 'bye',
|
||||
waitingQueueMessage: 'please wait',
|
||||
departmentsAllowedToForward: ['dep-2'],
|
||||
showOnRegistration: false,
|
||||
type: 'd',
|
||||
};
|
||||
|
||||
expect(json(await converter.convertDepartment(department as any))).to.deep.equal({
|
||||
id: 'dep-1',
|
||||
name: 'Support',
|
||||
email: 'support@example.com',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
enabled: true,
|
||||
numberOfAgents: 3,
|
||||
showOnOfflineForm: true,
|
||||
description: 'Support dept',
|
||||
offlineMessageChannelName: 'offline',
|
||||
requestTagBeforeClosingChat: true,
|
||||
chatClosingTags: ['done'],
|
||||
abandonedRoomsCloseCustomMessage: 'bye',
|
||||
waitingQueueMessage: 'please wait',
|
||||
departmentsAllowedToForward: ['dep-2'],
|
||||
showOnRegistration: false,
|
||||
_unmappedProperties_: { type: 'd' },
|
||||
});
|
||||
});
|
||||
|
||||
it('convertAppDepartment maps an app department back and merges unmapped fields', () => {
|
||||
const appDepartment = {
|
||||
id: 'dep-1',
|
||||
name: 'Support',
|
||||
email: 'support@example.com',
|
||||
updatedAt: updated,
|
||||
enabled: true,
|
||||
numberOfAgents: 3,
|
||||
showOnOfflineForm: true,
|
||||
showOnRegistration: false,
|
||||
description: 'Support dept',
|
||||
offlineMessageChannelName: 'offline',
|
||||
requestTagBeforeClosingChat: true,
|
||||
chatClosingTags: ['done'],
|
||||
abandonedRoomsCloseCustomMessage: 'bye',
|
||||
waitingQueueMessage: 'please wait',
|
||||
departmentsAllowedToForward: ['dep-2'],
|
||||
_unmappedProperties_: { extra: 1 },
|
||||
};
|
||||
|
||||
expect(json(converter.convertAppDepartment(appDepartment as any))).to.deep.equal({
|
||||
_id: 'dep-1',
|
||||
name: 'Support',
|
||||
email: 'support@example.com',
|
||||
_updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
enabled: true,
|
||||
numAgents: 3,
|
||||
showOnOfflineForm: true,
|
||||
showOnRegistration: false,
|
||||
description: 'Support dept',
|
||||
offlineMessageChannelName: 'offline',
|
||||
requestTagBeforeClosingChat: true,
|
||||
chatClosingTags: ['done'],
|
||||
abandonedRoomsCloseCustomMessage: 'bye',
|
||||
waitingQueueMessage: 'please wait',
|
||||
departmentsAllowedToForward: ['dep-2'],
|
||||
extra: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppRolesConverter', () => {
|
||||
it('convertRole maps a role and buckets unmapped fields', async () => {
|
||||
const converter = new AppRolesConverter();
|
||||
const role = {
|
||||
_id: 'admin',
|
||||
name: 'admin',
|
||||
description: 'Administrator',
|
||||
mandatory2fa: true,
|
||||
protected: true,
|
||||
scope: 'Users',
|
||||
_updatedAt: updated,
|
||||
};
|
||||
|
||||
expect(json(await converter.convertRole(role as any))).to.deep.equal({
|
||||
id: 'admin',
|
||||
name: 'admin',
|
||||
description: 'Administrator',
|
||||
mandatory2fa: true,
|
||||
protected: true,
|
||||
scope: 'Users',
|
||||
_unmappedProperties_: { _updatedAt: '2024-02-02T00:00:00.000Z' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppVideoConferencesConverter', () => {
|
||||
const converter = new AppVideoConferencesConverter();
|
||||
const call = {
|
||||
_id: 'vc-1',
|
||||
type: 'direct',
|
||||
rid: 'room-1',
|
||||
status: 1,
|
||||
createdBy: { _id: 'user-1', username: 'john.doe' },
|
||||
createdAt: ts,
|
||||
};
|
||||
const expected = {
|
||||
_id: 'vc-1',
|
||||
type: 'direct',
|
||||
rid: 'room-1',
|
||||
status: 1,
|
||||
createdBy: { _id: 'user-1', username: 'john.doe' },
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
it('convertVideoConference passes the call through', () => {
|
||||
expect(json(converter.convertVideoConference(call as any))).to.deep.equal(expected);
|
||||
});
|
||||
|
||||
it('convertAppVideoConference passes the call through', () => {
|
||||
expect(json(converter.convertAppVideoConference(call as any))).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppContactsConverter', () => {
|
||||
const converter = new AppContactsConverter();
|
||||
|
||||
it('convertContact clones the contact unchanged', async () => {
|
||||
const contact = {
|
||||
_id: 'contact-1',
|
||||
_updatedAt: updated,
|
||||
name: 'Jane',
|
||||
phones: [{ phoneNumber: '+15550000' }],
|
||||
emails: [{ address: 'jane@example.com' }],
|
||||
unknown: false,
|
||||
customFields: { tier: 'gold' },
|
||||
createdAt: ts,
|
||||
channels: [
|
||||
{
|
||||
name: 'sms',
|
||||
verified: true,
|
||||
visitor: { visitorId: 'visitor-1', source: { type: 'sms', id: 'sms-1' } },
|
||||
blocked: false,
|
||||
lastChat: { _id: 'room-1', ts },
|
||||
},
|
||||
],
|
||||
importIds: ['imp-1'],
|
||||
};
|
||||
|
||||
expect(json(await converter.convertContact(contact as any))).to.deep.equal(json(contact));
|
||||
});
|
||||
|
||||
it('convertAppContact deep-maps an app contact back, dropping unmapped fields at each level', async () => {
|
||||
const appContact = {
|
||||
_id: 'contact-1',
|
||||
_updatedAt: updated,
|
||||
name: 'Jane',
|
||||
phones: [{ phoneNumber: '+15550000', extra: 'drop-me' }],
|
||||
emails: [{ address: 'jane@example.com' }],
|
||||
contactManager: 'mgr-1',
|
||||
unknown: false,
|
||||
conflictingFields: [{ field: 'name', value: 'Janet' }],
|
||||
customFields: { tier: 'gold' },
|
||||
channels: [
|
||||
{
|
||||
name: 'sms',
|
||||
verified: true,
|
||||
visitor: { visitorId: 'visitor-1', source: { type: 'sms', id: 'sms-1' } },
|
||||
blocked: false,
|
||||
field: 'phone',
|
||||
value: '+15550000',
|
||||
verifiedAt: ts,
|
||||
details: { type: 'sms', id: 'sms-1', alias: 'a', label: 'l', sidebarIcon: 'i', defaultIcon: 'd', destination: 'x' },
|
||||
lastChat: { _id: 'room-1', ts },
|
||||
},
|
||||
],
|
||||
createdAt: ts,
|
||||
lastChat: { _id: 'room-1', ts },
|
||||
importIds: ['imp-1'],
|
||||
};
|
||||
|
||||
expect(json(await converter.convertAppContact(appContact as any))).to.deep.equal({
|
||||
_id: 'contact-1',
|
||||
_updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
name: 'Jane',
|
||||
phones: [{ phoneNumber: '+15550000' }],
|
||||
emails: [{ address: 'jane@example.com' }],
|
||||
contactManager: 'mgr-1',
|
||||
unknown: false,
|
||||
conflictingFields: [{ field: 'name', value: 'Janet' }],
|
||||
customFields: { tier: 'gold' },
|
||||
channels: [
|
||||
{
|
||||
name: 'sms',
|
||||
verified: true,
|
||||
visitor: {
|
||||
visitorId: 'visitor-1',
|
||||
source: { type: 'sms', id: 'sms-1' },
|
||||
},
|
||||
blocked: false,
|
||||
field: 'phone',
|
||||
value: '+15550000',
|
||||
verifiedAt: '2024-01-01T00:00:00.000Z',
|
||||
details: {
|
||||
type: 'sms',
|
||||
id: 'sms-1',
|
||||
alias: 'a',
|
||||
label: 'l',
|
||||
sidebarIcon: 'i',
|
||||
defaultIcon: 'd',
|
||||
destination: 'x',
|
||||
},
|
||||
lastChat: { _id: 'room-1', ts: '2024-01-01T00:00:00.000Z' },
|
||||
},
|
||||
],
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
lastChat: { _id: 'room-1', ts: '2024-01-01T00:00:00.000Z' },
|
||||
importIds: ['imp-1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppUploadsConverter', () => {
|
||||
const converter = new AppUploadsConverter(stubOrch);
|
||||
|
||||
it('convertToApp maps an upload and resolves room/user/visitor via converters', async () => {
|
||||
const upload = {
|
||||
_id: 'up-1',
|
||||
name: 'file.png',
|
||||
size: 1234,
|
||||
type: 'image/png',
|
||||
store: 'GridFS',
|
||||
description: 'a file',
|
||||
complete: true,
|
||||
uploading: false,
|
||||
extension: 'png',
|
||||
progress: 1,
|
||||
etag: 'etag-1',
|
||||
path: '/path',
|
||||
token: 'up-tok',
|
||||
url: 'http://example/file.png',
|
||||
_updatedAt: updated,
|
||||
uploadedAt: ts,
|
||||
rid: 'room-1',
|
||||
userId: 'user-1',
|
||||
visitorToken: 'tok-abc',
|
||||
};
|
||||
|
||||
expect(json(await converter.convertToApp(upload as any))).to.deep.equal({
|
||||
id: 'up-1',
|
||||
name: 'file.png',
|
||||
size: 1234,
|
||||
type: 'image/png',
|
||||
store: 'GridFS',
|
||||
description: 'a file',
|
||||
complete: true,
|
||||
uploading: false,
|
||||
extension: 'png',
|
||||
progress: 1,
|
||||
etag: 'etag-1',
|
||||
path: '/path',
|
||||
token: 'up-tok',
|
||||
url: 'http://example/file.png',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
uploadedAt: '2024-01-01T00:00:00.000Z',
|
||||
room: { __converted: 'rooms', id: 'room-1' },
|
||||
user: { __converted: 'users', id: 'user-1' },
|
||||
visitor: { __converted: 'visitors', token: 'tok-abc' },
|
||||
_unmappedProperties_: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('convertToRocketChat maps an app upload back and merges unmapped fields', () => {
|
||||
const appUpload = {
|
||||
id: 'up-1',
|
||||
name: 'file.png',
|
||||
size: 1234,
|
||||
type: 'image/png',
|
||||
extension: 'png',
|
||||
description: 'a file',
|
||||
store: 'GridFS',
|
||||
etag: 'etag-1',
|
||||
path: '/path',
|
||||
complete: true,
|
||||
uploading: false,
|
||||
progress: 1,
|
||||
token: 'up-tok',
|
||||
url: 'http://example/file.png',
|
||||
updatedAt: updated,
|
||||
uploadedAt: ts,
|
||||
room: { id: 'room-1' },
|
||||
user: { id: 'user-1' },
|
||||
visitor: { token: 'tok-abc' },
|
||||
_unmappedProperties_: { extraUpload: true },
|
||||
};
|
||||
|
||||
expect(json(converter.convertToRocketChat(appUpload as any))).to.deep.equal({
|
||||
_id: 'up-1',
|
||||
name: 'file.png',
|
||||
size: 1234,
|
||||
type: 'image/png',
|
||||
extension: 'png',
|
||||
description: 'a file',
|
||||
store: 'GridFS',
|
||||
etag: 'etag-1',
|
||||
complete: true,
|
||||
uploading: false,
|
||||
progress: 1,
|
||||
token: 'up-tok',
|
||||
url: 'http://example/file.png',
|
||||
_updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
uploadedAt: '2024-01-01T00:00:00.000Z',
|
||||
rid: 'room-1',
|
||||
userId: 'user-1',
|
||||
visitorToken: 'tok-abc',
|
||||
extraUpload: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('convertToRocketChat does not throw when the app upload has no room (e.g. deleted room)', () => {
|
||||
const appUpload = {
|
||||
id: 'up-1',
|
||||
name: 'file.png',
|
||||
user: { id: 'user-1' },
|
||||
};
|
||||
|
||||
const result = json(converter.convertToRocketChat(appUpload as any)) as Record<string, unknown>;
|
||||
|
||||
expect(result).to.deep.equal({ _id: 'up-1', name: 'file.png', userId: 'user-1' });
|
||||
expect(result).to.not.have.property('rid');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Decode-direction golden snapshots for the async, cross-converter converters (rooms, messages,
|
||||
* threads). These are the highest-risk converters for mapping drift (the `transformMappedData` ->
|
||||
* `mappedDecodeAsync` swap), yet previously had no golden coverage. Cross-converter lookups resolve
|
||||
* through `stubOrch`, so these snapshots run without a database.
|
||||
*/
|
||||
describe('apps converters — decode golden snapshots (rooms/messages/threads)', () => {
|
||||
describe('AppRoomsConverter.convertRoom', () => {
|
||||
const converter = new AppRoomsConverter(stubOrch);
|
||||
|
||||
it('maps a channel room: renames, boolean/type function fields, creator lookup, bucket', async () => {
|
||||
const result = await converter.convertRoom({
|
||||
_id: 'GENERAL',
|
||||
t: 'c',
|
||||
fname: 'General',
|
||||
name: 'general',
|
||||
u: { _id: 'owner-1' },
|
||||
msgs: 10,
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
default: true,
|
||||
ro: false,
|
||||
customFields: { a: 1 },
|
||||
teamId: 'team-1',
|
||||
teamMain: true,
|
||||
federated: false,
|
||||
lastMessage: { _id: 'lm-1' },
|
||||
} as any);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
id: 'GENERAL',
|
||||
displayName: 'General',
|
||||
slugifiedName: 'general',
|
||||
messageCount: 10,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
customFields: { a: 1 },
|
||||
teamId: 'team-1',
|
||||
isTeamMain: true,
|
||||
isFederated: false,
|
||||
isDefault: true,
|
||||
isReadOnly: false,
|
||||
displaySystemMessages: true,
|
||||
type: 'c',
|
||||
creator: { __converted: 'users', id: 'owner-1' },
|
||||
_unmappedProperties_: { lastMessage: { _id: 'lm-1' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('maps a livechat room: visitor/contact/department/closedBy/servedBy/responseBy/parentRoom + secure fields', async () => {
|
||||
const result = await converter.convertRoom({
|
||||
_id: 'lc-1',
|
||||
t: 'l',
|
||||
fname: 'Chat',
|
||||
msgs: 3,
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
closer: 'user',
|
||||
closedBy: { _id: 'closer-1' },
|
||||
servedBy: { _id: 'agent-1' },
|
||||
responseBy: { _id: 'resp-1' },
|
||||
v: { _id: 'visitor-1', phone: '+15550000', lastMessageTs: ts },
|
||||
departmentId: 'dep-1',
|
||||
contactId: 'contact-1',
|
||||
prid: 'parent-1',
|
||||
open: true,
|
||||
waitingResponse: true,
|
||||
sysMes: false,
|
||||
abacAttributes: { level: 'secret' },
|
||||
spare: 'keep',
|
||||
} as any);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
'id': 'lc-1',
|
||||
'displayName': 'Chat',
|
||||
'messageCount': 3,
|
||||
'createdAt': '2024-01-01T00:00:00.000Z',
|
||||
'updatedAt': '2024-02-02T00:00:00.000Z',
|
||||
'isWaitingResponse': true,
|
||||
'isOpen': true,
|
||||
'closer': 'user',
|
||||
'isDefault': false,
|
||||
'isReadOnly': false,
|
||||
'displaySystemMessages': false,
|
||||
'type': 'l',
|
||||
'visitor': { __converted: 'visitors', id: 'visitor-1' },
|
||||
'contact': { __converted: 'contacts', id: 'contact-1' },
|
||||
'visitorChannelInfo': { phone: '+15550000', lastMessageTs: '2024-01-01T00:00:00.000Z' },
|
||||
'department': { __converted: 'departments', id: 'dep-1' },
|
||||
'closedBy': { __converted: 'users', id: 'closer-1' },
|
||||
'servedBy': { __converted: 'users', id: 'agent-1' },
|
||||
'responseBy': { __converted: 'users', id: 'resp-1' },
|
||||
'parentRoom': { __converted: 'rooms', id: 'parent-1' },
|
||||
'@@SecureFields': [{ permission: 'abac.read', name: 'abacAttributes', value: { level: 'secret' } }],
|
||||
'_unmappedProperties_': {
|
||||
v: { _id: 'visitor-1', phone: '+15550000', lastMessageTs: '2024-01-01T00:00:00.000Z' },
|
||||
contactId: 'contact-1',
|
||||
spare: 'keep',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppRoomsConverter.convertRoomRaw', () => {
|
||||
const converter = new AppRoomsConverter(stubOrch);
|
||||
|
||||
it('maps a raw room without cross-converter resolution (inline user/visitor lookups)', async () => {
|
||||
const result = await converter.convertRoomRaw({
|
||||
_id: 'lc-2',
|
||||
t: 'l',
|
||||
fname: 'Raw',
|
||||
msgs: 1,
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
v: { _id: 'visitor-2', username: 'guest', extra: 'x' },
|
||||
closer: 'visitor',
|
||||
closedBy: { _id: 'visitor-2', username: 'guest' },
|
||||
u: { _id: 'owner-2', username: 'owner', name: 'Owner' },
|
||||
leftover: true,
|
||||
} as any);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
id: 'lc-2',
|
||||
displayName: 'Raw',
|
||||
messageCount: 1,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
closer: 'visitor',
|
||||
visitor: { id: 'visitor-2', username: 'guest', extra: 'x' },
|
||||
displaySystemMessages: true,
|
||||
type: 'l',
|
||||
creator: { _id: 'owner-2', username: 'owner', name: 'Owner' },
|
||||
closedBy: { _id: 'visitor-2', username: 'guest' },
|
||||
_unmappedProperties_: { leftover: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppMessagesConverter.convertMessage', () => {
|
||||
const converter = new AppMessagesConverter(stubOrch);
|
||||
|
||||
it('maps a message: room/editor/sender lookups, attachment sub-map, bucket', async () => {
|
||||
const result = await converter.convertMessage({
|
||||
_id: 'msg-1',
|
||||
rid: 'GENERAL',
|
||||
tmid: 'thread-1',
|
||||
u: { _id: 'user-1', username: 'u1' },
|
||||
msg: 'hello',
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
t: 'p',
|
||||
editedBy: { _id: 'editor-1' },
|
||||
editedAt: updated,
|
||||
emoji: ':smile:',
|
||||
alias: 'alias-1',
|
||||
groupable: false,
|
||||
attachments: [
|
||||
{
|
||||
text: 'attachment text',
|
||||
ts,
|
||||
author_name: 'Author',
|
||||
author_link: 'http://a',
|
||||
author_icon: 'http://i',
|
||||
title: 'Title',
|
||||
title_link: 'http://t',
|
||||
image_url: 'http://img',
|
||||
fields: [{ title: 'f', value: 'v' }],
|
||||
},
|
||||
],
|
||||
spare: 'keep',
|
||||
} as any);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
id: 'msg-1',
|
||||
threadId: 'thread-1',
|
||||
text: 'hello',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
editedAt: '2024-02-02T00:00:00.000Z',
|
||||
emoji: ':smile:',
|
||||
alias: 'alias-1',
|
||||
groupable: false,
|
||||
type: 'p',
|
||||
room: { __converted: 'rooms', id: 'GENERAL' },
|
||||
editor: { __converted: 'users', id: 'editor-1' },
|
||||
attachments: [
|
||||
{
|
||||
text: 'attachment text',
|
||||
imageUrl: 'http://img',
|
||||
fields: [{ title: 'f', value: 'v' }],
|
||||
author: { name: 'Author', link: 'http://a', icon: 'http://i' },
|
||||
title: { value: 'Title', link: 'http://t' },
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
_unmappedProperties_: {},
|
||||
},
|
||||
],
|
||||
sender: { __converted: 'users', id: 'user-1' },
|
||||
_unmappedProperties_: { spare: 'keep' },
|
||||
});
|
||||
});
|
||||
|
||||
it('sender falls back to the original message.u when the primary lookup resolves to nothing', async () => {
|
||||
// Old system messages from visitors have no `token`, so the primary lookup can miss; the
|
||||
// fallback must still receive the original sender rather than the already-deleted `message.u`.
|
||||
const fallbackOrch: any = {
|
||||
getConverters: () => ({
|
||||
get: (key: string) => ({
|
||||
convertById: async () => undefined,
|
||||
convertToApp: (u: any) => (u ? { __fallback: key, id: u._id } : undefined),
|
||||
convertByToken: async () => undefined,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
const fallbackConverter = new AppMessagesConverter(fallbackOrch);
|
||||
|
||||
const result = await fallbackConverter.convertMessage({
|
||||
_id: 'msg-3',
|
||||
rid: 'GENERAL',
|
||||
u: { _id: 'user-9', username: 'u9' },
|
||||
msg: 'hi',
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
t: 'p',
|
||||
} as any);
|
||||
|
||||
expect((json(result) as any).sender).to.deep.equal({ __fallback: 'users', id: 'user-9' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppMessagesConverter.convertMessageRaw', () => {
|
||||
const converter = new AppMessagesConverter(stubOrch);
|
||||
|
||||
it('maps a raw message (roomId/editor/sender/threadMsgCount pass-through, bucket)', async () => {
|
||||
const result = await converter.convertMessageRaw({
|
||||
_id: 'msg-2',
|
||||
rid: 'GENERAL',
|
||||
u: { _id: 'user-2' },
|
||||
msg: 'raw',
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
t: 'p',
|
||||
tcount: 4,
|
||||
editedBy: { _id: 'editor-2' },
|
||||
leftover: 1,
|
||||
} as any);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
id: 'msg-2',
|
||||
text: 'raw',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
roomId: 'GENERAL',
|
||||
editor: { _id: 'editor-2' },
|
||||
sender: { _id: 'user-2' },
|
||||
threadMsgCount: 4,
|
||||
type: 'p',
|
||||
_unmappedProperties_: { leftover: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppThreadsConverter.convertMessage', () => {
|
||||
const converter = new AppThreadsConverter(stubOrch);
|
||||
const convertUserById = (async (id: string) => ({ __converted: 'users', id })) as any;
|
||||
const convertToApp = ((u: any) => ({ __convertedToApp: 'users', id: u?._id })) as any;
|
||||
|
||||
it('maps a thread reply: injected room, editor/sender via passed-in lookups, attachment sub-map, bucket', async () => {
|
||||
const result = await converter.convertMessage(
|
||||
{
|
||||
_id: 'tmsg-1',
|
||||
rid: 'GENERAL',
|
||||
tmid: 'thread-1',
|
||||
u: { _id: 'user-3', username: 'u3' },
|
||||
msg: 'reply',
|
||||
ts,
|
||||
_updatedAt: updated,
|
||||
editedBy: { _id: 'editor-3' },
|
||||
editedAt: updated,
|
||||
attachments: [{ text: 'a', ts, title: 'T', title_link: 'http://t' }],
|
||||
spare: 'keep',
|
||||
} as any,
|
||||
{ id: 'GENERAL' } as any,
|
||||
convertUserById,
|
||||
convertToApp,
|
||||
);
|
||||
|
||||
expect(json(result)).to.deep.equal({
|
||||
id: 'tmsg-1',
|
||||
threadId: 'thread-1',
|
||||
text: 'reply',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-02-02T00:00:00.000Z',
|
||||
editedAt: '2024-02-02T00:00:00.000Z',
|
||||
room: { id: 'GENERAL' },
|
||||
attachments: [
|
||||
{
|
||||
text: 'a',
|
||||
title: { value: 'T', link: 'http://t' },
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
_unmappedProperties_: {},
|
||||
},
|
||||
],
|
||||
sender: { __converted: 'users', id: 'user-3' },
|
||||
_unmappedProperties_: {
|
||||
rid: 'GENERAL',
|
||||
u: { _id: 'user-3', username: 'u3' },
|
||||
editedBy: { _id: 'editor-3' },
|
||||
spare: 'keep',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
179
apps/meteor/tests/unit/app/apps/server/messages.tests.js
Normal file
179
apps/meteor/tests/unit/app/apps/server/messages.tests.js
Normal file
@@ -0,0 +1,179 @@
|
||||
import { expect } from 'chai';
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
import { appMessageMock, appMessageInvalidRoomMock, appPartialMessageMock } from './mocks/data/messages.data';
|
||||
import { MessagesMock } from './mocks/models/Messages.mock';
|
||||
import { RoomsMock } from './mocks/models/Rooms.mock';
|
||||
import { UsersMock } from './mocks/models/Users.mock';
|
||||
import { AppServerOrchestratorMock } from './mocks/orchestrator.mock';
|
||||
|
||||
const { AppMessagesConverter } = proxyquire.noCallThru().load('../../../../../app/apps/server/converters/messages', {
|
||||
'@rocket.chat/random': {
|
||||
Random: {
|
||||
id: () => 1,
|
||||
},
|
||||
},
|
||||
'@rocket.chat/models': {
|
||||
Rooms: new RoomsMock(),
|
||||
Messages: new MessagesMock(),
|
||||
Users: new UsersMock(),
|
||||
},
|
||||
'@rocket.chat/core-typings': {
|
||||
isMessageFromVisitor: (message) => 'token' in message,
|
||||
},
|
||||
});
|
||||
|
||||
describe('The AppMessagesConverter instance', () => {
|
||||
let messagesConverter;
|
||||
let messagesMock;
|
||||
|
||||
before(() => {
|
||||
const orchestrator = new AppServerOrchestratorMock();
|
||||
|
||||
const usersConverter = orchestrator.getConverters().get('users');
|
||||
|
||||
usersConverter.convertById = function convertUserByIdStub(id) {
|
||||
return UsersMock.convertedData[id];
|
||||
};
|
||||
|
||||
usersConverter.convertToApp = function convertUserToAppStub(user) {
|
||||
return {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
};
|
||||
};
|
||||
|
||||
orchestrator.getConverters().get('rooms').convertById = async function convertRoomByIdStub(id) {
|
||||
return RoomsMock.convertedData[id];
|
||||
};
|
||||
|
||||
messagesConverter = new AppMessagesConverter(orchestrator);
|
||||
messagesMock = new MessagesMock();
|
||||
});
|
||||
|
||||
const createdAt = new Date('2019-03-30T01:22:08.389Z');
|
||||
const updatedAt = new Date('2019-03-30T01:22:08.412Z');
|
||||
|
||||
describe('when converting a message from Rocket.Chat to the Engine schema', () => {
|
||||
it('should return `undefined` when `msgObj` is falsy', async () => {
|
||||
const appMessage = await messagesConverter.convertMessage(undefined);
|
||||
|
||||
expect(appMessage).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return a proper schema', async () => {
|
||||
const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock'));
|
||||
|
||||
expect(appMessage).to.have.property('id', 'SimpleMessageMock');
|
||||
expect(appMessage).to.have.property('createdAt').which.equalTime(createdAt);
|
||||
expect(appMessage).to.have.property('updatedAt').which.equalTime(updatedAt);
|
||||
expect(appMessage).to.have.property('groupable', false);
|
||||
expect(appMessage).to.have.property('sender').which.includes({ id: 'rocket.cat' });
|
||||
expect(appMessage).to.have.property('room').which.includes({ id: 'GENERAL' });
|
||||
|
||||
expect(appMessage).not.to.have.property('editor');
|
||||
expect(appMessage).not.to.have.property('attachments');
|
||||
expect(appMessage).not.to.have.property('reactions');
|
||||
expect(appMessage).not.to.have.property('avatarUrl');
|
||||
expect(appMessage).not.to.have.property('alias');
|
||||
expect(appMessage).not.to.have.property('customFields');
|
||||
expect(appMessage).not.to.have.property('emoji');
|
||||
});
|
||||
|
||||
it('should not mutate the original message object', () => {
|
||||
const rocketchatMessageMock = messagesMock.findOneById('SimpleMessageMock');
|
||||
|
||||
messagesConverter.convertMessage(rocketchatMessageMock);
|
||||
|
||||
expect(rocketchatMessageMock).to.deep.equal({
|
||||
_id: 'SimpleMessageMock',
|
||||
t: 'uj',
|
||||
rid: 'GENERAL',
|
||||
ts: new Date('2019-03-30T01:22:08.389Z'),
|
||||
msg: 'rocket.cat',
|
||||
u: {
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
},
|
||||
groupable: false,
|
||||
_updatedAt: new Date('2019-03-30T01:22:08.412Z'),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return basic sender info when it's not a Rocket.Chat user (e.g. Livechat Guest)", async () => {
|
||||
const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('LivechatGuestMessageMock'));
|
||||
|
||||
expect(appMessage).to.have.property('sender').which.includes({
|
||||
id: 'guest1234',
|
||||
username: 'guest1234',
|
||||
name: 'Livechat Guest',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when converting a message from the Engine schema back to Rocket.Chat', () => {
|
||||
it('should return `undefined` when `message` is falsy', async () => {
|
||||
const rocketchatMessage = await messagesConverter.convertAppMessage(undefined);
|
||||
|
||||
expect(rocketchatMessage).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return a proper schema', async () => {
|
||||
const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock);
|
||||
|
||||
expect(rocketchatMessage).to.have.property('_id', 'appMessageMock');
|
||||
expect(rocketchatMessage).to.have.property('rid', 'GENERAL');
|
||||
expect(rocketchatMessage).to.have.property('groupable', false);
|
||||
expect(rocketchatMessage).to.have.property('ts').which.equalTime(createdAt);
|
||||
expect(rocketchatMessage).to.have.property('_updatedAt').which.equalTime(updatedAt);
|
||||
expect(rocketchatMessage).to.have.property('u').which.includes({
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
name: 'Rocket.Cat',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a proper schema when receiving a partial object', async () => {
|
||||
const rocketchatMessage = await messagesConverter.convertAppMessage(appPartialMessageMock, true);
|
||||
|
||||
expect(rocketchatMessage).to.have.property('_id', 'appPartialMessageMock');
|
||||
expect(rocketchatMessage).to.have.property('groupable', false);
|
||||
expect(rocketchatMessage).to.have.property('emoji', ':smirk:');
|
||||
expect(rocketchatMessage).to.have.property('alias', 'rocket.feline');
|
||||
|
||||
expect(rocketchatMessage).to.not.have.property('ts');
|
||||
expect(rocketchatMessage).to.not.have.property('u');
|
||||
expect(rocketchatMessage).to.not.have.property('rid');
|
||||
expect(rocketchatMessage).to.not.have.property('_updatedAt');
|
||||
});
|
||||
|
||||
it('should merge `_unmappedProperties_` into the returned message', async () => {
|
||||
const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock);
|
||||
|
||||
expect(rocketchatMessage).not.to.have.property('_unmappedProperties_');
|
||||
expect(rocketchatMessage).to.have.property('t', 'uj');
|
||||
});
|
||||
|
||||
it('should not merge `_unmappedProperties_` into the returned message when receiving a partial object', async () => {
|
||||
const invalidPartialMessage = structuredClone(appPartialMessageMock);
|
||||
invalidPartialMessage._unmappedProperties_ = {
|
||||
t: 'uj',
|
||||
};
|
||||
|
||||
const rocketchatMessage = await messagesConverter.convertAppMessage(invalidPartialMessage, true);
|
||||
|
||||
expect(rocketchatMessage).to.not.have.property('_unmappedProperties_');
|
||||
expect(rocketchatMessage).to.not.have.property('t');
|
||||
});
|
||||
|
||||
it('should throw if message has an invalid room', async () => {
|
||||
try {
|
||||
await messagesConverter.convertAppMessage(appMessageInvalidRoomMock);
|
||||
} catch (e) {
|
||||
expect(e).to.be.an.instanceOf(Error);
|
||||
expect(e.message).to.equal('Invalid room provided on the message.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export class BaseModelMock {
|
||||
findOneById(id) {
|
||||
return this.data[id];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BaseModelMock } from './BaseModel.mock';
|
||||
|
||||
export class MessagesMock extends BaseModelMock {
|
||||
data = {
|
||||
SimpleMessageMock: {
|
||||
_id: 'SimpleMessageMock',
|
||||
t: 'uj',
|
||||
rid: 'GENERAL',
|
||||
ts: new Date('2019-03-30T01:22:08.389Z'),
|
||||
msg: 'rocket.cat',
|
||||
u: {
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
},
|
||||
groupable: false,
|
||||
_updatedAt: new Date('2019-03-30T01:22:08.412Z'),
|
||||
},
|
||||
|
||||
LivechatGuestMessageMock: {
|
||||
_id: 'LivechatGuestMessageMock',
|
||||
rid: 'LivechatRoom',
|
||||
msg: 'Help wanted',
|
||||
token: 'guest-token',
|
||||
alias: 'Livechat Guest',
|
||||
ts: new Date('2019-04-06T03:57:28.263Z'),
|
||||
u: {
|
||||
_id: 'guest1234',
|
||||
username: 'guest1234',
|
||||
name: 'Livechat Guest',
|
||||
},
|
||||
_updatedAt: new Date('2019-04-06T03:57:28.278Z'),
|
||||
mentions: [],
|
||||
channels: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { BaseModelMock } from './BaseModel.mock';
|
||||
|
||||
export class RoomsMock extends BaseModelMock {
|
||||
data = {
|
||||
GENERAL: {
|
||||
_id: 'GENERAL',
|
||||
ts: new Date('2019-03-27T20:51:36.808Z'),
|
||||
t: 'c',
|
||||
name: 'general',
|
||||
usernames: [],
|
||||
msgs: 31,
|
||||
usersCount: 3,
|
||||
default: true,
|
||||
_updatedAt: new Date('2019-04-10T17:44:34.931Z'),
|
||||
lastMessage: {
|
||||
_id: 1,
|
||||
t: 'uj',
|
||||
rid: 'GENERAL',
|
||||
ts: new Date('2019-03-30T01:22:08.389Z'),
|
||||
msg: 'rocket.cat',
|
||||
u: {
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
},
|
||||
groupable: false,
|
||||
_updatedAt: new Date('2019-03-30T01:22:08.412Z'),
|
||||
},
|
||||
lm: new Date('2019-04-10T17:44:34.873Z'),
|
||||
},
|
||||
|
||||
LivechatRoom: {
|
||||
_id: 'LivechatRoom',
|
||||
msgs: 41,
|
||||
usersCount: 1,
|
||||
lm: new Date('2019-04-07T23:45:25.407Z'),
|
||||
fname: 'Livechat Guest',
|
||||
t: 'l',
|
||||
ts: new Date('2019-04-06T03:56:17.040Z'),
|
||||
v: {
|
||||
_id: 'yDLaWs5Rzf5mzQsmB',
|
||||
username: 'guest-4',
|
||||
token: 'tkps932ccsl6me7intd3',
|
||||
status: 'away',
|
||||
},
|
||||
servedBy: {
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
ts: new Date('2019-04-06T03:56:17.040Z'),
|
||||
},
|
||||
cl: false,
|
||||
open: true,
|
||||
_updatedAt: new Date('2019-04-07T23:45:25.469Z'),
|
||||
lastMessage: {
|
||||
_id: 'zgEMhaMLCyDPu7xMn',
|
||||
rid: 'JceP6CZrpcA4j3NNe',
|
||||
msg: 'a',
|
||||
ts: new Date('2019-04-07T23:45:25.407Z'),
|
||||
u: {
|
||||
_id: '3Wz2wANqwrd7Hu5Fo',
|
||||
username: 'dgubert',
|
||||
name: 'Douglas Gubert',
|
||||
},
|
||||
_updatedAt: new Date('2019-04-07T23:45:25.433Z'),
|
||||
mentions: [],
|
||||
channels: [],
|
||||
},
|
||||
metrics: {
|
||||
v: {
|
||||
lq: new Date('2019-04-06T03:57:28.263Z'),
|
||||
},
|
||||
reaction: {
|
||||
fd: new Date('2019-04-06T03:57:17.083Z'),
|
||||
ft: 60.043,
|
||||
tt: 52144.278,
|
||||
},
|
||||
response: {
|
||||
avg: 26072.0655,
|
||||
fd: new Date('2019-04-06T03:57:17.083Z'),
|
||||
ft: 59.896,
|
||||
total: 2,
|
||||
tt: 52144.131,
|
||||
},
|
||||
servedBy: {
|
||||
lr: new Date('2019-04-06T18:25:32.394Z'),
|
||||
},
|
||||
},
|
||||
responseBy: {
|
||||
_id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static convertedData = {
|
||||
GENERAL: {
|
||||
id: 'GENERAL',
|
||||
slugifiedName: 'general',
|
||||
displayName: undefined,
|
||||
creator: undefined,
|
||||
createdAt: new Date('2019-03-27T20:51:36.808Z'),
|
||||
type: 'c',
|
||||
messageCount: 31,
|
||||
displaySystemMessages: true,
|
||||
isReadOnly: false,
|
||||
isDefault: true,
|
||||
updatedAt: new Date('2019-04-10T17:44:34.931Z'),
|
||||
lastModifiedAt: new Date('2019-04-10T17:44:34.873Z'),
|
||||
customFields: {},
|
||||
},
|
||||
|
||||
GENERALPartial: {
|
||||
id: 'GENERAL',
|
||||
slugifiedName: 'general',
|
||||
displaySystemMessages: true,
|
||||
updatedAt: new Date('2019-04-10T17:44:34.931Z'),
|
||||
},
|
||||
|
||||
GENERALPartialWithOptionalProps: {
|
||||
id: 'GENERAL',
|
||||
slugifiedName: 'general',
|
||||
displaySystemMessages: true,
|
||||
updatedAt: new Date('2019-04-10T17:44:34.931Z'),
|
||||
messageCount: 40,
|
||||
type: 'c',
|
||||
},
|
||||
|
||||
UpdatedRoom: {
|
||||
customFields: { custom: 'field' },
|
||||
},
|
||||
|
||||
LivechatRoom: {
|
||||
id: 'LivechatRoom',
|
||||
slugifiedName: undefined,
|
||||
displayName: 'Livechat Guest',
|
||||
creator: undefined,
|
||||
createdAt: new Date('2019-04-06T03:56:17.040Z'),
|
||||
type: 'l', // Apps-Engine defines the wrong type for livechat rooms
|
||||
messageCount: 41,
|
||||
displaySystemMessages: true,
|
||||
isReadOnly: false,
|
||||
isDefault: false,
|
||||
updatedAt: new Date('2019-04-07T23:45:25.469Z'),
|
||||
lastModifiedAt: new Date('2019-04-07T23:45:25.407Z'),
|
||||
customFields: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BaseModelMock } from './BaseModel.mock';
|
||||
|
||||
export class UsersMock extends BaseModelMock {
|
||||
data = {
|
||||
'rocket.cat': {
|
||||
_id: 'rocket.cat',
|
||||
createdAt: new Date('2019-03-27T20:51:36.821Z'),
|
||||
avatarOrigin: 'local',
|
||||
name: 'Rocket.Cat',
|
||||
username: 'rocket.cat',
|
||||
status: 'online',
|
||||
statusDefault: 'online',
|
||||
utcOffset: 0,
|
||||
active: true,
|
||||
type: 'bot',
|
||||
_updatedAt: new Date('2019-03-30T01:11:50.496Z'),
|
||||
roles: ['bot'],
|
||||
},
|
||||
};
|
||||
|
||||
static convertedData = {
|
||||
'rocket.cat': {
|
||||
id: 'rocket.cat',
|
||||
username: 'rocket.cat',
|
||||
emails: [
|
||||
{
|
||||
address: 'rocketcat@rocket.chat',
|
||||
verified: true,
|
||||
},
|
||||
],
|
||||
type: 'bot',
|
||||
isEnabled: true,
|
||||
name: 'Rocket.Cat',
|
||||
roles: ['bot'],
|
||||
status: 'online',
|
||||
statusConnection: 'online',
|
||||
utcOffset: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastLoginAt: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MessagesMock } from './Messages.mock';
|
||||
import { RoomsMock } from './Rooms.mock';
|
||||
import { UsersMock } from './Users.mock';
|
||||
|
||||
export const Messages = new MessagesMock();
|
||||
export const Rooms = new RoomsMock();
|
||||
export const Users = new UsersMock();
|
||||
@@ -0,0 +1,108 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export class AppServerOrchestratorMock {
|
||||
constructor() {
|
||||
this._marketplaceClient = {};
|
||||
|
||||
this._model = {};
|
||||
this._logModel = {};
|
||||
this._persistModel = {};
|
||||
this._storage = {};
|
||||
this._logStorage = {};
|
||||
|
||||
this._converters = new Map();
|
||||
this._converters.set('messages', {});
|
||||
this._converters.set('rooms', {});
|
||||
this._converters.set('settings', {});
|
||||
this._converters.set('users', {});
|
||||
|
||||
this._bridges = {};
|
||||
|
||||
this._manager = {};
|
||||
|
||||
this._communicators = new Map();
|
||||
this._communicators.set('methods', {});
|
||||
this._communicators.set('notifier', {});
|
||||
this._communicators.set('restapi', {});
|
||||
}
|
||||
|
||||
getMarketplaceClient() {
|
||||
return this._marketplaceClient;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
getPersistenceModel() {
|
||||
return this._persistModel;
|
||||
}
|
||||
|
||||
getStorage() {
|
||||
return this._storage;
|
||||
}
|
||||
|
||||
getLogStorage() {
|
||||
return this._logStorage;
|
||||
}
|
||||
|
||||
getConverters() {
|
||||
return this._converters;
|
||||
}
|
||||
|
||||
getBridges() {
|
||||
return this._bridges;
|
||||
}
|
||||
|
||||
getNotifier() {
|
||||
return this._communicators.get('notifier');
|
||||
}
|
||||
|
||||
getManager() {
|
||||
return this._manager;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
isLoaded() {
|
||||
return this.getManager().areAppsLoaded();
|
||||
}
|
||||
|
||||
isDebugging() {
|
||||
return true;
|
||||
}
|
||||
|
||||
debugLog(...args) {
|
||||
if (this.isDebugging()) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
// Don't try to load it again if it has
|
||||
// already been loaded
|
||||
if (this.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._manager
|
||||
.load()
|
||||
.then((affs) => console.log(`Loaded the Apps Framework and loaded a total of ${affs.length} Apps!`))
|
||||
.catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err));
|
||||
}
|
||||
|
||||
unload() {
|
||||
// Don't try to unload it if it's already been
|
||||
// unlaoded or wasn't unloaded to start with
|
||||
if (!this.isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._manager
|
||||
.unload()
|
||||
.then(() => console.log('Unloaded the Apps Framework.'))
|
||||
.catch((err) => console.warn('Failed to unload the Apps Framework!', err));
|
||||
}
|
||||
}
|
||||
177
apps/meteor/tests/unit/app/apps/server/rooms.tests.ts
Normal file
177
apps/meteor/tests/unit/app/apps/server/rooms.tests.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { IAppRoomsConverter, IAppsRoom } from '@rocket.chat/apps';
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import { expect } from 'chai';
|
||||
import { before, describe, it } from 'mocha';
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
import { MessagesMock } from './mocks/models/Messages.mock';
|
||||
import { RoomsMock } from './mocks/models/Rooms.mock';
|
||||
import { UsersMock } from './mocks/models/Users.mock';
|
||||
import { AppServerOrchestratorMock } from './mocks/orchestrator.mock';
|
||||
|
||||
const { AppRoomsConverter } = proxyquire.noCallThru().load('../../../../../app/apps/server/converters/rooms', {
|
||||
'@rocket.chat/random': {
|
||||
Random: {
|
||||
id: () => 1,
|
||||
},
|
||||
},
|
||||
'@rocket.chat/models': {
|
||||
Rooms: new RoomsMock(),
|
||||
Messages: new MessagesMock(),
|
||||
Users: new UsersMock(),
|
||||
},
|
||||
});
|
||||
|
||||
describe('The AppMessagesConverter instance', () => {
|
||||
let roomConverter: IAppRoomsConverter;
|
||||
let roomsMock: RoomsMock;
|
||||
|
||||
before(() => {
|
||||
const orchestrator = new AppServerOrchestratorMock();
|
||||
|
||||
const usersConverter = orchestrator.getConverters().get('users');
|
||||
|
||||
usersConverter.convertById = function convertUserByIdStub(id: string) {
|
||||
return UsersMock.convertedData[id as 'rocket.cat'] || undefined;
|
||||
};
|
||||
|
||||
usersConverter.convertToApp = function convertUserToAppStub(user: UsersMock['data']['rocket.cat']) {
|
||||
return {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
};
|
||||
};
|
||||
|
||||
orchestrator.getConverters().get('messages').convertById = async function convertRoomByIdStub(_id: string) {
|
||||
return {};
|
||||
};
|
||||
|
||||
roomConverter = new AppRoomsConverter(orchestrator);
|
||||
roomsMock = new RoomsMock();
|
||||
});
|
||||
|
||||
describe('when converting a room from Rocket.Chat to the Engine schema', () => {
|
||||
it('should return `undefined` when `originalRoom` is falsy', async () => {
|
||||
const appRoom = await roomConverter.convertRoom(undefined);
|
||||
|
||||
expect(appRoom).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return a proper schema', async () => {
|
||||
const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL'];
|
||||
const appRoom = await roomConverter.convertRoom(mockedRoom as unknown as IRoom);
|
||||
|
||||
expect(appRoom).to.have.property('id', mockedRoom._id);
|
||||
expect(appRoom).to.have.property('type', mockedRoom.t);
|
||||
expect(appRoom).to.have.property('slugifiedName', mockedRoom.name);
|
||||
expect(appRoom).to.have.property('createdAt').which.equalTime(mockedRoom.ts);
|
||||
expect(appRoom).to.have.property('updatedAt').which.equalTime(mockedRoom._updatedAt);
|
||||
expect(appRoom).to.have.property('messageCount', mockedRoom.msgs);
|
||||
});
|
||||
|
||||
it('should not mutate the original room object', async () => {
|
||||
const rocketchatRoomMock = structuredClone(roomsMock.findOneById('GENERAL'));
|
||||
|
||||
await roomConverter.convertRoom(rocketchatRoomMock);
|
||||
|
||||
expect(rocketchatRoomMock).to.deep.equal(roomsMock.findOneById('GENERAL'));
|
||||
});
|
||||
|
||||
it('should add an `_unmappedProperties_` field to the converted room which contains the `lastMessage` property of the room', async () => {
|
||||
const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL'];
|
||||
const appMessage = await roomConverter.convertRoom(mockedRoom as unknown as IRoom);
|
||||
|
||||
expect(appMessage).to.have.property('_unmappedProperties_').which.has.property('lastMessage').to.deep.equal(mockedRoom.lastMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when converting a room from the Engine schema back to Rocket.Chat', () => {
|
||||
it('should return `undefined` when `room` is falsy', async () => {
|
||||
const rocketchatMessage = await roomConverter.convertAppRoom(undefined);
|
||||
|
||||
expect(rocketchatMessage).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return a proper schema', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERAL as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom);
|
||||
|
||||
expect(rocketchatRoom).to.have.property('_id', appRoom.id);
|
||||
expect(rocketchatRoom).to.have.property('ts', appRoom.createdAt);
|
||||
expect(rocketchatRoom).to.have.property('lm', appRoom.lastModifiedAt);
|
||||
expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt);
|
||||
expect(rocketchatRoom).to.have.property('t', appRoom.type);
|
||||
expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName);
|
||||
});
|
||||
|
||||
it('should return a proper schema when receiving a partial object', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERALPartial as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true);
|
||||
|
||||
expect(rocketchatRoom).to.have.property('_id', appRoom.id);
|
||||
expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName);
|
||||
expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages);
|
||||
expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt);
|
||||
|
||||
expect(rocketchatRoom).to.not.have.property('msgs');
|
||||
expect(rocketchatRoom).to.not.have.property('ro');
|
||||
expect(rocketchatRoom).to.not.have.property('default');
|
||||
expect(rocketchatRoom).to.not.have.property('t');
|
||||
});
|
||||
|
||||
it('should return a proper schema when receiving a partial object', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true);
|
||||
|
||||
expect(rocketchatRoom).to.have.property('_id', appRoom.id);
|
||||
expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName);
|
||||
expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages);
|
||||
expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt);
|
||||
expect(rocketchatRoom).to.have.property('msgs', appRoom.messageCount);
|
||||
expect(rocketchatRoom).to.have.property('t', 'c');
|
||||
|
||||
expect(rocketchatRoom).to.not.have.property('ro');
|
||||
expect(rocketchatRoom).to.not.have.property('default');
|
||||
});
|
||||
|
||||
it('should not include properties that are not present in the app room', async () => {
|
||||
const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true);
|
||||
|
||||
expect(rocketchatRoom).to.have.property('customFields');
|
||||
expect(rocketchatRoom).to.not.have.property('_id');
|
||||
expect(rocketchatRoom).to.not.have.property('t');
|
||||
});
|
||||
|
||||
it('should not include name as undefined if the room doesnt have a name property', async () => {
|
||||
const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true);
|
||||
|
||||
expect(rocketchatRoom.name).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should include a name if the source room has slugifiedName property', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom;
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true);
|
||||
|
||||
expect(rocketchatRoom.name).to.equal(appRoom.slugifiedName);
|
||||
});
|
||||
|
||||
it('should not use _unmappedProperties when the room is a partial object', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom;
|
||||
// @ts-expect-error - _unmappedProperties
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, true);
|
||||
|
||||
expect(rocketchatRoom).to.not.have.property('unmapped');
|
||||
});
|
||||
|
||||
it('should use _unmappedProperties when the room is a partial object', async () => {
|
||||
const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom;
|
||||
// @ts-expect-error - _unmappedProperties
|
||||
const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, false);
|
||||
|
||||
expect(rocketchatRoom).to.have.property('unmapped', 'property');
|
||||
});
|
||||
});
|
||||
});
|
||||
70
apps/meteor/tests/unit/app/emoji/emojiNativeParser.spec.ts
Normal file
70
apps/meteor/tests/unit/app/emoji/emojiNativeParser.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
|
||||
import { getEmojiData } from '../../../../app/emoji-native/lib/generateEmojiData';
|
||||
import { getEmojiConfig } from '../../../../app/emoji-native/lib/getEmojiConfig';
|
||||
import { shortnameToUnicode } from '../../../../app/emoji-native/lib/shortnameToUnicode';
|
||||
|
||||
describe('emoji-native shortcode resolution', () => {
|
||||
describe('shortnameToUnicode', () => {
|
||||
it('resolves skin-tone variants stored under emojione alternate names', () => {
|
||||
expect(shortnameToUnicode(':thumbsup_tone3:')).to.equal('👍🏽');
|
||||
expect(shortnameToUnicode(':thumbup_tone3:')).to.equal('👍🏽');
|
||||
expect(shortnameToUnicode(':clap_tone2:')).to.equal('👏🏼');
|
||||
expect(shortnameToUnicode(':pray_tone4:')).to.equal('🙏🏾');
|
||||
});
|
||||
|
||||
it('still resolves the emojibase primary skin-tone name', () => {
|
||||
expect(shortnameToUnicode(':+1_tone3:')).to.equal('👍🏽');
|
||||
});
|
||||
|
||||
it('resolves regional indicator letters', () => {
|
||||
expect(shortnameToUnicode(':regional_indicator_a:')).to.equal('🇦');
|
||||
expect(shortnameToUnicode(':regional_indicator_z:')).to.equal('🇿');
|
||||
});
|
||||
|
||||
it('resolves legacy emojione base aliases', () => {
|
||||
expect(shortnameToUnicode(':digit_zero:')).to.equal('0️⃣');
|
||||
expect(shortnameToUnicode(':digit_nine:')).to.equal('9️⃣');
|
||||
expect(shortnameToUnicode(':pound_symbol:')).to.equal('#️⃣');
|
||||
expect(shortnameToUnicode(':asterisk_symbol:')).to.equal('*️⃣');
|
||||
expect(shortnameToUnicode(':tone3:')).to.equal('🏽');
|
||||
});
|
||||
|
||||
it('leaves unknown shortcodes untouched', () => {
|
||||
expect(shortnameToUnicode(':not_a_real_emoji:')).to.equal(':not_a_real_emoji:');
|
||||
});
|
||||
|
||||
it('resolves multiple shortcodes within a single string', () => {
|
||||
expect(shortnameToUnicode('hi :thumbsup_tone3: and :regional_indicator_a:')).to.equal('hi 👍🏽 and 🇦');
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
const { render } = getEmojiConfig({ list: {}, packages: {} });
|
||||
|
||||
it('renders a known shortcode as an emoji span', () => {
|
||||
expect(render(':smile:')).to.contain('class="emoji"');
|
||||
});
|
||||
|
||||
it('leaves prototype property names untouched', () => {
|
||||
expect(render(':toString:')).to.equal(':toString:');
|
||||
expect(render('topic :toString: end')).to.equal('topic :toString: end');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmojiData', () => {
|
||||
it('registers regional indicators in emojiList but keeps them out of the picker', () => {
|
||||
const { emojiList, emojisByCategory } = getEmojiData();
|
||||
expect(emojiList[':regional_indicator_a:']?.unicode).to.equal('🇦');
|
||||
expect(emojisByCategory.flags).to.not.include('regional_indicator_a');
|
||||
});
|
||||
|
||||
it('registers alternate skin-tone shortnames alongside the primary', () => {
|
||||
const { emojiList } = getEmojiData();
|
||||
expect(emojiList[':thumbsup_tone3:']?.unicode).to.equal('👍🏽');
|
||||
expect(emojiList[':thumbup_tone3:']?.unicode).to.equal('👍🏽');
|
||||
expect(emojiList[':+1_tone3:']?.unicode).to.equal('👍🏽');
|
||||
});
|
||||
});
|
||||
});
|
||||
136
apps/meteor/tests/unit/app/emoji/helpers.spec.ts
Normal file
136
apps/meteor/tests/unit/app/emoji/helpers.spec.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach, before } from 'mocha';
|
||||
|
||||
import { getEmojiConfig } from '../../../../app/emoji-native/lib/getEmojiConfig';
|
||||
import { getEmojisBySearchTerm, updateRecent, removeFromRecent, replaceEmojiInRecent } from '../../../../client/lib/emoji/helpers';
|
||||
import { emoji } from '../../../../client/lib/emoji/lib';
|
||||
|
||||
const registerNativeEmojis = () => {
|
||||
const config = getEmojiConfig(emoji);
|
||||
|
||||
emoji.packages.native = {
|
||||
emojiCategories: config.emojiCategories as any,
|
||||
emojisByCategory: config.emojisByCategory,
|
||||
toneList: config.toneList,
|
||||
render: config.render,
|
||||
renderPicker: config.renderPicker,
|
||||
sprites: config.sprites,
|
||||
};
|
||||
|
||||
for (const [key, currentEmoji] of Object.entries(config.emojiList)) {
|
||||
currentEmoji.emojiPackage = 'native';
|
||||
emoji.list[key] = currentEmoji as any;
|
||||
|
||||
if (currentEmoji.shortnames) {
|
||||
currentEmoji.shortnames.forEach((shortname: string) => {
|
||||
emoji.list[shortname] = currentEmoji as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const registerCustomEmoji = (name: string) => {
|
||||
if (!emoji.packages.emojiCustom) {
|
||||
emoji.packages.emojiCustom = {
|
||||
emojiCategories: [{ key: 'rocket', i18n: 'Custom' as any }],
|
||||
emojisByCategory: { rocket: [] },
|
||||
toneList: {},
|
||||
render: (html: string) => html,
|
||||
renderPicker: () => '<span class="emoji-custom" />',
|
||||
} as any;
|
||||
}
|
||||
emoji.packages.emojiCustom.emojisByCategory.rocket.push(name);
|
||||
emoji.list[`:${name}:`] = { emojiPackage: 'emojiCustom', name } as any;
|
||||
};
|
||||
|
||||
describe('Emoji Client Helpers', () => {
|
||||
beforeEach(() => {
|
||||
emoji.packages.base.emojisByCategory.recent = [];
|
||||
});
|
||||
|
||||
describe('getEmojisBySearchTerm', () => {
|
||||
before(registerNativeEmojis);
|
||||
|
||||
const search = (term: string) => getEmojisBySearchTerm(term, 0, [], () => undefined);
|
||||
const names = (term: string) => search(term).map((result) => result.emoji);
|
||||
const rendersThumbsUp = (term: string) => search(term).some((result) => result.image?.includes('👍'));
|
||||
|
||||
it('finds an emoji by its primary shortcode', () => {
|
||||
expect(names('+1')).to.include('+1');
|
||||
});
|
||||
|
||||
it('finds an emoji by its first alias (thumbsup -> 👍)', () => {
|
||||
expect(rendersThumbsUp('thumbsup')).to.be.true;
|
||||
});
|
||||
|
||||
it('finds an emoji by a secondary alias (thumbup -> 👍)', () => {
|
||||
expect(rendersThumbsUp('thumbup')).to.be.true;
|
||||
});
|
||||
|
||||
it('matches aliases partially (thumb -> thumbsup)', () => {
|
||||
expect(names('thumb')).to.include('thumbsup');
|
||||
});
|
||||
|
||||
it('does not list the same emoji more than once', () => {
|
||||
const images = search('grinning').map((result) => result.image);
|
||||
expect(images.length).to.equal(new Set(images).size);
|
||||
});
|
||||
|
||||
it('excludes skin-tone variants from the results', () => {
|
||||
expect(names('+1').some((name) => /_tone[1-5]/.test(name))).to.be.false;
|
||||
});
|
||||
|
||||
it('applies the selected skin tone when searching by an alias', () => {
|
||||
const result = getEmojisBySearchTerm('thumbsup', 2, [], () => undefined).find(({ image }) => image?.includes('👍'));
|
||||
|
||||
expect(result).to.exist;
|
||||
expect(result?.image).to.include('👍🏼');
|
||||
expect(result?.emoji).to.equal('thumbsup_tone2');
|
||||
});
|
||||
|
||||
it('finds a custom emoji whose name ends in a mixed skin-tone suffix (CORE-2473)', () => {
|
||||
registerCustomEmoji('mycustom_tone1-2');
|
||||
expect(names('mycustom_tone1-2')).to.include('mycustom_tone1-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRecent', () => {
|
||||
it('should update recent emojis with the provided emojis', () => {
|
||||
const recentEmojis = ['emoji1', 'emoji2'];
|
||||
updateRecent(recentEmojis);
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.contain('emoji1');
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.contain('emoji2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromRecent', () => {
|
||||
it('should remove a specific emoji from recent emojis', () => {
|
||||
emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2', 'emoji3'];
|
||||
removeFromRecent('emoji2', emoji.packages.base.emojisByCategory.recent);
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.not.include('emoji2');
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji3']);
|
||||
});
|
||||
|
||||
it('should do nothing if the emoji is not in the recent list', () => {
|
||||
emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2'];
|
||||
removeFromRecent('emoji3', emoji.packages.base.emojisByCategory.recent);
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceEmojiInRecent', () => {
|
||||
it('should replace an existing emoji with a new one in recent emojis', () => {
|
||||
emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2', 'emoji3'];
|
||||
replaceEmojiInRecent({ oldEmoji: 'emoji2', newEmoji: 'emoji4' });
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.not.include('emoji2');
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.include('emoji4');
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji4', 'emoji3']);
|
||||
});
|
||||
|
||||
it('should do nothing if the emoji to replace is not in the recent list', () => {
|
||||
emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2'];
|
||||
replaceEmojiInRecent({ oldEmoji: 'emoji3', newEmoji: 'emoji4' });
|
||||
expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
62
apps/meteor/tests/unit/app/highlight-words/helper.tests.js
Normal file
62
apps/meteor/tests/unit/app/highlight-words/helper.tests.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { highlightWords, getRegexHighlight, getRegexHighlightUrl } from '../../../../client/lib/highlightWords';
|
||||
|
||||
describe('helper', () => {
|
||||
describe('highlightWords', () => {
|
||||
it('highlights the correct words', () => {
|
||||
const res = highlightWords(
|
||||
'here is some word',
|
||||
['word'].map((highlight) => ({
|
||||
highlight,
|
||||
regex: getRegexHighlight(highlight),
|
||||
urlRegex: getRegexHighlightUrl(highlight),
|
||||
})),
|
||||
);
|
||||
|
||||
expect(res).to.be.equal('here is some <mark class="highlight-text">word</mark>');
|
||||
});
|
||||
|
||||
describe('handles links', () => {
|
||||
it('not highlighting one link', () => {
|
||||
const res = highlightWords(
|
||||
'here we go https://somedomain.com/here-some.word/pulls more words after',
|
||||
['word'].map((highlight) => ({
|
||||
highlight,
|
||||
regex: getRegexHighlight(highlight),
|
||||
urlRegex: getRegexHighlightUrl(highlight),
|
||||
})),
|
||||
);
|
||||
|
||||
expect(res).to.be.equal('here we go https://somedomain.com/here-some.word/pulls more words after');
|
||||
});
|
||||
|
||||
it('not highlighting two links', () => {
|
||||
const msg = 'here https://somedomain.com/here-some-foo/pulls more words after http://www.domain.com/some.foo/bar words after';
|
||||
const res = highlightWords(
|
||||
msg,
|
||||
['foo'].map((highlight) => ({
|
||||
highlight,
|
||||
regex: getRegexHighlight(highlight),
|
||||
urlRegex: getRegexHighlightUrl(highlight),
|
||||
})),
|
||||
);
|
||||
|
||||
expect(res).to.be.equal(msg);
|
||||
});
|
||||
|
||||
it('not highlighting link but keep words on message highlighted', () => {
|
||||
const res = highlightWords(
|
||||
'here we go https://somedomain.com/here-some.foo/pulls more foo after',
|
||||
['foo'].map((highlight) => ({
|
||||
highlight,
|
||||
regex: getRegexHighlight(highlight),
|
||||
urlRegex: getRegexHighlightUrl(highlight),
|
||||
})),
|
||||
);
|
||||
|
||||
expect(res).to.be.equal('here we go https://somedomain.com/here-some.foo/pulls more <mark class="highlight-text">foo</mark> after');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
24
apps/meteor/tests/unit/app/markdown/client.mocks.js
Normal file
24
apps/meteor/tests/unit/app/markdown/client.mocks.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
const mocks = {
|
||||
'meteor/meteor': {
|
||||
'Meteor': {
|
||||
absoluteUrl() {
|
||||
return 'http://localhost:3000/';
|
||||
},
|
||||
},
|
||||
'@global': true,
|
||||
},
|
||||
'@rocket.chat/random': {
|
||||
'Random': {
|
||||
id() {
|
||||
return Math.random().toString().replace('0.', 'A');
|
||||
},
|
||||
},
|
||||
'@global': true,
|
||||
},
|
||||
};
|
||||
|
||||
export const { Markdown } = proxyquire.noCallThru().load('../../../../app/markdown/lib/markdown', mocks);
|
||||
export const { original } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/original/original', mocks);
|
||||
export const { filtered } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/filtered/filtered', mocks);
|
||||
479
apps/meteor/tests/unit/app/markdown/client.tests.js
Normal file
479
apps/meteor/tests/unit/app/markdown/client.tests.js
Normal file
@@ -0,0 +1,479 @@
|
||||
import { escapeHTML } from '@rocket.chat/tools';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { Markdown, original, filtered } from './client.mocks';
|
||||
|
||||
const wrapper = (text, tag) => `<span class="copyonly">${tag}</span>${text}<span class="copyonly">${tag}</span>`;
|
||||
const boldWrapper = (text) => wrapper(`<strong>${text}</strong>`, '*');
|
||||
const italicWrapper = (text) => wrapper(`<em>${text}</em>`, '_');
|
||||
const strikeWrapper = (text) => wrapper(`<strike>${text}</strike>`, '~');
|
||||
const headerWrapper = (text, level) => `<h${level}>${text}</h${level}>`;
|
||||
const quoteWrapper = (text) =>
|
||||
`<blockquote class="background-transparent-darker-before"><span class="copyonly">></span>${text}</blockquote>`;
|
||||
const linkWrapped = (link, title) => `<a data-title="${link}" href="${link}" target="_blank" rel="noopener noreferrer">${title}</a>`;
|
||||
const inlinecodeWrapper = (text) => wrapper(`<span><code class="code-colors inline">${text}</code></span>`, '`');
|
||||
const codeWrapper = (text, lang) =>
|
||||
`<pre><code class='code-colors hljs ${lang}'><span class='copyonly'>\`\`\`<br></span>${text}<span class='copyonly'><br>\`\`\`</span></code></pre>`;
|
||||
|
||||
const bold = {
|
||||
'**': '**',
|
||||
'* *': '* *',
|
||||
'** *': '** *',
|
||||
'** **': '** **',
|
||||
'* Hello*': '* Hello*',
|
||||
'*Hello *': '*Hello *',
|
||||
':custom*emoji*case:': `:custom${boldWrapper('emoji')}case:`,
|
||||
'text*hello*text': `text${boldWrapper('hello')}text`,
|
||||
'*hello*text': `${boldWrapper('hello')}text`,
|
||||
'text*hello*': `text${boldWrapper('hello')}`,
|
||||
'*Hel lo*': boldWrapper('Hel lo'),
|
||||
'*Hello*': boldWrapper('Hello'),
|
||||
'**Hello**': boldWrapper('Hello'),
|
||||
'**Hello*': `*${boldWrapper('Hello')}`,
|
||||
'*Hello**': `${boldWrapper('Hello')}*`,
|
||||
'Hello': 'Hello',
|
||||
'*Hello': '*Hello',
|
||||
'Hello*': 'Hello*',
|
||||
'He*llo': 'He*llo',
|
||||
'***Hello***': `*${boldWrapper('Hello')}*`,
|
||||
'***Hello**': `*${boldWrapper('Hello')}`,
|
||||
'*Hello* this is dog': `${boldWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says *Hello*': `Rocket cat says ${boldWrapper('Hello')}`,
|
||||
'He said *Hello* to her': `He said ${boldWrapper('Hello')} to her`,
|
||||
'**Hello** this is dog': `${boldWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says **Hello**': `Rocket cat says ${boldWrapper('Hello')}`,
|
||||
'He said **Hello** to her': `He said ${boldWrapper('Hello')} to her`,
|
||||
'He was a**nn**oyed': `He was a${boldWrapper('nn')}oyed`,
|
||||
'There are two o in f*oo*tball': `There are two o in f${boldWrapper('oo')}tball`,
|
||||
};
|
||||
|
||||
const italic = {
|
||||
'__': '__',
|
||||
'_ _': '_ _',
|
||||
'__ _': '__ _',
|
||||
'__ __': '__ __',
|
||||
'_ Hello_': '_ Hello_',
|
||||
'_Hello _': '_Hello _',
|
||||
':custom_emoji_case:': ':custom_emoji_case:',
|
||||
'text_hello_text': 'text_hello_text',
|
||||
'_hello_text': '_hello_text',
|
||||
'text_hello_': 'text_hello_',
|
||||
'_Hel lo_': italicWrapper('Hel lo'),
|
||||
'_Hello_': italicWrapper('Hello'),
|
||||
'__Hello__': italicWrapper('Hello'),
|
||||
'__Hello_': `_${italicWrapper('Hello')}`,
|
||||
'_Hello__': `${italicWrapper('Hello')}_`,
|
||||
'Hello': 'Hello',
|
||||
'_Hello': '_Hello',
|
||||
'Hello_': 'Hello_',
|
||||
'He_llo': 'He_llo',
|
||||
'___Hello___': `_${italicWrapper('Hello')}_`,
|
||||
'___Hello__': `_${italicWrapper('Hello')}`,
|
||||
'_Hello_ this is dog': `${italicWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says _Hello_': `Rocket cat says ${italicWrapper('Hello')}`,
|
||||
'He said _Hello_ to her': `He said ${italicWrapper('Hello')} to her`,
|
||||
'__Hello__ this is dog': `${italicWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says __Hello__': `Rocket cat says ${italicWrapper('Hello')}`,
|
||||
'He said __Hello__ to her': `He said ${italicWrapper('Hello')} to her`,
|
||||
};
|
||||
|
||||
const strike = {
|
||||
'~~': '~~',
|
||||
'~ ~': '~ ~',
|
||||
'~~ ~': '~~ ~',
|
||||
'~~ ~~': '~~ ~~',
|
||||
'~ Hello~': '~ Hello~',
|
||||
'~Hello ~': '~Hello ~',
|
||||
':custom~emoji~case:': `:custom${strikeWrapper('emoji')}case:`,
|
||||
'text~hello~text': `text${strikeWrapper('hello')}text`,
|
||||
'~hello~text': `${strikeWrapper('hello')}text`,
|
||||
'text~hello~': `text${strikeWrapper('hello')}`,
|
||||
'~Hel lo~': strikeWrapper('Hel lo'),
|
||||
'~Hello~': strikeWrapper('Hello'),
|
||||
'~~Hello~~': strikeWrapper('Hello'),
|
||||
'~~Hello~': `~${strikeWrapper('Hello')}`,
|
||||
'~Hello~~': `${strikeWrapper('Hello')}~`,
|
||||
'Hello': 'Hello',
|
||||
'~Hello': '~Hello',
|
||||
'Hello~': 'Hello~',
|
||||
'He~llo': 'He~llo',
|
||||
'~~~Hello~~~': `~${strikeWrapper('Hello')}~`,
|
||||
'~~~Hello~~': `~${strikeWrapper('Hello')}`,
|
||||
'~Hello~ this is dog': `${strikeWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says ~Hello~': `Rocket cat says ${strikeWrapper('Hello')}`,
|
||||
'He said ~Hello~ to her': `He said ${strikeWrapper('Hello')} to her`,
|
||||
'~~Hello~~ this is dog': `${strikeWrapper('Hello')} this is dog`,
|
||||
'Rocket cat says ~~Hello~~': `Rocket cat says ${strikeWrapper('Hello')}`,
|
||||
'He said ~~Hello~~ to her': `He said ${strikeWrapper('Hello')} to her`,
|
||||
};
|
||||
|
||||
const headersLevel1 = {
|
||||
'# Hello': headerWrapper('Hello', 1),
|
||||
'# Rocket.Cat': headerWrapper('Rocket.Cat', 1),
|
||||
'# Hi': headerWrapper('Hi', 1),
|
||||
'# Hello this is dog': headerWrapper('Hello this is dog', 1),
|
||||
'# Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 1),
|
||||
'# He said Hello to her': headerWrapper('He said Hello to her', 1),
|
||||
'#Hello': '#Hello',
|
||||
'#Hello#': '#Hello#',
|
||||
'He#llo': 'He#llo',
|
||||
};
|
||||
|
||||
const headersLevel2 = {
|
||||
'## Hello': headerWrapper('Hello', 2),
|
||||
'## Rocket.Cat': headerWrapper('Rocket.Cat', 2),
|
||||
'## Hi': headerWrapper('Hi', 2),
|
||||
'## Hello this is dog': headerWrapper('Hello this is dog', 2),
|
||||
'## Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 2),
|
||||
'## He said Hello to her': headerWrapper('He said Hello to her', 2),
|
||||
'##Hello': '##Hello',
|
||||
'##Hello##': '##Hello##',
|
||||
'He##llo': 'He##llo',
|
||||
};
|
||||
|
||||
const headersLevel3 = {
|
||||
'### Hello': headerWrapper('Hello', 3),
|
||||
'### Rocket.Cat': headerWrapper('Rocket.Cat', 3),
|
||||
'### Hi': headerWrapper('Hi', 3),
|
||||
'### Hello this is dog': headerWrapper('Hello this is dog', 3),
|
||||
'### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 3),
|
||||
'### He said Hello to her': headerWrapper('He said Hello to her', 3),
|
||||
'###Hello': '###Hello',
|
||||
'###Hello###': '###Hello###',
|
||||
'He###llo': 'He###llo',
|
||||
};
|
||||
|
||||
const headersLevel4 = {
|
||||
'#### Hello': headerWrapper('Hello', 4),
|
||||
'#### Rocket.Cat': headerWrapper('Rocket.Cat', 4),
|
||||
'#### Hi': headerWrapper('Hi', 4),
|
||||
'#### Hello this is dog': headerWrapper('Hello this is dog', 4),
|
||||
'#### Rocket cat says Hello': headerWrapper('Rocket cat says Hello', 4),
|
||||
'#### He said Hello to her': headerWrapper('He said Hello to her', 4),
|
||||
'####Hello': '####Hello',
|
||||
'####Hello####': '####Hello####',
|
||||
'He####llo': 'He####llo',
|
||||
};
|
||||
|
||||
const quote = {
|
||||
'>Hello': escapeHTML('>Hello'),
|
||||
'>Rocket.Cat': escapeHTML('>Rocket.Cat'),
|
||||
'>Hi': escapeHTML('>Hi'),
|
||||
'> Hello this is dog': escapeHTML('> Hello this is dog'),
|
||||
'> Rocket cat says Hello': escapeHTML('> Rocket cat says Hello'),
|
||||
'> He said Hello to her': escapeHTML('> He said Hello to her'),
|
||||
'> He said Hello to her ': escapeHTML('> He said Hello to her '),
|
||||
'<Hello': escapeHTML('<Hello'),
|
||||
'<Rocket.Cat>': escapeHTML('<Rocket.Cat>'),
|
||||
' >Hi': escapeHTML(' >Hi'),
|
||||
'Hello > this is dog': escapeHTML('Hello > this is dog'),
|
||||
'Roc>ket cat says Hello': escapeHTML('Roc>ket cat says Hello'),
|
||||
'He said Hello to her>': escapeHTML('He said Hello to her>'),
|
||||
'>Hello': quoteWrapper('Hello'),
|
||||
'>Rocket.Cat': quoteWrapper('Rocket.Cat'),
|
||||
'>Hi': quoteWrapper('Hi'),
|
||||
'> Hello this is dog': quoteWrapper(' Hello this is dog'),
|
||||
'> Rocket cat says Hello': quoteWrapper(' Rocket cat says Hello'),
|
||||
'> He said Hello to her': quoteWrapper(' He said Hello to her'),
|
||||
'<Hello': escapeHTML('<Hello'),
|
||||
'<Rocket.Cat>': escapeHTML('<Rocket.Cat>'),
|
||||
' >Hi': escapeHTML(' >Hi'),
|
||||
'Hello > this is dog': escapeHTML('Hello > this is dog'),
|
||||
'Roc>ket cat says Hello': escapeHTML('Roc>ket cat says Hello'),
|
||||
'He said Hello to her>': escapeHTML('He said Hello to her>'),
|
||||
};
|
||||
|
||||
const link = {
|
||||
'<http://link|Text>': escapeHTML('<http://link|Text>'),
|
||||
'<https://open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<https://open.rocket.chat/|Open Site For Rocket.Chat>'),
|
||||
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML(
|
||||
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>',
|
||||
),
|
||||
'<https://rocket.chat/|Rocket.Chat Site>': '&lt;https://rocket.chat/|Rocket.Chat Site&gt;',
|
||||
'<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': escapeHTML(
|
||||
'<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>',
|
||||
),
|
||||
'<http://linkText>': escapeHTML('<http://linkText>'),
|
||||
'<https:open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML('<https:open.rocket.chat/ | Open Site For Rocket.Chat>'),
|
||||
'https://open.rocket.chat/|Open Site For Rocket.Chat': escapeHTML('https://open.rocket.chat/|Open Site For Rocket.Chat'),
|
||||
'<www.open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<www.open.rocket.chat/|Open Site For Rocket.Chat>'),
|
||||
'<htps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<htps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<ttps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<ttps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<tps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<tps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<open.rocket.chat/|Open Site For Rocket.Chat>'),
|
||||
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': escapeHTML(
|
||||
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>',
|
||||
),
|
||||
|
||||
'<http://invalid link|Text>': escapeHTML('<http://invalid link|Text>'),
|
||||
'<http://link|Text>': linkWrapped('http://link', 'Text'),
|
||||
'<https://open.rocket.chat/|Open Site For Rocket.Chat>': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
|
||||
'<https://open.rocket.chat/ | Open Site For Rocket.Chat>': linkWrapped(
|
||||
encodeURI('https://open.rocket.chat/ '),
|
||||
' Open Site For Rocket.Chat',
|
||||
),
|
||||
'<https://rocket.chat/|Rocket.Chat Site>': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
|
||||
'<https://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': linkWrapped(
|
||||
'https://rocket.chat/docs/developer-guides/testing/#testing',
|
||||
'Testing Entry on Rocket.Chat Docs Site',
|
||||
),
|
||||
'<http://linkText>': escapeHTML('<http://linkText>'),
|
||||
'<https:open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML('<https:open.rocket.chat/ | Open Site For Rocket.Chat>'),
|
||||
'<www.open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<www.open.rocket.chat/|Open Site For Rocket.Chat>'),
|
||||
'<htps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<htps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<ttps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<ttps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<tps://rocket.chat/|Rocket.Chat Site>': escapeHTML('<tps://rocket.chat/|Rocket.Chat Site>'),
|
||||
'<open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<open.rocket.chat/|Open Site For Rocket.Chat>'),
|
||||
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>': escapeHTML(
|
||||
'<htts://rocket.chat/docs/developer-guides/testing/#testing|Testing Entry on Rocket.Chat Docs Site>',
|
||||
),
|
||||
|
||||
'[Text](http://invalid link)': '[Text](http://invalid link)',
|
||||
'[Text](http://link)': linkWrapped('http://link', 'Text'),
|
||||
'[Open Site For Rocket.Chat](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', 'Open Site For Rocket.Chat'),
|
||||
'[ Open Site For Rocket.Chat ](https://open.rocket.chat/)': linkWrapped('https://open.rocket.chat/', ' Open Site For Rocket.Chat '),
|
||||
'[Rocket.Chat Site](https://rocket.chat/)': linkWrapped('https://rocket.chat/', 'Rocket.Chat Site'),
|
||||
'[Testing Entry on Rocket.Chat Docs Site](https://rocket.chat/docs/developer-guides/testing/#testing)': linkWrapped(
|
||||
'https://rocket.chat/docs/developer-guides/testing/#testing',
|
||||
'Testing Entry on Rocket.Chat Docs Site',
|
||||
),
|
||||
'[](http://linkText)': '[](http://linkText)',
|
||||
'[text]': '[text]',
|
||||
'[Open Site For Rocket.Chat](https:open.rocket.chat/)': '[Open Site For Rocket.Chat](https:open.rocket.chat/)',
|
||||
'[Open Site For Rocket.Chat](www.open.rocket.chat/)': '[Open Site For Rocket.Chat](www.open.rocket.chat/)',
|
||||
'[Rocket.Chat Site](htps://rocket.chat/)': '[Rocket.Chat Site](htps://rocket.chat/)',
|
||||
'[Rocket.Chat Site](ttps://rocket.chat/)': '[Rocket.Chat Site](ttps://rocket.chat/)',
|
||||
'[Rocket.Chat Site](tps://rocket.chat/)': '[Rocket.Chat Site](tps://rocket.chat/)',
|
||||
'[Open Site For Rocket.Chat](open.rocket.chat/)': '[Open Site For Rocket.Chat](open.rocket.chat/)',
|
||||
'[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)':
|
||||
'[Testing Entry on Rocket.Chat Docs Site](htts://rocket.chat/docs/developer-guides/testing/#testing)',
|
||||
'[Text](http://link?param1=1¶m2=2)': linkWrapped('http://link?param1=1&param2=2', 'Text'),
|
||||
'[Testing Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation))': linkWrapped(
|
||||
'https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)',
|
||||
'Testing Double parentheses',
|
||||
),
|
||||
'[Testing data after Double parentheses](https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla)': linkWrapped(
|
||||
'https://en.wikipedia.org/wiki/Disambiguation_(disambiguation)/blabla/bla',
|
||||
'Testing data after Double parentheses',
|
||||
),
|
||||
};
|
||||
|
||||
Object.entries(link).forEach(([key, value]) => {
|
||||
link[`before (test) ${key} after (test)`] = `before (test) ${value} after (test)`;
|
||||
});
|
||||
|
||||
const inlinecode = {
|
||||
'`code`': inlinecodeWrapper('code'),
|
||||
'`code` begin': `${inlinecodeWrapper('code')} begin`,
|
||||
'End `code`': `End ${inlinecodeWrapper('code')}`,
|
||||
'Middle `code` middle': `Middle ${inlinecodeWrapper('code')} middle`,
|
||||
'`code`begin': `${inlinecodeWrapper('code')}begin`,
|
||||
'End`code`': `End${inlinecodeWrapper('code')}`,
|
||||
'Middle`code`middle': `Middle${inlinecodeWrapper('code')}middle`,
|
||||
};
|
||||
|
||||
const code = {
|
||||
'```code```': codeWrapper('<span class="hljs-keyword">code</span>', 'clean'),
|
||||
'```code': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
|
||||
'```code\n': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
|
||||
'```\ncode\n```': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
|
||||
'```code\n```': codeWrapper('<span class="hljs-keyword">code</span>\n', 'clean'),
|
||||
'```\ncode```': codeWrapper('<span class="hljs-keyword">code</span>', 'clean'),
|
||||
"```javascript\nvar a = 'log';\nconsole.log(a);```": codeWrapper(
|
||||
'<span class="hljs-keyword">var</span> a = <span class="hljs-string">'log'</span>;\n<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(a);',
|
||||
'javascript',
|
||||
),
|
||||
'```*code*```': codeWrapper('<span class="hljs-emphasis">*code*</span>', 'markdown'),
|
||||
'```**code**```': codeWrapper('<span class="hljs-strong">**code**</span>', 'markdown'),
|
||||
'```__code__```': codeWrapper('<span class="hljs-strong">__code__</span>', 'markdown'),
|
||||
};
|
||||
|
||||
const nested = {
|
||||
'> some quote\n`window.location.reload();`': `${quoteWrapper(' some quote')}${inlinecodeWrapper('window.location.reload();')}`,
|
||||
};
|
||||
|
||||
/*
|
||||
* Markdown Filters
|
||||
*/
|
||||
const boldFiltered = {
|
||||
'*Hello*': 'Hello',
|
||||
'**Hello**': 'Hello',
|
||||
'*Hello**': 'Hello',
|
||||
'He*llo': 'He*llo',
|
||||
'*Hello': '*Hello',
|
||||
'Hello*': 'Hello*',
|
||||
'***Hello***': '***Hello***',
|
||||
'***Hello**': '***Hello**',
|
||||
'*Hello* there': 'Hello there',
|
||||
'**Hello** there': 'Hello there',
|
||||
'Hi, *Hello*': 'Hi, Hello',
|
||||
'Hi, **Hello**': 'Hi, Hello',
|
||||
'Hi, *Hello* how are you?': 'Hi, Hello how are you?',
|
||||
'Hi, **Hello** how are you?': 'Hi, Hello how are you?',
|
||||
};
|
||||
|
||||
const italicFiltered = {
|
||||
'_Hello_': 'Hello',
|
||||
'__Hello__': 'Hello',
|
||||
'_Hello__': 'Hello',
|
||||
'He_llo': 'He_llo',
|
||||
'_Hello': '_Hello',
|
||||
'__Hello': '__Hello',
|
||||
'Hello_': 'Hello_',
|
||||
'___Hello___': '___Hello___',
|
||||
'___Hello__': '___Hello__',
|
||||
'_Hello_ there': 'Hello there',
|
||||
'__Hello__ there': 'Hello there',
|
||||
'Hi, _Hello_': 'Hi, Hello',
|
||||
'Hi, __Hello__': 'Hi, Hello',
|
||||
'Hi, _Hello_ how are you?': 'Hi, Hello how are you?',
|
||||
'Hi, __Hello__ how are you?': 'Hi, Hello how are you?',
|
||||
};
|
||||
|
||||
const strikeFiltered = {
|
||||
'~Hello~': 'Hello',
|
||||
'~~Hello~~': 'Hello',
|
||||
'~~Hello': '~~Hello',
|
||||
'~Hello~~': 'Hello',
|
||||
'He~llo': 'He~llo',
|
||||
'~Hello': '~Hello',
|
||||
'Hello~': 'Hello~',
|
||||
'~~~Hello~~~': '~~~Hello~~~',
|
||||
'~~~Hello~~': '~~~Hello~~',
|
||||
'~Hello~ there': 'Hello there',
|
||||
'~~Hello~~ there': 'Hello there',
|
||||
'Hi, ~Hello~': 'Hi, Hello',
|
||||
'Hi, ~~Hello~~': 'Hi, Hello',
|
||||
'Hi, ~Hello~ how are you?': 'Hi, Hello how are you?',
|
||||
'Hi, ~~Hello~~ how are you?': 'Hi, Hello how are you?',
|
||||
};
|
||||
|
||||
const headingFiltered = {
|
||||
'# Hello': 'Hello',
|
||||
'## Hello': 'Hello',
|
||||
'### Hello': 'Hello',
|
||||
'#### Hello': 'Hello',
|
||||
'#Hello': '#Hello',
|
||||
'##Hello': '##Hello',
|
||||
'###Hello': '###Hello',
|
||||
'####Hello': '####Hello',
|
||||
'He#llo': 'He#llo',
|
||||
'# Hello there': 'Hello there',
|
||||
'Hi, # Hello': 'Hi, # Hello',
|
||||
'Hi, # Hello there': 'Hi, # Hello there',
|
||||
};
|
||||
|
||||
const quoteFiltered = {
|
||||
'>Hello': 'Hello',
|
||||
'> Hello': ' Hello',
|
||||
'>>>\nHello\n<<<': 'Hello',
|
||||
'>>>\nHello there!\n<<<': 'Hello there!',
|
||||
'>>>\n Hello there! \n<<<': ' Hello there! ',
|
||||
};
|
||||
|
||||
const linkFiltered = {
|
||||
'[Text](http://link)': 'Text',
|
||||
'[Open Site For Rocket.Chat](https://open.rocket.chat/)': 'Open Site For Rocket.Chat',
|
||||
'[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': ' Open Site For Rocket.Chat',
|
||||
'[Rocket.Chat Site](https://rocket.chat/)': 'Rocket.Chat Site',
|
||||
'<http://link|Text>': 'Text',
|
||||
'<http://link|Text for test>': 'Text for test',
|
||||
};
|
||||
|
||||
const inlinecodeFiltered = {
|
||||
'`code`': 'code',
|
||||
'`code` begin': 'code begin',
|
||||
'End `code`': 'End code',
|
||||
'Middle `code` middle': 'Middle code middle',
|
||||
'`code`begin': 'codebegin',
|
||||
'End`code`': 'Endcode',
|
||||
'Middle`code`middle': 'Middlecodemiddle',
|
||||
};
|
||||
|
||||
const blockcodeFiltered = {
|
||||
'```code```': 'code',
|
||||
'```code': 'code',
|
||||
'code```': 'code',
|
||||
'Here ```code``` lies': 'Here code lies',
|
||||
'Here```code```lies': 'Herecodelies',
|
||||
};
|
||||
|
||||
const defaultObjectTest = (result, object, objectKey) => expect(result.html).to.be.equal(object[objectKey]);
|
||||
|
||||
const testObject = (object, parser = original, test = defaultObjectTest) => {
|
||||
Object.keys(object).forEach((objectKey) => {
|
||||
describe(objectKey, () => {
|
||||
const message = parser === original ? { html: escapeHTML(objectKey) } : objectKey;
|
||||
const result = parser === original ? Markdown.mountTokensBack(parser(message)) : { html: parser(message) };
|
||||
it(`should be equal to ${object[objectKey]}`, () => {
|
||||
test(result, object, objectKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Original', () => {
|
||||
describe('Bold', () => testObject(bold));
|
||||
|
||||
describe('Italic', () => testObject(italic));
|
||||
|
||||
describe('Strike', () => testObject(strike));
|
||||
|
||||
describe('Headers', () => {
|
||||
describe('Level 1', () => testObject(headersLevel1));
|
||||
|
||||
describe('Level 2', () => testObject(headersLevel2));
|
||||
|
||||
describe('Level 3', () => testObject(headersLevel3));
|
||||
|
||||
describe('Level 4', () => testObject(headersLevel4));
|
||||
});
|
||||
|
||||
describe('Quote', () => testObject(quote));
|
||||
|
||||
describe('Link', () => testObject(link));
|
||||
|
||||
describe('Inline Code', () => testObject(inlinecode));
|
||||
|
||||
describe('Code', () => testObject(code));
|
||||
|
||||
describe('Nested', () => testObject(nested));
|
||||
});
|
||||
|
||||
describe('Filtered', () => {
|
||||
describe('BoldFilter', () => testObject(boldFiltered, filtered));
|
||||
|
||||
describe('Italic', () => testObject(italicFiltered, filtered));
|
||||
|
||||
describe('StrikeFilter', () => testObject(strikeFiltered, filtered));
|
||||
|
||||
describe('HeadingFilter', () => testObject(headingFiltered, filtered));
|
||||
|
||||
describe('QuoteFilter', () => testObject(quoteFiltered, filtered));
|
||||
|
||||
describe('LinkFilter', () => testObject(linkFiltered, filtered));
|
||||
|
||||
describe('inlinecodeFilter', () => testObject(inlinecodeFiltered, filtered));
|
||||
|
||||
describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered));
|
||||
});
|
||||
|
||||
// describe('Marked', function() {
|
||||
// describe('Bold', () => testObject(bold, marked));
|
||||
|
||||
// describe('Italic', () => testObject(italic, marked));
|
||||
|
||||
// describe('Strike', () => testObject(strike, marked));
|
||||
|
||||
// describe('Headers', () => {
|
||||
// describe('Level 1', () => testObject(headersLevel1, marked));
|
||||
|
||||
// describe('Level 2', () => testObject(headersLevel2, marked));
|
||||
|
||||
// describe('Level 3', () => testObject(headersLevel3, marked));
|
||||
|
||||
// describe('Level 4', () => testObject(headersLevel4, marked));
|
||||
// });
|
||||
|
||||
// describe('Quote', () => testObject(quote, marked));
|
||||
// });
|
||||
212
apps/meteor/tests/unit/app/markdown/hljs.tests.ts
Normal file
212
apps/meteor/tests/unit/app/markdown/hljs.tests.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
import hljs, { register } from '../../../../app/markdown/lib/hljs';
|
||||
|
||||
const lazyLanguages: string[] = [
|
||||
'onec',
|
||||
'abnf',
|
||||
'accesslog',
|
||||
'actionscript',
|
||||
'ada',
|
||||
'apache',
|
||||
'applescript',
|
||||
'arduino',
|
||||
'armasm',
|
||||
'asciidoc',
|
||||
'aspectj',
|
||||
'autohotkey',
|
||||
'autoit',
|
||||
'avrasm',
|
||||
'awk',
|
||||
'axapta',
|
||||
'bash',
|
||||
'basic',
|
||||
'bnf',
|
||||
'brainfuck',
|
||||
'cal',
|
||||
'capnproto',
|
||||
'ceylon',
|
||||
'clean',
|
||||
'clojure',
|
||||
'clojure-repl',
|
||||
'cmake',
|
||||
'coffeescript',
|
||||
'coq',
|
||||
'cos',
|
||||
'cpp',
|
||||
'crmsh',
|
||||
'crystal',
|
||||
'csp',
|
||||
'css',
|
||||
'd',
|
||||
'dart',
|
||||
'delphi',
|
||||
'diff',
|
||||
'django',
|
||||
'dns',
|
||||
'dockerfile',
|
||||
'dos',
|
||||
'dsconfig',
|
||||
'dts',
|
||||
'dust',
|
||||
'ebnf',
|
||||
'elixir',
|
||||
'elm',
|
||||
'erb',
|
||||
'erlang',
|
||||
'excel',
|
||||
'fix',
|
||||
'flix',
|
||||
'fortran',
|
||||
'fsharp',
|
||||
'gams',
|
||||
'gauss',
|
||||
'gcode',
|
||||
'gherkin',
|
||||
'glsl',
|
||||
'go',
|
||||
'golo',
|
||||
'gradle',
|
||||
'groovy',
|
||||
'haml',
|
||||
'handlebars',
|
||||
'haskell',
|
||||
'haxe',
|
||||
'hsp',
|
||||
'http',
|
||||
'hy',
|
||||
'inform7',
|
||||
'ini',
|
||||
'irpf90',
|
||||
'java',
|
||||
'javascript',
|
||||
'jboss-cli',
|
||||
'json',
|
||||
'julia',
|
||||
'julia-repl',
|
||||
'kotlin',
|
||||
'lasso',
|
||||
'ldif',
|
||||
'leaf',
|
||||
'less',
|
||||
'lisp',
|
||||
'livecodeserver',
|
||||
'livescript',
|
||||
'llvm',
|
||||
'lsl',
|
||||
'lua',
|
||||
'makefile',
|
||||
'markdown',
|
||||
'mathematica',
|
||||
'matlab',
|
||||
'maxima',
|
||||
'mel',
|
||||
'mercury',
|
||||
'mipsasm',
|
||||
'mizar',
|
||||
'perl',
|
||||
'mojolicious',
|
||||
'monkey',
|
||||
'moonscript',
|
||||
'n1ql',
|
||||
'nginx',
|
||||
'nix',
|
||||
'nsis',
|
||||
'objectivec',
|
||||
'ocaml',
|
||||
'openscad',
|
||||
'oxygene',
|
||||
'parser3',
|
||||
'pf',
|
||||
'php',
|
||||
'pony',
|
||||
'powershell',
|
||||
'processing',
|
||||
'profile',
|
||||
'prolog',
|
||||
'protobuf',
|
||||
'puppet',
|
||||
'purebasic',
|
||||
'python',
|
||||
'q',
|
||||
'qml',
|
||||
'r',
|
||||
'rib',
|
||||
'roboconf',
|
||||
'rsl',
|
||||
'ruleslanguage',
|
||||
'rust',
|
||||
'scala',
|
||||
'scheme',
|
||||
'scilab',
|
||||
'scss',
|
||||
'shell',
|
||||
'smali',
|
||||
'smalltalk',
|
||||
'sml',
|
||||
'sqf',
|
||||
'sql',
|
||||
'stan',
|
||||
'stata',
|
||||
'step21',
|
||||
'stylus',
|
||||
'subunit',
|
||||
'swift',
|
||||
'taggerscript',
|
||||
'yaml',
|
||||
'tap',
|
||||
'tcl',
|
||||
'thrift',
|
||||
'tp',
|
||||
'twig',
|
||||
'typescript',
|
||||
'vala',
|
||||
'vbnet',
|
||||
'vbscript',
|
||||
'verilog',
|
||||
'vhdl',
|
||||
'vim',
|
||||
'x86asm',
|
||||
'xl',
|
||||
'xquery',
|
||||
'zephir',
|
||||
];
|
||||
|
||||
const eagerLanguages = ['markdown', 'clean', 'javascript'];
|
||||
|
||||
describe('hljs', () => {
|
||||
describe('eagerly registered languages', () => {
|
||||
eagerLanguages.forEach((lang) => {
|
||||
it(`should register '${lang}' when the module is loaded`, () => {
|
||||
expect(hljs.getLanguage(lang)).to.be.an('object');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
afterEach(() => {
|
||||
hljs
|
||||
.listLanguages()
|
||||
.filter((lang) => !eagerLanguages.includes(lang))
|
||||
.forEach((lang) => hljs.unregisterLanguage(lang));
|
||||
});
|
||||
|
||||
lazyLanguages.forEach((lang) => {
|
||||
it(`should register a syntax definition for '${lang}'`, async () => {
|
||||
hljs.unregisterLanguage(lang);
|
||||
|
||||
await register(lang);
|
||||
|
||||
expect(hljs.listLanguages()).to.include(lang);
|
||||
});
|
||||
});
|
||||
|
||||
['', 'not-a-language'].forEach((lang) => {
|
||||
it(`should fall back to plaintext for '${lang}'`, async () => {
|
||||
await register(lang);
|
||||
|
||||
expect(hljs.listLanguages()).to.include('plaintext');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
43
apps/meteor/tests/unit/app/markdown/markdown.mocks.ts
Normal file
43
apps/meteor/tests/unit/app/markdown/markdown.mocks.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
export type MarkdownToken = { token: string; type?: string; text: string; noHtml?: string };
|
||||
|
||||
export type ParsedMessage = { html?: string; msg?: string; tokens?: MarkdownToken[] };
|
||||
|
||||
export type MarkdownOptions = { supportSchemesForLink?: string; headers?: boolean; rootUrl?: string };
|
||||
|
||||
const mocks = {
|
||||
'meteor/meteor': {
|
||||
Meteor: {
|
||||
absoluteUrl() {
|
||||
return 'http://localhost:3000/';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const { Markdown, filterMarkdown, createMarkdownMessageRenderer, createMarkdownNotificationRenderer } = proxyquire
|
||||
.noCallThru()
|
||||
.load('../../../../app/markdown/lib/markdown', mocks) as typeof import('../../../../app/markdown/lib/markdown');
|
||||
|
||||
export const { original } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/original/original', mocks) as {
|
||||
original: (message: ParsedMessage, options?: MarkdownOptions) => ParsedMessage;
|
||||
};
|
||||
|
||||
export const { code } = proxyquire
|
||||
.noCallThru()
|
||||
.load('../../../../app/markdown/lib/parser/original/code', mocks) as typeof import('../../../../app/markdown/lib/parser/original/code');
|
||||
|
||||
const copyonly = (text: string, marker: string) => `<span class="copyonly">${marker}</span>${text}<span class="copyonly">${marker}</span>`;
|
||||
|
||||
export const markup = {
|
||||
bold: (text: string) => copyonly(`<strong>${text}</strong>`, '*'),
|
||||
inlineCode: (text: string) => copyonly(`<span><code class="code-colors inline">${text}</code></span>`, '`'),
|
||||
anchor: (url: string, title: string, target = '_blank') =>
|
||||
`<a data-title="${url}" href="${url}" target="${target}" rel="noopener noreferrer">${title}</a>`,
|
||||
image: (url: string, title: string, target = '_blank') =>
|
||||
`<a data-title="${url}" href="${url}" title="${title}" target="${target}" rel="noopener noreferrer">` +
|
||||
`<div class="inline-image" style="background-image: url(${url});"></div></a>`,
|
||||
blockquote: (text: string, marker = '>') =>
|
||||
`<blockquote class="background-transparent-darker-before"><span class="copyonly">${marker}</span>${text}</blockquote>`,
|
||||
};
|
||||
235
apps/meteor/tests/unit/app/markdown/markdown.tests.ts
Normal file
235
apps/meteor/tests/unit/app/markdown/markdown.tests.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { escapeHTML } from '@rocket.chat/tools';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import type { MarkdownOptions, ParsedMessage } from './markdown.mocks';
|
||||
import {
|
||||
Markdown,
|
||||
original,
|
||||
code,
|
||||
filterMarkdown,
|
||||
createMarkdownMessageRenderer,
|
||||
createMarkdownNotificationRenderer,
|
||||
markup,
|
||||
} from './markdown.mocks';
|
||||
import hljs from '../../../../app/markdown/lib/hljs';
|
||||
|
||||
const { bold, inlineCode, anchor, image, blockquote } = markup;
|
||||
|
||||
const rootUrl = 'http://localhost:3000/';
|
||||
const defaultOptions = { supportSchemesForLink: 'http,https', headers: true, rootUrl };
|
||||
|
||||
const render = (text: string, options: MarkdownOptions = defaultOptions) =>
|
||||
Markdown.mountTokensBack(original({ html: text }, options)).html;
|
||||
const renderEscaped = (text: string, options: MarkdownOptions = defaultOptions) => render(escapeHTML(text), options);
|
||||
|
||||
describe('Markdown entry points', () => {
|
||||
it('should escape HTML before parsing', () => {
|
||||
expect(Markdown.parse('<img src=x onerror=alert(1)> *Hello*')).to.equal(`<img src=x onerror=alert(1)> ${bold('Hello')}`);
|
||||
});
|
||||
|
||||
it('should keep HTML when parsing unescaped input', () => {
|
||||
expect(Markdown.parseNotEscaped('<b>x</b> *Hello*')).to.equal(`<b>x</b> ${bold('Hello')}`);
|
||||
});
|
||||
|
||||
it('should leave the tokens unmounted when parsing a message', () => {
|
||||
const message = Markdown.parseMessageNotEscaped({ html: '`code`' });
|
||||
|
||||
expect(message.tokens).to.have.lengthOf(1);
|
||||
expect(message.tokens?.[0].type).to.equal('inlinecode');
|
||||
expect(message.html).to.equal(message.tokens?.[0].token);
|
||||
});
|
||||
|
||||
it('should expose the code parser', () => {
|
||||
const message = Markdown.code({ html: '`code`' });
|
||||
|
||||
expect(Markdown.mountTokensBack(message).html).to.equal(inlineCode('code'));
|
||||
});
|
||||
|
||||
it('should strip the markdown syntax when filtering', () => {
|
||||
expect(Markdown.filterMarkdownFromMessage('*Hello* `there`')).to.equal('Hello there');
|
||||
expect(filterMarkdown('*Hello* `there`')).to.equal('Hello there');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown token restoration', () => {
|
||||
it('should return the message untouched when it has no tokens', () => {
|
||||
const message = { html: 'plain text' };
|
||||
|
||||
expect(Markdown.mountTokensBack(message)).to.equal(message);
|
||||
expect(message.html).to.equal('plain text');
|
||||
});
|
||||
|
||||
it('should restore the plain text representation when useHtml is false', () => {
|
||||
const message = Markdown.parseMessageNotEscaped({ html: 'see `code`' });
|
||||
|
||||
expect(Markdown.mountTokensBack(message, false).html).to.equal('see `code`');
|
||||
});
|
||||
|
||||
it('should restore tokens that are only revealed by another token', () => {
|
||||
const message = {
|
||||
html: 'start =!=outer=!=',
|
||||
tokens: [
|
||||
{ token: '=!=inner=!=', text: '<em>inner</em>' },
|
||||
{ token: '=!=outer=!=', text: '<b>=!=inner=!=</b>' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(Markdown.mountTokensBack(message).html).to.equal('start <b><em>inner</em></b>');
|
||||
});
|
||||
|
||||
it('should leave tokens that never appear in the message', () => {
|
||||
const message = {
|
||||
html: 'start =!=present=!=',
|
||||
tokens: [
|
||||
{ token: '=!=absent=!=', text: '<em>absent</em>' },
|
||||
{ token: '=!=present=!=', text: '<b>present</b>' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(Markdown.mountTokensBack(message).html).to.equal('start <b>present</b>');
|
||||
});
|
||||
|
||||
it('should not treat $ sequences in the token text as replacement patterns', () => {
|
||||
const message = { html: '=!=t=!=', tokens: [{ token: '=!=t=!=', text: '$& $1 $$' }] };
|
||||
|
||||
expect(Markdown.mountTokensBack(message).html).to.equal('$& $1 $$');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown renderers', () => {
|
||||
const renderMessage = createMarkdownMessageRenderer({
|
||||
rootUrl: 'http://localhost:3000/',
|
||||
supportSchemesForLink: 'http,https',
|
||||
headers: true,
|
||||
});
|
||||
|
||||
it('should render markdown of a non-empty message', () => {
|
||||
expect(renderMessage({ html: '# Hello' })?.html).to.equal('<h1>Hello</h1>');
|
||||
});
|
||||
|
||||
const emptyMessages: (ParsedMessage | undefined)[] = [undefined, { html: '' }, { html: ' \n\t ' }];
|
||||
|
||||
emptyMessages.forEach((message, index) => {
|
||||
it(`should return the message as-is when there is nothing to render (case ${index})`, () => {
|
||||
expect(renderMessage(message)).to.equal(message);
|
||||
});
|
||||
});
|
||||
|
||||
it('should strip the markdown syntax when rendering a notification', () => {
|
||||
expect(createMarkdownNotificationRenderer()('*Hello* there')).to.equal('Hello there');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Original parser', () => {
|
||||
it('should convert every new line to a <br>', () => {
|
||||
expect(render('a\nb\nc')).to.equal('a<br>b<br>c');
|
||||
});
|
||||
|
||||
it('should not render headings when the option is disabled', () => {
|
||||
expect(render('# Hello', { ...defaultOptions, headers: false })).to.equal('# Hello');
|
||||
});
|
||||
|
||||
describe('blockquotes', () => {
|
||||
it('should render consecutive single-line blockquotes without a line break in between', () => {
|
||||
expect(renderEscaped('>a\n>b')).to.equal(`${blockquote('a')}${blockquote('b')}`);
|
||||
});
|
||||
|
||||
it('should render a multiline blockquote delimited by >>> and <<<', () => {
|
||||
expect(renderEscaped('>>>\nline1\nline2\n<<<')).to.equal(
|
||||
blockquote('line1<br>line2<span class="copyonly"><<<</span>', '>>>'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('links', () => {
|
||||
it('should render an internal link without a target', () => {
|
||||
expect(render(`[Home](${rootUrl}channel/general)`)).to.equal(anchor(`${rootUrl}channel/general`, 'Home', ''));
|
||||
expect(render(`<${rootUrl}channel/general|Home>`)).to.equal(anchor(`${rootUrl}channel/general`, 'Home', ''));
|
||||
});
|
||||
|
||||
it('should not render a link whose URL is not parseable', () => {
|
||||
expect(render('[Text](http://[)')).to.equal('[Text](http://[)');
|
||||
expect(render('<http://[|Text>')).to.equal('<http://[|Text>');
|
||||
});
|
||||
|
||||
it('should not render a link whose URL contains another token', () => {
|
||||
expect(render('[Text](http://example.com/`code`)')).to.equal(`[Text](http://example.com/${inlineCode('code')})`);
|
||||
});
|
||||
|
||||
it('should not render a link whose title holds a disallowed token', () => {
|
||||
expect(render('[`code`](http://example.com/)')).to.equal(`[${inlineCode('code')}](http://example.com/)`);
|
||||
expect(render('<http://example.com/|`code`>')).to.equal(`<http://example.com/|${inlineCode('code')}>`);
|
||||
});
|
||||
|
||||
it('should render a link whose title only looks like a token', () => {
|
||||
expect(render('[=!=abcdefghijklmnopq=!=](http://example.com/)')).to.equal(anchor('http://example.com/', '=!=abcdefghijklmnopq=!='));
|
||||
});
|
||||
|
||||
it('should only render the schemes it was configured with', () => {
|
||||
expect(render('[Text](ftp://example.com/)', { ...defaultOptions, supportSchemesForLink: 'ftp' })).to.equal(
|
||||
anchor('ftp://example.com/', 'Text'),
|
||||
);
|
||||
expect(render('[Text](http://example.com/)', { ...defaultOptions, supportSchemesForLink: 'ftp' })).to.equal(
|
||||
'[Text](http://example.com/)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('images', () => {
|
||||
it('should render an external image with target _blank', () => {
|
||||
expect(render('')).to.equal(image('http://example.com/a.png', 'alt'));
|
||||
});
|
||||
|
||||
it('should render an internal image without a target', () => {
|
||||
expect(render(``)).to.equal(image(`${rootUrl}a.png`, 'alt', ''));
|
||||
});
|
||||
|
||||
it('should not render an image whose URL is not parseable', () => {
|
||||
expect(render('')).to.equal('');
|
||||
});
|
||||
|
||||
it('should not render an image whose title holds a disallowed token', () => {
|
||||
expect(render('')).to.equal(``);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Code parser', () => {
|
||||
it('should leave a message without code untouched', () => {
|
||||
expect(code({ html: 'no code here' }).tokens).to.be.undefined;
|
||||
expect(code({}).html).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should close an unclosed code block in both html and msg', () => {
|
||||
const message: ParsedMessage = { html: '```code', msg: '```code' };
|
||||
|
||||
code(message);
|
||||
|
||||
expect(message.msg).to.equal('```code\n```');
|
||||
expect(message.html).to.equal(message.tokens?.[0].token);
|
||||
});
|
||||
|
||||
it('should restore the original fenced block when tokens are mounted back as text', () => {
|
||||
const message = original({ html: '```\ncode\n```' }, defaultOptions);
|
||||
|
||||
expect(Markdown.mountTokensBack(message, false).html).to.equal('```\ncode\n```');
|
||||
});
|
||||
|
||||
it('should fall back to automatic highlighting when highlighting the explicit language fails', () => {
|
||||
const consoleError = sinon.stub(console, 'error');
|
||||
const highlight = sinon.stub(hljs, 'highlight').throws(new Error('highlighting failed'));
|
||||
const highlightAuto = sinon.spy(hljs, 'highlightAuto');
|
||||
|
||||
try {
|
||||
const message = code({ html: '```javascript\nvar a;\n```' });
|
||||
|
||||
expect(highlight.args).to.deep.equal([['javascript', 'var a;\n']]);
|
||||
expect(highlightAuto.calledOnce).to.be.true;
|
||||
expect(message.tokens?.[0].text).to.contain(`code-colors hljs ${highlightAuto.firstCall.returnValue.language}'`);
|
||||
expect(consoleError.calledOnce).to.be.true;
|
||||
} finally {
|
||||
sinon.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
314
apps/meteor/tests/unit/app/mentions/client.tests.js
Normal file
314
apps/meteor/tests/unit/app/mentions/client.tests.js
Normal file
@@ -0,0 +1,314 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { MentionsParser } from '../../../../app/mentions/lib/MentionsParser';
|
||||
|
||||
let mentionsParser;
|
||||
beforeEach(() => {
|
||||
mentionsParser = new MentionsParser({
|
||||
pattern: () => '[0-9a-zA-Z-_.]+',
|
||||
me: () => 'me',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mention', () => {
|
||||
describe('getUserMentions', () => {
|
||||
describe('for simple text, no mentions', () => {
|
||||
const result = [];
|
||||
['#rocket.cat', 'hello rocket.cat how are you?'].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for one user', () => {
|
||||
const result = ['@rocket.cat'];
|
||||
[
|
||||
'@rocket.cat',
|
||||
' @rocket.cat ',
|
||||
'hello @rocket.cat',
|
||||
// 'hello,@rocket.cat', // this test case is ignored since is not compatible with the message box behavior
|
||||
'@rocket.cat, hello',
|
||||
'@rocket.cat,hello',
|
||||
'hello @rocket.cat how are you?',
|
||||
].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text));
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should return without the "." from "@rocket.cat."', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat.'));
|
||||
});
|
||||
|
||||
it.skip('should return without the "_" from "@rocket.cat_"', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat_'));
|
||||
});
|
||||
|
||||
it.skip('should return without the "-" from "@rocket.cat-"', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat-'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for two users', () => {
|
||||
const result = ['@rocket.cat', '@all'];
|
||||
[
|
||||
'@rocket.cat @all',
|
||||
' @rocket.cat @all ',
|
||||
'hello @rocket.cat and @all',
|
||||
'@rocket.cat, hello @all',
|
||||
'hello @rocket.cat and @all how are you?',
|
||||
].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannelMentions', () => {
|
||||
describe('for simple text, no mentions', () => {
|
||||
const result = [];
|
||||
['@rocket.cat', 'hello rocket.cat how are you?'].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for one channel', () => {
|
||||
const result = ['#general'];
|
||||
['#general', ' #general ', 'hello #general', '#general, hello', 'hello #general, how are you?'].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text));
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should return without the "." from "#general."', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general.'));
|
||||
});
|
||||
|
||||
it.skip('should return without the "_" from "#general_"', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general_'));
|
||||
});
|
||||
|
||||
it.skip('should return without the "-" from "#general."', () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general-'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for two channels', () => {
|
||||
const result = ['#general', '#other'];
|
||||
[
|
||||
'#general #other',
|
||||
' #general #other',
|
||||
'hello #general and #other',
|
||||
'#general, hello #other',
|
||||
'hello #general #other, how are you?',
|
||||
].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for url with fragments', () => {
|
||||
const result = [];
|
||||
['http://localhost/#general'].forEach((text) => {
|
||||
it(`should return nothing from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for messages with url and channels', () => {
|
||||
const result = ['#general'];
|
||||
['http://localhost/#general #general'].forEach((text) => {
|
||||
it(`should return "${JSON.stringify(result)}" from "${text}"`, () => {
|
||||
expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const message = {
|
||||
mentions: [
|
||||
{ username: 'rocket.cat', name: 'Rocket.Cat' },
|
||||
{ username: 'admin', name: 'Admin' },
|
||||
{ username: 'me', name: 'Me' },
|
||||
{ username: 'specialchars', name: '<img onerror=alert(hello)>' },
|
||||
],
|
||||
channels: [
|
||||
{ name: 'general', _id: '42' },
|
||||
{ name: 'rocket.cat', _id: '169' },
|
||||
],
|
||||
};
|
||||
|
||||
describe('replace methods', () => {
|
||||
describe('replaceUsers', () => {
|
||||
it('should render for @all', () => {
|
||||
const result = mentionsParser.replaceUsers('@all', message, 'me');
|
||||
expect(result).to.be.equal('<a class="mention-link mention-link--all mention-link--group" data-group="all">all</a>');
|
||||
});
|
||||
|
||||
const str2 = 'rocket.cat';
|
||||
|
||||
it(`should render for "@${str2}"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`@${str2}`, message, 'me');
|
||||
expect(result).to.be.equal(`<a class="mention-link mention-link--user" data-username="${str2}" title="${str2}">${str2}</a>`);
|
||||
});
|
||||
|
||||
it(`should render for "hello ${str2}"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`hello @${str2}`, message, 'me');
|
||||
expect(result).to.be.equal(`hello <a class="mention-link mention-link--user" data-username="${str2}" title="${str2}">${str2}</a>`);
|
||||
});
|
||||
|
||||
it('should render for unknow/private user "hello @unknow"', () => {
|
||||
const result = mentionsParser.replaceUsers('hello @unknow', message, 'me');
|
||||
expect(result).to.be.equal('hello @unknow');
|
||||
});
|
||||
|
||||
it('should render for me', () => {
|
||||
const result = mentionsParser.replaceUsers('hello @me', message, 'me');
|
||||
expect(result).to.be.equal('hello <a class="mention-link mention-link--me mention-link--user" data-username="me" title="me">me</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUsers (RealNames)', () => {
|
||||
beforeEach(() => {
|
||||
mentionsParser.useRealName = () => true;
|
||||
});
|
||||
|
||||
it('should render for @all', () => {
|
||||
const result = mentionsParser.replaceUsers('@all', message, 'me');
|
||||
expect(result).to.be.equal('<a class="mention-link mention-link--all mention-link--group" data-group="all">all</a>');
|
||||
});
|
||||
|
||||
const str2 = 'rocket.cat';
|
||||
const str2Name = 'Rocket.Cat';
|
||||
|
||||
it(`should render for "@${str2}"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`@${str2}`, message, 'me');
|
||||
expect(result).to.be.equal(`<a class="mention-link mention-link--user" data-username="${str2}" title="${str2}">${str2Name}</a>`);
|
||||
});
|
||||
|
||||
it(`should render for "hello @${str2}"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`hello @${str2}`, message, 'me');
|
||||
expect(result).to.be.equal(
|
||||
`hello <a class="mention-link mention-link--user" data-username="${str2}" title="${str2}">${str2Name}</a>`,
|
||||
);
|
||||
});
|
||||
|
||||
const specialchars = 'specialchars';
|
||||
const specialcharsName = '<img onerror=alert(hello)>';
|
||||
|
||||
it(`should escape special characters in "hello @${specialchars}"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`hello @${specialchars}`, message, 'me');
|
||||
expect(result).to.be.equal(
|
||||
`hello <a class="mention-link mention-link--user" data-username="${specialchars}" title="${specialchars}">${specialcharsName}</a>`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should render for "hello<br>@${str2} <br>"`, () => {
|
||||
const result = mentionsParser.replaceUsers(`hello<br>@${str2} <br>`, message, 'me');
|
||||
expect(result).to.be.equal(
|
||||
`hello<br><a class="mention-link mention-link--user" data-username="${str2}" title="${str2}">${str2Name}</a> <br>`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render for unknow/private user "hello @unknow"', () => {
|
||||
const result = mentionsParser.replaceUsers('hello @unknow', message, 'me');
|
||||
expect(result).to.be.equal('hello @unknow');
|
||||
});
|
||||
|
||||
it('should render for me', () => {
|
||||
const result = mentionsParser.replaceUsers('hello @me', message, 'me');
|
||||
expect(result).to.be.equal('hello <a class="mention-link mention-link--me mention-link--user" data-username="me" title="me">Me</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceChannels', () => {
|
||||
it('should render for #general', () => {
|
||||
const result = mentionsParser.replaceChannels('#general', message);
|
||||
expect('<).to.be.equal(class="mention-link mention-link--room" data-channel="42">#general</a>', result);
|
||||
});
|
||||
|
||||
const str2 = '#rocket.cat';
|
||||
|
||||
it(`should render for ${str2}`, () => {
|
||||
const result = mentionsParser.replaceChannels(str2, message);
|
||||
expect(result).to.be.equal(`<a class="mention-link mention-link--room" data-channel="169">${str2}</a>`);
|
||||
});
|
||||
|
||||
it(`should render for "hello ${str2}"`, () => {
|
||||
const result = mentionsParser.replaceChannels(`hello ${str2}`, message);
|
||||
expect(result).to.be.equal(`hello <a class="mention-link mention-link--room" data-channel="169">${str2}</a>`);
|
||||
});
|
||||
|
||||
it('should render for unknow/private channel "hello #unknow"', () => {
|
||||
const result = mentionsParser.replaceChannels('hello #unknow', message);
|
||||
expect(result).to.be.equal('hello #unknow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse all', () => {
|
||||
it('should render for #general', () => {
|
||||
message.html = '#general';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('<a class="mention-link mention-link--room" data-channel="42">#general</a>');
|
||||
});
|
||||
|
||||
it('should render for "#general and @rocket.cat', () => {
|
||||
message.html = '#general and @rocket.cat';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal(
|
||||
'<a class="mention-link mention-link--room" data-channel="42">#general</a> and <a class="mention-link mention-link--user" data-username="rocket.cat" title="rocket.cat">rocket.cat</a>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render for "', () => {
|
||||
message.html = '';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('');
|
||||
});
|
||||
|
||||
it('should render for "simple text', () => {
|
||||
message.html = 'simple text';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('simple text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse all (RealNames)', () => {
|
||||
beforeEach(() => {
|
||||
mentionsParser.useRealName = () => true;
|
||||
});
|
||||
|
||||
it('should render for #general', () => {
|
||||
message.html = '#general';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('<a class="mention-link mention-link--room" data-channel="42">#general</a>');
|
||||
});
|
||||
|
||||
it('should render for "#general and @rocket.cat', () => {
|
||||
message.html = '#general and @rocket.cat';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal(
|
||||
'<a class="mention-link mention-link--room" data-channel="42">#general</a> and <a class="mention-link mention-link--user" data-username="rocket.cat" title="rocket.cat">Rocket.Cat</a>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render for "', () => {
|
||||
message.html = '';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('');
|
||||
});
|
||||
|
||||
it('should render for "simple text', () => {
|
||||
message.html = 'simple text';
|
||||
const result = mentionsParser.parse(message, 'me');
|
||||
expect(result.html).to.be.equal('simple text');
|
||||
});
|
||||
});
|
||||
});
|
||||
162
apps/meteor/tests/unit/app/utils/lib/getURL.tests.js
Normal file
162
apps/meteor/tests/unit/app/utils/lib/getURL.tests.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import { expect } from 'chai';
|
||||
import proxyquire from 'proxyquire';
|
||||
|
||||
import { ltrim, rtrim } from '../../../../../lib/utils/stringUtils';
|
||||
|
||||
const { _getURL } = proxyquire.noCallThru().load('../../../../../app/utils/lib/getURL', {
|
||||
'meteor/meteor': {
|
||||
'Meteor': {
|
||||
absoluteUrl() {
|
||||
return 'http://localhost:3000/';
|
||||
},
|
||||
},
|
||||
'@global': true,
|
||||
},
|
||||
});
|
||||
|
||||
const testPaths = (o, _processPath) => {
|
||||
let processPath = _processPath;
|
||||
if (o._root_url_path_prefix !== '') {
|
||||
processPath = (path) => _processPath(o._root_url_path_prefix + path);
|
||||
}
|
||||
|
||||
const cloudDeepLinkUrl = 'https://go.rocket.chat';
|
||||
|
||||
expect(_getURL('', o, cloudDeepLinkUrl)).to.be.equal(processPath(''));
|
||||
expect(_getURL('/', o, cloudDeepLinkUrl)).to.be.equal(processPath(''));
|
||||
expect(_getURL('//', o, cloudDeepLinkUrl)).to.be.equal(processPath(''));
|
||||
expect(_getURL('///', o, cloudDeepLinkUrl)).to.be.equal(processPath(''));
|
||||
expect(_getURL('/channel', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel'));
|
||||
expect(_getURL('/channel/', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel'));
|
||||
expect(_getURL('/channel//', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel'));
|
||||
expect(_getURL('/channel/123', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123'));
|
||||
expect(_getURL('/channel/123/', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123'));
|
||||
expect(_getURL('/channel/123?id=456&name=test', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123?id=456&name=test'));
|
||||
expect(_getURL('/channel/123/?id=456&name=test', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123?id=456&name=test'));
|
||||
};
|
||||
|
||||
const getCloudUrl = (_site_url, path) => {
|
||||
path = ltrim(path, '/');
|
||||
const url = `https://go.rocket.chat/?host=${encodeURIComponent(_site_url.replace(/https?:\/\//, ''))}&path=${encodeURIComponent(path)}`;
|
||||
if (_site_url.includes('http://')) {
|
||||
return `${url}&secure=no`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const testCases = (options) => {
|
||||
const _site_url = rtrim(options._site_url, '/');
|
||||
|
||||
if (!options.cloud) {
|
||||
if (options._cdn_prefix === '') {
|
||||
if (options.full && !options.cdn) {
|
||||
it('should return with host if full: true', () => {
|
||||
testPaths(options, (path) => _site_url + path);
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.full && options.cdn) {
|
||||
it('should return without cdn prefix if cdn: true', () => {
|
||||
testPaths(options, (path) => path);
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.full && !options.cdn) {
|
||||
it('should return without host by default', () => {
|
||||
testPaths(options, (path) => path);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.full && options.cdn) {
|
||||
it('should return with host if full: true and cdn: true', () => {
|
||||
testPaths(options, (path) => _site_url + path);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (options.full && !options.cdn) {
|
||||
it('should return with host if full: true', () => {
|
||||
testPaths(options, (path) => _site_url + path);
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.full && options.cdn) {
|
||||
it('should return with cdn prefix if cdn: true', () => {
|
||||
testPaths(options, (path) => options._cdn_prefix + path);
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.full && !options.cdn) {
|
||||
it('should return without host by default', () => {
|
||||
testPaths(options, (path) => path);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.full && options.cdn) {
|
||||
it('should return with cdn prefix if full: true and cdn: true', () => {
|
||||
testPaths(options, (path) => options._cdn_prefix + path);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (options._cdn_prefix === '') {
|
||||
if (!options.full && options.cdn) {
|
||||
it('should return with cloud host if cdn: true', () => {
|
||||
testPaths(options, (path) => getCloudUrl(_site_url, path));
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.full && !options.cdn) {
|
||||
it('should return with cloud host if full: fase and cdn: false', () => {
|
||||
testPaths(options, (path) => getCloudUrl(_site_url, path));
|
||||
});
|
||||
}
|
||||
} else if (!options.full && !options.cdn) {
|
||||
it('should return with cloud host if full: fase and cdn: false', () => {
|
||||
testPaths(options, (path) => getCloudUrl(_site_url, path));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const testCasesForOptions = (description, options) => {
|
||||
describe(description, () => {
|
||||
testCases({ ...options, cdn: false, full: false, cloud: false });
|
||||
testCases({ ...options, cdn: true, full: false, cloud: false });
|
||||
testCases({ ...options, cdn: false, full: true, cloud: false });
|
||||
testCases({ ...options, cdn: false, full: false, cloud: true });
|
||||
testCases({ ...options, cdn: true, full: true, cloud: false });
|
||||
testCases({ ...options, cdn: false, full: true, cloud: true });
|
||||
testCases({ ...options, cdn: true, full: false, cloud: true });
|
||||
testCases({ ...options, cdn: true, full: true, cloud: true });
|
||||
});
|
||||
};
|
||||
|
||||
describe('getURL', () => {
|
||||
testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000/', {
|
||||
_cdn_prefix: '',
|
||||
_root_url_path_prefix: '',
|
||||
_site_url: 'http://localhost:3000/',
|
||||
});
|
||||
|
||||
testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000', {
|
||||
_cdn_prefix: '',
|
||||
_root_url_path_prefix: '',
|
||||
_site_url: 'http://localhost:3000',
|
||||
});
|
||||
|
||||
testCasesForOptions('getURL with CDN, no PREFIX for http://localhost:3000/', {
|
||||
_cdn_prefix: 'https://cdn.com',
|
||||
_root_url_path_prefix: '',
|
||||
_site_url: 'http://localhost:3000/',
|
||||
});
|
||||
|
||||
testCasesForOptions('getURL with CDN, PREFIX for http://localhost:3000/', {
|
||||
_cdn_prefix: 'https://cdn.com',
|
||||
_root_url_path_prefix: 'sub',
|
||||
_site_url: 'http://localhost:3000/',
|
||||
});
|
||||
|
||||
testCasesForOptions('getURL with CDN, PREFIX for https://localhost:3000/', {
|
||||
_cdn_prefix: 'https://cdn.com',
|
||||
_root_url_path_prefix: 'sub',
|
||||
_site_url: 'https://localhost:3000/',
|
||||
});
|
||||
});
|
||||
49
apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts
Normal file
49
apps/meteor/tests/unit/app/utils/lib/restrictions.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { fileUploadIsValidContentTypeFromSettings } from '../../../../../app/utils/lib/restrictions';
|
||||
|
||||
describe('fileUploadIsValidContentTypeFromSettings', () => {
|
||||
it('should return true if type is not defined and whiteList is not defined', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings(undefined, '', '')).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if type is not defined and whiteList is defined', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings(undefined, 'image/jpeg', '')).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if type is defined and whiteList is not defined', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', '')).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true if type is defined and whiteList is defined and type is in whiteList', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', 'image/jpeg', '')).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if type is defined and whiteList is defined and type is not in whiteList', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/png', 'image/jpeg', '')).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if type is defined and whiteList is not defined and type is in blackList', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', 'image/jpeg')).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if type is defined and whiteList is not defined and type is not in blackList', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/png', '', 'image/jpeg')).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true if type is defined and whiteList is defined and type is in whiteList with wildcard', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', 'image/*', '')).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if type is defined and whiteList is defined and type is not in whiteList with wildcard', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('text/plain', 'image/*', '')).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if type is defined and whiteList is not defined and type is in blackList with wildcard', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('image/jpeg', '', 'image/*')).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if type is defined and whiteList is defined and type is not in blackList with wildcard', () => {
|
||||
expect(fileUploadIsValidContentTypeFromSettings('text/plain', '', 'image/*')).to.be.true;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user