matrix-public-archive/test/e2e-tests.js

956 lines
38 KiB
JavaScript
Raw Normal View History

2022-02-22 15:06:29 -07:00
'use strict';
process.env.NODE_ENV = 'test';
2022-02-22 15:06:29 -07:00
const assert = require('assert');
const path = require('path');
const http = require('http');
const urlJoin = require('url-join');
2022-02-24 02:27:53 -07:00
const escapeStringRegexp = require('escape-string-regexp');
const { parseHTML } = require('linkedom');
const { readFile } = require('fs').promises;
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
const { fetchEndpointAsText, fetchEndpointAsJson } = require('../server/lib/fetch-endpoint');
const config = require('../server/lib/config');
const {
getTestClientForHs,
createTestRoom,
getCanonicalAlias,
joinRoom,
sendEvent,
sendMessage,
createMessagesInRoom,
updateProfile,
uploadContent,
} = require('./client-utils');
const testMatrixServerUrl1 = config.get('testMatrixServerUrl1');
const testMatrixServerUrl2 = config.get('testMatrixServerUrl2');
assert(testMatrixServerUrl1);
assert(testMatrixServerUrl2);
2022-02-24 02:27:53 -07:00
const basePath = config.get('basePath');
assert(basePath);
2022-02-24 02:27:53 -07:00
const interactive = config.get('interactive');
const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(basePath);
2022-02-22 15:06:29 -07:00
const HOMESERVER_URL_TO_PRETTY_NAME_MAP = {
[testMatrixServerUrl1]: 'hs1',
[testMatrixServerUrl2]: 'hs2',
};
2022-02-22 15:06:29 -07:00
describe('matrix-public-archive', () => {
2022-02-24 02:27:53 -07:00
let server;
before(() => {
// Start the archive server
server = require('../server/server');
});
after(() => {
2022-02-24 02:27:53 -07:00
if (!interactive) {
server.close();
}
});
describe('Test fixture homeservers', () => {
// Sanity check that our test homeservers can actually federate with each
// other. The rest of the tests won't work properly if this isn't working.
it('Test federation between fixture homeservers', async () => {
const hs1Client = await getTestClientForHs(testMatrixServerUrl1);
const hs2Client = await getTestClientForHs(testMatrixServerUrl2);
// Create a room on hs2
const hs2RoomId = await createTestRoom(hs2Client);
const room2EventIds = await createMessagesInRoom({
client: hs2Client,
roomId: hs2RoomId,
numMessages: 10,
prefix: HOMESERVER_URL_TO_PRETTY_NAME_MAP[hs2Client.homeserverUrl],
});
2022-02-22 19:55:42 -07:00
// Join hs1 to a room on hs2 (federation)
await joinRoom({
client: hs1Client,
roomId: hs2RoomId,
viaServers: 'hs2',
});
2022-02-22 19:55:42 -07:00
// From, hs1, make sure we can fetch messages from hs2
const messagesEndpoint = urlJoin(
hs1Client.homeserverUrl,
`_matrix/client/r0/rooms/${hs2RoomId}/messages?limit=5&dir=b&filter={"types":["m.room.message"]}`
);
const messageResData = await fetchEndpointAsJson(messagesEndpoint, {
accessToken: hs1Client.accessToken,
2022-02-22 19:55:42 -07:00
});
// Make sure it returned some messages
assert.strictEqual(messageResData.chunk.length, 5);
// Make sure all of the messages belong to the room
messageResData.chunk.map((event) => {
const isEventInRoomFromHs2 = room2EventIds.some((room2EventId) => {
return room2EventId === event.event_id;
});
// Make sure the message belongs to the room on hs2
assert.strictEqual(
isEventInRoomFromHs2,
true,
`Expected ${event.event_id} (${event.type}: "${
event.content.body
}") to be in room on hs2=${JSON.stringify(room2EventIds)}`
);
});
});
});
describe('Archive', () => {
// Use a fixed date at the start of the UTC day so that the tests are
// consistent. Otherwise, the tests could fail when they start close to
// midnight and it rolls over to the next day.
const archiveDate = new Date(Date.UTC(2022, 0, 3));
let archiveUrl;
let numMessagesSent = 0;
afterEach(() => {
if (interactive) {
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
// eslint-disable-next-line no-console
console.log('Interactive URL for test', archiveUrl);
}
// Reset `numMessagesSent` between tests so each test starts from the
// beginning of the day and we don't run out of minutes in the day to send
// messages in (we space messages out by a minute so the timestamp visibly
// changes in the UI).
numMessagesSent = 0;
// Reset any custom modifications made for a particular test
config.reset();
});
// Sends a message and makes sure that a timestamp was provided
async function sendMessageOnArchiveDate(options) {
const minute = 1000 * 60;
// Adjust the timestamp by a minute each time so there is some visual difference.
options.timestamp = archiveDate.getTime() + minute * numMessagesSent;
numMessagesSent++;
return sendMessage(options);
}
// Sends a message and makes sure that a timestamp was provided
async function sendEventOnArchiveDate(options) {
const minute = 1000 * 60;
// Adjust the timestamp by a minute each time so there is some visual difference.
options.timestamp = archiveDate.getTime() + minute * numMessagesSent;
numMessagesSent++;
return sendEvent(options);
}
2022-02-22 19:55:42 -07:00
describe('Archive room view', () => {
it('shows all events in a given day', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Just render the page initially so that the archiver user is already
// joined to the page. We don't want their join event masking the one-off
// problem where we're missing the latest message in the room. We just use the date now
// because it will find whatever events backwards no matter when they were sent.
await fetchEndpointAsText(
matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, new Date())
);
const messageTextList = [
`Amontons' First Law: The force of friction is directly proportional to the applied load.`,
`Amontons' Second Law: The force of friction is independent of the apparent area of contact.`,
// We're aiming for this to be the last message in the room
`Coulomb's Law of Friction: Kinetic friction is independent of the sliding velocity.`,
];
// TODO: Can we use `createMessagesInRoom` here instead?
const eventIds = [];
for (const messageText of messageTextList) {
const eventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
msgtype: 'm.text',
body: messageText,
},
});
eventIds.push(eventId);
}
// Sanity check that we actually sent some messages
assert.strictEqual(eventIds.length, 3);
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the messages are visible
for (let i = 0; i < eventIds.length; i++) {
const eventId = eventIds[i];
const eventText = messageTextList[i];
assert.match(
dom.document.querySelector(`[data-event-id="${eventId}"]`).outerHTML,
new RegExp(`.*${escapeStringRegexp(eventText)}.*`)
);
}
});
// eslint-disable-next-line max-statements
it('can render diverse messages', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
2022-02-24 12:06:19 -07:00
const userAvatarBuffer = Buffer.from(
// Purple PNG pixel
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mPsD9j0HwAFmQKScbjOAwAAAABJRU5ErkJggg==',
'base64'
);
const userAvatarMxcUri = await uploadContent({
client,
roomId,
data: userAvatarBuffer,
fileName: 'client user avatar',
});
const displayName = `${client.userId}-some-display-name`;
await updateProfile({
client,
displayName,
avatarUrl: userAvatarMxcUri,
});
2022-02-24 12:06:19 -07:00
// TODO: Set avatar of room
2022-02-24 12:06:19 -07:00
// Test image
// via https://en.wikipedia.org/wiki/Friction#/media/File:Friction_between_surfaces.jpg (CaoHao)
const imageBuffer = await readFile(
path.resolve(__dirname, './fixtures/friction_between_surfaces.jpg')
);
const imageFileName = 'friction_between_surfaces.jpg';
const mxcUri = await uploadContent({
client,
roomId,
data: imageBuffer,
fileName: imageFileName,
});
const imageEventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
body: imageFileName,
info: {
size: 17471,
mimetype: 'image/jpeg',
w: 640,
h: 312,
'xyz.amorgan.blurhash': 'LkR3G|IU?w%NbxbIemae_NxuD$M{',
},
msgtype: 'm.image',
url: mxcUri,
},
});
// A normal text message
const normalMessageText1 =
'^ Figure 1: Simulated blocks with fractal rough surfaces, exhibiting static frictional interactions';
const normalMessageEventId1 = await sendMessageOnArchiveDate({
client,
roomId,
content: {
msgtype: 'm.text',
body: normalMessageText1,
},
2022-02-24 12:06:19 -07:00
});
// Another normal text message
const normalMessageText2 =
'The topography of the Moon has been measured with laser altimetry and stereo image analysis.';
const normalMessageEventId2 = await sendMessageOnArchiveDate({
client,
roomId,
content: {
msgtype: 'm.text',
body: normalMessageText2,
},
});
2022-02-24 12:06:19 -07:00
// Test replies
const replyMessageText = `The concentration of maria on the near side likely reflects the substantially thicker crust of the highlands of the Far Side, which may have formed in a slow-velocity impact of a second moon of Earth a few tens of millions of years after the Moon's formation.`;
const replyMessageEventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
'org.matrix.msc1767.message': [
{
body: '> <@ericgittertester:my.synapse.server> ${normalMessageText2}',
mimetype: 'text/plain',
},
{
body: `<mx-reply><blockquote><a href="https://matrix.to/#/${roomId}/${normalMessageEventId2}?via=my.synapse.server">In reply to</a> <a href="https://matrix.to/#/@ericgittertester:my.synapse.server">@ericgittertester:my.synapse.server</a><br>${normalMessageText2}</blockquote></mx-reply>${replyMessageText}`,
mimetype: 'text/html',
},
],
body: `> <@ericgittertester:my.synapse.server> ${normalMessageText2}\n\n${replyMessageText}`,
msgtype: 'm.text',
format: 'org.matrix.custom.html',
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/${roomId}/${normalMessageEventId2}?via=my.synapse.server">In reply to</a> <a href="https://matrix.to/#/@ericgittertester:my.synapse.server">@ericgittertester:my.synapse.server</a><br>${normalMessageText2}</blockquote></mx-reply>${replyMessageText}`,
'm.relates_to': {
'm.in_reply_to': {
event_id: normalMessageEventId2,
},
},
},
});
// Test to make sure we can render the page when the reply is missing the
// event it's replying to (the relation).
const replyMissingRelationMessageText = `While the giant-impact theory explains many lines of evidence, some questions are still unresolved, most of which involve the Moon's composition.`;
const missingRelationEventId = '$someMissingEvent';
const replyMissingRelationMessageEventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
'org.matrix.msc1767.message': [
{
body: '> <@ericgittertester:my.synapse.server> some missing message',
mimetype: 'text/plain',
},
{
body: `<mx-reply><blockquote><a href="https://matrix.to/#/${roomId}/${missingRelationEventId}?via=my.synapse.server">In reply to</a> <a href="https://matrix.to/#/@ericgittertester:my.synapse.server">@ericgittertester:my.synapse.server</a><br>some missing message</blockquote></mx-reply>${replyMissingRelationMessageText}`,
mimetype: 'text/html',
},
],
body: `> <@ericgittertester:my.synapse.server> some missing message\n\n${replyMissingRelationMessageText}`,
msgtype: 'm.text',
format: 'org.matrix.custom.html',
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/${roomId}/${missingRelationEventId}?via=my.synapse.server">In reply to</a> <a href="https://matrix.to/#/@ericgittertester:my.synapse.server">@ericgittertester:my.synapse.server</a><br>some missing message</blockquote></mx-reply>${replyMissingRelationMessageText}`,
'm.relates_to': {
'm.in_reply_to': {
event_id: missingRelationEventId,
},
},
},
});
// Test reactions
const reactionText = '😅';
await sendEventOnArchiveDate({
client,
roomId,
eventType: 'm.reaction',
content: {
'm.relates_to': {
rel_type: 'm.annotation',
event_id: replyMessageEventId,
key: reactionText,
},
},
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
2022-02-24 12:06:19 -07:00
const dom = parseHTML(archivePageHtml);
2022-02-24 12:06:19 -07:00
// Make sure the user display name is visible on the message
2022-02-24 12:06:19 -07:00
assert.match(
dom.document.querySelector(`[data-event-id="${imageEventId}"]`).outerHTML,
new RegExp(`.*${escapeStringRegexp(displayName)}.*`)
2022-02-24 12:06:19 -07:00
);
// Make sure the user avatar is visible on the message
const avatarImageElement = dom.document.querySelector(
`[data-event-id="${imageEventId}"] [data-testid="avatar"] img`
);
assert(avatarImageElement);
assert.match(avatarImageElement.getAttribute('src'), new RegExp(`^http://.*`));
// Make sure the image message is visible
const imageElement = dom.document.querySelector(
`[data-event-id="${imageEventId}"] [data-testid="media"] img`
);
assert(imageElement);
assert.match(imageElement.getAttribute('src'), new RegExp(`^http://.*`));
assert.strictEqual(imageElement.getAttribute('alt'), imageFileName);
// Make sure the normal message is visible
assert.match(
dom.document.querySelector(`[data-event-id="${normalMessageEventId1}"]`).outerHTML,
new RegExp(`.*${escapeStringRegexp(normalMessageText1)}.*`)
);
// Make sure the other normal message is visible
assert.match(
dom.document.querySelector(`[data-event-id="${normalMessageEventId2}"]`).outerHTML,
new RegExp(`.*${escapeStringRegexp(normalMessageText2)}.*`)
);
const replyMessageElement = dom.document.querySelector(
`[data-event-id="${replyMessageEventId}"]`
);
// Make sure the reply text is there
assert.match(
replyMessageElement.outerHTML,
new RegExp(`.*${escapeStringRegexp(replyMessageText)}.*`)
);
// Make sure it also includes the message we're replying to
assert.match(
replyMessageElement.outerHTML,
new RegExp(`.*${escapeStringRegexp(normalMessageEventId2)}.*`)
);
const replyMissingRelationMessageElement = dom.document.querySelector(
`[data-event-id="${replyMissingRelationMessageEventId}"]`
);
// Make sure the reply text is there.
// We don't care about the message we're replying to because it's missing on purpose.
assert.match(
replyMissingRelationMessageElement.outerHTML,
new RegExp(`.*${escapeStringRegexp(replyMissingRelationMessageText)}.*`)
);
// Make sure the reaction also exists
assert.match(
replyMessageElement.outerHTML,
new RegExp(`.*${escapeStringRegexp(reactionText)}.*`)
);
});
it(`can render day back in time from room on remote homeserver we haven't backfilled from`, async () => {
const hs2Client = await getTestClientForHs(testMatrixServerUrl2);
// Create a room on hs2
const hs2RoomId = await createTestRoom(hs2Client);
const room2EventIds = await createMessagesInRoom({
client: hs2Client,
roomId: hs2RoomId,
numMessages: 3,
prefix: HOMESERVER_URL_TO_PRETTY_NAME_MAP[hs2Client.homeserverUrl],
timestamp: archiveDate.getTime(),
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(hs2RoomId, archiveDate, {
// Since hs1 doesn't know about this room on hs2 yet, we have to provide
// a via server to ask through.
viaServers: [HOMESERVER_URL_TO_PRETTY_NAME_MAP[testMatrixServerUrl2]],
});
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the messages are visible
assert.deepStrictEqual(
room2EventIds.map((eventId) => {
return dom.document
.querySelector(`[data-event-id="${eventId}"]`)
?.getAttribute('data-event-id');
}),
room2EventIds
);
});
it('redirects to last day with message history', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Send an event in the room so we have some day of history to redirect to
const eventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
msgtype: 'm.text',
body: 'some message in the history',
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
},
});
const expectedEventIdsOnDay = [eventId];
// Visit `/:roomIdOrAlias` and expect to be redirected to the last day with events
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomId);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the messages from the day we expect to get redirected to are visible
assert.deepStrictEqual(
expectedEventIdsOnDay.map((eventId) => {
return dom.document
.querySelector(`[data-event-id="${eventId}"]`)
?.getAttribute('data-event-id');
}),
expectedEventIdsOnDay
);
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
});
it('still shows surrounding messages on a day with no messages', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Send an event in the room so there is some history to display in the
// surroundings and everything doesn't just 404 because we can't find
// any event.
const eventId = await sendMessageOnArchiveDate({
client,
roomId,
content: {
msgtype: 'm.text',
body: 'some message in the history',
},
});
const expectedEventIdsToBeDisplayed = [eventId];
// Visit the archive on the day ahead of where there are messages
const visitArchiveDate = new Date(Date.UTC(2022, 0, 5));
assert(
visitArchiveDate > archiveDate,
'The date we visit the archive (`visitArchiveDate`) should be after where the messages were sent (`archiveDate`)'
);
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, visitArchiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
2022-02-24 02:27:53 -07:00
// Make sure the summary exists on the page
assert(
dom.document.querySelector(
`[data-testid="not-enough-events-summary-kind-no-events-in-day"]`
)
);
2022-02-24 02:27:53 -07:00
// Make sure the messages there are some messages from the surrounding days
assert.deepStrictEqual(
expectedEventIdsToBeDisplayed.map((eventId) => {
return dom.document
.querySelector(`[data-event-id="${eventId}"]`)
?.getAttribute('data-event-id');
}),
expectedEventIdsToBeDisplayed
);
});
it('shows no events summary when no messages at or before the given day', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// We purposely send no events in the room
2022-02-24 02:27:53 -07:00
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
2022-02-24 02:27:53 -07:00
const dom = parseHTML(archivePageHtml);
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
// Make sure the summary exists on the page
assert(
dom.document.querySelector(
`[data-testid="not-enough-events-summary-kind-no-events-at-all"]`
)
);
});
Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events (#43) Add test to make sure the archive doesn't fail when event for event relation is missing and not included in list of provided events. Like if someone is replying to an event that was from long ago out of our range. In the case of missing relations, Hydrogen does `_loadContextEntryNotInTimeline` because it can't find the event locally which throws an `uncaughtException`. Before https://github.com/matrix-org/matrix-public-archive/pull/51, the `uncaughtException` killed the Hydrogen `child_process` before it could pass back the HTML. Now this PR mainly just adds a test to make sure it works. ``` TypeError: Cannot read properties of undefined (reading 'storeNames') at TimelineReader.readById (hydrogen-web\target\lib-build\hydrogen.cjs.js:12483:33) at Timeline._getEventFromStorage (hydrogen-web\target\lib-build\hydrogen.cjs.js:12762:46) at Timeline._loadContextEntryNotInTimeline (hydrogen-web\target\lib-build\hydrogen.cjs.js:12747:35) at Timeline._loadContextEntriesWhereNeeded (hydrogen-web\target\lib-build\hydrogen.cjs.js:12741:14) at Timeline.addEntries (hydrogen-web\target\lib-build\hydrogen.cjs.js:12699:10) at mountHydrogen (4-hydrogen-vm-render-script.js:204:12) at 4-hydrogen-vm-render-script.js:353:1 at Script.runInContext (node:vm:139:12) at _renderHydrogenToStringUnsafe (matrix-public-archive\server\hydrogen-render\3-render-hydrogen-to-string-unsafe.js:102:41) at async process.<anonymous> (matrix-public-archive\server\hydrogen-render\2-render-hydrogen-to-string-fork-script.js:18:27) ```
2022-08-29 18:42:18 -06:00
it(`will redirect to hour pagination when there are too many messages on the same day`, async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Set this low so we can easily create more than the limit
config.set('archiveMessageLimit', 3);
2022-02-22 19:55:42 -07:00
// Create more messages than the limit
await createMessagesInRoom({
client,
roomId: roomId,
// This is larger than the `archiveMessageLimit` we set
numMessages: 5,
prefix: 'events in room',
timestamp: archiveDate.getTime(),
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
assert.match(archivePageHtml, /TODO: Redirect user to smaller hour range/);
});
it(`will not redirect to hour pagination when there are too many messages from surrounding days`, async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Set this low so we can easily create more than the limit
config.set('archiveMessageLimit', 3);
// Create more messages than the limit on a previous day
const previousArchiveDate = new Date(Date.UTC(2022, 0, 2));
assert(
previousArchiveDate < archiveDate,
`The previousArchiveDate=${previousArchiveDate} should be before the archiveDate=${archiveDate}`
);
const surroundEventIds = await createMessagesInRoom({
client,
roomId: roomId,
// This is larger than the `archiveMessageLimit` we set
numMessages: 2,
prefix: 'events in room',
timestamp: previousArchiveDate.getTime(),
});
// Create more messages than the limit
const eventIdsOnDay = await createMessagesInRoom({
client,
roomId: roomId,
// This is larger than the `archiveMessageLimit` we set
numMessages: 2,
prefix: 'events in room',
timestamp: archiveDate.getTime(),
});
2022-02-22 19:55:42 -07:00
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
2022-02-24 02:27:53 -07:00
const dom = parseHTML(archivePageHtml);
2022-02-24 02:27:53 -07:00
// Make sure the messages are displayed
const expectedEventIdsToBeDisplayed = [].concat(surroundEventIds).concat(eventIdsOnDay);
assert.deepStrictEqual(
expectedEventIdsToBeDisplayed.map((eventId) => {
return dom.document
.querySelector(`[data-event-id="${eventId}"]`)
?.getAttribute('data-event-id');
}),
expectedEventIdsToBeDisplayed
);
});
});
describe('Room directory', () => {
it('room search narrows down results', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
// This is just an extra room to fill out the room directory and make sure
// that it does not appear when searching.
await createTestRoom(client);
// Then create two rooms we will find with search
//
// We use a `timeToken` so that we can namespace these rooms away from other
// test runs against the same homeserver
const timeToken = Date.now();
const roomPlanetPrefix = `planet-${timeToken}`;
const roomSaturnId = await createTestRoom(client, {
name: `${roomPlanetPrefix}-saturn`,
});
const roomMarsId = await createTestRoom(client, {
name: `${roomPlanetPrefix}-mars`,
});
// Browse the room directory without search to see many rooms
archiveUrl = matrixPublicArchiveURLCreator.roomDirectoryUrl();
const roomDirectoryPageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(roomDirectoryPageHtml);
const roomsOnPageWithoutSearch = [
...dom.document.querySelectorAll(`[data-testid="room-card"]`),
].map((roomCardEl) => {
return roomCardEl.getAttribute('data-room-id');
});
// Then browse the room directory again, this time with the search
// narrowing down results.
archiveUrl = matrixPublicArchiveURLCreator.roomDirectoryUrl({
searchTerm: roomPlanetPrefix,
});
const roomDirectoryWithSearchPageHtml = await fetchEndpointAsText(archiveUrl);
const domWithSearch = parseHTML(roomDirectoryWithSearchPageHtml);
const roomsOnPageWithSearch = [
...domWithSearch.document.querySelectorAll(`[data-testid="room-card"]`),
].map((roomCardEl) => {
return roomCardEl.getAttribute('data-room-id');
});
// Assert that the rooms we searched for are visible
assert.deepStrictEqual(roomsOnPageWithSearch.sort(), [roomSaturnId, roomMarsId].sort());
// Sanity check that search does something. Assert that it's not showing
// the same results as if we didn't make any search.
assert.notDeepStrictEqual(roomsOnPageWithSearch.sort(), roomsOnPageWithoutSearch.sort());
});
it('can show rooms from another remote server', async () => {
const hs2Client = await getTestClientForHs(testMatrixServerUrl2);
// Create some rooms on hs2
//
// We use a `timeToken` so that we can namespace these rooms away from other
// test runs against the same homeserver
const timeToken = Date.now();
const roomPlanetPrefix = `remote-planet-${timeToken}`;
const roomXId = await createTestRoom(hs2Client, {
name: `${roomPlanetPrefix}-x`,
});
const roomYId = await createTestRoom(hs2Client, {
name: `${roomPlanetPrefix}-y`,
});
archiveUrl = matrixPublicArchiveURLCreator.roomDirectoryUrl({
homeserver: HOMESERVER_URL_TO_PRETTY_NAME_MAP[testMatrixServerUrl2],
searchTerm: roomPlanetPrefix,
});
const roomDirectoryWithSearchPageHtml = await fetchEndpointAsText(archiveUrl);
const domWithSearch = parseHTML(roomDirectoryWithSearchPageHtml);
// Make sure the `?homserver` is selected in the homeserver selector `<select>`
const selectedHomeserverOptionElement = domWithSearch.document.querySelector(
`[data-testid="homeserver-select"] option[selected]`
);
assert.strictEqual(
selectedHomeserverOptionElement.getAttribute('value'),
HOMESERVER_URL_TO_PRETTY_NAME_MAP[testMatrixServerUrl2]
);
const roomsOnPageWithSearch = [
...domWithSearch.document.querySelectorAll(`[data-testid="room-card"]`),
].map((roomCardEl) => {
return roomCardEl.getAttribute('data-room-id');
});
// Assert that the rooms we searched for on remote hs2 are visible
assert.deepStrictEqual(roomsOnPageWithSearch.sort(), [roomXId, roomYId].sort());
});
});
describe('access controls', () => {
it('not allowed to view private room even when the archiver user is in the room', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client, {
preset: 'private_chat',
initial_state: [],
});
try {
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomId);
await fetchEndpointAsText(archiveUrl);
assert.fail(
'We expect the request to fail with a 403 since the archive should not be able to view a private room'
);
} catch (err) {
assert.strictEqual(err.response.status, 403);
}
});
it('search engines allowed to index `world_readable` room', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomId);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the `<meta name="robots" ...>` tag does NOT exist on the
// page telling search engines not to index it
assert.strictEqual(dom.document.querySelector(`meta[name="robots"]`), null);
});
it('search engines not allowed to index `public` room', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client, {
// The default options for the test rooms adds a
// `m.room.history_visiblity` state event so we override that here so
// it's only a public room.
initial_state: [],
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomId);
const archivePageHtml = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the `<meta name="robots" ...>` tag exists on the page
// telling search engines not to index it
assert.strictEqual(
dom.document.querySelector(`meta[name="robots"]`)?.getAttribute('content'),
'noindex, nofollow'
);
});
});
describe('redirects', () => {
const controller = new AbortController();
const { signal } = controller;
// We have to use this over `fetch` because `fetch` does not allow us to manually
// follow redirects and get the resultant URL, see
// https://github.com/whatwg/fetch/issues/763
function httpRequest(url) {
return new Promise((resolve, reject) => {
const req = http.request(url, { signal });
req.on('response', (res) => {
resolve(res);
});
req.on('error', (err) => {
reject(err);
});
// This must be called for the request to actually go out
req.end();
});
}
after(() => {
// Abort any in-flight request
controller.abort();
});
describe('general', () => {
let testRedirects = [];
before(async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
const roomIdWithoutSigil = roomId.replace(/^(#|!)/, '');
const canonicalAlias = await getCanonicalAlias({ client, roomId });
const canonicalAliasWithoutSigil = canonicalAlias.replace(/^(#|!)/, '');
const nowDate = new Date();
// Gives the date in YYYY/mm/dd format.
// date.toISOString() -> 2022-02-16T23:20:04.709Z
const urlDate = nowDate.toISOString().split('T')[0].replaceAll('-', '/');
// Warn if it's close to the end of the UTC day. This test could be a flakey
// and cause a failure if the room was created just before midnight (UTC) and
// this timestamp comes after midnight. The redirect would want to go to the
// day before when the latest event was created instead of the `nowDate` we
// expected in the URL's.
//
// We could lookup the date of the latest event to use the `origin_server_ts`
// from ourselves which may be less faff than this big warning but 🤷 - that's
// kinda like making sure `/timestamp_to_event` works by using
// `/timestamp_to_event`.
const utcMidnightOfNowDay = Date.UTC(
nowDate.getUTCFullYear(),
nowDate.getUTCMonth(),
nowDate.getUTCDate() + 1
);
if (utcMidnightOfNowDay - nowDate.getTime() < 30 * 1000 /* 30 seconds */) {
// eslint-disable-next-line no-console
console.warn(
`Test is being run at the end of the UTC day. This could result in a flakey ` +
`failure where the room was created before midnight (UTC) but the \`nowDate\` ` +
`was after midnight meaning our expected URL's would be a day ahead. Since ` +
`this is an e2e test we can't control the date/time exactly.`
);
}
testRedirects.push(
...[
{
description:
'Visiting via a room ID will keep the URL as a room ID (avoid the canonical alias taking precedence)',
from: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}`),
to: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}/date/${urlDate}`),
},
{
description: 'Visiting via a room alias will keep the URL as a room alias',
from: urlJoin(basePath, `/r/${canonicalAliasWithoutSigil}`),
to: urlJoin(basePath, `/r/${canonicalAliasWithoutSigil}/date/${urlDate}`),
},
{
description:
'Removes the `?via` query parameter after joining from hitting the first endpoint (via parameter only necessary for room IDs)',
from: urlJoin(
basePath,
`/roomid/${roomIdWithoutSigil}?via=${HOMESERVER_URL_TO_PRETTY_NAME_MAP[testMatrixServerUrl1]}`
),
to: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}/date/${urlDate}`),
},
]
);
});
it('redirects people to the correct place (see internal test matrix)', async () => {
assert(testRedirects.length > 0, 'Expected more than 0 redirects to be tested');
for (const testRedirect of testRedirects) {
const res = await httpRequest(testRedirect.from);
// This should be a temporary redirect because the latest date will change
// as new events are sent for example.
assert.strictEqual(res.statusCode, 302, 'We expected a 302 temporary redirect');
// Make sure it redirected to the right location
assert.strictEqual(res.headers.location, testRedirect.to, testRedirect.description);
}
});
});
describe('fix honest mistakes and redirect to the correct place', () => {
let testRedirects = [];
before(async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
const roomIdWithoutSigil = roomId.replace(/^(#|!)/, '');
const canonicalAlias = await getCanonicalAlias({ client, roomId });
const canonicalAliasWithoutSigil = canonicalAlias.replace(/^(#|!)/, '');
testRedirects.push(
...[
{
description: 'Using a room ID with a prefix sigil (pasting a room ID)',
from: urlJoin(basePath, `/roomid/${roomId}`),
to: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}`),
},
{
description: 'Using a room ID with a prefix sigil with extra path after',
from: urlJoin(basePath, `/roomid/${roomId}/date/2022/09/20?via=my.synapse.server`),
to: urlJoin(
basePath,
`/roomid/${roomIdWithoutSigil}/date/2022/09/20?via=my.synapse.server`
),
},
{
description: 'Pasting a room ID with a prefix sigil after the domain root',
from: urlJoin(basePath, `/${roomId}`),
to: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}`),
},
{
description: 'URI encoded room ID',
from: urlJoin(basePath, `/${encodeURIComponent(roomId)}`),
to: urlJoin(basePath, `/roomid/${roomIdWithoutSigil}`),
},
{
description: 'URI encoded canonical alias',
from: urlJoin(basePath, `/${encodeURIComponent(canonicalAlias)}`),
to: urlJoin(basePath, `/r/${canonicalAliasWithoutSigil}`),
},
{
description:
'URI encoded canonical alias but on a visiting `/roomid/xxx` will redirect to alias `/r/xxx`',
from: urlJoin(basePath, `/roomid/${encodeURIComponent(canonicalAlias)}`),
to: urlJoin(basePath, `/r/${canonicalAliasWithoutSigil}`),
},
]
);
});
it('redirects people to the correct place (see internal test matrix)', async () => {
assert(testRedirects.length > 0, 'Expected more than 0 redirects to be tested');
for (const testRedirect of testRedirects) {
const res = await httpRequest(testRedirect.from);
assert.strictEqual(
res.statusCode,
301,
'We expected a 301 permanent redirect for mistakes'
);
// Make sure it redirected to the right location
assert.strictEqual(res.headers.location, testRedirect.to, testRedirect.description);
}
});
});
});
});
2022-02-22 15:06:29 -07:00
});