From 689a7342c2bec348877ae34dd3ce4ed59d859954 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 17:06:56 +0000 Subject: [PATCH] test: tighten msteams regression assertions --- extensions/msteams/src/inbound.test.ts | 5 +++- extensions/msteams/src/mentions.test.ts | 27 +++++++++++++------ extensions/msteams/src/messenger.test.ts | 15 ++++++++--- .../src/monitor-handler.file-consent.test.ts | 26 ++++++++++++++---- extensions/msteams/src/policy.test.ts | 14 +++++++--- extensions/msteams/src/polls-store.test.ts | 5 +++- extensions/msteams/src/polls.test.ts | 9 +++++-- 7 files changed, 76 insertions(+), 25 deletions(-) diff --git a/extensions/msteams/src/inbound.test.ts b/extensions/msteams/src/inbound.test.ts index ecee5835b18..9b112afee8f 100644 --- a/extensions/msteams/src/inbound.test.ts +++ b/extensions/msteams/src/inbound.test.ts @@ -35,7 +35,10 @@ describe("msteams inbound", () => { it("parses string timestamps", () => { const ts = parseMSTeamsActivityTimestamp("2024-01-01T00:00:00.000Z"); - expect(ts?.toISOString()).toBe("2024-01-01T00:00:00.000Z"); + if (!ts) { + throw new Error("expected MSTeams timestamp parser to return a Date"); + } + expect(ts.toISOString()).toBe("2024-01-01T00:00:00.000Z"); }); it("passes through Date instances", () => { diff --git a/extensions/msteams/src/mentions.test.ts b/extensions/msteams/src/mentions.test.ts index bddb1383887..d16b6fe681e 100644 --- a/extensions/msteams/src/mentions.test.ts +++ b/extensions/msteams/src/mentions.test.ts @@ -1,6 +1,14 @@ import { describe, expect, it } from "vitest"; import { buildMentionEntities, formatMentionText, parseMentions } from "./mentions.js"; +function requireFirstEntity(result: ReturnType) { + const entity = result.entities[0]; + if (!entity) { + throw new Error("expected parseMentions to return at least one entity"); + } + return entity; +} + describe("parseMentions", () => { it("parses single mention", () => { const result = parseMentions("Hello @[John Doe](28:a1b2c3-d4e5f6)!"); @@ -58,7 +66,7 @@ describe("parseMentions", () => { const result = parseMentions("@[John Peter Smith](28:a1b2c3)"); expect(result.text).toBe("John Peter Smith"); - expect(result.entities[0]?.mentioned.name).toBe("John Peter Smith"); + expect(requireFirstEntity(result).mentioned.name).toBe("John Peter Smith"); }); it("trims whitespace from id and name", () => { @@ -90,7 +98,7 @@ describe("parseMentions", () => { }); // Verify entity text exactly matches what's in the formatted text - const entityText = result.entities[0]?.text; + const entityText = requireFirstEntity(result).text; expect(result.text).toContain(entityText); expect(result.text.indexOf(entityText)).toBe(0); }); @@ -108,8 +116,9 @@ describe("parseMentions", () => { // Only the real mention should be parsed; the documentation example should be left as-is expect(result.entities).toHaveLength(1); - expect(result.entities[0]?.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); - expect(result.entities[0]?.mentioned.name).toBe("タナカ タロウ"); + const firstEntity = requireFirstEntity(result); + expect(firstEntity.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + expect(firstEntity.mentioned.name).toBe("タナカ タロウ"); // The documentation pattern must remain untouched in the text expect(result.text).toContain("`@[表示名](ユーザーID)`"); @@ -118,25 +127,27 @@ describe("parseMentions", () => { it("accepts Bot Framework IDs (28:xxx)", () => { const result = parseMentions("@[Bot](28:abc-123)"); expect(result.entities).toHaveLength(1); - expect(result.entities[0]?.mentioned.id).toBe("28:abc-123"); + expect(requireFirstEntity(result).mentioned.id).toBe("28:abc-123"); }); it("accepts Bot Framework IDs with non-hex payloads (29:xxx)", () => { const result = parseMentions("@[Bot](29:08q2j2o3jc09au90eucae)"); expect(result.entities).toHaveLength(1); - expect(result.entities[0]?.mentioned.id).toBe("29:08q2j2o3jc09au90eucae"); + expect(requireFirstEntity(result).mentioned.id).toBe("29:08q2j2o3jc09au90eucae"); }); it("accepts org-scoped IDs with extra segments (8:orgid:...)", () => { const result = parseMentions("@[User](8:orgid:2d8c2d2c-1111-2222-3333-444444444444)"); expect(result.entities).toHaveLength(1); - expect(result.entities[0]?.mentioned.id).toBe("8:orgid:2d8c2d2c-1111-2222-3333-444444444444"); + expect(requireFirstEntity(result).mentioned.id).toBe( + "8:orgid:2d8c2d2c-1111-2222-3333-444444444444", + ); }); it("accepts AAD object IDs (UUIDs)", () => { const result = parseMentions("@[User](a1b2c3d4-e5f6-7890-abcd-ef1234567890)"); expect(result.entities).toHaveLength(1); - expect(result.entities[0]?.mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + expect(requireFirstEntity(result).mentioned.id).toBe("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); }); it("rejects non-ID strings as mention targets", () => { diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 92f161341de..969850a2758 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -222,7 +222,10 @@ describe("msteams messenger", () => { conversation?: { id?: string }; }; expect(ref.activityId).toBeUndefined(); - expect(ref.conversation?.id).toBe("19:abc@thread.tacv2"); + if (!ref.conversation?.id) { + throw new Error("expected Teams top-level send to preserve conversation id"); + } + expect(ref.conversation.id).toBe("19:abc@thread.tacv2"); }); it("preserves parsed mentions when appending OneDrive fallback file links", async () => { @@ -262,11 +265,15 @@ describe("msteams messenger", () => { expect(ids).toEqual(["id:one"]); expect(graphUploadMockState.uploadAndShareOneDrive).toHaveBeenCalledOnce(); expect(sent).toHaveLength(1); - expect(sent[0]?.text).toContain("Hello John"); - expect(sent[0]?.text).toContain( + const firstSent = sent[0]; + if (!firstSent?.text) { + throw new Error("expected Teams message send to include rendered text"); + } + expect(firstSent.text).toContain("Hello John"); + expect(firstSent.text).toContain( "📎 [upload.txt](https://onedrive.example.com/share/item123)", ); - expect(sent[0]?.entities).toEqual([ + expect(firstSent.entities).toEqual([ { type: "mention", text: "John", diff --git a/extensions/msteams/src/monitor-handler.file-consent.test.ts b/extensions/msteams/src/monitor-handler.file-consent.test.ts index 39b6ea1b1ff..dc0df94d15b 100644 --- a/extensions/msteams/src/monitor-handler.file-consent.test.ts +++ b/extensions/msteams/src/monitor-handler.file-consent.test.ts @@ -147,6 +147,14 @@ function createConsentInvokeHarness(params: { return { uploadId, handler, context, sendActivity }; } +function requirePendingUpload(uploadId: string) { + const upload = getPendingUpload(uploadId); + if (!upload) { + throw new Error(`expected pending upload ${uploadId}`); + } + return upload; +} + describe("msteams file consent invoke authz", () => { beforeEach(() => { setMSTeamsRuntime(runtimeStub); @@ -161,7 +169,7 @@ describe("msteams file consent invoke authz", () => { action: "accept", }); - await handler.run?.(context); + await handler.run(context); // invokeResponse should be sent immediately expect(sendActivity).toHaveBeenCalledWith( @@ -186,7 +194,7 @@ describe("msteams file consent invoke authz", () => { action: "accept", }); - await handler.run?.(context); + await handler.run(context); // invokeResponse should be sent immediately expect(sendActivity).toHaveBeenCalledWith( @@ -200,7 +208,11 @@ describe("msteams file consent invoke authz", () => { ); expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled(); - expect(getPendingUpload(uploadId)).toBeDefined(); + expect(requirePendingUpload(uploadId)).toMatchObject({ + conversationId: "19:victim@thread.v2", + filename: "secret.txt", + contentType: "text/plain", + }); }); it("ignores cross-conversation decline invoke and keeps pending upload", async () => { @@ -209,7 +221,7 @@ describe("msteams file consent invoke authz", () => { action: "decline", }); - await handler.run?.(context); + await handler.run(context); // invokeResponse should be sent immediately expect(sendActivity).toHaveBeenCalledWith( @@ -219,7 +231,11 @@ describe("msteams file consent invoke authz", () => { ); expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled(); - expect(getPendingUpload(uploadId)).toBeDefined(); + expect(requirePendingUpload(uploadId)).toMatchObject({ + conversationId: "19:victim@thread.v2", + filename: "secret.txt", + contentType: "text/plain", + }); expect(sendActivity).toHaveBeenCalledTimes(1); }); }); diff --git a/extensions/msteams/src/policy.test.ts b/extensions/msteams/src/policy.test.ts index 60342573355..c15ad103549 100644 --- a/extensions/msteams/src/policy.test.ts +++ b/extensions/msteams/src/policy.test.ts @@ -47,8 +47,11 @@ describe("msteams policy", () => { conversationId: "chan456", }); - expect(res.teamConfig?.requireMention).toBe(false); - expect(res.channelConfig?.requireMention).toBe(true); + if (!res.teamConfig || !res.channelConfig) { + throw new Error("expected matched team and channel config"); + } + expect(res.teamConfig.requireMention).toBe(false); + expect(res.channelConfig.requireMention).toBe(true); expect(res.allowlistConfigured).toBe(true); expect(res.allowed).toBe(true); expect(res.channelMatchKey).toBe("chan456"); @@ -82,8 +85,11 @@ describe("msteams policy", () => { it("matches team and channel by name when dangerous name matching is enabled", () => { const res = resolveNamedTeamRouteConfig(true); - expect(res.teamConfig?.requireMention).toBe(true); - expect(res.channelConfig?.requireMention).toBe(false); + if (!res.teamConfig || !res.channelConfig) { + throw new Error("expected matched named team and channel config"); + } + expect(res.teamConfig.requireMention).toBe(true); + expect(res.channelConfig.requireMention).toBe(false); expect(res.allowed).toBe(true); }); }); diff --git a/extensions/msteams/src/polls-store.test.ts b/extensions/msteams/src/polls-store.test.ts index ff70f13d4ab..48deecfd01f 100644 --- a/extensions/msteams/src/polls-store.test.ts +++ b/extensions/msteams/src/polls-store.test.ts @@ -33,6 +33,9 @@ describe.each([ selections: ["0", "1"], }); - expect(poll?.votes["user-1"]).toEqual(["0"]); + if (!poll) { + throw new Error("poll store did not return the updated poll"); + } + expect(poll.votes["user-1"]).toEqual(["0"]); }); }); diff --git a/extensions/msteams/src/polls.test.ts b/extensions/msteams/src/polls.test.ts index ab851946194..32a6401d619 100644 --- a/extensions/msteams/src/polls.test.ts +++ b/extensions/msteams/src/polls.test.ts @@ -17,7 +17,9 @@ describe("msteams polls", () => { options: ["Pizza", "Sushi"], }); - expect(card.pollId).toBeTruthy(); + expect(card.pollId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + ); expect(card.fallbackText).toContain("Poll: Lunch?"); expect(card.fallbackText).toContain("1. Pizza"); expect(card.fallbackText).toContain("2. Sushi"); @@ -54,6 +56,9 @@ describe("msteams polls", () => { selections: ["0", "1"], }); const stored = await store.getPoll("poll-2"); - expect(stored?.votes["user-1"]).toEqual(["0"]); + if (!stored) { + throw new Error("expected stored poll after recordVote"); + } + expect(stored.votes["user-1"]).toEqual(["0"]); }); });