diff --git a/specs/PKPass.spec.cjs b/specs/PKPass.spec.cjs
new file mode 100644
index 0000000..1446d9a
--- /dev/null
+++ b/specs/PKPass.spec.cjs
@@ -0,0 +1,1334 @@
+// @ts-check
+///
+
+const {
+ describe,
+ expect,
+ beforeEach,
+ it,
+ afterEach,
+} = require("@jest/globals");
+
+const fs = require("node:fs/promises");
+const path = require("node:path");
+const { Buffer } = require("node:buffer");
+
+const { default: PKPass } = require("../lib/PKPass");
+const { default: FieldsArray } = require("../lib/FieldsArray");
+const { filesSymbol, freezeSymbol } = require("../lib/Bundle");
+const Messages = require("../lib/messages");
+
+const {
+ localizationSymbol,
+ certificatesSymbol,
+ propsSymbol,
+ passTypeSymbol,
+ importMetadataSymbol,
+ closePassSymbol,
+ createManifestSymbol,
+} = require("../lib/PKPass");
+
+console.log("PKPass", PKPass);
+debugger;
+
+/**
+ * @typedef {import("../lib/schemas").PassFields} PassFields
+ * @typedef {import("../lib/schemas").PassProps} PassProps
+ */
+
+describe("PKPass", () => {
+ /**
+ * @type {PKPass}
+ */
+
+ let pass;
+
+ const baseCerts = {
+ signerCert: "",
+ signerKey: "",
+ wwdr: "",
+ signerKeyPassphrase: "p477w0rb",
+ };
+
+ beforeEach(() => {
+ pass = new PKPass({});
+ jest.spyOn(console, "warn");
+ });
+
+ afterEach(() => {
+ // restore the spy created with spyOn
+ jest.restoreAllMocks();
+ });
+
+ describe("constructor", () => {
+ it("should warn about a non-object buffer parameter", () => {
+ debugger;
+ pass = new PKPass(undefined, baseCerts);
+
+ expect(console.warn).toHaveBeenCalledWith(
+ Messages.INIT.INVALID_BUFFERS.replace("%s", "undefined"),
+ );
+ });
+ });
+
+ describe("setBeacons", () => {
+ it("should reset instance.props['beacons'] if 'null' is passed as value", () => {
+ pass.setBeacons({
+ proximityUUID: "0000000000-00000000",
+ major: 4,
+ minor: 3,
+ relevantText: "This is not the Kevin you are looking for.",
+ });
+
+ if (!pass.props["beacons"]) {
+ throw new Error(
+ `Missing beacons object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["beacons"].length).toBe(1);
+
+ pass.setBeacons(null);
+
+ expect(pass.props["beacons"]).toBeUndefined();
+ });
+
+ it("should filter out invalid beacons objects", () => {
+ /** This is invalid, major should be greater than minor */
+ pass.setBeacons(
+ {
+ proximityUUID: "0000000000-00000000",
+ major: 2,
+ minor: 3,
+ relevantText: "This is not the Kevin you are looking for.",
+ },
+ // @ts-expect-error
+ {
+ major: 2,
+ minor: 3,
+ },
+ {
+ proximityUUID: "0000000000-00000",
+ major: 2,
+ minor: 1,
+ },
+ );
+
+ if (!pass.props["beacons"]) {
+ throw new Error(
+ `Missing beacons object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["beacons"].length).toBe(1);
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setBeacons(null)).toBeUndefined();
+ expect(
+ pass.setBeacons({
+ proximityUUID: "0000000000-00000000",
+ major: 2,
+ minor: 3,
+ relevantText: "This is not the Kevin you are looking for.",
+ }),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("setLocations", () => {
+ it("should reset instance.props['locations'] if 'null' is passed as value", () => {
+ pass.setLocations({
+ longitude: 0.25456342344,
+ latitude: 0.26665773234,
+ });
+
+ if (!pass.props["locations"]) {
+ throw new Error(
+ `Missing locations object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["locations"].length).toBe(1);
+
+ pass.setLocations(null);
+
+ expect(pass.props["locations"]).toBeUndefined();
+ });
+
+ it("should filter out invalid beacons objects", () => {
+ pass.setLocations(
+ {
+ // @ts-expect-error
+ longitude: "unknown",
+ // @ts-expect-error
+ latitude: "unknown",
+ },
+ {
+ altitude: "say hello from here",
+ longitude: 0.25456342344,
+ },
+ {
+ longitude: 0.25456342344,
+ latitude: 0.26665773234,
+ altitude: 12552.31233321,
+ relevantText:
+ /** Hi mom, see how do I fly! */
+ "Ciao mamma, guarda come volooo!",
+ },
+ );
+
+ if (!pass.props["locations"]) {
+ throw new Error(
+ `Missing locations object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["locations"].length).toBe(1);
+ expect(pass.props["locations"][0].longitude).toBe(0.25456342344);
+ expect(pass.props["locations"][0].latitude).toBe(0.26665773234);
+ expect(pass.props["locations"][0].altitude).toBe(12552.31233321);
+ expect(pass.props["locations"][0].relevantText).toBe(
+ "Ciao mamma, guarda come volooo!",
+ );
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setLocations(null)).toBeUndefined();
+ expect(
+ pass.setLocations({
+ longitude: 0.25456342344,
+ latitude: 0.26665773234,
+ altitude: 12552.31233321,
+ }),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("setNFC", () => {
+ it("should reset instance.props['nfc'] if 'null' is passed as value", () => {
+ pass.setNFC({
+ encryptionPublicKey: "mimmo",
+ message: "No message for you here",
+ });
+
+ expect(pass.props["nfc"]).toEqual({
+ encryptionPublicKey: "mimmo",
+ message: "No message for you here",
+ });
+
+ pass.setNFC(null);
+
+ expect(pass.props["nfc"]).toBeUndefined();
+ });
+
+ it("should throw on invalid objects received", () => {
+ expect(() =>
+ pass.setNFC({
+ // @ts-expect-error
+ requiresAuth: false,
+ encryptionPublicKey: "Nope",
+ }),
+ ).toThrow();
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setNFC(null)).toBeUndefined();
+ expect(
+ pass.setNFC({
+ encryptionPublicKey: "mimmo",
+ message: "No message for you here",
+ }),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("setExpirationDate", () => {
+ it("should reset instance.props['expirationDate'] if 'null' is passed as value", () => {
+ pass.setExpirationDate(new Date(2020, 6, 1, 0, 0, 0, 0));
+ // Month starts from 0 in Date Object when used this way, therefore
+ // we expect one month more
+ expect(pass.props["expirationDate"]).toBe("2020-07-01T00:00:00Z");
+
+ pass.setExpirationDate(null);
+
+ expect(pass.props["expirationDate"]).toBeUndefined();
+ });
+
+ it("expects a Date object as the only argument", () => {
+ pass.setExpirationDate(new Date(2020, 6, 1, 0, 0, 0, 0));
+ // Month starts from 0 in Date Object when used this way, therefore
+ // we expect one month more
+ expect(pass.props["expirationDate"]).toBe("2020-07-01T00:00:00Z");
+ });
+
+ it("should throw if an invalid date is received", () => {
+ // @ts-expect-error
+ expect(() => pass.setExpirationDate("32/18/228317")).toThrowError(
+ new TypeError(
+ "Cannot set expirationDate. Invalid date 32/18/228317",
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setExpirationDate(undefined)).toThrowError(
+ new TypeError(
+ "Cannot set expirationDate. Invalid date undefined",
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setExpirationDate(5)).toThrowError(
+ new TypeError("Cannot set expirationDate. Invalid date 5"),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setExpirationDate({})).toThrowError(
+ new TypeError(
+ "Cannot set expirationDate. Invalid date [object Object]",
+ ),
+ );
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setExpirationDate(null)).toBeUndefined();
+ expect(
+ pass.setExpirationDate(new Date(2020, 6, 1, 0, 0, 0, 0)),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("setRelevantDate", () => {
+ it("should reset instance.props['relevantDate'] if 'null' is passed as value", () => {
+ pass.setRelevantDate(new Date(2020, 6, 1, 0, 0, 0, 0));
+ // Month starts from 0 in Date Object when used this way, therefore
+ // we expect one month more
+ expect(pass.props["relevantDate"]).toBe("2020-07-01T00:00:00Z");
+
+ pass.setRelevantDate(null);
+
+ expect(pass.props["relevantDate"]).toBeUndefined();
+ });
+
+ it("expects a Date object as the only argument", () => {
+ pass.setRelevantDate(new Date("10-04-2021"));
+ expect(pass.props["relevantDate"]).toBe("2021-10-04T00:00:00Z");
+ });
+
+ it("should throw if an invalid date is received", () => {
+ // @ts-expect-error
+ expect(() => pass.setRelevantDate("32/18/228317")).toThrowError(
+ new TypeError(
+ "Cannot set relevantDate. Invalid date 32/18/228317",
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setRelevantDate(undefined)).toThrowError(
+ new TypeError(
+ "Cannot set relevantDate. Invalid date undefined",
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setRelevantDate(5)).toThrowError(
+ new TypeError("Cannot set relevantDate. Invalid date 5"),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.setRelevantDate({})).toThrowError(
+ new TypeError(
+ "Cannot set relevantDate. Invalid date [object Object]",
+ ),
+ );
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setRelevantDate(null)).toBeUndefined();
+ expect(
+ pass.setRelevantDate(new Date(2020, 6, 1, 0, 0, 0, 0)),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("setBarcodes", () => {
+ it("shouldn't apply changes if no data is passed", () => {
+ const props = pass.props["barcodes"] || [];
+ const oldAmountOfBarcodes = props?.length ?? 0;
+
+ pass.setBarcodes();
+
+ expect(pass.props["barcodes"]?.length || 0).toBe(
+ oldAmountOfBarcodes,
+ );
+ });
+
+ it("should autogenerate all the barcodes objects if a string is provided as message", () => {
+ pass.setBarcodes("28363516282");
+ expect(pass.props["barcodes"]?.length).toBe(4);
+ });
+
+ it("should save changes if object conforming to Schemas.Barcode are provided", () => {
+ pass.setBarcodes({
+ message: "28363516282",
+ format: "PKBarcodeFormatPDF417",
+ messageEncoding: "utf8",
+ });
+
+ expect(pass.props["barcodes"]?.length).toBe(1);
+ });
+
+ it("should add 'messageEncoding' if missing in valid Schema.Barcode parameters", () => {
+ pass.setBarcodes({
+ message: "28363516282",
+ format: "PKBarcodeFormatPDF417",
+ });
+
+ if (!pass.props["barcodes"]) {
+ throw new Error(
+ "Missing barcodes object inside props. Something is definitely wrong here.",
+ );
+ }
+
+ expect(pass.props["barcodes"][0].messageEncoding).toBe(
+ "iso-8859-1",
+ );
+ });
+
+ it("should ignore objects without 'message' property in Schema.Barcode", () => {
+ pass.setBarcodes(
+ {
+ format: "PKBarcodeFormatCode128",
+ message: "No one can validate meeee",
+ },
+ // @ts-expect-error
+ {
+ format: "PKBarcodeFormatPDF417",
+ },
+ );
+
+ if (!pass.props["barcodes"]) {
+ throw new Error(
+ "Missing barcodes object inside props. Something is definitely wrong here.",
+ );
+ }
+
+ expect(pass.props["barcodes"].length).toBe(1);
+ });
+
+ it("should ignore objects and values that not comply with Schema.Barcodes", () => {
+ /**
+ * @type {Parameters}
+ */
+
+ const setBarcodesArguments = [
+ // @ts-expect-error
+ 5,
+ // @ts-expect-error
+ 10,
+ // @ts-expect-error
+ 15,
+ {
+ message: "28363516282",
+ format: "PKBarcodeFormatPDF417",
+ },
+ // @ts-expect-error
+ 7,
+ // @ts-expect-error
+ 1,
+ ];
+
+ pass.setBarcodes(...setBarcodesArguments);
+
+ if (!pass.props["barcodes"]) {
+ throw new Error(
+ `Missing barcodes object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["barcodes"].length).toBe(1);
+ });
+
+ it("should reset barcodes content if parameter is null", () => {
+ pass.setBarcodes({
+ message: "28363516282",
+ format: "PKBarcodeFormatPDF417",
+ messageEncoding: "utf8",
+ });
+
+ if (!pass.props["barcodes"]) {
+ throw new Error(
+ `Missing barcodes object inside props. Something is definitely wrong here.`,
+ );
+ }
+
+ expect(pass.props["barcodes"].length).toBe(1);
+
+ pass.setBarcodes(null);
+ expect(pass.props["barcodes"]).toBe(undefined);
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.setBarcodes(null)).toBeUndefined();
+ expect(
+ pass.setBarcodes({
+ message: "28363516282",
+ format: "PKBarcodeFormatPDF417",
+ messageEncoding: "utf8",
+ }),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("transitType", () => {
+ it("should accept a new value only if the pass is a boarding pass", () => {
+ const passBP = new PKPass(
+ {
+ "pass.json": Buffer.from(
+ JSON.stringify({
+ boardingPass: {},
+ }),
+ ),
+ },
+ baseCerts,
+ {},
+ );
+
+ const passCP = new PKPass(
+ {
+ "pass.json": Buffer.from(
+ JSON.stringify({
+ coupon: {},
+ }),
+ ),
+ },
+ baseCerts,
+ {},
+ );
+
+ passBP.transitType = "PKTransitTypeAir";
+ expect(passBP.transitType).toBe("PKTransitTypeAir");
+
+ expect(
+ () => (passCP.transitType = "PKTransitTypeAir"),
+ ).toThrowError(
+ new TypeError(Messages.TRANSIT_TYPE.UNEXPECTED_PASS_TYPE),
+ );
+
+ /** boardingPass property doesn't exists, so it throws */
+ expect(() => passCP.transitType).toThrow();
+ });
+ });
+
+ describe("certificates", () => {
+ it("should throw an error if certificates provided are not complete or invalid", () => {
+ expect(() => {
+ // @ts-expect-error
+ pass.certificates = {
+ signerCert: "",
+ };
+ }).toThrow();
+
+ expect(() => {
+ pass.certificates = {
+ // @ts-expect-error
+ signerCert: 5,
+ // @ts-expect-error
+ signerKey: 3,
+ wwdr: "",
+ };
+ }).toThrow();
+
+ expect(() => {
+ pass.certificates = {
+ // @ts-expect-error
+ signerCert: undefined,
+ // @ts-expect-error
+ signerKey: null,
+ wwdr: "",
+ };
+ }).toThrow();
+ });
+
+ it("should accept complete object", () => {
+ pass.certificates = baseCerts;
+ expect(pass[certificatesSymbol]).toEqual(baseCerts);
+
+ pass = new PKPass({}, baseCerts);
+ expect(pass[certificatesSymbol]).toEqual(baseCerts);
+ });
+ });
+
+ describe("fields getters", () => {
+ it("should throw error if a type has not been defined", () => {
+ expect(() => pass.primaryFields).toThrowError(TypeError);
+ expect(() => pass.secondaryFields).toThrowError(TypeError);
+ expect(() => pass.auxiliaryFields).toThrowError(TypeError);
+ expect(() => pass.headerFields).toThrowError(TypeError);
+ expect(() => pass.backFields).toThrowError(TypeError);
+ });
+
+ it("should return an instance of FieldsArray if a type have been set", () => {
+ pass.type = "boardingPass";
+
+ expect(pass.primaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.secondaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.auxiliaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.headerFields).toBeInstanceOf(FieldsArray);
+ expect(pass.backFields).toBeInstanceOf(FieldsArray);
+
+ /** Resetting Fields, when setting type */
+ pass.type = "coupon";
+
+ expect(pass.primaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.secondaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.auxiliaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.headerFields).toBeInstanceOf(FieldsArray);
+ expect(pass.backFields).toBeInstanceOf(FieldsArray);
+
+ /** Resetting Fields, when setting type */
+ pass.type = "storeCard";
+
+ expect(pass.primaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.secondaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.auxiliaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.headerFields).toBeInstanceOf(FieldsArray);
+ expect(pass.backFields).toBeInstanceOf(FieldsArray);
+
+ /** Resetting Fields, when setting type */
+ pass.type = "eventTicket";
+
+ expect(pass.primaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.secondaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.auxiliaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.headerFields).toBeInstanceOf(FieldsArray);
+ expect(pass.backFields).toBeInstanceOf(FieldsArray);
+
+ /** Resetting Fields, when setting type */
+ pass.type = "generic";
+
+ expect(pass.primaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.secondaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.auxiliaryFields).toBeInstanceOf(FieldsArray);
+ expect(pass.headerFields).toBeInstanceOf(FieldsArray);
+ expect(pass.backFields).toBeInstanceOf(FieldsArray);
+ });
+ });
+
+ describe("type", () => {
+ describe("getter", () => {
+ it("should return undefined if no type have been setted", () => {
+ expect(pass.type).toBeUndefined();
+ });
+
+ it("should return a type if set through pass.json", () => {
+ pass.addBuffer(
+ "pass.json",
+ Buffer.from(
+ JSON.stringify({
+ boardingPass: {},
+ }),
+ ),
+ );
+
+ expect(pass.type).toBe("boardingPass");
+ });
+ });
+
+ describe("setter", () => {
+ it("should throw error if a non recognized type is assigned", () => {
+ expect(
+ () =>
+ // @ts-expect-error
+ (pass.type = "asfdg"),
+ ).toThrow();
+ });
+
+ it("should save the new type under a Symbol in class instance", () => {
+ pass.type = "boardingPass";
+ expect(pass[passTypeSymbol]).toBe("boardingPass");
+ });
+
+ it("should reset fields if they have been previously set", () => {
+ pass.type = "boardingPass";
+
+ const {
+ primaryFields,
+ secondaryFields,
+ auxiliaryFields,
+ headerFields,
+ backFields,
+ } = pass;
+
+ pass.type = "coupon";
+
+ expect(pass.primaryFields).not.toBe(primaryFields);
+ expect(pass.secondaryFields).not.toBe(secondaryFields);
+ expect(pass.auxiliaryFields).not.toBe(auxiliaryFields);
+ expect(pass.headerFields).not.toBe(headerFields);
+ expect(pass.backFields).not.toBe(backFields);
+ });
+
+ it("should delete the previous type if previously setted", () => {
+ pass.type = "boardingPass";
+ pass.type = "coupon";
+
+ expect(pass["boardingPass"]).toBeUndefined();
+ });
+ });
+ });
+
+ describe("languages", () => {
+ it("should get updated when translations gets added through localize", () => {
+ expect(pass.languages.length).toBe(0);
+ expect(pass.languages).toEqual([]);
+
+ pass.localize("en", {
+ buon_giorno: "Good Morning",
+ buona_sera: "Good Evening",
+ });
+
+ expect(pass.languages.length).toBe(1);
+ expect(pass.languages).toEqual(["en"]);
+ });
+
+ it("should get updated when translations are added through .addBuffer", () => {
+ const validTranslationStringsEN = `
+/* Insert Element menu item */
+"Insert Element" = "Insert Element";
+/* Error string used for unknown error types. */
+"ErrorString_1" = "An unknown error occurred.";
+ `;
+
+ pass.addBuffer(
+ "en.lproj/pass.strings",
+ Buffer.from(validTranslationStringsEN),
+ );
+
+ const validTranslationStringsIT = `
+"Insert Element" = "Inserisci elemento";
+"ErrorString_1" = "Un errore sconosciuto รจ accaduto";
+ `;
+
+ pass.addBuffer(
+ "it.lproj/pass.strings",
+ Buffer.from(validTranslationStringsIT),
+ );
+
+ expect(pass.languages).toEqual(["en", "it"]);
+ });
+ });
+
+ describe("localize", () => {
+ it("should fail throw if lang is not a string", () => {
+ // @ts-expect-error
+ expect(() => pass.localize(null)).toThrowError(
+ new TypeError(
+ Messages.LANGUAGES.INVALID_LANG.replace("%s", "object"),
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.localize(undefined)).toThrowError(
+ new TypeError(
+ Messages.LANGUAGES.INVALID_LANG.replace("%s", "undefined"),
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.localize(5)).toThrowError(
+ new TypeError(
+ Messages.LANGUAGES.INVALID_LANG.replace("%s", "number"),
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.localize(true)).toThrowError(
+ new TypeError(
+ Messages.LANGUAGES.INVALID_LANG.replace("%s", "boolean"),
+ ),
+ );
+
+ // @ts-expect-error
+ expect(() => pass.localize({})).toThrowError(
+ new TypeError(
+ Messages.LANGUAGES.INVALID_LANG.replace("%s", "object"),
+ ),
+ );
+ });
+
+ it("should warn developer if no translations have been passed", () => {
+ // @ts-expect-error
+ pass.localize("en");
+ pass.localize("en", {});
+
+ expect(console.warn).toHaveBeenCalledWith(
+ Messages.LANGUAGES.NO_TRANSLATIONS.replace("%s", "en"),
+ );
+
+ expect(console.warn).toHaveBeenCalledTimes(2);
+ });
+
+ it("should create a new language record if some translations are specifies", () => {
+ pass.localize("en", {
+ buon_giorno: "Good Morning",
+ buona_sera: "Good Evening",
+ });
+
+ expect(pass[localizationSymbol]["en"]).toEqual({
+ buon_giorno: "Good Morning",
+ buona_sera: "Good Evening",
+ });
+ });
+
+ it("should accept later translations and merge them with existing ones", () => {
+ pass.localize("it", {
+ say_hi: "ciao",
+ say_gb: "arrivederci",
+ });
+
+ pass.localize("it", {
+ say_good_morning: "buongiorno",
+ say_good_evening: "buonasera",
+ });
+
+ expect(pass[localizationSymbol]["it"]).toEqual({
+ say_hi: "ciao",
+ say_gb: "arrivederci",
+ say_good_morning: "buongiorno",
+ say_good_evening: "buonasera",
+ });
+ });
+
+ it("should delete a language, all of its translations and all of its files, when null is passed as parameter", () => {
+ pass.addBuffer("it.lproj/icon@3x.png", Buffer.alloc(0));
+ pass.addBuffer("en.lproj/icon@3x.png", Buffer.alloc(0));
+
+ pass.localize("it", null);
+ pass.localize("en", null);
+
+ expect(pass[localizationSymbol]["it"]).toBeUndefined();
+ expect(pass[localizationSymbol]["en"]).toBeUndefined();
+
+ expect(pass[filesSymbol]["it.lproj/icon@3x.png"]).toBeUndefined();
+ expect(pass[filesSymbol]["en.lproj/icon@3x.png"]).toBeUndefined();
+ });
+
+ it("should always return undefined", () => {
+ expect(pass.localize("it", null)).toBeUndefined();
+ expect(
+ pass.localize("it", {
+ say_good_morning: "buongiorno",
+ say_good_evening: "buonasera",
+ }),
+ ).toBeUndefined();
+ });
+ });
+
+ describe("addBuffer", () => {
+ it("should filter out silently manifest and signature files", () => {
+ pass.addBuffer("manifest.json", Buffer.alloc(0));
+ pass.addBuffer("signature", Buffer.alloc(0));
+
+ expect(Object.keys(pass[filesSymbol]).length).toBe(0);
+ });
+
+ it("should accept a pass.json only if not yet imported", () => {
+ pass.addBuffer(
+ "pass.json",
+ Buffer.from(
+ JSON.stringify({
+ boardingPass: {},
+ serialNumber: "555555",
+ }),
+ ),
+ );
+
+ expect(Object.keys(pass[filesSymbol]).length).toBe(1);
+
+ /** Adding it again */
+
+ pass.addBuffer(
+ "pass.json",
+ Buffer.from(
+ JSON.stringify({
+ boardingPass: {},
+ serialNumber: "555555",
+ }),
+ ),
+ );
+
+ /** Expecting it to get ignored */
+ expect(Object.keys(pass[filesSymbol]).length).toBe(1);
+ });
+
+ it("should accept personalization.json only if it is a valid JSON", () => {
+ pass.addBuffer(
+ "personalization.json",
+ Buffer.from(
+ JSON.stringify({
+ description:
+ "A test description for a test personalization",
+ requiredPersonalizationFields: [
+ "PKPassPersonalizationFieldName",
+ "PKPassPersonalizationFieldPostalCode",
+ "PKPassPersonalizationFieldEmailAddress",
+ ],
+ }),
+ ),
+ );
+
+ expect(pass[filesSymbol]["personalization.json"]).toBeInstanceOf(
+ Buffer,
+ );
+ });
+
+ it("should reject invalid personalization.json", () => {
+ pass.addBuffer(
+ "personalization.json",
+ Buffer.from(
+ JSON.stringify({
+ requiredPersonalizationFields: [
+ "PKPassPersonalizationFieldName",
+ "PKPassPersonalizationFieldEmailAddressaworng",
+ ],
+ }),
+ ),
+ );
+
+ expect(pass[filesSymbol]["personalization.json"]).toBeUndefined();
+ });
+
+ it("should redirect .strings files to localization", () => {
+ const validTranslationStrings = `
+/* Insert Element menu item */
+"Insert Element" = "Insert Element";
+/* Error string used for unknown error types. */
+"ErrorString_1" = "An unknown error occurred.";
+ `;
+
+ pass.addBuffer(
+ "en.lproj/pass.strings",
+ Buffer.from(validTranslationStrings),
+ );
+
+ expect(pass[filesSymbol]["en.lproj/pass.string"]).toBeUndefined();
+ expect(pass[localizationSymbol]["en"]).toEqual({
+ "Insert Element": "Insert Element",
+ ErrorString_1: "An unknown error occurred.",
+ });
+ });
+
+ it("should ignore invalid .strings files", () => {
+ const invalidTranslationStrings = `
+"Insert Element"="Insert Element
+"ErrorString_1= "An unknown error occurred."
+ `;
+
+ pass.addBuffer(
+ "en.lproj/pass.strings",
+ Buffer.from(invalidTranslationStrings),
+ );
+
+ expect(pass[filesSymbol]["en.lproj/pass.string"]).toBeUndefined();
+ expect(pass[localizationSymbol]["en"]).toBeUndefined();
+ });
+
+ it("should convert Windows paths to single UNIX slash", () => {
+ if (path.sep === "\\") {
+ pass.addBuffer("en.lproj\\icon@2x.png", Buffer.alloc(0));
+
+ expect(pass[filesSymbol]["en.lproj/icon@2x.png"]).toBeDefined();
+ expect(
+ pass[filesSymbol]["en.lproj\\icon@2x.png"],
+ ).toBeUndefined();
+ }
+ });
+ });
+
+ describe("[importMetadataSymbol]", () => {
+ it("should read data and set type", () => {
+ pass[importMetadataSymbol]({
+ boardingPass: {},
+ });
+
+ expect(pass.type).toBe("boardingPass");
+ });
+
+ it("should push fields to their own fields if a type is found", () => {
+ /**
+ * @type {PassFields["headerFields"][0]}
+ */
+
+ const baseField = {
+ key: "0",
+ value: "n/a",
+ label: "n/d",
+ };
+
+ pass[importMetadataSymbol]({
+ boardingPass: {
+ primaryFields: [{ ...baseField, key: "pf0" }],
+ secondaryFields: [{ ...baseField, key: "sf0" }],
+ auxiliaryFields: [{ ...baseField, key: "af0" }],
+ headerFields: [{ ...baseField, key: "hf0" }],
+ backFields: [{ ...baseField, key: "bf0" }],
+ },
+ });
+
+ expect(pass.primaryFields[0]).toEqual({ ...baseField, key: "pf0" });
+ expect(pass.secondaryFields[0]).toEqual({
+ ...baseField,
+ key: "sf0",
+ });
+ expect(pass.auxiliaryFields[0]).toEqual({
+ ...baseField,
+ key: "af0",
+ });
+ expect(pass.headerFields[0]).toEqual({ ...baseField, key: "hf0" });
+ expect(pass.backFields[0]).toEqual({ ...baseField, key: "bf0" });
+ });
+ });
+
+ describe("[closePassSymbol]", () => {
+ beforeEach(() => {
+ /** @type {PassProps} */
+
+ const partialPassJson = {
+ coupon: {
+ headerFields: [],
+ primaryFields: [],
+ auxiliaryFields: [],
+ secondaryFields: [],
+ backFields: [],
+ },
+ serialNumber: "h12kj5b12k3331",
+ };
+
+ pass.addBuffer(
+ "pass.json",
+ Buffer.from(JSON.stringify(partialPassJson)),
+ );
+ });
+
+ it("should add props to pass.json", () => {
+ pass.setBarcodes({
+ format: "PKBarcodeFormatQR",
+ message: "meh a test barcode",
+ });
+
+ pass.addBuffer("icon@2x.png", Buffer.alloc(0));
+
+ pass[closePassSymbol](true);
+
+ expect(
+ JSON.parse(pass[filesSymbol]["pass.json"].toString("utf-8")),
+ ).toEqual({
+ formatVersion: 1,
+ coupon: {
+ headerFields: [],
+ primaryFields: [],
+ auxiliaryFields: [],
+ secondaryFields: [],
+ backFields: [],
+ },
+ serialNumber: "h12kj5b12k3331",
+ barcodes: [
+ {
+ format: "PKBarcodeFormatQR",
+ message: "meh a test barcode",
+ messageEncoding: "iso-8859-1",
+ },
+ ],
+ });
+ });
+
+ it("Should warn user if no icons have been added to bundle", () => {
+ pass[closePassSymbol](true);
+
+ expect(console.warn).toHaveBeenCalledWith(
+ "At least one icon file is missing in your bundle. Your pass won't be openable by any Apple Device.",
+ );
+ });
+
+ it("should create back again pass.strings files", () => {
+ pass.localize("it", {
+ home: "casa",
+ ciao: "hello",
+ cosa: "thing",
+ });
+
+ pass[closePassSymbol](true);
+
+ expect(pass[filesSymbol]["it.lproj/pass.strings"].length).not.toBe(
+ 0,
+ );
+ });
+
+ it("should remove all personalisation if requirements are not met", () => {
+ pass.addBuffer(
+ "personalization.json",
+ Buffer.from(
+ JSON.stringify({
+ description:
+ "A test description for a test personalization",
+ requiredPersonalizationFields: [
+ "PKPassPersonalizationFieldName",
+ "PKPassPersonalizationFieldPostalCode",
+ "PKPassPersonalizationFieldEmailAddress",
+ ],
+ }),
+ ),
+ );
+
+ pass.addBuffer("personalizationLogo@2x.png", Buffer.alloc(0));
+
+ pass[closePassSymbol](true);
+
+ /** Pass is missing nfc data. */
+ expect(pass[filesSymbol]["personalization.json"]).toBeUndefined();
+ expect(
+ pass[filesSymbol]["personalizationLogo@2x.png"],
+ ).toBeUndefined();
+
+ /** Next: pass will miss personalizationLogo */
+
+ pass.addBuffer(
+ "personalization.json",
+ Buffer.from(
+ JSON.stringify({
+ description:
+ "A test description for a test personalization",
+ requiredPersonalizationFields: [
+ "PKPassPersonalizationFieldName",
+ "PKPassPersonalizationFieldPostalCode",
+ "PKPassPersonalizationFieldEmailAddress",
+ ],
+ }),
+ ),
+ );
+
+ pass.setNFC({
+ message: "nfc-encrypted-message",
+ encryptionPublicKey: "none",
+ });
+
+ pass[closePassSymbol](true);
+
+ expect(pass[filesSymbol]["personalization.json"]).toBeUndefined();
+ expect(
+ pass[filesSymbol]["personalizationLogo@2x.png"],
+ ).toBeUndefined();
+ });
+
+ it("should throw if no pass type have specified", () => {
+ pass.type = undefined; /** reset */
+
+ expect(() => pass[closePassSymbol](true)).toThrowError(
+ new TypeError(Messages.CLOSE.MISSING_TYPE),
+ );
+ });
+
+ it("should throw if a boarding pass is exported without a transitType", () => {
+ pass.type = "boardingPass";
+
+ expect(() => pass[closePassSymbol](true)).toThrowError(
+ new TypeError(Messages.CLOSE.MISSING_TRANSIT_TYPE),
+ );
+ });
+ });
+
+ describe("[createManifestSymbol]", () => {
+ it("should create a list of SHA-1s", () => {
+ pass.addBuffer("icon.png", Buffer.alloc(0));
+ pass.addBuffer("icon@2x.png", Buffer.alloc(0));
+ pass.addBuffer("icon@3x.png", Buffer.alloc(0));
+
+ expect(
+ JSON.parse(pass[createManifestSymbol]().toString("utf-8")),
+ ).toEqual({
+ "icon.png": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "icon@2x.png": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "icon@3x.png": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ });
+ });
+
+ it("List of Sha-1 of localized files should not contain Windows '\\' slash", () => {
+ if (path.sep === "\\") {
+ pass.addBuffer("en.lproj\\icon.png", Buffer.alloc(0));
+ pass.addBuffer("en.lproj\\icon@2x.png", Buffer.alloc(0));
+ pass.addBuffer("en.lproj\\icon@3x.png", Buffer.alloc(0));
+
+ const parsedResult = Object.keys(
+ JSON.parse(pass[createManifestSymbol]().toString("utf-8")),
+ );
+
+ expect(parsedResult[0]).toMatch(/en\.lproj\/icon\.png/);
+ expect(parsedResult[1]).toMatch(/en\.lproj\/icon@2x\.png/);
+ expect(parsedResult[2]).toMatch(/en\.lproj\/icon@3x\.png/);
+ }
+ });
+ });
+
+ describe("[static] from", () => {
+ it("should throw if source is unavailable", async () => {
+ expect.assertions(2);
+
+ try {
+ // @ts-expect-error
+ await PKPass.from();
+ } catch (err) {
+ expect(err).toEqual(
+ new TypeError(
+ Messages.FROM.MISSING_SOURCE.replace("%s", "undefined"),
+ ),
+ );
+ }
+
+ try {
+ // @ts-expect-error
+ await PKPass.from(null);
+ } catch (err) {
+ expect(err).toEqual(
+ new TypeError(
+ Messages.FROM.MISSING_SOURCE.replace("%s", "null"),
+ ),
+ );
+ }
+ });
+
+ describe("Prebuilt PKPass", () => {
+ it("should copy all source's buffers into current one", async () => {
+ pass.addBuffer(
+ "index@2x.png",
+ Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]),
+ );
+
+ const newPass = await PKPass.from(pass);
+
+ expect(
+ newPass[filesSymbol]["index@2x.png"].equals(
+ pass[filesSymbol]["index@2x.png"],
+ ),
+ ).toBe(true);
+ expect(newPass[filesSymbol]["index@2x.png"]).not.toBe(
+ pass[filesSymbol]["index@2x.png"],
+ );
+ });
+
+ it("should accept additional properties to be added to new buffer and ignore unknown props", async () => {
+ const newPass = await PKPass.from(pass, {
+ description: "mimmoh",
+ serialNumber: "626621523738123",
+ // @ts-expect-error
+ insert_here_invalid_unknown_parameter_name: false,
+ });
+
+ expect(newPass[propsSymbol]["description"]).toBe("mimmoh");
+ expect(newPass[propsSymbol]["serialNumber"]).toBe(
+ "626621523738123",
+ );
+ expect(
+ newPass[propsSymbol][
+ "insert_here_invalid_unknown_parameter_name"
+ ],
+ ).toBeUndefined();
+ });
+ });
+
+ describe("Template", () => {
+ it("should reject invalid templates", async () => {
+ const failures = await Promise.allSettled([
+ // @ts-expect-error
+ PKPass.from(5),
+ // @ts-expect-error
+ PKPass.from({}),
+ /** Empty model validation error */
+ PKPass.from({ model: "" }),
+ /** Missing model error and no certificates */
+ // @ts-expect-error
+ PKPass.from({ certificates: {} }),
+ // @ts-expect-error
+ PKPass.from(""),
+ // @ts-expect-error
+ PKPass.from(true),
+ // @ts-expect-error
+ PKPass.from([]),
+ ]);
+
+ for (const failure of failures) {
+ expect(failure.status).toBe("rejected");
+ }
+ });
+
+ it("should read model from fileSystem and props", async () => {
+ const footerFile = await fs.readFile(
+ path.resolve(
+ __dirname,
+ "../examples/models/exampleBooking.pass/footer.png",
+ ),
+ );
+
+ const newPass = await PKPass.from(
+ {
+ model: path.resolve(
+ __dirname,
+ "../examples/models/exampleBooking.pass",
+ ),
+ certificates: {
+ ...baseCerts,
+ },
+ },
+ {
+ voided: true,
+ },
+ );
+
+ expect(Object.keys(newPass[filesSymbol]).length).toBe(7);
+
+ expect(newPass[filesSymbol]["footer.png"]).not.toBeUndefined();
+ expect(
+ newPass[filesSymbol]["footer.png"].equals(footerFile),
+ ).toBe(true);
+ expect(newPass[filesSymbol]["footer.png"]).not.toBe(footerFile);
+
+ expect(newPass[propsSymbol]["voided"]).toBe(true);
+ });
+ });
+ });
+
+ describe("[static] pack", () => {
+ beforeEach(() => {
+ /** Bypass to avoid signature and manifest generation */
+ pass[freezeSymbol]();
+ });
+
+ it("should should throw error if not all the files passed are PKPasses", () => {
+ expect(
+ // @ts-expect-error
+ () => PKPass.pack(pass, "pass.json", pass),
+ ).toThrowError(new Error(Messages.PACK.INVALID));
+ });
+
+ it("should output a frozen bundle of bundles", () => {
+ const pkPassesBundle = PKPass.pack(pass, pass);
+
+ expect(
+ pkPassesBundle[filesSymbol]["packed-pass-1.pkpass"],
+ ).toBeInstanceOf(Buffer);
+ expect(
+ pkPassesBundle[filesSymbol]["packed-pass-2.pkpass"],
+ ).toBeInstanceOf(Buffer);
+
+ expect(pkPassesBundle.isFrozen).toBe(true);
+ });
+
+ it("should output a bundle with pkpasses mimetype", () => {
+ const pkPassesBundle = PKPass.pack(pass, pass);
+
+ expect(pkPassesBundle.mimeType).toBe(
+ "application/vnd.apple.pkpasses",
+ );
+ });
+ });
+});