mirror of
https://github.com/bitwarden/browser.git
synced 2025-04-14 20:07:04 +02:00
* PM-2041 - (1) Bring over WebauthnApiService + required models from existing #5493 PR (2) Per discussion with Andreas, remove unnecessary methods from WebauthnApiService * PM-2041 - Rename responses folder to response to match rest of codebase * PM-2041 - Recreate BaseLoginViaWebAuthnComponent and then web implementation of it. * PM-2041 - Web routing module - add LoginViaWebAuthnComponent and associated route "login-with-passkey" * PM-2041 - InjectionTokens - add new navigator credentials injection token which provides the CredentialsContainer interface of the Credential Management API and exposes methods to request credentials and notify the user agent when events such as successful sign in or sign out happen * PM-2041 - Rename WebauthnApiService & abstraction to WebAuthnLoginApiService * PM-2041 - Rename WebauthnLoginApiService to WebAuthnAdminApiService * PM-2041 - Bring over first draft of webauthn-login.service + abstraction; register on jslib-services.module. * PM-2041 - Bring over web & base login component changes to add login with passkey button if feature flag enabled. * PM-2041 - WebAuthnAdminApi - update list of TODOs based on conversation with Andreas * PM-2041 - Login.module - cleanup todo after conversation w/ Andreas * PM-2041 - Move utils out of web and into common auth/utils and renamed to webauthn-utils * PM-2041 - Update userDecryptionOptions to support new webauthn prf decryption option * PM-2041 - (1) Recreate webauthn-login service with updated logic (2) Move files from webauthn to webauthn-login (3) Recreate webauthn-login.strategy with updated logic * PM-2041 - Remove completed TODO * PM-2041 - Fix login-via-webauthn component imports + fix name (missing n) * PM-2041 - Missed this change when renaming LoginViaWebAuthComponent to LoginViaWebAuthnComponent * PM-2041 - Add WebAuthnLoginApiService to jslib-services.module * PM-2041 - Remove unused param from WebAuthnLoginApiServiceAbstraction as we aren't supporting non-discoverable passkeys for MVP * PM-2041 - WebAuthnLoginApiService - remove email and target correct endpoint for getCredentialAssertionOptions(...) call * PM-2041 - WebAuthnLoginStrategy - (1) Remove unused dep (2) Add safeguard checks to setUserKey(...) logic similar to SSO login strategy * PM-2041 - BaseLoginViaWebAuthnComponent - Rewrite authenticate logic to use new methods on webAuthnLoginService * PM-2041 - UserDecryptionOptionsResponse - update naming of webAuthn options object to match server response * PM-2041 - WebAuthnLoginAssertionResponseRequest - (1) clean up TODO (2) Fix response property name to match server * PM-2041 - WebAuthnTokenRequest - must stringify device response b/c sending as form data * PM-2041 - AuthService - Add WebAuthnLoginCredentials and WebAuthnLoginStrategy support to auth service * PM-2041 - WIP tests for WebAuthnLoginService * PM-2041 - UserDecryptionOptions - Rename WebAuthnPrfOptions to singular WebAuthnPrfOption to match server * PM-2041 - Add TODO in login comp * PM-2041 - (1) Update WebAuthnLoginService.assertCredential(...) to add a check to ensure we cannot leak PRF credentials to the BW server by mistake (2) Add credential to view names for clarity (3) Add JS doc style comments to WebAuthnLoginServiceAbstraction * PM-2041 - Login.component.html - (1) Center passkey login button (2) Use correct user passkey icon * PM-2041 - Utils + tests - (1) Add new hexStringToArrayBuffer(...) method (2) Add tests for existing fromBufferToHex(...) (3) Add tests for new hexStringToArrayBuffer(...) method * PM-2041 - Fix broken import * PM-2041 - WebAuthnLoginResponseRequest - Adjust warning to be correct * PM-2041 - Webauthn-utils - createSymmetricKeyFromPrf(...) - add return type * PM-2041 - WebAuthnLoginService spec file - good progress on figuring out how to test passkey assertion process. Tests are passing, but need to add more setup logic around the MockAuthenticatorAssertionResponse in order to be able to confirm the output is correct. * PM-2041 - Utils + Utils Spec file changes - (1) Add new fromB64ToArrayBuffer(...) method (2) Add tests for existing fromBufferToB64(...) (3) Add tests for new fromB64ToArrayBuffer(...) method (4) Add round trip conversion tests in both directions * PM-2041 - Utils.spec - update round trip conversion tests between hex string and array buffer. * PM-2041 - WebAuthnLoginService.spec - assertCredential(...) happy path test passing * PM-2041 - WebAuthnLoginAssertionResponseRequest - Add interface * PM-2041 - WebAuthnLoginAssertionResponseRequest data should be UrlB64 strings per discussion w/ Andreas * PM-2041 - WebAuthnLoginService Spec file - Per feedback, reverse approaches to generating test data (go from array buffer to b64 strings vs the reverse) to avoid using math.random which can introduce test inconsistency * PM-2041 - Finish testing assertCredential(...) * PM-2041 - WebAuthnLoginService tests completed - tested logIn method * PM-2041 - Login html - add "or" between standard email login and passkey login * PM-2041 - WebAuthnLoginStrategy test start * PM-2041 - After rebase - BaseLoginViaWebAuthnComponent - Must rename ForceResetPasswordReason to ForceSetPasswordReason + refactor post login routing logic to match other auth owned flows. * PM-2401 - Desktop - login comp - fix desktop build * PM-2041 - Browser - login comp - fix build issue * PM-2401 - WIP on webauthn-login.strategy testing * PM-2401 - Finish testing webauthn login strategy * PM-2041 - WebAuthnAdminApiService renamed to WebAuthnLoginAdminApiService * PM-2041 - Remove unnecessary comment * PM-2041 - Per PR feedback, remove noMargin and just add mb-3 * PM-2041 - Per PR feedback, remove unused 2FA and remember email logic (2FA isn't supported right now and we aren't using non-discoverable credentials so we aren't using a user entered email) * PM-2401 - BaseLoginViaWebAuthnComponent - improve error handling to allow users to retry w/ another passkey * PM-2401 - Per PR feedback, provide translated message to cover all invalid passkey scenarios. * PM-2401 - WebAuthnLoginService - per PR feedback, remove unnecessary from * PM-2041 - WebAuthnLoginCredentialAssertionView - per PR feedback, use actual key type * PM-2401 - Per PR feedback, remove WebAuthnLoginStrategy constructor as it is identical to its super class constructor * PM-2041 - WebAuthnLoginService tests - use first value from to improve tests * PM-2401 - Fix WebAuthnLoginService build issue after changing SymmetricCryptoKey to PrfKey * PM-2041 - WebAuthnLoginServiceAbstraction remove incorrect undefined from getCredentialAssertionOptions() abstraction * PM-2041 - Refacor WebAuthn login service tests based on PR feedback * PM-2041 - Per PR feedback, remove NAVIGATOR_CREDENTIALS injection token and just use WINDOW directly for WebAuthnLoginService * PM-2041 - WebAuthnLoginServiceAbstraction - per PR feedback, improve assertCredential jsdocs with return info * PM-2041 - Per PR feedback, update WebAuthnLoginStrategy logInTwoFactor(...) to return an exception if attempted to be called. * PM-2041 - WebAuthnLoginResponseRequest - per PR feedback, replace fromBufferToB64(...) with fromBufferToUrlB64(...) * PM-2041 - AssertionOptionsResponse - use doc comment per PR feedback * PM-2041 - Per PR feedback, adjust location of helpers and mocks in WebAuthnLoginStrategy test file * PM-2041 - Adjust WebAuthnLoginService tests to take the WebAuthnLoginResponseRequest change to use fromBufferToUrlB64(...) into account to get tests to pass again * PM-2041 - WebAuthnLoginStrategy - adjust test name to match convention per PR feedback * PM-2041 - More test tweaks - (1) Rename method (2) Support strict * PM-2041 - Per PR feedback, AssertionOptionsResponse constructor should null check allowCredentials b/c it is optional * PM-2041 - Per PR Feedback, remove duplicated fromB64ToArrayBuffer(...) from utils and update tests. * PM-2041 - Per PR feedback, rename WebAuthnTokenRequest to WebAuthnLoginTokenRequest * PM-2041 - Per discussion with product and Andreas, add 2FA transition handling just in case we add server support in the future. * feat: stretch PRF key (#6927) * feat: stretch PRF key includes necessary utils -> service refactors * feat: add tests * [PM-2041] feat: assertion-options `POST` -> `GET` * [PM-2041] chore: remove unused properties * [PM-2041] fix: set private key * [PM-2041] feat: remove all 2FA related fields * [PM-2041] chore: clean up 2FA comments * [PM-2041] chore: document `webauthn-login-prf-crypto.service.abstraction.ts` * [PM-2041] chore: document webauthn login services --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
627 lines
24 KiB
TypeScript
627 lines
24 KiB
TypeScript
import * as path from "path";
|
|
|
|
import { Utils } from "./utils";
|
|
|
|
describe("Utils Service", () => {
|
|
describe("getDomain", () => {
|
|
it("should fail for invalid urls", () => {
|
|
expect(Utils.getDomain(null)).toBeNull();
|
|
expect(Utils.getDomain(undefined)).toBeNull();
|
|
expect(Utils.getDomain(" ")).toBeNull();
|
|
expect(Utils.getDomain('https://bit!:"_&ward.com')).toBeNull();
|
|
expect(Utils.getDomain("bitwarden")).toBeNull();
|
|
});
|
|
|
|
it("should fail for data urls", () => {
|
|
expect(Utils.getDomain("data:image/jpeg;base64,AAA")).toBeNull();
|
|
});
|
|
|
|
it("should fail for about urls", () => {
|
|
expect(Utils.getDomain("about")).toBeNull();
|
|
expect(Utils.getDomain("about:")).toBeNull();
|
|
expect(Utils.getDomain("about:blank")).toBeNull();
|
|
});
|
|
|
|
it("should fail for file url", () => {
|
|
expect(Utils.getDomain("file:///C://somefolder/form.pdf")).toBeNull();
|
|
});
|
|
|
|
it("should handle urls without protocol", () => {
|
|
expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("wrong://bitwarden.com")).toBe("bitwarden.com");
|
|
});
|
|
|
|
it("should handle valid urls", () => {
|
|
expect(Utils.getDomain("bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("http://bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("https://bitwarden.com")).toBe("bitwarden.com");
|
|
|
|
expect(Utils.getDomain("www.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("http://www.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("https://www.bitwarden.com")).toBe("bitwarden.com");
|
|
|
|
expect(Utils.getDomain("vault.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("http://vault.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("https://vault.bitwarden.com")).toBe("bitwarden.com");
|
|
|
|
expect(Utils.getDomain("www.vault.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("http://www.vault.bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("https://www.vault.bitwarden.com")).toBe("bitwarden.com");
|
|
|
|
expect(
|
|
Utils.getDomain("user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
|
).toBe("bitwarden.com");
|
|
expect(
|
|
Utils.getDomain("http://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
|
).toBe("bitwarden.com");
|
|
expect(
|
|
Utils.getDomain("https://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
|
).toBe("bitwarden.com");
|
|
|
|
expect(Utils.getDomain("bitwarden.unknown")).toBe("bitwarden.unknown");
|
|
expect(Utils.getDomain("http://bitwarden.unknown")).toBe("bitwarden.unknown");
|
|
expect(Utils.getDomain("https://bitwarden.unknown")).toBe("bitwarden.unknown");
|
|
});
|
|
|
|
it("should handle valid urls with an underscore in subdomain", () => {
|
|
expect(Utils.getDomain("my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("http://my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
|
expect(Utils.getDomain("https://my_vault.bitwarden.com/")).toBe("bitwarden.com");
|
|
});
|
|
|
|
it("should support urls containing umlauts", () => {
|
|
expect(Utils.getDomain("bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getDomain("http://bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getDomain("https://bütwarden.com")).toBe("bütwarden.com");
|
|
|
|
expect(Utils.getDomain("subdomain.bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getDomain("http://subdomain.bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getDomain("https://subdomain.bütwarden.com")).toBe("bütwarden.com");
|
|
});
|
|
|
|
it("should support punycode urls", () => {
|
|
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getDomain("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
|
|
expect(Utils.getDomain("subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getDomain("http://subdomain.xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getDomain("https://subdomain.xn--btwarden-65a.com")).toBe(
|
|
"xn--btwarden-65a.com"
|
|
);
|
|
});
|
|
|
|
it("should support localhost", () => {
|
|
expect(Utils.getDomain("localhost")).toBe("localhost");
|
|
expect(Utils.getDomain("http://localhost")).toBe("localhost");
|
|
expect(Utils.getDomain("https://localhost")).toBe("localhost");
|
|
});
|
|
|
|
it("should support localhost with subdomain", () => {
|
|
expect(Utils.getDomain("subdomain.localhost")).toBe("localhost");
|
|
expect(Utils.getDomain("http://subdomain.localhost")).toBe("localhost");
|
|
expect(Utils.getDomain("https://subdomain.localhost")).toBe("localhost");
|
|
});
|
|
|
|
it("should support IPv4", () => {
|
|
expect(Utils.getDomain("192.168.1.1")).toBe("192.168.1.1");
|
|
expect(Utils.getDomain("http://192.168.1.1")).toBe("192.168.1.1");
|
|
expect(Utils.getDomain("https://192.168.1.1")).toBe("192.168.1.1");
|
|
});
|
|
|
|
it("should support IPv6", () => {
|
|
expect(Utils.getDomain("[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
expect(Utils.getDomain("http://[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
expect(Utils.getDomain("https://[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
});
|
|
|
|
it("should reject invalid hostnames", () => {
|
|
expect(Utils.getDomain("https://mywebsite.com$.mywebsite.com")).toBeNull();
|
|
expect(Utils.getDomain("https://mywebsite.com!.mywebsite.com")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getHostname", () => {
|
|
it("should fail for invalid urls", () => {
|
|
expect(Utils.getHostname(null)).toBeNull();
|
|
expect(Utils.getHostname(undefined)).toBeNull();
|
|
expect(Utils.getHostname(" ")).toBeNull();
|
|
expect(Utils.getHostname('https://bit!:"_&ward.com')).toBeNull();
|
|
});
|
|
|
|
it("should fail for data urls", () => {
|
|
expect(Utils.getHostname("data:image/jpeg;base64,AAA")).toBeNull();
|
|
});
|
|
|
|
it("should fail for about urls", () => {
|
|
expect(Utils.getHostname("about")).toBe("about");
|
|
expect(Utils.getHostname("about:")).toBeNull();
|
|
expect(Utils.getHostname("about:blank")).toBeNull();
|
|
});
|
|
|
|
it("should fail for file url", () => {
|
|
expect(Utils.getHostname("file:///C:/somefolder/form.pdf")).toBeNull();
|
|
});
|
|
|
|
it("should handle valid urls", () => {
|
|
expect(Utils.getHostname("bitwarden")).toBe("bitwarden");
|
|
expect(Utils.getHostname("http://bitwarden")).toBe("bitwarden");
|
|
expect(Utils.getHostname("https://bitwarden")).toBe("bitwarden");
|
|
|
|
expect(Utils.getHostname("bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getHostname("http://bitwarden.com")).toBe("bitwarden.com");
|
|
expect(Utils.getHostname("https://bitwarden.com")).toBe("bitwarden.com");
|
|
|
|
expect(Utils.getHostname("www.bitwarden.com")).toBe("www.bitwarden.com");
|
|
expect(Utils.getHostname("http://www.bitwarden.com")).toBe("www.bitwarden.com");
|
|
expect(Utils.getHostname("https://www.bitwarden.com")).toBe("www.bitwarden.com");
|
|
|
|
expect(Utils.getHostname("vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
|
expect(Utils.getHostname("http://vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
|
expect(Utils.getHostname("https://vault.bitwarden.com")).toBe("vault.bitwarden.com");
|
|
|
|
expect(Utils.getHostname("www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
|
expect(Utils.getHostname("http://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
|
expect(Utils.getHostname("https://www.vault.bitwarden.com")).toBe("www.vault.bitwarden.com");
|
|
|
|
expect(
|
|
Utils.getHostname("user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
|
).toBe("bitwarden.com");
|
|
expect(
|
|
Utils.getHostname("https://user:password@bitwarden.com:8080/password/sites?and&query#hash")
|
|
).toBe("bitwarden.com");
|
|
expect(Utils.getHostname("https://bitwarden.unknown")).toBe("bitwarden.unknown");
|
|
});
|
|
|
|
it("should handle valid urls with an underscore in subdomain", () => {
|
|
expect(Utils.getHostname("my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
|
expect(Utils.getHostname("http://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
|
expect(Utils.getHostname("https://my_vault.bitwarden.com/")).toBe("my_vault.bitwarden.com");
|
|
});
|
|
|
|
it("should support urls containing umlauts", () => {
|
|
expect(Utils.getHostname("bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getHostname("http://bütwarden.com")).toBe("bütwarden.com");
|
|
expect(Utils.getHostname("https://bütwarden.com")).toBe("bütwarden.com");
|
|
|
|
expect(Utils.getHostname("subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
|
expect(Utils.getHostname("http://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
|
expect(Utils.getHostname("https://subdomain.bütwarden.com")).toBe("subdomain.bütwarden.com");
|
|
});
|
|
|
|
it("should support punycode urls", () => {
|
|
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
expect(Utils.getHostname("xn--btwarden-65a.com")).toBe("xn--btwarden-65a.com");
|
|
|
|
expect(Utils.getHostname("subdomain.xn--btwarden-65a.com")).toBe(
|
|
"subdomain.xn--btwarden-65a.com"
|
|
);
|
|
expect(Utils.getHostname("http://subdomain.xn--btwarden-65a.com")).toBe(
|
|
"subdomain.xn--btwarden-65a.com"
|
|
);
|
|
expect(Utils.getHostname("https://subdomain.xn--btwarden-65a.com")).toBe(
|
|
"subdomain.xn--btwarden-65a.com"
|
|
);
|
|
});
|
|
|
|
it("should support localhost", () => {
|
|
expect(Utils.getHostname("localhost")).toBe("localhost");
|
|
expect(Utils.getHostname("http://localhost")).toBe("localhost");
|
|
expect(Utils.getHostname("https://localhost")).toBe("localhost");
|
|
});
|
|
|
|
it("should support localhost with subdomain", () => {
|
|
expect(Utils.getHostname("subdomain.localhost")).toBe("subdomain.localhost");
|
|
expect(Utils.getHostname("http://subdomain.localhost")).toBe("subdomain.localhost");
|
|
expect(Utils.getHostname("https://subdomain.localhost")).toBe("subdomain.localhost");
|
|
});
|
|
|
|
it("should support IPv4", () => {
|
|
expect(Utils.getHostname("192.168.1.1")).toBe("192.168.1.1");
|
|
expect(Utils.getHostname("http://192.168.1.1")).toBe("192.168.1.1");
|
|
expect(Utils.getHostname("https://192.168.1.1")).toBe("192.168.1.1");
|
|
});
|
|
|
|
it("should support IPv6", () => {
|
|
expect(Utils.getHostname("[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
expect(Utils.getHostname("http://[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
expect(Utils.getHostname("https://[2620:fe::fe]")).toBe("2620:fe::fe");
|
|
});
|
|
});
|
|
|
|
describe("newGuid", () => {
|
|
it("should create a valid guid", () => {
|
|
const validGuid =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
expect(Utils.newGuid()).toMatch(validGuid);
|
|
});
|
|
});
|
|
|
|
describe("fromByteStringToArray", () => {
|
|
it("should handle null", () => {
|
|
expect(Utils.fromByteStringToArray(null)).toEqual(null);
|
|
});
|
|
});
|
|
|
|
function runInBothEnvironments(testName: string, testFunc: () => void): void {
|
|
const environments = [
|
|
{ isNode: true, name: "Node environment" },
|
|
{ isNode: false, name: "non-Node environment" },
|
|
];
|
|
|
|
environments.forEach((env) => {
|
|
it(`${testName} in ${env.name}`, () => {
|
|
Utils.isNode = env.isNode;
|
|
testFunc();
|
|
});
|
|
});
|
|
}
|
|
|
|
const asciiHelloWorldArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100];
|
|
const b64HelloWorldString = "aGVsbG8gd29ybGQ=";
|
|
|
|
describe("fromBufferToB64(...)", () => {
|
|
const originalIsNode = Utils.isNode;
|
|
|
|
afterEach(() => {
|
|
Utils.isNode = originalIsNode;
|
|
});
|
|
|
|
runInBothEnvironments("should convert an ArrayBuffer to a b64 string", () => {
|
|
const buffer = new Uint8Array(asciiHelloWorldArray).buffer;
|
|
const b64String = Utils.fromBufferToB64(buffer);
|
|
expect(b64String).toBe(b64HelloWorldString);
|
|
});
|
|
|
|
runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => {
|
|
const buffer = new Uint8Array([]).buffer;
|
|
const b64String = Utils.fromBufferToB64(buffer);
|
|
expect(b64String).toBe("");
|
|
});
|
|
|
|
runInBothEnvironments("should return null for null input", () => {
|
|
const b64String = Utils.fromBufferToB64(null);
|
|
expect(b64String).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("fromB64ToArray(...)", () => {
|
|
runInBothEnvironments("should convert a b64 string to an Uint8Array", () => {
|
|
const expectedArray = new Uint8Array(asciiHelloWorldArray);
|
|
|
|
const resultArray = Utils.fromB64ToArray(b64HelloWorldString);
|
|
|
|
expect(resultArray).toEqual(expectedArray);
|
|
});
|
|
|
|
runInBothEnvironments("should return null for null input", () => {
|
|
const expectedArray = Utils.fromB64ToArray(null);
|
|
expect(expectedArray).toBeNull();
|
|
});
|
|
|
|
// Hmmm... this passes in browser but not in node
|
|
// as node doesn't throw an error for invalid base64 strings.
|
|
// It instead produces a buffer with the bytes that could be decoded
|
|
// and ignores the rest after an invalid character.
|
|
// https://github.com/nodejs/node/issues/8569
|
|
// This could be mitigated with a regex check before decoding...
|
|
// runInBothEnvironments("should throw an error for invalid base64 string", () => {
|
|
// const invalidB64String = "invalid base64";
|
|
// expect(() => {
|
|
// Utils.fromB64ToArrayBuffer(invalidB64String);
|
|
// }).toThrow();
|
|
// });
|
|
});
|
|
|
|
describe("Base64 and ArrayBuffer round trip conversions", () => {
|
|
const originalIsNode = Utils.isNode;
|
|
|
|
afterEach(() => {
|
|
Utils.isNode = originalIsNode;
|
|
});
|
|
|
|
runInBothEnvironments(
|
|
"should correctly round trip convert from ArrayBuffer to base64 and back",
|
|
() => {
|
|
// Start with a known ArrayBuffer
|
|
const originalArray = new Uint8Array(asciiHelloWorldArray);
|
|
const originalBuffer = originalArray.buffer;
|
|
|
|
// Convert ArrayBuffer to a base64 string
|
|
const b64String = Utils.fromBufferToB64(originalBuffer);
|
|
|
|
// Convert that base64 string back to an ArrayBuffer
|
|
const roundTrippedBuffer = Utils.fromB64ToArray(b64String).buffer;
|
|
const roundTrippedArray = new Uint8Array(roundTrippedBuffer);
|
|
|
|
// Compare the original ArrayBuffer with the round-tripped ArrayBuffer
|
|
expect(roundTrippedArray).toEqual(originalArray);
|
|
}
|
|
);
|
|
|
|
runInBothEnvironments(
|
|
"should correctly round trip convert from base64 to ArrayBuffer and back",
|
|
() => {
|
|
// Convert known base64 string to ArrayBuffer
|
|
const bufferFromB64 = Utils.fromB64ToArray(b64HelloWorldString).buffer;
|
|
|
|
// Convert the ArrayBuffer back to a base64 string
|
|
const roundTrippedB64String = Utils.fromBufferToB64(bufferFromB64);
|
|
|
|
// Compare the original base64 string with the round-tripped base64 string
|
|
expect(roundTrippedB64String).toBe(b64HelloWorldString);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("fromBufferToHex(...)", () => {
|
|
const originalIsNode = Utils.isNode;
|
|
|
|
afterEach(() => {
|
|
Utils.isNode = originalIsNode;
|
|
});
|
|
|
|
/**
|
|
* Creates a string that represents a sequence of hexadecimal byte values in ascending order.
|
|
* Each byte value corresponds to its position in the sequence.
|
|
*
|
|
* @param {number} length - The number of bytes to include in the string.
|
|
* @return {string} A string of hexadecimal byte values in sequential order.
|
|
*
|
|
* @example
|
|
* // Returns '000102030405060708090a0b0c0d0e0f101112...ff'
|
|
* createSequentialHexByteString(256);
|
|
*/
|
|
function createSequentialHexByteString(length: number) {
|
|
let sequentialHexString = "";
|
|
for (let i = 0; i < length; i++) {
|
|
// Convert the number to a hex string and pad with leading zeros if necessary
|
|
const hexByte = i.toString(16).padStart(2, "0");
|
|
sequentialHexString += hexByte;
|
|
}
|
|
return sequentialHexString;
|
|
}
|
|
|
|
runInBothEnvironments("should convert an ArrayBuffer to a hex string", () => {
|
|
const buffer = new Uint8Array([0, 1, 10, 16, 255]).buffer;
|
|
const hexString = Utils.fromBufferToHex(buffer);
|
|
expect(hexString).toBe("00010a10ff");
|
|
});
|
|
|
|
runInBothEnvironments("should handle an empty buffer", () => {
|
|
const buffer = new ArrayBuffer(0);
|
|
const hexString = Utils.fromBufferToHex(buffer);
|
|
expect(hexString).toBe("");
|
|
});
|
|
|
|
runInBothEnvironments(
|
|
"should correctly convert a large buffer containing a repeating sequence of all 256 unique byte values to hex",
|
|
() => {
|
|
const largeBuffer = new Uint8Array(1024).map((_, index) => index % 256).buffer;
|
|
const hexString = Utils.fromBufferToHex(largeBuffer);
|
|
const expectedHexString = createSequentialHexByteString(256).repeat(4);
|
|
expect(hexString).toBe(expectedHexString);
|
|
}
|
|
);
|
|
|
|
runInBothEnvironments("should correctly convert a buffer with a single byte to hex", () => {
|
|
const singleByteBuffer = new Uint8Array([0xab]).buffer;
|
|
const hexString = Utils.fromBufferToHex(singleByteBuffer);
|
|
expect(hexString).toBe("ab");
|
|
});
|
|
|
|
runInBothEnvironments(
|
|
"should correctly convert a buffer with an odd number of bytes to hex",
|
|
() => {
|
|
const oddByteBuffer = new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89]).buffer;
|
|
const hexString = Utils.fromBufferToHex(oddByteBuffer);
|
|
expect(hexString).toBe("0123456789");
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("hexStringToArrayBuffer(...)", () => {
|
|
test("should convert a hex string to an ArrayBuffer correctly", () => {
|
|
const hexString = "ff0a1b"; // Arbitrary hex string
|
|
const expectedResult = new Uint8Array([255, 10, 27]).buffer;
|
|
const result = Utils.hexStringToArrayBuffer(hexString);
|
|
expect(new Uint8Array(result)).toEqual(new Uint8Array(expectedResult));
|
|
});
|
|
|
|
test("should throw an error if the hex string length is not even", () => {
|
|
const hexString = "abc"; // Odd number of characters
|
|
expect(() => {
|
|
Utils.hexStringToArrayBuffer(hexString);
|
|
}).toThrow("HexString has to be an even length");
|
|
});
|
|
|
|
test("should convert a hex string representing zero to an ArrayBuffer correctly", () => {
|
|
const hexString = "00";
|
|
const expectedResult = new Uint8Array([0]).buffer;
|
|
const result = Utils.hexStringToArrayBuffer(hexString);
|
|
expect(new Uint8Array(result)).toEqual(new Uint8Array(expectedResult));
|
|
});
|
|
|
|
test("should handle an empty hex string", () => {
|
|
const hexString = "";
|
|
const expectedResult = new ArrayBuffer(0);
|
|
const result = Utils.hexStringToArrayBuffer(hexString);
|
|
expect(result).toEqual(expectedResult);
|
|
});
|
|
|
|
test("should convert a long hex string to an ArrayBuffer correctly", () => {
|
|
const hexString = "0102030405060708090a0b0c0d0e0f";
|
|
const expectedResult = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
|
|
.buffer;
|
|
const result = Utils.hexStringToArrayBuffer(hexString);
|
|
expect(new Uint8Array(result)).toEqual(new Uint8Array(expectedResult));
|
|
});
|
|
});
|
|
|
|
describe("ArrayBuffer and Hex string round trip conversions", () => {
|
|
runInBothEnvironments(
|
|
"should allow round-trip conversion from ArrayBuffer to hex and back",
|
|
() => {
|
|
const originalBuffer = new Uint8Array([10, 20, 30, 40, 255]).buffer; // arbitrary buffer
|
|
const hexString = Utils.fromBufferToHex(originalBuffer);
|
|
const roundTripBuffer = Utils.hexStringToArrayBuffer(hexString);
|
|
expect(new Uint8Array(roundTripBuffer)).toEqual(new Uint8Array(originalBuffer));
|
|
}
|
|
);
|
|
|
|
runInBothEnvironments(
|
|
"should allow round-trip conversion from hex to ArrayBuffer and back",
|
|
() => {
|
|
const hexString = "0a141e28ff"; // arbitrary hex string
|
|
const bufferFromHex = Utils.hexStringToArrayBuffer(hexString);
|
|
const roundTripHexString = Utils.fromBufferToHex(bufferFromHex);
|
|
expect(roundTripHexString).toBe(hexString);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe("mapToRecord", () => {
|
|
it("should handle null", () => {
|
|
expect(Utils.mapToRecord(null)).toEqual(null);
|
|
});
|
|
|
|
it("should handle empty map", () => {
|
|
expect(Utils.mapToRecord(new Map())).toEqual({});
|
|
});
|
|
|
|
it("should handle convert a Map to a Record", () => {
|
|
const map = new Map([
|
|
["key1", "value1"],
|
|
["key2", "value2"],
|
|
]);
|
|
expect(Utils.mapToRecord(map)).toEqual({ key1: "value1", key2: "value2" });
|
|
});
|
|
|
|
it("should handle convert a Map to a Record with non-string keys", () => {
|
|
const map = new Map([
|
|
[1, "value1"],
|
|
[2, "value2"],
|
|
]);
|
|
const result = Utils.mapToRecord(map);
|
|
expect(result).toEqual({ 1: "value1", 2: "value2" });
|
|
expect(Utils.recordToMap(result)).toEqual(map);
|
|
});
|
|
|
|
it("should not convert an object if it's not a map", () => {
|
|
const obj = { key1: "value1", key2: "value2" };
|
|
expect(Utils.mapToRecord(obj as any)).toEqual(obj);
|
|
});
|
|
});
|
|
|
|
describe("recordToMap", () => {
|
|
it("should handle null", () => {
|
|
expect(Utils.recordToMap(null)).toEqual(null);
|
|
});
|
|
|
|
it("should handle empty record", () => {
|
|
expect(Utils.recordToMap({})).toEqual(new Map());
|
|
});
|
|
|
|
it("should handle convert a Record to a Map", () => {
|
|
const record = { key1: "value1", key2: "value2" };
|
|
expect(Utils.recordToMap(record)).toEqual(new Map(Object.entries(record)));
|
|
});
|
|
|
|
it("should handle convert a Record to a Map with non-string keys", () => {
|
|
const record = { 1: "value1", 2: "value2" };
|
|
const result = Utils.recordToMap(record);
|
|
expect(result).toEqual(
|
|
new Map([
|
|
[1, "value1"],
|
|
[2, "value2"],
|
|
])
|
|
);
|
|
expect(Utils.mapToRecord(result)).toEqual(record);
|
|
});
|
|
|
|
it("should not convert an object if already a map", () => {
|
|
const map = new Map([
|
|
["key1", "value1"],
|
|
["key2", "value2"],
|
|
]);
|
|
expect(Utils.recordToMap(map as any)).toEqual(map);
|
|
});
|
|
});
|
|
|
|
describe("encodeRFC3986URIComponent", () => {
|
|
it("returns input string with expected encoded chars", () => {
|
|
expect(Utils.encodeRFC3986URIComponent("test'user@example.com")).toBe(
|
|
"test%27user%40example.com"
|
|
);
|
|
expect(Utils.encodeRFC3986URIComponent("(test)user@example.com")).toBe(
|
|
"%28test%29user%40example.com"
|
|
);
|
|
expect(Utils.encodeRFC3986URIComponent("testuser!@example.com")).toBe(
|
|
"testuser%21%40example.com"
|
|
);
|
|
expect(Utils.encodeRFC3986URIComponent("Test*User@example.com")).toBe(
|
|
"Test%2AUser%40example.com"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("normalizePath", () => {
|
|
it("removes a single traversal", () => {
|
|
expect(Utils.normalizePath("../test")).toBe("test");
|
|
});
|
|
|
|
it("removes deep traversals", () => {
|
|
expect(Utils.normalizePath("../../test")).toBe("test");
|
|
});
|
|
|
|
it("removes intermediate traversals", () => {
|
|
expect(Utils.normalizePath("test/../test")).toBe("test");
|
|
});
|
|
|
|
it("removes multiple encoded traversals", () => {
|
|
expect(
|
|
Utils.normalizePath("api/sends/access/..%2f..%2f..%2fapi%2fsends%2faccess%2fsendkey")
|
|
).toBe(path.normalize("api/sends/access/sendkey"));
|
|
});
|
|
});
|
|
|
|
describe("getUrl", () => {
|
|
it("assumes a http protocol if no protocol is specified", () => {
|
|
const urlString = "www.exampleapp.com.au:4000";
|
|
|
|
const actual = Utils.getUrl(urlString);
|
|
|
|
expect(actual.protocol).toBe("http:");
|
|
});
|
|
});
|
|
|
|
describe("daysRemaining", () => {
|
|
beforeAll(() => {
|
|
const now = new Date(2023, 9, 2, 10);
|
|
jest.spyOn(Date, "now").mockReturnValue(now.getTime());
|
|
});
|
|
|
|
afterAll(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it("should return 0 for equal dates", () => {
|
|
expect(Utils.daysRemaining(new Date(2023, 9, 2))).toBe(0);
|
|
expect(Utils.daysRemaining(new Date(2023, 9, 2, 12))).toBe(0);
|
|
});
|
|
|
|
it("should return 0 for dates in the past", () => {
|
|
expect(Utils.daysRemaining(new Date(2020, 5, 11))).toBe(0);
|
|
expect(Utils.daysRemaining(new Date(2023, 9, 1))).toBe(0);
|
|
});
|
|
|
|
it("should handle future dates", () => {
|
|
expect(Utils.daysRemaining(new Date(2023, 9, 3, 10))).toBe(1);
|
|
expect(Utils.daysRemaining(new Date(2023, 10, 12, 10))).toBe(41);
|
|
// leap year
|
|
expect(Utils.daysRemaining(new Date(2024, 9, 2, 10))).toBe(366);
|
|
});
|
|
});
|
|
});
|