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

1988 lines
89 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 RethrownError = require('../server/lib/rethrown-error');
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 {
MS_LOOKUP,
TIME_PRECISION_VALUES,
DIRECTION,
} = require('matrix-public-archive-shared/lib/reference-values');
const { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, ONE_MINUTE_IN_MS, ONE_SECOND_IN_MS } = MS_LOOKUP;
const {
getTestClientForAs,
getTestClientForHs,
createTestRoom,
getCanonicalAlias,
joinRoom,
sendEvent,
sendMessage,
createMessagesInRoom,
updateProfile,
uploadContent,
} = require('./test-utils/client-utils');
const TestError = require('./test-utils/test-error');
const parseRoomDayMessageStructure = require('./test-utils/parse-room-day-message-structure');
const parseArchiveUrlForRoom = require('./test-utils/parse-archive-url-for-room');
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',
};
function assertExpectedTimePrecisionAgainstUrl(expectedTimePrecision, url) {
const urlObj = new URL(url, basePath);
const urlPathname = urlObj.pathname;
// First check the URL has the appropriate time precision
if (expectedTimePrecision === null) {
assert.doesNotMatch(
urlPathname,
/T[\d:]*?$/,
`Expected the URL to *not* have any time precision but saw ${urlPathname}`
);
} else if (expectedTimePrecision === TIME_PRECISION_VALUES.minutes) {
assert.match(urlPathname, /T\d\d:\d\d$/);
} else if (expectedTimePrecision === TIME_PRECISION_VALUES.seconds) {
assert.match(urlPathname, /T\d\d:\d\d:\d\d$/);
} else {
throw new Error(
`\`expectedTimePrecision\` was an unexpected value ${expectedTimePrecision} which we don't know how to assert here`
);
}
}
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 { eventIds: 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 { data: 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.
// January 15th, 2022
const archiveDate = new Date(Date.UTC(2022, 0, 15));
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 { data: 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 { data: 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 { eventIds: 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 { data: 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 most recent 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 most recent day with events
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomId);
const { data: 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, 20));
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 { data: 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 (empty view)', 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 { data: 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('404 when trying to view a future day', async () => {
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
try {
const TWO_DAYS_IN_MS = 2 * ONE_DAY_IN_MS;
await fetchEndpointAsText(
matrixPublicArchiveURLCreator.archiveUrlForDate(
roomId,
new Date(Date.now() + TWO_DAYS_IN_MS)
)
);
assert.fail(
new TestError(
`We expect the request to fail with a 404 since you can't view the future in the archive but it succeeded`
)
);
} catch (err) {
if (err instanceof TestError) {
throw err;
}
assert.strictEqual(
err?.response?.status,
404,
`Expected err.response.status=${err?.response?.status} to be 404 but error was: ${err.stack}`
);
}
});
describe('time selector', () => {
it('shows time selector when there are too many messages from the same day', async () => {
// Set this low so it's easy to hit the limit
config.set('archiveMessageLimit', 3);
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
await createMessagesInRoom({
client,
roomId,
// This should be greater than the `archiveMessageLimit`
numMessages: 10,
prefix: `foo`,
timestamp: archiveDate.getTime(),
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging
increment: ONE_HOUR_IN_MS,
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const { data: archivePageHtml } = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the time selector is visible
const timeSelectorElement = dom.document.querySelector(`[data-testid="time-selector"]`);
assert(timeSelectorElement);
});
it('shows time selector when there are too many messages from the same day but paginated forward into days with no messages', async () => {
// Set this low so it's easy to hit the limit
config.set('archiveMessageLimit', 3);
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
await createMessagesInRoom({
client,
roomId,
// This should be greater than the `archiveMessageLimit`
numMessages: 10,
prefix: `foo`,
timestamp: archiveDate.getTime(),
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging
increment: ONE_HOUR_IN_MS,
});
// Visit a day after when the messages were sent but there weren't
const visitArchiveDate = new Date(Date.UTC(2022, 0, 20));
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 { data: archivePageHtml } = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the time selector is visible
const timeSelectorElement = dom.document.querySelector(`[data-testid="time-selector"]`);
assert(timeSelectorElement);
});
it('does not show time selector when all events from the same day but not over the limit', async () => {
// Set this low so we don't have to deal with many messages in the tests
config.set('archiveMessageLimit', 5);
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
await createMessagesInRoom({
client,
roomId,
// This should be lesser than the `archiveMessageLimit`
numMessages: 3,
prefix: `foo`,
timestamp: archiveDate.getTime(),
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging
increment: ONE_HOUR_IN_MS,
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const { data: archivePageHtml } = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the time selector is *NOT* visible
const timeSelectorElement = dom.document.querySelector(`[data-testid="time-selector"]`);
assert.strictEqual(timeSelectorElement, null);
});
it('does not show time selector when showing events from multiple days', async () => {
// Set this low so we don't have to deal with many messages in the tests
config.set('archiveMessageLimit', 5);
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
// Create more messages than the archiveMessageLimit across many days but we
// should not go over the limit on a daily basis
const dayBeforeArchiveDateTs = Date.UTC(
archiveDate.getUTCFullYear(),
archiveDate.getUTCMonth(),
archiveDate.getUTCDate() - 1
);
await createMessagesInRoom({
client,
roomId,
// This should be lesser than the `archiveMessageLimit`
numMessages: 3,
prefix: `foo`,
timestamp: dayBeforeArchiveDateTs,
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging
increment: ONE_HOUR_IN_MS,
});
await createMessagesInRoom({
client,
roomId,
// This should be lesser than the `archiveMessageLimit`
numMessages: 3,
prefix: `foo`,
timestamp: archiveDate.getTime(),
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging
increment: ONE_HOUR_IN_MS,
});
archiveUrl = matrixPublicArchiveURLCreator.archiveUrlForDate(roomId, archiveDate);
const { data: archivePageHtml } = await fetchEndpointAsText(archiveUrl);
const dom = parseHTML(archivePageHtml);
// Make sure the time selector is *NOT* visible
const timeSelectorElement = dom.document.querySelector(`[data-testid="time-selector"]`);
assert.strictEqual(timeSelectorElement, null);
});
});
describe('Jump forwards and backwards', () => {
const jumpTestCases = [
{
// In order to jump from the 1st page to the 2nd, we first jump forward 4
// messages, then back-track to the first date boundary which is day3. We do
// this so that we don't start from day4 backwards which would miss messages
// because there are more than 5 messages in between day4 and day2.
//
// Even though there is overlap between the pages, our scroll continues from
// the event where the 1st page starts.
testName: 'can jump forward to the next activity',
// Create enough surround messages on nearby days that overflow the page
// limit but don't overflow the limit on a single day basis. We create 4
// days of messages so we can see a seamless continuation from page1 to
// page2.
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|--jump-fwd-4-messages-->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02',
page1: {
url: '/r/room1/date/2022/01/02',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/03?at=$event7',
action: null,
},
},
{
// This test is just slightly different and jumps further into day4 (just a
// slight variation to make sure it still does the correct thing)
//
// In order to jump from the 1st page to the 2nd, we first jump forward 4
// messages, then back-track to the first date boundary which is day3. There
// is exactly 5 messages between day4 and day2 which would be a perfect next
// page but because we hit the middle of day4, we have no idea how many more
// messages are in day4.
//
// Even though there is overlap between the pages, our scroll continues from
// the event where the 1st page starts.
testName: 'can jump forward to the next activity2',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|--jump-fwd-4-messages-->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02',
page1: {
url: '/r/room1/date/2022/01/02',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/03?at=$event7',
action: null,
},
},
{
// In order to jump from the 1st page to the 2nd, we first "jump" forward 4
// messages by paginating `/messages?limit=4` but it only returns 2x
// messages (event11 and event12) which is less than our limit of 4, so we
// know we reached the end and can simply display the day that the latest
// event occured on.
testName: 'can jump forward to the latest activity in the room (same day)',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|--jump-fwd-4-messages-->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/04T01:00',
page1: {
url: '/r/room1/date/2022/01/04T01:00',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/04?at=$event11',
action: null,
},
},
{
// In order to jump from the 1st page to the 2nd, we first "jump" forward 4
// messages by paginating `/messages?limit=4` but it only returns 3x
// messages (event10, event11, event12) which is less than our limit of 4,
// so we know we reached the end and can simply display the day that the
// latest event occured on.
testName: 'can jump forward to the latest activity in the room (different day)',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|---jump-fwd-4-messages--->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/03',
page1: {
url: '/r/room1/date/2022/01/03',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/04?at=$event10',
action: null,
},
},
// This test currently doesn't work because it finds the primordial room
// creation events which are created in now time vs the timestamp massaging we
// do for the message fixtures. We can uncomment this once Synapse supports
// timestamp massaging for `/createRoom`, see
// https://github.com/matrix-org/synapse/issues/15346
//
// {
// // In order to jump from the 1st page to the 2nd, we first "jump" forward 4
// // messages by paginating `/messages?limit=4` but it returns no messages
// // which is less than our limit of 4, so we know we reached the end and can
// // simply TODO
// testName:
// 'can jump forward to the latest activity in the room (when already viewing the latest activity)',
// roomDayMessageStructureString: `
// [room1 ]
// 1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
// [day1 ] [day2 ] [day3 ] [day4 ]
// [page1 ]
// |---jump-fwd-4-messages--->|
// [page2 ]
// `,
// startUrl: '/r/room1/date/2022/01/04',
// page1: {
// url: '/r/room1/date/2022/01/04',
// action: 'next',
// },
// page2: {
// // If we can't find any more messages to paginate to, we just progress the
// // date forward by a day so we can display the empty view for that day.
// //
// // TODO: This page probably doesn't need a `?at=` continue event
// url: '/r/room1/date/2022/01/05?at=TODO',
// action: null,
// },
// },
{
// Test to make sure we can jump from the 1st page to the 2nd page forwards
// even when it exactly paginates to the last message of the next day. We're
// testing this specifically to ensure that you actually jump to the next
// day (previously failed with naive flawed code).
//
// In order to jump from the 1st page to the 2nd, we first jump forward 3
// messages, then back-track to the first date boundary which is the nearest
// hour backwards from event9. We use the nearest hour because there is
// less than a day of gap between event6 and event9 and we fallback from
// nearest day to hour boundary.
//
testName:
'can jump forward to the next activity even when it exactly goes to the end of the next day',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|-jump-fwd-3-msg->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02',
page1: {
url: '/r/room1/date/2022/01/02',
action: 'next',
},
page2: {
// We expect the URL to look like `T02:00` because we're rendering part way
// through day3 and while we could get away with just hour precision, the
// default precision has hours and minutes.
url: '/r/room1/date/2022/01/03T02:00?at=$event7',
action: null,
},
},
{
// From the first page with too many messages, starting at event5(page1
// rangeStart), we look backwards for the closest event. Because we find
// event4 as the closest, which is from a different day from event9(page1
// rangeEnd), we can just display the day where event5 resides.
//
// Even though there is overlap between
// the pages, our scroll continues from the event where the 1st page starts.
testName: 'can jump backward to the previous activity',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/03',
page1: {
url: '/r/room1/date/2022/01/03',
action: 'previous',
},
page2: {
// Continuing from the first event of day2 since we already saw the rest
// of day2 in the first page
url: '/r/room1/date/2022/01/02?at=$event4',
action: null,
},
},
{
// In order to jump from the 1st page to the 2nd, we first jump forward 8
// messages, then back-track to the first date boundary which is the nearest
// day backwards from event20. We use the nearest day because there is more
// than a day of gap between event12 and event20.
testName:
'can jump forward over many quiet days without losing any messages in the gap',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14 <-- 15 <-- 16 <-- 17 <-- 18 <-- 19 <-- 20 <-- 21
[day1 ] [day2 ] [day3 ] [day4 ] [day5 ] [day6 ] [day7 ] [day8 ]
[page1 ]
|------------------jump-fwd-8-msg---------------------->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/04',
page1: {
url: '/r/room1/date/2022/01/04',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/07?at=$event13',
action: null,
},
},
{
// From the first page with too many messages, starting at event13 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event12 as the closest, which is from the a different day from event21
// (page1 rangeEnd), we can just display the day where event12 resides.
testName: 'can jump backward to the previous activity with many small quiet days',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14 <-- 15 <-- 16 <-- 17 <-- 18 <-- 19 <-- 20 <-- 21
[day1 ] [day2 ] [day3 ] [day4 ] [day5 ] [day6 ] [day7 ] [day8 ] [day9 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/09',
page1: {
url: '/r/room1/date/2022/01/09',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/06?at=$event12',
action: null,
},
},
{
// Test to make sure we can jump forwards from the 1st page to the 2nd page
// with too many messages to display on a single day.
//
// We jump forward 4 messages (`archiveMessageLimit`), then back-track to
// the nearest hour which starts us from event9, and then we display 5
// messages because we fetch one more than `archiveMessageLimit` to
// determine overflow.
testName: 'can jump forward to the next activity and land in too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14 <-- 15
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
|--jump-fwd-4-messages-->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02',
page1: {
url: '/r/room1/date/2022/01/02',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/03T03:00?at=$event7',
action: null,
},
},
{
// Test to make sure we can jump backwards from the 1st page to the 2nd page
// with too many messages to display on a single day.
//
// From the first page with too many messages, starting at event10 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event9 as the closest, which is from the a different day from event14
// (page1 rangeEnd), we can just display the day where event9 resides.
testName: 'can jump backward to the previous activity and land in too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ] [day4 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/04',
page1: {
url: '/r/room1/date/2022/01/04',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/02?at=$event9',
action: null,
},
},
{
// We jump forward 4 messages (`archiveMessageLimit`) to event12, then
// back-track to the nearest hour which starts off at event11 and render the
// page with 5 messages because we fetch one more than `archiveMessageLimit`
// to determine overflow.
testName:
'can jump forward from one day with too many messages into the next day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ]
[page1 ]
|---jump-fwd-4-messages--->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02',
page1: {
url: '/r/room1/date/2022/01/02',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/03T03:00?at=$event9',
action: null,
},
},
{
// From the first page with too many messages, starting at event10 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event9 as the closest, which is from the same day as event14 (page1
// rangeEnd), we round up to the nearest hour so that the URL encompasses it
// when looking backwards.
testName:
'can jump backward from one day with too many messages into the previous day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/03',
page1: {
url: '/r/room1/date/2022/01/03',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/03T01:00?at=$event9',
action: null,
},
},
{
// We jump forward 4 messages (`archiveMessageLimit`) to event12, then
// back-track to the nearest hour which starts off at event11 and render the
// page with 5 messages because we fetch one more than `archiveMessageLimit`
// to determine overflow.
testName:
'can jump forward from one day with too many messages into the same day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ]
[page1 ]
|---jump-fwd-4-messages--->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02T6:00',
page1: {
url: '/r/room1/date/2022/01/02T6:00',
action: 'next',
},
page2: {
url: '/r/room1/date/2022/01/02T09:00?at=$event9',
action: null,
},
},
{
// From the first page with too many messages, starting at event10 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event9 as the closest, which is from the same day as event14 (page1
// rangeEnd), we round up to the nearest hour so that the URL encompasses it
// when looking backwards.
testName:
'can jump backward from one day with too many messages into the same day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02T11:00',
page1: {
url: '/r/room1/date/2022/01/02T11:00',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/02T06:00?at=$event8',
action: null,
},
},
{
// We jump forward 4 messages (`archiveMessageLimit`) to event12, then
// back-track to the nearest hour which starts off at event11 and render the
// page with 5 messages because we fetch one more than `archiveMessageLimit`
// to determine overflow.
testName:
'can jump forward from the middle of one day with too many messages into the next day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ]
[page1 ]
|---jump-fwd-4-messages--->|
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/02T06:00',
page1: {
url: '/r/room1/date/2022/01/02T06:00',
action: 'next',
},
page2: {
// Continuing from the unseen event in day2
url: '/r/room1/date/2022/01/03T02:00?at=$event9',
action: null,
},
},
{
// From the first page with too many messages, starting at event10 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event9 as the closest, which is from the same day as event14 (page1
// rangeEnd), we round up to the nearest hour so that the URL encompasses it
// when looking backwards.
testName:
'can jump backward from the middle of one day with too many messages into the previous day with too many messages',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/03T06:00',
page1: {
url: '/r/room1/date/2022/01/03T06:00',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/03T01:00?at=$event8',
action: null,
},
},
{
// From the first page with too many messages, starting at event8 (page1
// rangeStart), we look backwards for the closest event. Because we find
// event7 as the closest, which is from a different day than event12 (page1
// rangeEnd), we can just display the day where event7 resides.
//
testName:
'can jump backward from the start of one day with too many messages into the previous day with exactly the limit',
roomDayMessageStructureString: `
[room1 ]
1 <-- 2 <-- 3 <-- 4 <-- 5 <-- 6 <-- 7 <-- 8 <-- 9 <-- 10 <-- 11 <-- 12 <-- 13 <-- 14
[day1 ] [day2 ] [day3 ]
[page1 ]
[page2 ]
`,
startUrl: '/r/room1/date/2022/01/03T05:00',
page1: {
url: '/r/room1/date/2022/01/03T05:00',
action: 'previous',
},
page2: {
url: '/r/room1/date/2022/01/02?at=$event7',
action: null,
},
},
];
jumpTestCases.forEach((testCase) => {
// eslint-disable-next-line max-statements, complexity
it(testCase.testName, async () => {
// Setup
// --------------------------------------
// --------------------------------------
const eventMap = new Map();
const fancyIdentifierToEventIdMap = new Map();
const eventIdToFancyIdentifierMap = new Map();
function convertFancyIdentifierListToDebugEventIds(fancyEventIdentifiers) {
// eslint-disable-next-line max-nested-callbacks
return fancyEventIdentifiers.map((fancyId) => {
const eventId = fancyIdentifierToEventIdMap.get(fancyId);
if (!eventId) {
throw new Error(
`Unable to find ${fancyId} in the fancyIdentifierToEventMap=${JSON.stringify(
Object.fromEntries(fancyIdentifierToEventIdMap.entries()),
null,
2
)}`
);
}
const ts = eventMap.get(eventId)?.originServerTs;
const tsDebugString = ts && `${new Date(ts).toISOString()} (${ts})`;
return `${eventId} (${fancyId}) - ${tsDebugString}`;
});
}
function convertEventIdsToDebugEventIds(eventIds) {
// eslint-disable-next-line max-nested-callbacks
return eventIds.map((eventId) => {
const fancyEventId = eventIdToFancyIdentifierMap.get(eventId);
if (!fancyEventId) {
throw new Error(
`Unable to find ${eventId} in the eventIdToFancyIdentifierMap=${JSON.stringify(
Object.fromEntries(eventIdToFancyIdentifierMap.entries()),
null,
2
)}`
);
}
const ts = eventMap.get(eventId)?.originServerTs;
const tsDebugString = ts && `${new Date(ts).toISOString()} (${ts})`;
return `${eventId} (${fancyEventId}) - ${tsDebugString}`;
});
}
const client = await getTestClientForHs(testMatrixServerUrl1);
const { rooms, archiveMessageLimit, pages } = parseRoomDayMessageStructure(
testCase.roomDayMessageStructureString
);
const fancyIdentifierToRoomIdMap = new Map();
const roomIdToFancyIdentifierMap = new Map();
for (const [roomIndex, room] of rooms.entries()) {
// TODO: upgradeRoom to link one room to another
const roomId = await createTestRoom(client);
const fancyRoomId = `#room${roomIndex + 1}`;
fancyIdentifierToRoomIdMap.set(fancyRoomId, roomId);
roomIdToFancyIdentifierMap.set(roomId, fancyRoomId);
// Join the archive user to the room before we create the test messages to
// avoid problems jumping to the latest activity since we can't control the
// timestamp of the membership event.
const archiveAppServiceUserClient = await getTestClientForAs();
await joinRoom({
client: archiveAppServiceUserClient,
roomId: roomId,
});
// Just spread things out a bit so the event times are more obvious
// and stand out from each other while debugging and so we just have
// to deal with hour time slicing
const eventSendTimeIncrement = ONE_HOUR_IN_MS;
for (const eventMeta of room.events) {
const archiveDate = new Date(Date.UTC(2022, 0, eventMeta.dayNumber, 0, 0, 0, 1));
const originServerTs =
archiveDate.getTime() + eventMeta.eventIndexInDay * eventSendTimeIncrement;
const content = {
msgtype: 'm.text',
body: `event${eventMeta.eventNumber} - day${eventMeta.dayNumber}.${eventMeta.eventIndexInDay}`,
};
const eventId = await sendMessage({
client,
roomId,
content,
// Technically, we don't have to set the timestamp to be unique or sequential but
// it still seems like a good idea to make the tests more clear.
timestamp: originServerTs,
});
eventMap.set(eventId, {
roomId,
originServerTs,
content,
});
const fancyEventId = `$event${eventMeta.eventNumber}`;
fancyIdentifierToEventIdMap.set(fancyEventId, eventId);
eventIdToFancyIdentifierMap.set(eventId, fancyEventId);
}
}
// Now Test
// --------------------------------------
// --------------------------------------
// Make sure the archive is configured as the test expects
config.set('archiveMessageLimit', archiveMessageLimit);
// eslint-disable-next-line max-nested-callbacks
const pagesKeyList = Object.keys(testCase).filter((key) => {
const isPageKey = key.startsWith('page');
if (isPageKey) {
assert.match(key, /page\d+/);
return true;
}
return false;
});
assert(
pagesKeyList.length > 0,
'You must have at least one `pageX` of expectations in your jump test case'
);
// Make sure the page are in order
// eslint-disable-next-line max-nested-callbacks
pagesKeyList.reduce((prevPageCount, currentPageKey) => {
const pageNumber = parseInt(currentPageKey.match(/\d+$/)[0], 10);
assert(
prevPageCount + 1 === pageNumber,
`Page numbers must be sorted in each test case but found ` +
`${pageNumber} after ${prevPageCount} - pagesList=${pagesKeyList}`
);
return pageNumber;
}, 0);
// Get the URL for the first page to fetch
//
// Set the `archiveUrl` for debugging if the test fails here
const { roomIdOrAlias: startRoomFancyKey, urlDateTime: startUrlDateTime } =
parseArchiveUrlForRoom(urlJoin('https://example.com', testCase.startUrl));
const startRoomIdOrAlias = fancyIdentifierToRoomIdMap.get(startRoomFancyKey);
assert(
startRoomIdOrAlias,
`Could not find room ID for ${startRoomFancyKey} in our list of known rooms ${JSON.stringify(
Object.fromEntries(fancyIdentifierToRoomIdMap.entries())
)}`
);
archiveUrl = `${matrixPublicArchiveURLCreator.archiveUrlForRoom(
startRoomIdOrAlias
)}/date/${startUrlDateTime}`;
// Loop through all of the pages of the test and ensure expectations
let alreadyEncounteredLastPage = false;
for (const pageKey of pagesKeyList) {
try {
if (alreadyEncounteredLastPage) {
assert.fail(
'We should not see any more pages after we already saw a page without an action ' +
`which signals the end of expecations. Encountered ${pageKey} in ${pagesKeyList} ` +
'after we already thought we were done'
);
}
const pageTestMeta = testCase[pageKey];
const {
roomIdOrAlias: expectedRoomFancyId,
//urlDateTime: expectedUrlDateTime,
continueAtEvent: expectedContinueAtEvent,
} = parseArchiveUrlForRoom(urlJoin('https://example.com', pageTestMeta.url));
const expectedRoomId = fancyIdentifierToRoomIdMap.get(expectedRoomFancyId);
assert(
expectedRoomId,
`Could not find room ID for ${expectedRoomFancyId} in our list of known rooms ${JSON.stringify(
Object.fromEntries(fancyIdentifierToRoomIdMap.entries())
)}`
);
// Fetch the given page.
const { data: archivePageHtml, res: pageRes } = await fetchEndpointAsText(
archiveUrl
);
const pageDom = parseHTML(archivePageHtml);
const {
roomIdOrAliasUrlPart: actualRoomIdOrAliasUrlPart,
roomIdOrAlias: actualRoomId,
//urlDateTime: actualUrlDateTime,
continueAtEvent: actualContinueAtEventId,
} = parseArchiveUrlForRoom(pageRes.url);
const actualRoomFancyId = roomIdToFancyIdentifierMap.get(actualRoomId);
assert(
actualRoomFancyId,
`Could not find room ID for ${actualRoomId} in our list of known rooms ${JSON.stringify(
Object.fromEntries(roomIdToFancyIdentifierMap.entries())
)}`
);
let actualContinueAtEventFancyId;
if (actualContinueAtEventId) {
actualContinueAtEventFancyId =
eventIdToFancyIdentifierMap.get(actualContinueAtEventId);
assert(
actualContinueAtEventFancyId,
`Could not find event ID for ${actualContinueAtEventId} in our list of known events ${JSON.stringify(
Object.fromEntries(eventIdToFancyIdentifierMap.entries())
)}`
);
}
// Replace messy room ID's and event ID's that change with every test
// run with their fancy ID's which correlate with the test meta so it's
// easier to reason about things when the assertion fails.
let actualUrlWithFancyIdentifies = pageRes.url
.replace(
`/roomid/${actualRoomIdOrAliasUrlPart}`,
// Slice to remove the sigil
`/r/${actualRoomFancyId.slice(1)}`
)
.replace(actualContinueAtEventId, actualContinueAtEventFancyId);
// Assert the correct room and time precision in the URL
assert.match(
actualUrlWithFancyIdentifies,
new RegExp(`${escapeStringRegexp(pageTestMeta.url)}$`)
);
// If provided, assert that it's a smooth continuation to more messages.
// First by checking where the scroll is going to start from
if (expectedContinueAtEvent) {
const [expectedContinuationDebugEventId] =
convertFancyIdentifierListToDebugEventIds([expectedContinueAtEvent]);
const urlObj = new URL(pageRes.url, basePath);
const qs = new URLSearchParams(urlObj.search);
const continuationEventId = qs.get('at');
if (!continuationEventId) {
throw new Error(
`Expected ?at=$xxx query parameter to be defined in the URL=${pageRes.url} but it was ${continuationEventId}. We expect it to match ${expectedContinuationDebugEventId}`
);
}
const [continationDebugEventId] = convertEventIdsToDebugEventIds([
continuationEventId,
]);
assert.strictEqual(continationDebugEventId, expectedContinuationDebugEventId);
}
const eventIdsOnPage = [...pageDom.document.querySelectorAll(`[data-event-id]`)]
// eslint-disable-next-line max-nested-callbacks
.map((eventEl) => {
return eventEl.getAttribute('data-event-id');
});
const pageNumber = pageKey.replace('page', '');
const page = pages[pageNumber - 1];
const expectedEventsOnPage = page.events;
const expectedFancyIdsOnPage = expectedEventsOnPage.map(
// eslint-disable-next-line max-nested-callbacks
(event) => `$event${event.eventNumber}`
);
// Assert that the page contains all expected events
assert.deepEqual(
convertEventIdsToDebugEventIds(eventIdsOnPage),
convertFancyIdentifierListToDebugEventIds(expectedFancyIdsOnPage),
`Events on ${pageKey} should be as expected`
);
// Follow the next activity link. Aka, fetch messages for the 2nd page
let actionLinkSelector;
if (pageTestMeta.action === 'next') {
actionLinkSelector = '[data-testid="jump-to-next-activity-link"]';
} else if (pageTestMeta.action === 'previous') {
actionLinkSelector = '[data-testid="jump-to-previous-activity-link"]';
} else if (pageTestMeta.action === null) {
// No more pages to test ✅, move on
alreadyEncounteredLastPage = true;
continue;
} else {
throw new Error(
`Unexpected value for ${pageKey}.action=${pageTestMeta.action} that we don't know what to do with`
);
}
const jumpToActivityLinkEl = pageDom.document.querySelector(actionLinkSelector);
const jumpToActivityLinkHref = jumpToActivityLinkEl.getAttribute('href');
// Move to the next iteration of the loop
//
// Set this for debugging if the test fails here
archiveUrl = jumpToActivityLinkHref;
} catch (err) {
const errorWithContext = new RethrownError(
`Encountered error while asserting ${pageKey}: ${err.message}`,
err
);
// Copy these over so mocha generates a nice diff for us
if (err instanceof assert.AssertionError) {
errorWithContext.actual = err.actual;
errorWithContext.expected = err.expected;
}
throw errorWithContext;
}
}
});
});
const jumpPrecisionTestCases = [
{
durationMinLabel: 'day',
durationMinMs: ONE_DAY_IN_MS,
durationMaxLabel: 'multiple days',
durationMaxMs: 5 * ONE_DAY_IN_MS,
expectedTimePrecision: TIME_PRECISION_VALUES.none,
},
{
durationMinLabel: 'hour',
durationMinMs: ONE_HOUR_IN_MS,
durationMaxLabel: 'day',
durationMaxMs: ONE_DAY_IN_MS,
expectedTimePrecision: TIME_PRECISION_VALUES.minutes,
},
{
durationMinLabel: 'minute',
durationMinMs: ONE_MINUTE_IN_MS,
durationMaxLabel: 'hour',
durationMaxMs: ONE_HOUR_IN_MS,
expectedTimePrecision: TIME_PRECISION_VALUES.minutes,
},
{
durationMinLabel: 'second',
durationMinMs: ONE_SECOND_IN_MS,
durationMaxLabel: 'minute',
durationMaxMs: ONE_MINUTE_IN_MS,
expectedTimePrecision: TIME_PRECISION_VALUES.seconds,
},
// This one is expected to fail but we could support it (#support-ms-time-slice)
// {
// durationMinLabel: 'millisecond',
// durationMinMs: 1,
// durationMaxLabel: 'second',
// durationMaxMs: ONE_SECOND_IN_MS,
// // #support-ms-time-slice
// expectedTimePrecision: TIME_PRECISION_VALUES.milliseconds,
// },
];
[
{
directionLabel: 'backward',
directionValue: DIRECTION.backward,
},
{
directionLabel: 'forward',
directionValue: DIRECTION.forward,
},
].forEach(({ directionLabel, directionValue }) => {
describe(`/jump redirects to \`/date/...\` URL that encompasses closest event when looking ${directionLabel}`, () => {
// eslint-disable-next-line max-nested-callbacks
jumpPrecisionTestCases.forEach((testCase) => {
let testTitle;
if (directionValue === DIRECTION.backward) {
testTitle = `will jump to the nearest ${testCase.durationMinLabel} rounded up when the closest event is from the same ${testCase.durationMaxLabel} (but further than a ${testCase.durationMinLabel} away) as our currently displayed range of events`;
} else if (directionValue === DIRECTION.forward) {
testTitle = `will jump to the nearest ${testCase.durationMinLabel} rounded down when the last event in the next range is more than a ${testCase.durationMinLabel} away from our currently displayed range of events`;
}
// eslint-disable-next-line max-nested-callbacks
it(testTitle, async () => {
// The date should be just past midnight so we don't run into inclusive
// bounds leaking messages from one day into another.
const archiveDate = new Date(Date.UTC(2022, 0, 1, 0, 0, 0, 1));
config.set('archiveMessageLimit', 3);
const client = await getTestClientForHs(testMatrixServerUrl1);
const roomId = await createTestRoom(client);
const { eventIds, eventMap } = await createMessagesInRoom({
client,
roomId,
// Make sure there is enough space before and after the selected range
// for another page of history
numMessages: 10,
prefix: `foo`,
timestamp: archiveDate.getTime(),
increment: testCase.durationMinMs,
});
const fourthEvent = eventMap.get(eventIds[3]);
const sixthEvent = eventMap.get(eventIds[5]);
const jumpUrl = `${matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(roomId, {
dir: directionValue,
currentRangeStartTs: fourthEvent.originServerTs,
currentRangeEndTs: sixthEvent.originServerTs,
})}`;
// Fetch the given page.
const { res } = await fetchEndpointAsText(jumpUrl);
// Assert the correct time precision in the URL
assertExpectedTimePrecisionAgainstUrl(testCase.expectedTimePrecision, res.url);
});
});
});
});
});
});
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 { data: 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 { data: 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 { data: 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(
new TestError(
'We expect the request to fail with a 403 since the archive should not be able to view a private room but it succeeded'
)
);
} catch (err) {
if (err instanceof TestError) {
throw err;
}
assert.strictEqual(
err.response.status,
403,
`Expected err.response.status=${err?.response?.status} to be 403 but error was: ${err.stack}`
);
}
});
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 { data: 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 { data: 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 sometimes 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
});