element-web/test/DecryptionFailureTracker-test.ts
Hubert Chathi d5bf1022e9
Use a different error message for UTDs when you weren't in the room. (#12453)
* Use different messages for UTDs sent before login

* Playwright test for historical events

* Add some tests

* initial work on pre-join UTDs

* add playwright tests and remove old pre-join UTD logic

* run i18n script

* fix type error

* use different error code in PostHog for pre-join UTD

* don't remove old pre-join UTD logic yet

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2024-04-29 17:18:57 +00:00

447 lines
16 KiB
TypeScript

/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker";
async function createFailedDecryptionEvent(code?: DecryptionFailureCode) {
return await mkDecryptionFailureMatrixEvent({
roomId: "!room:id",
sender: "@alice:example.com",
code: code ?? DecryptionFailureCode.UNKNOWN_ERROR,
msg: ":(",
});
}
describe("DecryptionFailureTracker", function () {
it("tracks a failed decryption for a visible event", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.addVisibleEvent(failedDecryptionEvent);
tracker.eventDecrypted(failedDecryptionEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
});
it("tracks a failed decryption with expected raw error for a visible event", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent(
DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
);
let count = 0;
let reportedRawCode = "";
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number, _errCode: string, rawCode: string) => {
count += total;
reportedRawCode = rawCode;
},
() => "UnknownError",
);
tracker.addVisibleEvent(failedDecryptionEvent);
tracker.eventDecrypted(failedDecryptionEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
// Should add the rawCode to the event context
expect(reportedRawCode).toBe("OLM_UNKNOWN_MESSAGE_INDEX");
});
it("tracks a failed decryption for an event that becomes visible later", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.eventDecrypted(failedDecryptionEvent);
tracker.addVisibleEvent(failedDecryptionEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
});
it("does not track a failed decryption for an event that never becomes visible", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.eventDecrypted(failedDecryptionEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
// should not track a failure for an event that never became visible
expect(count).toBe(0);
});
it("does not track a failed decryption where the event is subsequently successfully decrypted", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(_total: number) => {
// should not track an event that has since been decrypted correctly
expect(true).toBe(false);
},
() => "UnknownError",
);
tracker.addVisibleEvent(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
// Indicate successful decryption.
await decryptExistingEvent(decryptedEvent, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
tracker.eventDecrypted(decryptedEvent, null);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
});
it(
"does not track a failed decryption where the event is subsequently successfully decrypted " +
"and later becomes visible",
async () => {
const decryptedEvent = await createFailedDecryptionEvent();
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(_total: number) => {
// should not track an event that has since been decrypted correctly
expect(true).toBe(false);
},
() => "UnknownError",
);
tracker.eventDecrypted(decryptedEvent);
// Indicate successful decryption.
await decryptExistingEvent(decryptedEvent, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
tracker.eventDecrypted(decryptedEvent);
tracker.addVisibleEvent(decryptedEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Immediately track the newest failures
tracker.trackFailures();
},
);
it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
const decryptedEvent2 = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.addVisibleEvent(decryptedEvent);
// Arbitrary number of failed decryptions for both events
tracker.eventDecrypted(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
tracker.eventDecrypted(decryptedEvent2);
tracker.eventDecrypted(decryptedEvent2);
tracker.addVisibleEvent(decryptedEvent2);
tracker.eventDecrypted(decryptedEvent2);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Simulated polling of `trackFailures`, an arbitrary number ( > 2 ) times
tracker.trackFailures();
tracker.trackFailures();
tracker.trackFailures();
tracker.trackFailures();
// should only track a single failure per event
expect(count).toBe(2);
});
it("should not track a failure for an event that was tracked previously", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
tracker.eventDecrypted(decryptedEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
// Indicate a second decryption, after having tracked the failure
tracker.eventDecrypted(decryptedEvent);
tracker.trackFailures();
// should only track a single failure per event
expect(count).toBe(1);
});
it.skip("should not track a failure for an event that was tracked in a previous session", async () => {
// This test uses localStorage, clear it beforehand
localStorage.clear();
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
tracker.eventDecrypted(decryptedEvent);
// Pretend "now" is Infinity
// NB: This saves to localStorage specific to DFT
tracker.checkFailures(Infinity);
tracker.trackFailures();
// Simulate the browser refreshing by destroying tracker and creating a new tracker
// @ts-ignore access to private constructor
const secondTracker = new DecryptionFailureTracker(
(total: number) => (count += total),
() => "UnknownError",
);
secondTracker.addVisibleEvent(decryptedEvent);
//secondTracker.loadTrackedEvents();
secondTracker.eventDecrypted(decryptedEvent);
secondTracker.checkFailures(Infinity);
secondTracker.trackFailures();
// should only track a single failure per event
expect(count).toBe(1);
});
it("should count different error codes separately for multiple failures with different error codes", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
(error: DecryptionFailureCode) =>
error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError",
);
const decryptedEvent1 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
const decryptedEvent2 = await createFailedDecryptionEvent(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
const decryptedEvent3 = await createFailedDecryptionEvent(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
tracker.addVisibleEvent(decryptedEvent1);
tracker.addVisibleEvent(decryptedEvent2);
tracker.addVisibleEvent(decryptedEvent3);
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
tracker.eventDecrypted(decryptedEvent1);
tracker.eventDecrypted(decryptedEvent2);
tracker.eventDecrypted(decryptedEvent2);
tracker.eventDecrypted(decryptedEvent3);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
//expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
expect(counts["OlmKeysNotSentError"]).toBe(2);
});
it("should aggregate error codes correctly", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
(_errorCode: string) => "OlmUnspecifiedError",
);
const decryptedEvent1 = await createFailedDecryptionEvent(
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
);
const decryptedEvent2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
const decryptedEvent3 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
tracker.addVisibleEvent(decryptedEvent1);
tracker.addVisibleEvent(decryptedEvent2);
tracker.addVisibleEvent(decryptedEvent3);
tracker.eventDecrypted(decryptedEvent1);
tracker.eventDecrypted(decryptedEvent2);
tracker.eventDecrypted(decryptedEvent3);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
expect(counts["OlmUnspecifiedError"]).toBe(3);
});
it("should remap error codes correctly", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
(errorCode: string) => Array.from(errorCode).reverse().join(""),
);
const decryptedEvent = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
tracker.addVisibleEvent(decryptedEvent);
tracker.eventDecrypted(decryptedEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
// should track remapped error code
expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1);
});
it("default error code mapper maps error codes correctly", async () => {
const errorCodes: string[] = [];
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(total: number, errorCode: string) => {
errorCodes.push(errorCode);
},
// @ts-ignore access to private member
DecryptionFailureTracker.instance.errorCodeMapFn,
);
const event1 = await createFailedDecryptionEvent(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
tracker.addVisibleEvent(event1);
tracker.eventDecrypted(event1);
const event2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
tracker.addVisibleEvent(event2);
tracker.eventDecrypted(event2);
const event3 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
tracker.addVisibleEvent(event3);
tracker.eventDecrypted(event3);
const event4 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED);
tracker.addVisibleEvent(event4);
tracker.eventDecrypted(event4);
const event5 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
tracker.addVisibleEvent(event5);
tracker.eventDecrypted(event5);
const event6 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
tracker.addVisibleEvent(event6);
tracker.eventDecrypted(event6);
const event7 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
tracker.addVisibleEvent(event7);
tracker.eventDecrypted(event7);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
tracker.trackFailures();
expect(errorCodes).toEqual([
"OlmKeysNotSentError",
"OlmIndexError",
"HistoricalMessage",
"HistoricalMessage",
"HistoricalMessage",
"ExpectedDueToMembership",
"UnknownError",
]);
});
});