UNPKG

@userfront/core

Version:
374 lines (321 loc) 11.3 kB
import { vi } from "vitest"; import Userfront from "../src/index.js"; import api from "../src/api.js"; import { unsetUser } from "../src/user.js"; import { createAccessToken, createIdToken, createRefreshToken, idTokenUserDefaults, createMfaRequiredResponse, setMfaRequired, } from "./config/utils.js"; import { assertAuthenticationDataMatches, mfaHeaders, noMfaHeaders, pkceParams, } from "./config/assertions.js"; import { loginWithPasswordMigrate } from "../src/password.migrate.js"; import { defaultHandleRedirect } from "../src/url.js"; import { defaultHandleTokens } from "../src/tokens.js"; import * as Pkce from "../src/pkce.js"; vi.mock("../src/api.js"); vi.mock("../src/refresh.js"); vi.mock("../src/url.js"); vi.mock("../src/tokens.js"); vi.mock("../src/pkce.js"); const tenantId = "abcd9876"; // Mock API response const mockResponse = { data: { tokens: { access: { value: createAccessToken() }, id: { value: createIdToken() }, refresh: { value: createRefreshToken() }, }, nonce: "nonce-value", redirectTo: "/dashboard", upstreamResponse: { arbitrary: "response", }, }, }; // Mock "MFA required" response const mockMfaRequiredResponse = createMfaRequiredResponse({ firstFactor: { strategy: "password", channel: "email", }, secondFactors: [ { strategy: "totp", channel: "authenticator", isConfiguredByUser: false, }, ], }); // Mock "PKCE required" response const mockPkceRequiredResponse = { data: { message: "PKCE required", authorizationCode: "auth-code", redirectTo: "my-app:/login", }, }; describe("loginWithPasswordMigrate()", () => { beforeEach(() => { Userfront.init(tenantId); vi.resetAllMocks(); unsetUser(); }); describe("with username & password", () => { it("should send a request, set access and ID cookies, and initiate nonce exchange", async () => { // Mock the API response api.post.mockImplementationOnce(() => mockResponse); // Call loginWithPasswordMigrate() const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", }; const data = await loginWithPasswordMigrate(payload); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, payload, noMfaHeaders ); // Should have returned the proper value expect(data).toEqual(mockResponse.data); // Should call defaultHandleTokens correctly expect(defaultHandleTokens).toHaveBeenCalledWith(data.tokens, data); // Should call defaultHandleRedirect correctly expect(defaultHandleRedirect).toHaveBeenCalledWith(data.redirectTo, data); }); it("should call handleUpstreamResponse before redirecting", async () => { // Mock the API response api.post.mockImplementationOnce(() => mockResponse); // Add a handleUpstreamResponse method const handleUpstreamResponse = vi.fn(); // Call loginWithPasswordMigrate() const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", handleUpstreamResponse, }; const data = await loginWithPasswordMigrate(payload); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, { emailOrUsername: payload.emailOrUsername, password: payload.password, }, noMfaHeaders ); // Should have returned the proper value expect(data).toEqual(mockResponse.data); // Should call defaultHandleTokens correctly expect(defaultHandleTokens).toHaveBeenCalledWith(data.tokens, data); // Should call defaultHandleRedirect correctly expect(defaultHandleRedirect).toHaveBeenCalledWith(data.redirectTo, data); // Should have called handleUpstreamResponse with the upstreamResponse expect(handleUpstreamResponse).toHaveBeenCalledWith( mockResponse.data.upstreamResponse, mockResponse.data ); }); it("should login and not redirect if redirect = false", async () => { // Update the userId to ensure it is overwritten const newAttrs = { userId: 1009, email: "someone-else@example.com", }; const mockResponseCopy = JSON.parse(JSON.stringify(mockResponse)); mockResponseCopy.data.tokens.id.value = createIdToken(newAttrs); // Mock the API response api.post.mockImplementationOnce(() => mockResponseCopy); // Call loginWithPasswordMigrate() with redirect = false const payload = { email: newAttrs.email, password: "something", }; const data = await loginWithPasswordMigrate({ redirect: false, ...payload, }); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, { emailOrUsername: payload.email, password: payload.password, }, noMfaHeaders ); // Should have returned the proper value expect(data).toEqual(mockResponseCopy.data); // Should call defaultHandleTokens correctly expect(defaultHandleTokens).toHaveBeenCalledWith(data.tokens, data); // Should not call defaultHandleRedirect expect(defaultHandleRedirect).not.toHaveBeenCalled(); }); it("should login and redirect to a provided path", async () => { api.post.mockImplementationOnce(() => mockResponse); // Call loginWithPasswordMigrate() with redirect = false const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", }; const redirect = "/path"; const data = await loginWithPasswordMigrate({ redirect, ...payload, }); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, payload, noMfaHeaders ); // Should have returned the proper value expect(data).toEqual(mockResponse.data); // Should call defaultHandleTokens correctly expect(defaultHandleTokens).toHaveBeenCalledWith(data.tokens, data); // Should call defaultHandleRedirect correctly expect(defaultHandleRedirect).toHaveBeenCalledWith(redirect, data); }); it("should set the noResetEmail option if provided", async () => { // Mock the API response api.post.mockImplementationOnce(() => mockResponse); // Call loginWithPasswordMigrate() const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", options: { noResetEmail: true, }, }; await loginWithPasswordMigrate(payload); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, { ...payload, options: { noResetEmail: true, }, }, noMfaHeaders ); }); it("should respond with whatever error the server sends", async () => { // Mock the API response const mockResponse = { response: { data: { error: "Bad Request", message: `That's a dumb email address.`, statusCode: 400, }, }, }; api.post.mockImplementationOnce(() => Promise.reject(mockResponse)); expect( loginWithPasswordMigrate({ email: "valid@example.com", password: "somevalidpassword", }) ).rejects.toEqual(new Error(mockResponse.response.data.message)); }); it("should handle an MFA Required response", async () => { // Return an MFA Required response api.post.mockImplementationOnce(() => mockMfaRequiredResponse); const payload = { email: "email@example.com", password: "something", }; const data = await loginWithPasswordMigrate(payload); // Should have sent the correct API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, { emailOrUsername: payload.email, password: payload.password, }, noMfaHeaders ); // Should have updated the MFA service state assertAuthenticationDataMatches(mockMfaRequiredResponse); // Should not have set the user object or redirected expect(defaultHandleTokens).not.toHaveBeenCalled(); expect(defaultHandleRedirect).not.toHaveBeenCalled(); // Should have returned MFA options & firstFactorToken expect(data).toEqual(mockMfaRequiredResponse.data); }); it("should include the firstFactorToken if this is the second factor", async () => { // Set up the MFA service setMfaRequired(); api.post.mockImplementationOnce(() => mockResponse); const payload = { email: "email@example.com", password: "something", }; await loginWithPasswordMigrate(payload); // Should have sent the correct API request, with MFA headers expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, { emailOrUsername: payload.email, password: payload.password, }, mfaHeaders ); }); describe("with PKCE", () => { it("login: should send a PKCE request if PKCE is required", async () => { Pkce.getPkceRequestQueryParams.mockImplementationOnce(() => ({ code_challenge: "code", })); // Mock the API response api.post.mockImplementationOnce(() => mockResponse); // Call loginWithPasswordMigrate() const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", }; await loginWithPasswordMigrate(payload); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, payload, pkceParams("code") ); }); it("login: should handle a PKCE Required response", async () => { Pkce.getPkceRequestQueryParams.mockImplementationOnce(() => ({ code_challenge: "code", })); api.post.mockImplementationOnce(() => mockPkceRequiredResponse); // Call loginWithPasswordMigrate() const payload = { emailOrUsername: idTokenUserDefaults.email, password: "something", }; const data = await loginWithPasswordMigrate(payload); // Should have sent the proper API request expect(api.post).toHaveBeenCalledWith( `/tenants/${tenantId}/auth/password/migrate`, payload, pkceParams("code") ); // Should have requested PKCE redirect with the correct params expect(Pkce.defaultHandlePkceRequired).toHaveBeenCalledWith( data.authorizationCode, data.redirectTo, data ); }); }); }); });