test: tighten msteams regression assertions

This commit is contained in:
Peter Steinberger
2026-03-22 17:06:56 +00:00
parent 090ac8831f
commit 689a7342c2
7 changed files with 76 additions and 25 deletions

View File

@@ -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", () => {

View File

@@ -1,6 +1,14 @@
import { describe, expect, it } from "vitest";
import { buildMentionEntities, formatMentionText, parseMentions } from "./mentions.js";
function requireFirstEntity(result: ReturnType<typeof parseMentions>) {
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("<at>John Peter Smith</at>");
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", () => {

View File

@@ -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 <at>John</at>");
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 <at>John</at>");
expect(firstSent.text).toContain(
"📎 [upload.txt](https://onedrive.example.com/share/item123)",
);
expect(sent[0]?.entities).toEqual([
expect(firstSent.entities).toEqual([
{
type: "mention",
text: "<at>John</at>",

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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"]);
});
});

View File

@@ -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"]);
});
});