mirror of
				https://github.com/teamhanko/hanko.git
				synced 2025-11-01 00:58:16 +08:00 
			
		
		
		
	 f5adfed572
			
		
	
	f5adfed572
	
	
	
		
			
			# 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);
 | |
|   });
 | |
| });
 |