mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-11-04 00:28:54 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			280 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/*
 | 
						|
 * Copyright 2002-2024 the original author or authors.
 | 
						|
 *
 | 
						|
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
 * you may not use this file except in compliance with the License.
 | 
						|
 * You may obtain a copy of the License at
 | 
						|
 *
 | 
						|
 *      https://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 *
 | 
						|
 * Unless required by applicable law or agreed to in writing, software
 | 
						|
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
 * See the License for the specific language governing permissions and
 | 
						|
 * limitations under the License.
 | 
						|
 */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
import "./bootstrap.js";
 | 
						|
import { expect, util, Assertion } from "chai";
 | 
						|
import { setupRegistration } from "../lib/webauthn-registration.js";
 | 
						|
import webauthn from "../lib/webauthn-core.js";
 | 
						|
import { assert, fake, match, stub } from "sinon";
 | 
						|
 | 
						|
describe("webauthn-registration", () => {
 | 
						|
  before(() => {
 | 
						|
    Assertion.addProperty("visible", function () {
 | 
						|
      const obj = util.flag(this, "object");
 | 
						|
      new Assertion(obj).to.have.nested.property("style.display", "block");
 | 
						|
    });
 | 
						|
    Assertion.addProperty("hidden", function () {
 | 
						|
      const obj = util.flag(this, "object");
 | 
						|
      new Assertion(obj).to.have.nested.property("style.display", "none");
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  describe("bootstrap", () => {
 | 
						|
    let registerStub;
 | 
						|
    let registerButton;
 | 
						|
    let labelField;
 | 
						|
    let errorPopup;
 | 
						|
    let successPopup;
 | 
						|
    let deleteForms;
 | 
						|
    let ui;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      registerStub = stub(webauthn, "register").resolves(undefined);
 | 
						|
      errorPopup = {
 | 
						|
        style: {
 | 
						|
          display: undefined,
 | 
						|
        },
 | 
						|
        textContent: undefined,
 | 
						|
      };
 | 
						|
      successPopup = {
 | 
						|
        style: {
 | 
						|
          display: undefined,
 | 
						|
        },
 | 
						|
        textContent: undefined,
 | 
						|
      };
 | 
						|
      registerButton = {
 | 
						|
        addEventListener: fake(),
 | 
						|
      };
 | 
						|
      labelField = {
 | 
						|
        value: undefined,
 | 
						|
      };
 | 
						|
      deleteForms = [];
 | 
						|
      ui = {
 | 
						|
        getSuccess: function () {
 | 
						|
          return successPopup;
 | 
						|
        },
 | 
						|
        getError: function () {
 | 
						|
          return errorPopup;
 | 
						|
        },
 | 
						|
        getRegisterButton: function () {
 | 
						|
          return registerButton;
 | 
						|
        },
 | 
						|
        getLabelInput: function () {
 | 
						|
          return labelField;
 | 
						|
        },
 | 
						|
        getDeleteForms: function () {
 | 
						|
          return deleteForms;
 | 
						|
        },
 | 
						|
      };
 | 
						|
      global.window = {
 | 
						|
        location: {
 | 
						|
          href: {},
 | 
						|
        },
 | 
						|
      };
 | 
						|
      global.console = {
 | 
						|
        error: stub(),
 | 
						|
      };
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(() => {
 | 
						|
      registerStub.restore();
 | 
						|
      delete global.window;
 | 
						|
    });
 | 
						|
 | 
						|
    describe("when webauthn is not supported", () => {
 | 
						|
      beforeEach(() => {
 | 
						|
        delete global.window.PublicKeyCredential;
 | 
						|
      });
 | 
						|
 | 
						|
      it("does not set up a click event listener", async () => {
 | 
						|
        await setupRegistration({}, "/", ui);
 | 
						|
 | 
						|
        assert.notCalled(registerButton.addEventListener);
 | 
						|
      });
 | 
						|
 | 
						|
      it("shows an error popup", async () => {
 | 
						|
        await setupRegistration({}, "/", ui);
 | 
						|
 | 
						|
        expect(errorPopup).to.be.visible;
 | 
						|
        expect(errorPopup.textContent).to.equal("WebAuthn is not supported");
 | 
						|
        expect(successPopup).to.be.hidden;
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe("when webauthn is supported", () => {
 | 
						|
      beforeEach(() => {
 | 
						|
        global.window.PublicKeyCredential = fake();
 | 
						|
      });
 | 
						|
 | 
						|
      it("hides the popups", async () => {
 | 
						|
        await setupRegistration({}, "/", ui);
 | 
						|
 | 
						|
        expect(successPopup).to.be.hidden;
 | 
						|
        expect(errorPopup).to.be.hidden;
 | 
						|
      });
 | 
						|
 | 
						|
      it("sets up a click event listener on the register button", async () => {
 | 
						|
        await setupRegistration({}, "/some/path", ui);
 | 
						|
 | 
						|
        assert.calledOnceWithMatch(registerButton.addEventListener, "click", match.typeOf("function"));
 | 
						|
      });
 | 
						|
 | 
						|
      describe(`when the query string contains "success"`, () => {
 | 
						|
        beforeEach(() => {
 | 
						|
          global.window.location.search = "?success&continue=true";
 | 
						|
        });
 | 
						|
 | 
						|
        it("shows the success popup", async () => {
 | 
						|
          await setupRegistration({}, "/", ui);
 | 
						|
 | 
						|
          expect(successPopup).to.be.visible;
 | 
						|
          expect(errorPopup).to.be.hidden;
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      describe("when the register button is clicked", () => {
 | 
						|
        const headers = { "x-header": "value" };
 | 
						|
        const contextPath = "/some/path";
 | 
						|
 | 
						|
        beforeEach(async () => {
 | 
						|
          await setupRegistration(headers, contextPath, ui);
 | 
						|
        });
 | 
						|
 | 
						|
        it("hides all the popups", async () => {
 | 
						|
          successPopup.textContent = "dummy-content";
 | 
						|
          successPopup.style.display = "block";
 | 
						|
          errorPopup.textContent = "dummy-content";
 | 
						|
          errorPopup.style.display = "block";
 | 
						|
 | 
						|
          await registerButton.addEventListener.firstCall.lastArg();
 | 
						|
 | 
						|
          expect(successPopup).to.be.hidden;
 | 
						|
          expect(errorPopup).to.be.hidden;
 | 
						|
        });
 | 
						|
 | 
						|
        it("calls register", async () => {
 | 
						|
          labelField.value = "passkey name";
 | 
						|
 | 
						|
          await registerButton.addEventListener.firstCall.lastArg();
 | 
						|
 | 
						|
          assert.calledOnceWithExactly(registerStub, headers, contextPath, labelField.value);
 | 
						|
        });
 | 
						|
 | 
						|
        it("navigates to success page", async () => {
 | 
						|
          labelField.value = "passkey name";
 | 
						|
 | 
						|
          await registerButton.addEventListener.firstCall.lastArg();
 | 
						|
 | 
						|
          expect(global.window.location.href).to.equal(`${contextPath}/webauthn/register?success`);
 | 
						|
        });
 | 
						|
 | 
						|
        it("handles errors", async () => {
 | 
						|
          registerStub.rejects(new Error("The registration failed"));
 | 
						|
 | 
						|
          await registerButton.addEventListener.firstCall.lastArg();
 | 
						|
 | 
						|
          expect(errorPopup.textContent).to.equal("The registration failed");
 | 
						|
          expect(errorPopup).to.be.visible;
 | 
						|
          expect(successPopup).to.be.hidden;
 | 
						|
          assert.calledOnceWithMatch(
 | 
						|
            global.console.error,
 | 
						|
            match.instanceOf(Error).and(match.has("message", "The registration failed")),
 | 
						|
          );
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      describe("delete", () => {
 | 
						|
        beforeEach(() => {
 | 
						|
          global.fetch = fake.resolves({ ok: true });
 | 
						|
        });
 | 
						|
 | 
						|
        afterEach(() => {
 | 
						|
          delete global.fetch;
 | 
						|
        });
 | 
						|
 | 
						|
        it("no errors when no forms", async () => {
 | 
						|
          await setupRegistration({}, "/some/path", ui);
 | 
						|
        });
 | 
						|
 | 
						|
        it("sets up forms for fetch", async () => {
 | 
						|
          const deleteFormOne = {
 | 
						|
            addEventListener: fake(),
 | 
						|
          };
 | 
						|
          const deleteFormTwo = {
 | 
						|
            addEventListener: fake(),
 | 
						|
          };
 | 
						|
          deleteForms = [deleteFormOne, deleteFormTwo];
 | 
						|
 | 
						|
          await setupRegistration({}, "", ui);
 | 
						|
 | 
						|
          assert.calledOnceWithMatch(deleteFormOne.addEventListener, "submit", match.typeOf("function"));
 | 
						|
          assert.calledOnceWithMatch(deleteFormTwo.addEventListener, "submit", match.typeOf("function"));
 | 
						|
        });
 | 
						|
 | 
						|
        describe("when the delete button is clicked", () => {
 | 
						|
          it("calls POST to the form action", async () => {
 | 
						|
            const contextPath = "/some/path";
 | 
						|
            const deleteForm = {
 | 
						|
              addEventListener: fake(),
 | 
						|
              action: `${contextPath}/webauthn/1234`,
 | 
						|
            };
 | 
						|
            deleteForms = [deleteForm];
 | 
						|
            const headers = {
 | 
						|
              "X-CSRF-TOKEN": "token",
 | 
						|
            };
 | 
						|
 | 
						|
            await setupRegistration(headers, contextPath, ui);
 | 
						|
 | 
						|
            const clickEvent = {
 | 
						|
              preventDefault: fake(),
 | 
						|
            };
 | 
						|
            await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
 | 
						|
            assert.calledOnce(clickEvent.preventDefault);
 | 
						|
            assert.calledOnceWithExactly(global.fetch, `/some/path/webauthn/1234`, {
 | 
						|
              method: "DELETE",
 | 
						|
              headers: {
 | 
						|
                "Content-Type": "application/json",
 | 
						|
                ...headers,
 | 
						|
              },
 | 
						|
            });
 | 
						|
            expect(global.window.location.href).to.equal(`/some/path/webauthn/register?success`);
 | 
						|
          });
 | 
						|
        });
 | 
						|
 | 
						|
        it("handles errors", async () => {
 | 
						|
          global.fetch = fake.rejects("Server threw an error");
 | 
						|
          global.window.location.href = "/initial/location";
 | 
						|
          const deleteForm = {
 | 
						|
            addEventListener: fake(),
 | 
						|
          };
 | 
						|
          deleteForms = [deleteForm];
 | 
						|
 | 
						|
          await setupRegistration({}, "", ui);
 | 
						|
          const clickEvent = { preventDefault: fake() };
 | 
						|
          await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
 | 
						|
 | 
						|
          expect(errorPopup).to.be.visible;
 | 
						|
          expect(errorPopup.textContent).to.equal("Server threw an error");
 | 
						|
          // URL does not change
 | 
						|
          expect(global.window.location.href).to.equal("/initial/location");
 | 
						|
        });
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |