"use strict";
const expect = require("chai").expect;
import {renderToString} from "@vue/server-test-utils";
import ParsedMessageTestWrapper from "../../components/ParsedMessageTestWrapper.vue";
async function getParsedMessageContents(text, message) {
let contents = await renderToString(ParsedMessageTestWrapper, {
propsData: {
text,
message,
},
});
// The wrapper adds a surrounding div to the message html, so we clean that out here
contents = contents.replace(/^
([^]+)<\/div>$/m, "$1");
return contents;
}
describe("IRC formatted message parser", () => {
it("should not introduce xss", async () => {
const testCases = [
{
input: "
",
expected:
'<img onerror=\'location.href="
//youtube.com"\'>',
},
{
input: '#&">bug',
expected:
'
#&">bug',
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should skip all <32 ASCII codes except linefeed", async () => {
const testCases = [
{
input: "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1B\x1D\x1D\x1E\x1Ftext\x0Awithcontrolcodestest",
expected:
'
text\nwithcontrolcodestest',
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should find urls", async () => {
const testCases = [
{
input: "irc://irc.example.com/thelounge",
expected:
'
' +
"irc://irc.example.com/thelounge" +
"",
},
{
input: "www.nooooooooooooooo.com",
expected:
'
' +
"www.nooooooooooooooo.com" +
"",
},
{
input: "look at https://thelounge.chat/ for more information",
expected:
"look at " +
'
' +
"https://thelounge.chat/" +
"" +
" for more information",
},
{
input: "use www.duckduckgo.com for privacy reasons",
expected:
"use " +
'
' +
"www.duckduckgo.com" +
"" +
" for privacy reasons",
},
{
input: "svn+ssh://example.org",
expected:
'
' +
"svn+ssh://example.org" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("url with a dot parsed correctly", async () => {
const input =
"bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx";
const correctResult =
"bonuspunkt: your URL parser misparses this URL: " +
'
' +
"https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" +
"";
const actual = await getParsedMessageContents(input);
expect(actual).to.deep.equal(correctResult);
});
it("should balance brackets", async () => {
const testCases = [
{
input: "
",
expected:
"<" +
'' +
"https://theos.kyriasis.com/~kyrias/stats/archlinux.html" +
"" +
">",
},
{
input: "abc (www.example.com)",
expected:
"abc (" +
'' +
"www.example.com" +
"" +
")",
},
{
input: "http://example.com/Test_(Page)",
expected:
'' +
"http://example.com/Test_(Page)" +
"",
},
{
input: "www.example.com/Test_(Page)",
expected:
'' +
"www.example.com/Test_(Page)" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find urls", async () => {
const testCases = [
{
input: "text www. text",
expected: "text www. text",
},
{
input: "http://.",
expected: "http://.",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should find channels", async () => {
const testCases = [
{
input: "#a",
expected:
'' +
"#a" +
"",
},
{
input: "#test",
expected:
'' +
"#test" +
"",
},
{
input: "#äöü",
expected:
'' +
"#äöü" +
"",
},
{
input: "inline #channel text",
expected:
"inline " +
'' +
"#channel" +
"" +
" text",
},
{
input: "#1,000",
expected:
'' +
"#1,000" +
"",
},
{
input: "@#a",
expected:
"@" +
'' +
"#a" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find channels", async () => {
const testCases = [
{
input: "hi#test",
expected: "hi#test",
},
{
input: "#",
expected: "#",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
[
{
name: "bold",
input: "\x02bold",
expected: 'bold',
},
{
name: "foreground color",
input: "\x038yellowText",
expected: 'yellowText',
},
{
name: "foreground and background colors (white)",
input: "\x030,0white,white",
expected: 'white,white',
},
{
name: "foreground and background colors",
input: "\x034,8yellowBGredText",
expected: 'yellowBGredText',
},
{
name: "hex foreground color",
input: "\x04663399rebeccapurple",
expected: 'rebeccapurple',
},
{
name: "hex foreground and background colors",
input: "\x04415364,ff9e18The Lounge",
expected: 'The Lounge',
},
{
name: "italic",
input: "\x1ditalic",
expected: 'italic',
},
{
name: "underline",
input: "\x1funderline",
expected: 'underline',
},
{
name: "strikethrough",
input: "\x1estrikethrough",
expected: 'strikethrough',
},
{
name: "monospace",
input: "\x11monospace",
expected: 'monospace',
},
{
name: "reverse with foreground and background colors",
input: "\x0313,1fg and bg \x16and reversed",
expected:
'fg and bg ' +
'and reversed',
},
{
name: "toggle reverse with foreground and background colors",
input: "\x0305,11text \x16reversed and \x16back again and \x16reversed",
expected:
'text ' +
'reversed and ' +
'back again and ' +
'reversed',
},
{
name: "escape reverse when new colors are applied",
input: "\x0311,02text \x16 reversed \x0304,05 and new style",
expected:
'text ' +
' reversed ' +
' and new style',
},
{
name: "resets",
input: "\x02bold\x038yellow\x02nonBold\x03default",
expected:
'bold' +
'yellow' +
'nonBold' +
"default",
},
{
name: "duplicates",
input: "\x02bold\x02 \x02bold\x02",
expected:
'bold' + " " + 'bold',
},
].forEach(({name, input, expected}) => {
it(`should handle style characters: ${name}`, async () => {
expect(await getParsedMessageContents(input)).to.equal(expected);
});
});
it("should find nicks", async () => {
const testCases = [
{
message: {
users: ["MaxLeiter"],
},
input: "test, MaxLeiter",
expected:
"test, " +
'' +
"MaxLeiter" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input, testCase.message))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find nicks", async () => {
const testCases = [
{
users: ["MaxLeiter, test"],
input: "#test-channelMaxLeiter",
expected:
'' +
"#test-channelMaxLeiter" +
"",
},
{
users: ["MaxLeiter, test"],
input: "https://www.MaxLeiter.com/test",
expected:
'' +
"https://www.MaxLeiter.com/test" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should go bonkers like mirc", async () => {
const testCases = [
{
input: "\x02irc\x0f://\x1dirc.example.com\x0f/\x034,8thelounge",
expected:
'' +
'irc' +
"://" +
'irc.example.com' +
"/" +
'thelounge' +
"",
},
{
input: "\x02#\x038,9thelounge",
expected:
'' +
'#' +
'thelounge' +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
// Emoji
[
{
name: "in text",
input: "Hello💬",
expected:
'Hello💬',
},
{
name: "complicated zero-join-width emoji",
input: "🤦🏿♀️",
expected:
'🤦🏿♀️',
},
{
name: "unicode 12 emojis",
input: "🧘🏿👨👨👧👧",
expected:
'🧘🏿👨👨👧👧',
},
{
name: "unicode 12 emojis with multiple modifiers",
input: "👩🏾🤝👨🏽",
expected: '👩🏾🤝👨🏽',
},
{
name: "with modifiers",
input: "🤷♀️",
expected:
'🤷♀️',
},
{
name: "with emoji variant selector",
input: "\u{2695}\u{FE0F}",
expected:
'\u{2695}\u{FE0F}',
},
{
name: "with text variant selector",
input: "\u{2695}\u{FE0E}",
expected: "\u{2695}\u{FE0E}", // this does not match because FE0E is specifically a text variant
},
{
name: "without variant selector",
input: "\u{2695}",
expected: "\u{2695}", // this does not match because emoji-regex expects \uFE0F as per the emoji specification
},
{
// FIXME: These multiple `span`s should be optimized into a single one. See https://github.com/thelounge/thelounge/issues/1783
name: "wrapped in style",
input: "Super \x034💚 green!",
expected:
'Super 💚 green!',
},
{
name: "wrapped in URLs",
input: "https://i.❤️.thelounge.chat",
// FIXME: Emoji in text should be `❤️`. See https://github.com/thelounge/thelounge/issues/1784
expected:
'https://i.❤️.thelounge.chat',
},
{
name: "wrapped in channels",
input: "#i❤️thelounge",
// FIXME: Emoji in text should be `❤️`. See https://github.com/thelounge/thelounge/issues/1784
expected:
'#i❤️thelounge',
},
].forEach(({name, input, expected}) => {
it(`should find emoji: ${name}`, async () => {
expect(await getParsedMessageContents(input)).to.equal(expected);
});
});
it("should optimize generated html", async () => {
const testCases = [
{
input: 'test \x0312#\x0312\x0312"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a',
expected:
"test " +
'' +
'#"testa' +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should trim common protocols", async () => {
const testCases = [
{
input: "like..http://example.com",
expected:
"like.." +
'' +
"http://example.com" +
"",
},
{
input: "like..HTTP://example.com",
expected:
"like.." +
'' +
"HTTP://example.com" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not find channel in fragment", async () => {
const testCases = [
{
input: "http://example.com/#hash",
expected:
'' +
"http://example.com/#hash" +
"",
},
];
const actual = await Promise.all(
testCases.map((testCase) => getParsedMessageContents(testCase.input))
);
const expected = testCases.map((testCase) => testCase.expected);
expect(actual).to.deep.equal(expected);
});
it("should not overlap parts", async () => {
const input = "Url: http://example.com/path Channel: ##channel";
const actual = await getParsedMessageContents(input);
expect(actual).to.equal(
'Url: http://example.com/path ' +
'Channel: ##channel'
);
});
it("should handle overlapping parts by using first starting", async () => {
const input = "#test-https://example.com";
const actual = await getParsedMessageContents(input);
expect(actual).to.equal(
'' +
"#test-https://example.com" +
""
);
});
it("should find links separated by tab character", async () => {
const input = "example.com\texample.org";
const actual = await getParsedMessageContents(input);
expect(actual).to.equal(
'example.com' +
' example.org'
);
});
});