Skip to content

Commit e4afa9f

Browse files
committed
100% test coverage
1 parent 3714496 commit e4afa9f

14 files changed

+376
-19
lines changed

src/ExtensibleEvents.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type EventInterpreter<TContentIn extends object = object, TEvent extends
3636
* on use-case.
3737
*/
3838
export class ExtensibleEvents {
39+
// Dev note: this is manually reset by the unit tests - adjust name in both places
40+
// if changed.
3941
private static _defaultInstance: ExtensibleEvents = new ExtensibleEvents();
4042

4143
private interpreters = new NamespacedMap<EventInterpreter<any>>([
@@ -65,7 +67,7 @@ export class ExtensibleEvents {
6567
* event types.
6668
*/
6769
public get unknownInterpretOrder(): NamespacedValue<string, string>[] {
68-
return this._unknownInterpretOrder ?? [];
70+
return this._unknownInterpretOrder;
6971
}
7072

7173
/**
@@ -133,8 +135,16 @@ export class ExtensibleEvents {
133135

134136
for (const tryType of this.unknownInterpretOrder) {
135137
if (this.interpreters.has(tryType)) {
136-
const val = this.interpreters.get(tryType)!(wireFormat);
137-
if (val) return val;
138+
try {
139+
const val = this.interpreters.get(tryType)!(wireFormat);
140+
if (val) return val;
141+
} catch (e) {
142+
if (e instanceof InvalidEventError) {
143+
continue; // clearly can't be parsed as the unknown type
144+
}
145+
// noinspection ExceptionCaughtLocallyJS
146+
throw e; // re-throw everything else
147+
}
138148
}
139149
}
140150

src/events/PollEndEvent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export class PollEndEvent extends ExtensibleEvent<M_POLL_END_EVENT_CONTENT> {
4646
super(wireFormat);
4747

4848
const rel = this.wireContent["m.relates_to"];
49-
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
49+
// noinspection SuspiciousTypeOfGuard
50+
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel.event_id !== "string") {
5051
throw new InvalidEventError("Relationship must be a reference to an event");
5152
}
5253

src/events/PollResponseEvent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export class PollResponseEvent extends ExtensibleEvent<M_POLL_RESPONSE_EVENT_CON
6161
super(wireFormat);
6262

6363
const rel = this.wireContent["m.relates_to"];
64-
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") {
64+
// noinspection SuspiciousTypeOfGuard
65+
if (!REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel.event_id !== "string") {
6566
throw new InvalidEventError("Relationship must be a reference to an event");
6667
}
6768

@@ -79,7 +80,7 @@ export class PollResponseEvent extends ExtensibleEvent<M_POLL_RESPONSE_EVENT_CON
7980
*/
8081
public validateAgainst(poll: Optional<PollStartEvent>): void {
8182
const response = M_POLL_RESPONSE.findIn<M_POLL_RESPONSE_SUBTYPE>(this.wireContent);
82-
if (!response?.answers || !Array.isArray(response?.answers)) {
83+
if (!response?.answers || !Array.isArray(response.answers)) {
8384
this.internalSpoiled = true;
8485
this.internalAnswerIds = [];
8586
return;

src/interpreters/legacy/MRoomMessage.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ export function parseMRoomMessage(wireEvent: IPartialEvent<IPartialLegacyContent
3838
return new MessageEvent(wireEvent as unknown as IPartialEvent<M_MESSAGE_EVENT_CONTENT>);
3939
}
4040

41-
const msgtype = wireEvent.content?.msgtype;
42-
const text = wireEvent.content?.body;
43-
const html = wireEvent.content?.format === "org.matrix.custom.html" ? wireEvent.content.formatted_body : null;
41+
if (!wireEvent.content) return null;
42+
43+
const msgtype = wireEvent.content.msgtype;
44+
const text = wireEvent.content.body;
45+
const html = wireEvent.content.format === "org.matrix.custom.html" ? wireEvent.content.formatted_body : null;
4446

4547
if (msgtype === "m.text") {
4648
return new MessageEvent({

test/ExtensibleEvents.test.ts

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
EventType,
2020
ExtensibleEvent,
2121
ExtensibleEvents,
22+
InvalidEventError,
2223
IPartialEvent,
2324
IPartialLegacyContent,
2425
M_EMOTE,
@@ -44,10 +45,15 @@ import {
4445
} from "../src";
4546

4647
describe("ExtensibleEvents", () => {
47-
// Note: we don't test the other static functions because it should be pretty
48-
// obvious when they fail. We'll just make sure that the static accessor works.
49-
it("should return an instance by default", () => {
50-
expect(ExtensibleEvents.defaultInstance).toBeDefined();
48+
afterEach(() => {
49+
// gutwrench the default instance into something safe/new to "reset" it
50+
(<any>ExtensibleEvents)._defaultInstance = new ExtensibleEvents();
51+
});
52+
53+
describe("static api", () => {
54+
it("should return an instance by default", () => {
55+
expect(ExtensibleEvents.defaultInstance).toBeDefined();
56+
});
5157
});
5258

5359
describe("unknown events", () => {
@@ -84,6 +90,23 @@ describe("ExtensibleEvents", () => {
8490
const event = new ExtensibleEvents().parse(input);
8591
expect(event).toBeFalsy();
8692
});
93+
94+
describe("static api", () => {
95+
afterEach(() => {
96+
ExtensibleEvents.unknownInterpretOrder = new ExtensibleEvents().unknownInterpretOrder;
97+
});
98+
99+
it("should persist the unknown interpret order", () => {
100+
expect(ExtensibleEvents.unknownInterpretOrder.length).toBeGreaterThan(0);
101+
102+
const testValue1 = new UnstableValue(null, "org.matrix.example.feature1");
103+
const testValue2 = new UnstableValue(null, "org.matrix.example.feature2");
104+
const array = [testValue1, testValue2];
105+
ExtensibleEvents.unknownInterpretOrder = array;
106+
107+
expect(ExtensibleEvents.unknownInterpretOrder).toBe(array);
108+
});
109+
});
87110
});
88111

89112
describe("custom events", () => {
@@ -149,6 +172,25 @@ describe("ExtensibleEvents", () => {
149172
expect(event).toBeDefined();
150173
expect(event instanceof MyCustomEvent).toBe(true);
151174
});
175+
176+
describe("static api", () => {
177+
it("should support custom interpreters", () => {
178+
const input: IPartialEvent<any> = {
179+
type: myNamespace.name,
180+
content: {
181+
hello: "world",
182+
},
183+
};
184+
185+
let event = ExtensibleEvents.parse(input);
186+
expect(event).toBeFalsy();
187+
188+
ExtensibleEvents.registerInterpreter(myNamespace, myInterpreter);
189+
event = ExtensibleEvents.parse(input);
190+
expect(event).toBeDefined();
191+
expect(event instanceof MyCustomEvent).toBe(true);
192+
});
193+
});
152194
});
153195

154196
describe("known events", () => {
@@ -278,4 +320,54 @@ describe("ExtensibleEvents", () => {
278320
expect(poll instanceof PollEndEvent).toBe(true);
279321
});
280322
});
323+
324+
describe("parse errors", () => {
325+
function myForcedInvalidInterpreter(wireEvent: IPartialEvent<any>): ExtensibleEvent {
326+
throw new InvalidEventError("deliberate throw of invalid type");
327+
}
328+
329+
function myExplodingInterpreter(wireEvent: IPartialEvent<any>): ExtensibleEvent {
330+
throw new Error("deliberate throw");
331+
}
332+
333+
it("should return null when InvalidEventError is raised", () => {
334+
const extev = new ExtensibleEvents();
335+
const namespace = new UnstableValue(null, "org.matrix.example");
336+
extev.registerInterpreter(namespace, myForcedInvalidInterpreter);
337+
const result = extev.parse({type: namespace.name, content: {unused: true}});
338+
expect(result).toBeNull();
339+
});
340+
341+
it("should return null when no known parser is found", () => {
342+
const extev = new ExtensibleEvents();
343+
const namespace = new UnstableValue(null, "org.matrix.example");
344+
const result = extev.parse({type: namespace.name, content: {unused: true}});
345+
expect(result).toBeNull();
346+
});
347+
348+
it("should throw if the parser throws an unknown error", () => {
349+
const extev = new ExtensibleEvents();
350+
const namespace = new UnstableValue(null, "org.matrix.example");
351+
extev.registerInterpreter(namespace, myExplodingInterpreter);
352+
expect(() => extev.parse({type: namespace.name, content: {unused: true}})).toThrow("deliberate throw");
353+
});
354+
355+
it("should throw if the parser throws an unknown error during unknown interpret order", () => {
356+
const extev = new ExtensibleEvents();
357+
const namespace = new UnstableValue(null, "org.matrix.example");
358+
const namespace2 = new UnstableValue(null, "org.matrix.example2");
359+
extev.registerInterpreter(namespace, myExplodingInterpreter);
360+
extev.unknownInterpretOrder = [namespace];
361+
expect(() => extev.parse({type: namespace2.name, content: {unused: true}})).toThrow("deliberate throw");
362+
});
363+
364+
describe("static api", () => {
365+
it("should return null when InvalidEventError is raised", () => {
366+
const namespace = new UnstableValue(null, "org.matrix.example");
367+
ExtensibleEvents.registerInterpreter(namespace, myForcedInvalidInterpreter);
368+
const result = ExtensibleEvents.parse({type: namespace.name, content: {unused: true}});
369+
expect(result).toBeNull();
370+
});
371+
});
372+
});
281373
});

test/NamespacedMap.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ describe("NamespacedMap", () => {
6363
expect(map.get(STABLE_UNSTABLE_NS)).toBe("val1");
6464
});
6565

66+
it("should lookup by altName (unstable) if it is the only option", () => {
67+
const map = asTestableMap(new NamespacedMap<string>());
68+
map.set(STABLE_UNSTABLE_NS, "val1");
69+
const tempNs = new NamespacedValue("wrong_stable", STABLE_UNSTABLE_NS.unstable);
70+
expect(map.internalMap.size).toBe(2);
71+
expect(map.internalMap.get(tempNs.name ?? "TEST_FAIL")).toBeUndefined();
72+
expect(map.internalMap.get(tempNs.altName ?? "TEST_FAIL")).toBe("val1");
73+
expect(map.hasNamespaced(tempNs.name ?? "TEST_FAIL")).toBe(false);
74+
expect(map.hasNamespaced(tempNs.altName ?? "TEST_FAIL")).toBe(true);
75+
expect(map.getNamespaced(tempNs.name ?? "TEST_FAIL")).toBeUndefined();
76+
expect(map.getNamespaced(tempNs.altName ?? "TEST_FAIL")).toBe("val1");
77+
expect(map.has(tempNs)).toBe(true);
78+
expect(map.get(tempNs)).toBe("val1");
79+
});
80+
81+
describe("get", () => {
82+
it("should return null if no valid keys are found", () => {
83+
const map = asTestableMap(new NamespacedMap<string>());
84+
expect(map.internalMap.size).toBe(0);
85+
expect(map.get(STABLE_UNSTABLE_NS)).toBeNull();
86+
});
87+
});
88+
6689
it("should only set stable if available", () => {
6790
const map = asTestableMap(new NamespacedMap<string>());
6891
map.set(STABLE_ONLY_NS, "val1");

test/NamespacedValue.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ describe("NamespacedValue", () => {
3030

3131
it("should support optionally stable values", () => {
3232
const ns = new NamespacedValue(null, UNSTABLE_VALUE);
33-
expect(ns.stable).toBe(null);
33+
expect(ns.stable).toBeNull();
3434
expect(ns.unstable).toBe(UNSTABLE_VALUE);
3535
expect(ns.name).toBe(UNSTABLE_VALUE);
36-
expect(ns.altName).toBe(null);
36+
expect(ns.altName).toBeNull();
3737
});
3838

3939
it("should support optionally unstable values", () => {
4040
const ns = new NamespacedValue(STABLE_VALUE, null);
4141
expect(ns.stable).toBe(STABLE_VALUE);
42-
expect(ns.unstable).toBe(null);
42+
expect(ns.unstable).toBeNull();
4343
expect(ns.name).toBe(STABLE_VALUE);
44-
expect(ns.altName).toBe(null);
44+
expect(ns.altName).toBeNull();
4545
});
4646

4747
it("should not support entirely optional values", () => {
@@ -138,10 +138,10 @@ describe("UnstableValue", () => {
138138

139139
it("should support optionally stable values", () => {
140140
const ns = new UnstableValue(null, UNSTABLE_VALUE);
141-
expect(ns.stable).toBe(null);
141+
expect(ns.stable).toBeNull();
142142
expect(ns.unstable).toBe(UNSTABLE_VALUE);
143143
expect(ns.name).toBe(UNSTABLE_VALUE);
144-
expect(ns.altName).toBe(null);
144+
expect(ns.altName).toBeNull();
145145
});
146146

147147
it("should not support optionally unstable values", () => {
@@ -210,6 +210,11 @@ describe("UnstableValue", () => {
210210
const ns = new UnstableValue(STABLE_VALUE, UNSTABLE_VALUE);
211211
expect(ns.findIn(obj)).toBeFalsy();
212212
});
213+
214+
it.each([null, undefined])("shouldn't explode when given a %s object", obj => {
215+
const ns = new UnstableValue(STABLE_VALUE, UNSTABLE_VALUE);
216+
expect(ns.findIn(obj)).toBeFalsy();
217+
});
213218
});
214219

215220
describe("includedIn", () => {

test/events/MessageEvent.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@ describe("MessageEvent", () => {
8383
expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
8484
});
8585

86+
it("should not find HTML if there isn't any", () => {
87+
const input: IPartialEvent<M_MESSAGE_EVENT_CONTENT> = {
88+
type: "org.example.message-like",
89+
content: {
90+
[M_MESSAGE.name]: [
91+
{body: "Text here", mimetype: "text/plain"},
92+
{body: "MD here", mimetype: "text/markdown"},
93+
],
94+
95+
// These should be ignored
96+
[M_TEXT.name]: "WRONG Text here",
97+
[M_HTML.name]: "WRONG HTML here",
98+
},
99+
};
100+
const message = new MessageEvent(input);
101+
expect(message.text).toBe("Text here");
102+
expect(message.html).toBeUndefined();
103+
expect(message.renderings.length).toBe(2);
104+
expect(message.renderings.some(r => r.mimetype === "text/plain" && r.body === "Text here")).toBe(true);
105+
expect(message.renderings.some(r => r.mimetype === "text/markdown" && r.body === "MD here")).toBe(true);
106+
});
107+
86108
it("should fail to parse missing text", () => {
87109
const input: IPartialEvent<M_MESSAGE_EVENT_CONTENT> = {
88110
type: "org.example.message-like",

test/events/PollEndEvent.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,13 @@ describe("PollEndEvent", () => {
109109
});
110110
});
111111
});
112+
113+
describe("isEquivalentTo", () => {
114+
it("should consider itself the same for M_POLL_END types", () => {
115+
const event = PollEndEvent.from("$poll", "Poll closed");
116+
expect(event.isEquivalentTo(M_POLL_END.name)).toBe(true);
117+
expect(event.isEquivalentTo(M_POLL_END.altName)).toBe(true);
118+
expect(event.isEquivalentTo("org.matrix.random")).toBe(false);
119+
});
120+
});
112121
});

test/events/PollResponseEvent.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ describe("PollResponseEvent", () => {
114114
});
115115

116116
describe("validateAgainst", () => {
117+
it("should spoil the vote when no response subevent", () => {
118+
const input: IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT> = {
119+
type: M_POLL_RESPONSE.name,
120+
content: {
121+
"m.relates_to": {
122+
rel_type: REFERENCE_RELATION.name,
123+
event_id: "$poll",
124+
},
125+
} as any, // force invalid type
126+
};
127+
const response = new PollResponseEvent(input);
128+
expect(response.spoiled).toBe(true);
129+
130+
response.validateAgainst(SAMPLE_POLL);
131+
expect(response.spoiled).toBe(true);
132+
});
133+
117134
it("should spoil the vote when no answers", () => {
118135
const input: IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT> = {
119136
type: M_POLL_RESPONSE.name,
@@ -192,6 +209,26 @@ describe("PollResponseEvent", () => {
192209
expect(response.spoiled).toBe(true);
193210
});
194211

212+
it("should spoil the vote when answers is not an array", () => {
213+
const input: IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT> = {
214+
type: M_POLL_RESPONSE.name,
215+
content: {
216+
"m.relates_to": {
217+
rel_type: REFERENCE_RELATION.name,
218+
event_id: "$poll",
219+
},
220+
[M_POLL_RESPONSE.name]: {
221+
answers: "yes",
222+
},
223+
} as any, // force invalid type
224+
};
225+
const response = new PollResponseEvent(input);
226+
expect(response.spoiled).toBe(true);
227+
228+
response.validateAgainst(SAMPLE_POLL);
229+
expect(response.spoiled).toBe(true);
230+
});
231+
195232
describe("consumer usage", () => {
196233
it("should spoil the vote when invalid answers are given", () => {
197234
const input: IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT> = {

0 commit comments

Comments
 (0)