spring-security/javascript/test/webauthn-registration.test.js

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");
});
});
});
});
});