it("extracts code and state from a valid callback URL", () => { const input = `${MSTEAMS_OAUTH_REDIRECT_URI}?code=abc123&state=${expectedState}`; const result = parseCallbackInput(input, expectedState);
expect(result).toEqual({ code: "abc123", state: expectedState });
});
it("returns error when code is missing from URL", () => { const input = `${MSTEAMS_OAUTH_REDIRECT_URI}?state=${expectedState}`; const result = parseCallbackInput(input, expectedState);
expect(result).toEqual({ error: "Missing 'code' parameter in URL" });
});
it("rejects bare authorization codes to prevent CSRF bypass", () => { const result = parseCallbackInput("bare-code-value", expectedState);
expect(result).toEqual({
error: "Paste the full redirect URL (including code and state parameters), not just the authorization code.",
});
});
it("returns error on empty input", () => { const result = parseCallbackInput("", expectedState);
expect(result).toEqual({ error: "No input provided" });
});
it("returns error when state is missing from a valid URL (CSRF protection)", () => { const input = `${MSTEAMS_OAUTH_REDIRECT_URI}?code=abc123`; const result = parseCallbackInput(input, expectedState);
expect(result).toEqual({
error: "Missing 'state' parameter in URL. Paste the full redirect URL.",
});
});
it("rejects bare codes even when expectedState is empty", () => { const result = parseCallbackInput("bare-code", "");
expect(result).toEqual({
error: "Paste the full redirect URL (including code and state parameters), not just the authorization code.",
});
});
});
describe("exchangeMSTeamsCodeForTokens", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
expect(tokens.accessToken).toBe("at-123");
expect(tokens.refreshToken).toBe("rt-456");
expect(tokens.scopes).toEqual(["ChatMessage.Send", "offline_access"]); // expiresAt should be roughly now + 3600s - 300s
expect(tokens.expiresAt).toBeGreaterThanOrEqual(now + 3300 * 1000 - 1000);
expect(tokens.expiresAt).toBeLessThanOrEqual(afterExchange + 3300 * 1000 + 2000);
// Verify the request was well-formed
expect(fetchSpy).toHaveBeenCalledOnce(); const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
expect(url).toBe(buildMSTeamsTokenEndpoint("tenant-1")); const body = new URLSearchParams(init.body as string);
expect(body.get("client_id")).toBe("client-1");
expect(body.get("client_secret")).toBe("secret-1");
expect(body.get("grant_type")).toBe("authorization_code");
expect(body.get("code")).toBe("auth-code");
expect(body.get("code_verifier")).toBe("pkce-verifier");
expect(body.get("redirect_uri")).toBe(MSTEAMS_OAUTH_REDIRECT_URI);
});
it("throws on a 400 error response", async () => {
fetchSpy.mockResolvedValueOnce( new Response(JSON.stringify({ error: "invalid_grant" }), {
status: 400,
headers: { "Content-Type": "application/json" },
}),
);
it("refreshes tokens using refresh_token grant and keeps old refresh token when Azure omits it", async () => { const now = Date.now();
fetchSpy.mockResolvedValueOnce(
responseJson({
access_token: "new-at", // Azure sometimes does not return a new refresh_token
expires_in: 3600,
scope: "ChatMessage.Send offline_access",
}),
);
expect(tokens.accessToken).toBe("new-at"); // Old refresh token should be preserved
expect(tokens.refreshToken).toBe("original-rt");
expect(tokens.scopes).toEqual(["ChatMessage.Send", "offline_access"]);
expect(tokens.expiresAt).toBeGreaterThanOrEqual(now + 3300 * 1000 - 1000);
// Verify the request body includes refresh_token grant type const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit]; const body = new URLSearchParams(init.body as string);
expect(body.get("grant_type")).toBe("refresh_token");
expect(body.get("refresh_token")).toBe("original-rt");
expect(body.get("client_secret")).toBe("secret-1");
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.