From 8c7eea31681e2405eeea33d875ff747b4acfbf4f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Tue, 4 Apr 2023 21:05:09 +0200 Subject: [PATCH] Converted PKPass spec in cjs --- specs/PKPass.spec.cjs | 1334 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1334 insertions(+) create mode 100644 specs/PKPass.spec.cjs 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", + ); + }); + }); +});