mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-28 14:47:13 +08:00
# Conflicts: # backend/config/config.go # backend/handler/passcode.go # frontend/frontend-sdk/src/lib/client/PasscodeClient.ts # frontend/frontend-sdk/src/lib/client/PasswordClient.ts # frontend/frontend-sdk/tests/lib/client/PasswordClient.spec.ts
260 lines
9.5 KiB
TypeScript
260 lines
9.5 KiB
TypeScript
import {
|
|
InvalidPasscodeError,
|
|
MaxNumOfPasscodeAttemptsReachedError,
|
|
PasscodeClient,
|
|
PasscodeExpiredError,
|
|
TechnicalError,
|
|
TooManyRequestsError,
|
|
} from "../../../src";
|
|
import { Response } from "../../../src/lib/client/HttpClient";
|
|
|
|
const userID = "test-user-1";
|
|
const passcodeID = "test-passcode-1";
|
|
const emailID = "test-email-1";
|
|
const passcodeTTL = 180;
|
|
const passcodeRetryAfter = 180;
|
|
const passcodeValue = "123456";
|
|
let passcodeClient: PasscodeClient;
|
|
|
|
beforeEach(() => {
|
|
passcodeClient = new PasscodeClient("http://test.api");
|
|
});
|
|
|
|
describe("PasscodeClient.initialize()", () => {
|
|
it("should initialize a passcode login", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.ok = true;
|
|
response._decodedJSON = { id: passcodeID, ttl: passcodeTTL };
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "setTTL");
|
|
jest.spyOn(passcodeClient.state, "setActiveID");
|
|
jest.spyOn(passcodeClient.state, "write");
|
|
|
|
const passcode = await passcodeClient.initialize(userID);
|
|
expect(passcode.id).toEqual(passcodeID);
|
|
expect(passcode.ttl).toEqual(passcodeTTL);
|
|
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.setTTL).toHaveBeenCalledWith(
|
|
userID,
|
|
passcodeTTL
|
|
);
|
|
expect(passcodeClient.state.setActiveID).toHaveBeenCalledWith(
|
|
userID,
|
|
passcodeID
|
|
);
|
|
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.client.post).toHaveBeenCalledWith(
|
|
"/passcode/login/initialize",
|
|
{ user_id: userID }
|
|
);
|
|
});
|
|
|
|
it("should initialize a passcode with specified email id", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.ok = true;
|
|
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
jest.spyOn(passcodeClient.state, "setEmailID");
|
|
|
|
await passcodeClient.initialize(userID, emailID, true);
|
|
|
|
expect(passcodeClient.state.setEmailID).toHaveBeenCalledWith(
|
|
userID,
|
|
emailID
|
|
);
|
|
expect(passcodeClient.client.post).toHaveBeenCalledWith(
|
|
"/passcode/login/initialize",
|
|
{ user_id: userID, email_id: emailID }
|
|
);
|
|
});
|
|
|
|
it("should restore the previous passcode", async () => {
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID);
|
|
jest.spyOn(passcodeClient.state, "getEmailID").mockReturnValue(emailID);
|
|
|
|
await expect(passcodeClient.initialize(userID, emailID)).resolves.toEqual({
|
|
id: passcodeID,
|
|
ttl: passcodeTTL,
|
|
});
|
|
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.getTTL).toHaveBeenCalledWith(userID);
|
|
expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID);
|
|
expect(passcodeClient.state.getEmailID).toHaveBeenCalledWith(userID);
|
|
});
|
|
|
|
it("should throw an error as long as email backoff is active", async () => {
|
|
jest
|
|
.spyOn(passcodeClient.state, "getResendAfter")
|
|
.mockReturnValue(passcodeRetryAfter);
|
|
|
|
await expect(passcodeClient.initialize(userID, emailID)).rejects.toThrow(
|
|
TooManyRequestsError
|
|
);
|
|
|
|
expect(passcodeClient.state.getResendAfter).toHaveBeenCalledWith(userID);
|
|
});
|
|
|
|
it("should throw error and set retry after in state on too many request response from API", async () => {
|
|
const xhr = new XMLHttpRequest();
|
|
const response = new Response(xhr);
|
|
|
|
response.status = 429;
|
|
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
jest
|
|
.spyOn(response.headers, "get")
|
|
.mockReturnValue(`${passcodeRetryAfter}`);
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "setResendAfter");
|
|
jest.spyOn(passcodeClient.state, "write");
|
|
|
|
await expect(passcodeClient.initialize(userID)).rejects.toThrowError(
|
|
TooManyRequestsError
|
|
);
|
|
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.setResendAfter).toHaveBeenCalledWith(
|
|
userID,
|
|
passcodeRetryAfter
|
|
);
|
|
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1);
|
|
expect(response.headers.get).toHaveBeenCalledWith("Retry-After");
|
|
});
|
|
|
|
it.each`
|
|
status | error
|
|
${401} | ${"Unauthorized error"}
|
|
${500} | ${"Technical error"}
|
|
`(
|
|
"should throw error when API response is not ok",
|
|
async ({ status, error }) => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.status = status;
|
|
response.ok = status >= 200 && status <= 299;
|
|
|
|
passcodeClient.client.post = jest.fn().mockResolvedValue(response);
|
|
|
|
const passcode = passcodeClient.initialize("test-user-1");
|
|
await expect(passcode).rejects.toThrowError(error);
|
|
}
|
|
);
|
|
|
|
it("should throw error on API communication failure", async () => {
|
|
passcodeClient.client.post = jest
|
|
.fn()
|
|
.mockRejectedValue(new Error("Test error"));
|
|
|
|
const passcode = passcodeClient.initialize("test-user-1");
|
|
await expect(passcode).rejects.toThrowError("Test error");
|
|
});
|
|
});
|
|
|
|
describe("PasscodeClient.finalize()", () => {
|
|
it("should finalize a passcode login", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.ok = true;
|
|
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "reset");
|
|
jest.spyOn(passcodeClient.state, "write");
|
|
jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID);
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
|
|
await expect(
|
|
passcodeClient.finalize(userID, passcodeValue)
|
|
).resolves.toBeUndefined();
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.reset).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID);
|
|
expect(passcodeClient.client.post).toHaveBeenCalledWith(
|
|
"/passcode/login/finalize",
|
|
{ id: passcodeID, code: passcodeValue }
|
|
);
|
|
});
|
|
|
|
it("should throw error when using an invalid passcode", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.status = 401;
|
|
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID);
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
|
|
await expect(
|
|
passcodeClient.finalize(userID, passcodeValue)
|
|
).rejects.toThrow(InvalidPasscodeError);
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID);
|
|
});
|
|
|
|
it("should throw error when reaching max passcode attempts", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
response.status = 410;
|
|
|
|
jest.spyOn(passcodeClient.state, "read");
|
|
jest.spyOn(passcodeClient.state, "reset");
|
|
jest.spyOn(passcodeClient.state, "write");
|
|
jest.spyOn(passcodeClient.state, "getActiveID").mockReturnValue(passcodeID);
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
jest.spyOn(passcodeClient.client, "post").mockResolvedValue(response);
|
|
|
|
await expect(
|
|
passcodeClient.finalize(userID, passcodeValue)
|
|
).rejects.toThrow(MaxNumOfPasscodeAttemptsReachedError);
|
|
expect(passcodeClient.state.read).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.reset).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.write).toHaveBeenCalledTimes(1);
|
|
expect(passcodeClient.state.getActiveID).toHaveBeenCalledWith(userID);
|
|
});
|
|
|
|
it("should throw error when the passcode has expired", async () => {
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(0);
|
|
const finalizeResponse = passcodeClient.finalize(userID, passcodeValue);
|
|
await expect(finalizeResponse).rejects.toThrowError(PasscodeExpiredError);
|
|
});
|
|
|
|
it("should throw error when API response is not ok", async () => {
|
|
const response = new Response(new XMLHttpRequest());
|
|
passcodeClient.client.post = jest.fn().mockResolvedValue(response);
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
|
|
const finalizeResponse = passcodeClient.finalize(userID, passcodeValue);
|
|
await expect(finalizeResponse).rejects.toThrowError(TechnicalError);
|
|
});
|
|
|
|
it("should throw error on API communication failure", async () => {
|
|
passcodeClient.client.post = jest
|
|
.fn()
|
|
.mockRejectedValue(new Error("Test error"));
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
|
|
const finalizeResponse = passcodeClient.finalize(userID, passcodeValue);
|
|
await expect(finalizeResponse).rejects.toThrowError("Test error");
|
|
});
|
|
});
|
|
|
|
describe("PasscodeClient.getTTL()", () => {
|
|
it("should return passcode TTL", async () => {
|
|
jest.spyOn(passcodeClient.state, "getTTL").mockReturnValue(passcodeTTL);
|
|
expect(passcodeClient.getTTL(userID)).toEqual(passcodeTTL);
|
|
});
|
|
});
|
|
|
|
describe("PasscodeClient.getResendAfter()", () => {
|
|
it("should return passcode resend after seconds", async () => {
|
|
jest
|
|
.spyOn(passcodeClient.state, "getResendAfter")
|
|
.mockReturnValue(passcodeRetryAfter);
|
|
expect(passcodeClient.getResendAfter(userID)).toEqual(passcodeRetryAfter);
|
|
});
|
|
});
|