diff --git a/extensions/msteams/src/graph-chat.test.ts b/extensions/msteams/src/graph-chat.test.ts deleted file mode 100644 index ba4c12ddd45..00000000000 --- a/extensions/msteams/src/graph-chat.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildTeamsFileInfoCard } from "./graph-chat.js"; - -describe("buildTeamsFileInfoCard", () => { - it("extracts a unique id from quoted etags and lowercases file extensions", () => { - expect( - buildTeamsFileInfoCard({ - eTag: '"{ABC-123},42"', - name: "Quarterly.Report.PDF", - webDavUrl: "https://sharepoint.example.com/file.pdf", - }), - ).toEqual({ - contentType: "application/vnd.microsoft.teams.card.file.info", - contentUrl: "https://sharepoint.example.com/file.pdf", - name: "Quarterly.Report.PDF", - content: { - uniqueId: "ABC-123", - fileType: "pdf", - }, - }); - }); - - it("keeps the raw etag when no version suffix exists and handles extensionless files", () => { - expect( - buildTeamsFileInfoCard({ - eTag: "plain-etag", - name: "README", - webDavUrl: "https://sharepoint.example.com/readme", - }), - ).toEqual({ - contentType: "application/vnd.microsoft.teams.card.file.info", - contentUrl: "https://sharepoint.example.com/readme", - name: "README", - content: { - uniqueId: "plain-etag", - fileType: "", - }, - }); - }); -}); diff --git a/extensions/msteams/src/graph-upload.test.ts b/extensions/msteams/src/graph-upload.test.ts index e69a4039fdb..572e5a0321d 100644 --- a/extensions/msteams/src/graph-upload.test.ts +++ b/extensions/msteams/src/graph-upload.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { withFetchPreconnect } from "../../../test/helpers/extensions/fetch-mock.js"; +import { buildTeamsFileInfoCard } from "./graph-chat.js"; import { resolveGraphChatId, uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js"; describe("graph upload helpers", () => { @@ -212,3 +213,41 @@ describe("resolveGraphChatId", () => { expect(result).toBeNull(); }); }); + +describe("buildTeamsFileInfoCard", () => { + it("extracts a unique id from quoted etags and lowercases file extensions", () => { + expect( + buildTeamsFileInfoCard({ + eTag: '"{ABC-123},42"', + name: "Quarterly.Report.PDF", + webDavUrl: "https://sharepoint.example.com/file.pdf", + }), + ).toEqual({ + contentType: "application/vnd.microsoft.teams.card.file.info", + contentUrl: "https://sharepoint.example.com/file.pdf", + name: "Quarterly.Report.PDF", + content: { + uniqueId: "ABC-123", + fileType: "pdf", + }, + }); + }); + + it("keeps the raw etag when no version suffix exists and handles extensionless files", () => { + expect( + buildTeamsFileInfoCard({ + eTag: "plain-etag", + name: "README", + webDavUrl: "https://sharepoint.example.com/readme", + }), + ).toEqual({ + contentType: "application/vnd.microsoft.teams.card.file.info", + contentUrl: "https://sharepoint.example.com/readme", + name: "README", + content: { + uniqueId: "plain-etag", + fileType: "", + }, + }); + }); +}); diff --git a/extensions/msteams/src/graph-users.test.ts b/extensions/msteams/src/graph-users.test.ts deleted file mode 100644 index 8b5f2b52dd0..00000000000 --- a/extensions/msteams/src/graph-users.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { searchGraphUsers } from "./graph-users.js"; -import { fetchGraphJson } from "./graph.js"; - -vi.mock("./graph.js", () => ({ - escapeOData: vi.fn((value: string) => value.replace(/'/g, "''")), - fetchGraphJson: vi.fn(), -})); - -describe("searchGraphUsers", () => { - beforeEach(() => { - vi.mocked(fetchGraphJson).mockReset(); - }); - - it("returns empty array for blank queries", async () => { - await expect(searchGraphUsers({ token: "token-1", query: " " })).resolves.toEqual([]); - expect(fetchGraphJson).not.toHaveBeenCalled(); - }); - - it("uses exact mail/upn filter lookup for email-like queries", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({ - value: [{ id: "user-1", displayName: "User One" }], - } as never); - - const result = await searchGraphUsers({ - token: "token-2", - query: "alice.o'hara@example.com", - }); - - expect(fetchGraphJson).toHaveBeenCalledWith({ - token: "token-2", - path: "/users?$filter=(mail%20eq%20'alice.o''hara%40example.com'%20or%20userPrincipalName%20eq%20'alice.o''hara%40example.com')&$select=id,displayName,mail,userPrincipalName", - }); - expect(result).toEqual([{ id: "user-1", displayName: "User One" }]); - }); - - it("uses displayName search with eventual consistency and custom top", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({ - value: [{ id: "user-2", displayName: "Bob" }], - } as never); - - const result = await searchGraphUsers({ - token: "token-3", - query: "bob", - top: 25, - }); - - expect(fetchGraphJson).toHaveBeenCalledWith({ - token: "token-3", - path: "/users?$search=%22displayName%3Abob%22&$select=id,displayName,mail,userPrincipalName&$top=25", - headers: { ConsistencyLevel: "eventual" }, - }); - expect(result).toEqual([{ id: "user-2", displayName: "Bob" }]); - }); - - it("falls back to default top and empty value handling", async () => { - vi.mocked(fetchGraphJson).mockResolvedValueOnce({} as never); - - await expect(searchGraphUsers({ token: "token-4", query: "carol" })).resolves.toEqual([]); - expect(fetchGraphJson).toHaveBeenCalledWith({ - token: "token-4", - path: "/users?$search=%22displayName%3Acarol%22&$select=id,displayName,mail,userPrincipalName&$top=10", - headers: { ConsistencyLevel: "eventual" }, - }); - }); -}); diff --git a/extensions/msteams/src/graph.test.ts b/extensions/msteams/src/graph.test.ts index a027070d1db..de393639c77 100644 --- a/extensions/msteams/src/graph.test.ts +++ b/extensions/msteams/src/graph.test.ts @@ -27,6 +27,7 @@ vi.mock("./token.js", () => ({ resolveMSTeamsCredentials: resolveMSTeamsCredentialsMock, })); +import { searchGraphUsers } from "./graph-users.js"; import { escapeOData, fetchGraphJson, @@ -162,4 +163,63 @@ describe("msteams graph helpers", () => { ); expect(calls[1]).toContain("/teams/team%2Fops/channels?$select=id,displayName"); }); + + it("returns no graph users for blank queries", async () => { + globalThis.fetch = vi.fn() as typeof fetch; + await expect(searchGraphUsers({ token: "token-1", query: " " })).resolves.toEqual([]); + expect(globalThis.fetch).not.toHaveBeenCalled(); + }); + + it("uses exact mail or UPN lookup for email-like graph user queries", async () => { + globalThis.fetch = vi.fn(async () => { + return new Response(JSON.stringify({ value: [{ id: "user-1", displayName: "User One" }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + const result = await searchGraphUsers({ + token: "token-2", + query: "alice.o'hara@example.com", + }); + + expect(result).toEqual([{ id: "user-1", displayName: "User One" }]); + expect(String(vi.mocked(globalThis.fetch).mock.calls[0]?.[0])).toContain( + "/users?$filter=(mail%20eq%20'alice.o''hara%40example.com'%20or%20userPrincipalName%20eq%20'alice.o''hara%40example.com')&$select=id,displayName,mail,userPrincipalName", + ); + }); + + it("uses displayName search with eventual consistency and default top handling", async () => { + globalThis.fetch = vi.fn(async (input) => { + const url = typeof input === "string" ? input : String(input); + if (url.includes("displayName%3Abob")) { + return new Response(JSON.stringify({ value: [{ id: "user-2", displayName: "Bob" }] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + return new Response(JSON.stringify({}), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + + await expect(searchGraphUsers({ token: "token-3", query: "bob", top: 25 })).resolves.toEqual([ + { id: "user-2", displayName: "Bob" }, + ]); + await expect(searchGraphUsers({ token: "token-4", query: "carol" })).resolves.toEqual([]); + + const calls = vi.mocked(globalThis.fetch).mock.calls; + expect(String(calls[0]?.[0])).toContain( + "/users?$search=%22displayName%3Abob%22&$select=id,displayName,mail,userPrincipalName&$top=25", + ); + expect(calls[0]?.[1]).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ ConsistencyLevel: "eventual" }), + }), + ); + expect(String(calls[1]?.[0])).toContain( + "/users?$search=%22displayName%3Acarol%22&$select=id,displayName,mail,userPrincipalName&$top=10", + ); + }); });