diff --git a/spec/PKPass.ts b/spec/PKPass.ts index 873883a..076248e 100644 --- a/spec/PKPass.ts +++ b/spec/PKPass.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { filesSymbol, freezeSymbol } from "../lib/Bundle"; import FieldsArray from "../lib/FieldsArray"; import { PassProps } from "../lib/schemas"; +import * as Messages from "../lib/messages"; import { default as PKPass, localizationSymbol, @@ -292,22 +293,6 @@ describe("PKPass", () => { ); }); - it("should throw error if a boolean parameter is received", () => { - // @ts-expect-error - expect(() => pass.setBarcodes(true)).toThrowError( - TypeError, - "Expected Schema.Barcode in setBarcodes but no one is valid.", - ); - }); - - it("should ignore if a number parameter is received", () => { - // @ts-expect-error - expect(() => pass.setBarcodes(42)).toThrowError( - TypeError, - "Expected Schema.Barcode in setBarcodes but no one is valid.", - ); - }); - it("should autogenerate all the barcodes objects if a string is provided as message", () => { pass.setBarcodes("28363516282"); expect(pass.props["barcodes"].length).toBe(4); @@ -424,8 +409,9 @@ describe("PKPass", () => { () => (passCP.transitType = "PKTransitTypeAir"), ).toThrowError( TypeError, - "Cannot set transitType on a pass with type different from 'boardingPass'.", + Messages.TRANSIT_TYPE.UNEXPECTED_PASS_TYPE, ); + expect(passCP.transitType).toBeUndefined(); }); }); @@ -600,30 +586,30 @@ describe("PKPass", () => { it("should fail throw if lang is not a string", () => { expect(() => pass.localize(null)).toThrowError( TypeError, - "Cannot set localization. Expected a string for 'lang' but received a object", + Messages.LANGUAGES.INVALID_TYPE.replace("%s", "object"), ); expect(() => pass.localize(undefined)).toThrowError( TypeError, - "Cannot set localization. Expected a string for 'lang' but received a undefined", + Messages.LANGUAGES.INVALID_TYPE.replace("%s", "undefined"), ); // @ts-expect-error expect(() => pass.localize(5)).toThrowError( TypeError, - "Cannot set localization. Expected a string for 'lang' but received a number", + Messages.LANGUAGES.INVALID_TYPE.replace("%s", "number"), ); // @ts-expect-error expect(() => pass.localize(true)).toThrowError( TypeError, - "Cannot set localization. Expected a string for 'lang' but received a boolean", + Messages.LANGUAGES.INVALID_TYPE.replace("%s", "boolean"), ); // @ts-expect-error expect(() => pass.localize({})).toThrowError( TypeError, - "Cannot set localization. Expected a string for 'lang' but received a object", + Messages.LANGUAGES.INVALID_TYPE.replace("%s", "object"), ); }); @@ -820,12 +806,12 @@ describe("PKPass", () => { }); describe("[closePassSymbol]", () => { - it("should add props to pass.json", () => { + beforeEach(() => { pass.addBuffer( "pass.json", Buffer.from( JSON.stringify({ - boardingPass: { + coupon: { headerFields: [], primaryFields: [], auxiliaryFields: [], @@ -836,7 +822,9 @@ describe("PKPass", () => { } as PassProps), ), ); + }); + it("should add props to pass.json", () => { pass.setBarcodes({ format: "PKBarcodeFormatQR", message: "meh a test barcode", @@ -849,7 +837,7 @@ describe("PKPass", () => { expect( JSON.parse(pass[filesSymbol]["pass.json"].toString("utf-8")), ).toEqual({ - boardingPass: { + coupon: { headerFields: [], primaryFields: [], auxiliaryFields: [], @@ -869,23 +857,6 @@ describe("PKPass", () => { it("Should warn user if no icons have been added to bundle", () => { console.warn = jasmine.createSpy("log"); - - pass.addBuffer( - "pass.json", - Buffer.from( - JSON.stringify({ - boardingPass: { - headerFields: [], - primaryFields: [], - auxiliaryFields: [], - secondaryFields: [], - backFields: [], - }, - serialNumber: "h12kj5b12k3331", - } as PassProps), - ), - ); - pass[closePassSymbol](true); expect(console.warn).toHaveBeenCalledWith( @@ -894,22 +865,6 @@ describe("PKPass", () => { }); it("should create back again pass.strings files", () => { - pass.addBuffer( - "pass.json", - Buffer.from( - JSON.stringify({ - boardingPass: { - headerFields: [], - primaryFields: [], - auxiliaryFields: [], - secondaryFields: [], - backFields: [], - }, - serialNumber: "h12kj5b12k3331", - } as PassProps), - ), - ); - pass.localize("it", { home: "casa", ciao: "hello", @@ -978,6 +933,24 @@ describe("PKPass", () => { pass[filesSymbol]["personalizationLogo@2x.png"], ).toBeUndefined(); }); + + it("should throw if no pass type have specified", () => { + pass.type = undefined; /** reset */ + + expect(() => pass[closePassSymbol](true)).toThrowError( + 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( + TypeError, + Messages.CLOSE.MISSING_TRANSIT_TYPE, + ); + }); }); describe("[static] from", () => { diff --git a/src/FieldsArray.ts b/src/FieldsArray.ts index a9b5fb1..ab08494 100644 --- a/src/FieldsArray.ts +++ b/src/FieldsArray.ts @@ -1,4 +1,5 @@ import * as Schemas from "./schemas"; +import formatMessage, * as Messages from "./messages"; /** * Class to represent lower-level keys pass fields @@ -24,15 +25,22 @@ export default class FieldsArray extends Array { const validFields = fieldsData.reduce( (acc: Schemas.Field[], current: Schemas.Field) => { try { - Schemas.assertValidity(Schemas.Field, current); + Schemas.assertValidity( + Schemas.Field, + current, + Messages.FIELDS.INVALID, + ); } catch (err) { - console.warn(`Cannot add field: ${err}`); + console.warn(err); return acc; } if (this[poolSymbol].has(current.key)) { console.warn( - `Cannot add field with key '${current.key}': another field already owns this key. Ignored.`, + formatMessage( + Messages.FIELDS.REPEATED_KEY, + current.key, + ), ); return acc; } diff --git a/src/PKPass.ts b/src/PKPass.ts index a1dbb02..ccd22a5 100644 --- a/src/PKPass.ts +++ b/src/PKPass.ts @@ -6,6 +6,7 @@ import * as Signature from "./Signature"; import * as Strings from "./StringsUtils"; import * as Utils from "./utils"; import { Stream } from "stream"; +import formatMessage, * as Messages from "./messages"; /** Exporting for tests specs */ export const propsSymbol = Symbol("props"); @@ -77,7 +78,11 @@ export default class PKPass extends Bundle { JSON.stringify(source[propsSymbol]), ); } else { - Schemas.assertValidity(Schemas.Template, source); + Schemas.assertValidity( + Schemas.Template, + source, + Messages.TEMPLATE.INVALID, + ); buffers = await getModelFolderContents(source.model); certificates = source.certificates; @@ -180,7 +185,11 @@ export default class PKPass extends Bundle { */ public set certificates(certs: Schemas.CertificatesSchema) { - Schemas.assertValidity(Schemas.CertificatesSchema, certs); + Schemas.assertValidity( + Schemas.CertificatesSchema, + certs, + Messages.CERTIFICATES.INVALID, + ); this[certificatesSymbol] = certs; } @@ -202,13 +211,15 @@ export default class PKPass extends Bundle { */ public set transitType(value: Schemas.TransitType) { - if (!this[propsSymbol].boardingPass) { - throw new TypeError( - "Cannot set transitType on a pass with type different from 'boardingPass'.", - ); + if (this.type !== "boardingPass") { + throw new TypeError(Messages.TRANSIT_TYPE.UNEXPECTED_PASS_TYPE); } - Schemas.assertValidity(Schemas.TransitType, value); + Schemas.assertValidity( + Schemas.TransitType, + value, + Messages.TRANSIT_TYPE.INVALID, + ); this[propsSymbol]["boardingPass"].transitType = value; } @@ -291,7 +302,11 @@ export default class PKPass extends Bundle { */ public set type(type: Schemas.PassTypesProps) { - Schemas.assertValidity(Schemas.PassType, type); + Schemas.assertValidity( + Schemas.PassType, + type, + Messages.PASS_TYPE.INVALID, + ); if (this.type) { /** @@ -360,9 +375,14 @@ export default class PKPass extends Bundle { return; } - this[importMetadataSymbol]( - validateJSONBuffer(buffer, Schemas.PassProps), - ); + try { + this[importMetadataSymbol]( + validateJSONBuffer(buffer, Schemas.PassProps), + ); + } catch (err) { + console.warn(formatMessage(Messages.PASS_SOURCE.INVALID, err)); + return; + } /** * Adding an empty buffer just for reference @@ -384,7 +404,7 @@ export default class PKPass extends Bundle { validateJSONBuffer(buffer, Schemas.Personalization); } catch (err) { console.warn( - "Personalization.json file has been omitted as invalid.", + formatMessage(Messages.PERSONALIZATION.INVALID, err), ); return; } @@ -456,18 +476,14 @@ export default class PKPass extends Bundle { } = data; if (Object.keys(this[propsSymbol]).length) { - console.warn( - "The imported pass.json's properties will be joined with the current setted props. You might lose some data.", - ); + console.warn(Messages.PASS_SOURCE.JOIN); } Object.assign(this[propsSymbol], otherPassData); if (!type) { if (!this[passTypeSymbol]) { - console.warn( - "Cannot find a valid type in pass.json. You won't be able to set fields until you won't set explicitly one.", - ); + console.warn(Messages.PASS_SOURCE.UNKNOWN_TYPE); } } else { this.type = type; @@ -517,6 +533,10 @@ export default class PKPass extends Bundle { private [closePassSymbol]( __test_disable_manifest_signature_generation__: boolean = false, ) { + if (!this.type) { + throw new TypeError(Messages.CLOSE.MISSING_TYPE); + } + const fileNames = Object.keys(this[filesSymbol]); const passJson = Buffer.from(JSON.stringify(this[propsSymbol])); @@ -524,9 +544,7 @@ export default class PKPass extends Bundle { const ICON_REGEX = /icon(?:@\d{1}x)?/; if (!fileNames.some((fileName) => ICON_REGEX.test(fileName))) { - console.warn( - "At least one icon file is missing in your bundle. Your pass won't be openable by any Apple Device.", - ); + console.warn(Messages.CLOSE.MISSING_ICON); } // *********************************** // @@ -571,7 +589,10 @@ export default class PKPass extends Bundle { for (let i = 0; i < fileNames.length; i++) { if (/personalization/.test(fileNames[i])) { console.warn( - `Personalization file '${fileNames[i]}' have been removed from the bundle as the requirements for personalization are not met.`, + formatMessage( + Messages.CLOSE.PERSONALIZATION_REMOVED, + fileNames[i], + ), ); delete this[filesSymbol][fileNames[i]]; @@ -579,6 +600,14 @@ export default class PKPass extends Bundle { } } + // ******************************** // + // *** BOARDING PASS VALIDATION *** // + // ******************************** // + + if (this.type === "boardingPass" && !this.transitType) { + throw new TypeError(Messages.CLOSE.MISSING_TRANSIT_TYPE); + } + // ****************************** // // *** SIGNATURE AND MANIFEST *** // // ****************************** // @@ -680,7 +709,7 @@ export default class PKPass extends Bundle { ) { if (typeof lang !== "string") { throw new TypeError( - `Cannot set localization. Expected a string for 'lang' but received a ${typeof lang}`, + formatMessage(Messages.LANGUAGES.INVALID_TYPE, typeof lang), ); } @@ -713,7 +742,7 @@ export default class PKPass extends Bundle { this[propsSymbol]["expirationDate"] = Utils.processDate(date); } catch (err) { throw new TypeError( - `Cannot set expirationDate. Invalid date ${date}`, + formatMessage(Messages.DATE.INVALID, "expirationDate", date), ); } } @@ -802,7 +831,7 @@ export default class PKPass extends Bundle { this[propsSymbol]["relevantDate"] = Utils.processDate(date); } catch (err) { throw new TypeError( - `Cannot set relevantDate. Invalid date ${date}`, + formatMessage(Messages.DATE.INVALID, "relevantDate", date), ); } } @@ -857,12 +886,6 @@ export default class PKPass extends Bundle { Schemas.Barcode, barcodes as Schemas.Barcode[], ); - - if (!finalBarcodes.length) { - throw new TypeError( - "Expected Schema.Barcode in setBarcodes but no one is valid.", - ); - } } this[propsSymbol]["barcodes"] = finalBarcodes; @@ -924,7 +947,7 @@ function validateJSONBuffer( try { contentAsJSON = JSON.parse(buffer.toString("utf8")); } catch (err) { - throw new TypeError("Cannot validate Pass.json: invalid JSON"); + throw new TypeError(Messages.JSON.INVALID); } return Schemas.validate(schema, contentAsJSON); diff --git a/src/getModelFolderContents.ts b/src/getModelFolderContents.ts index d146d2c..80d1707 100644 --- a/src/getModelFolderContents.ts +++ b/src/getModelFolderContents.ts @@ -1,6 +1,6 @@ import * as path from "path"; import * as Utils from "./utils"; -import formatMessage, { ERROR } from "./messages"; +import formatMessage, * as Messages from "./messages"; import { promises as fs } from "fs"; /** @@ -54,12 +54,15 @@ export default async function getModelFolderContents( if (err.syscall === "open") { // file opening failed throw new Error( - formatMessage(ERROR.MODELF_NOT_FOUND, err.path), + formatMessage( + Messages.MODELS.FILE_NO_OPEN, + JSON.stringify(err), + ), ); } else if (err.syscall === "scandir") { // directory reading failed throw new Error( - formatMessage(ERROR.MODELF_FILE_NOT_FOUND, err.path), + formatMessage(Messages.MODELS.DIR_NOT_FOUND, err.path), ); } } diff --git a/src/messages.ts b/src/messages.ts index a14692a..7c9db5c 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,71 +1,86 @@ -export const ERROR = { - CP_INIT: - "Something went really bad in the %s initialization! Look at the log below this message. It should contain all the infos about the problem: \n%s", - CP_NO_OPTS: - "Cannot initialize the pass or abstract model creation: no options were passed.", - CP_NO_CERTS: - "Cannot initialize the pass creation: no valid certificates were passed.", - PASSFILE_VALIDATION_FAILED: - "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", - REQUIR_VALID_FAILED: - "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", - MODEL_UNINITIALIZED: - "Provided model ( %s ) matched but unitialized or may not contain icon or a valid pass.json.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", - MODEL_NOT_VALID: - "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.", - MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", - MODELF_FILE_NOT_FOUND: "File %s not found.", - INVALID_CERTS: - "Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.", - INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.", - TRSTYPE_REQUIRED: - "Cannot proceed with pass creation. transitType field is required for boardingPasses.", - OVV_KEYS_BADFORMAT: - "Cannot proceed with pass creation due to bad keys format in overrides.", - NO_PASS_TYPE: - "Cannot proceed with pass creation. Model definition (pass.json) has no valid type in it.\nRefer to https://apple.co/2wzyL5J to choose a valid pass type.", +export const CERTIFICATES = { + INVALID: + "Invalid certificate(s) loaded. %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them", } as const; -export const DEBUG = { - TRSTYPE_NOT_VALID: - 'Transit type changing rejected as not compliant with Apple Specifications. Transit type would become "%s" but should be in [PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain]', - BRC_NOT_SUPPORTED: - "Format not found among barcodes. Cannot set backward compatibility.", - BRC_FORMATTYPE_UNMATCH: - "Format must be a string or null. Cannot set backward compatibility.", - BRC_AUTC_MISSING_DATA: - "Unable to autogenerate barcodes. Data is not a string.", - BRC_BW_FORMAT_UNSUPPORTED: - "This format is not supported (by Apple) for backward support. Please choose another one.", - BRC_NO_POOL: - "Cannot set barcode: no barcodes found. Please set barcodes first. Barcode is for retrocompatibility only.", - DATE_FORMAT_UNMATCH: "%s was not set due to incorrect date format.", - NFC_INVALID: - "Unable to set NFC properties: data not compliant with schema.", - PRS_INVALID: - "Unable to parse Personalization.json. File is not a valid JSON. Error: %s", - PRS_REMOVED: - "Personalization has been removed as it requires an NFC-enabled pass to work.", +export const TRANSIT_TYPE = { + UNEXPECTED_PASS_TYPE: + "Cannot set transitType on a pass with type different from boardingPass.", + INVALID: + "Cannot set transitType because not compliant with Apple specifications. Refer to https://apple.co/3DHuAG4 for more - %s", } as const; -type ERROR_OR_DEBUG_MESSAGE = - | typeof ERROR[keyof typeof ERROR] - | typeof DEBUG[keyof typeof DEBUG]; +export const PASS_TYPE = { + INVALID: + "Cannot set type because not compliant with Apple specifications. Refer to https://apple.co/3aFpSfg for a list of valid props - %s", +} as const; + +export const TEMPLATE = { + INVALID: "Cannot create pass from a template. %s", +} as const; + +export const FILTER_VALID = { + INVALID: "Cannot validate property. %s", +} as const; + +export const FIELDS = { + INVALID: "Cannot add field. %s", + REPEATED_KEY: + "Cannot add field with key '%s': another field already owns this key. Ignored.", +} as const; + +export const DATE = { + INVALID: "Cannot set %s. Invalid date %s", +} as const; + +export const LANGUAGES = { + INVALID_TYPE: + "Cannot set localization. Expected a string for 'lang' but received %s", +} as const; + +export const BARCODES = { + INVALID_POST: "", +} as const; + +export const PASS_SOURCE = { + INVALID: "Cannot add pass.json to bundle because it is invalid. %s", + UNKNOWN_TYPE: + "Cannot find a valid type in pass.json. You won't be able to set fields until you won't set explicitly one.", + JOIN: "The imported pass.json's properties will be joined with the current setted props. You might lose some data.", +} as const; + +export const PERSONALIZATION = { + INVALID: + "Cannot add personalization.json to bundle because it is invalid. %s", +} as const; + +export const JSON = { + INVALID: "Cannot parse JSON. Invalid file", +} as const; + +export const CLOSE = { + MISSING_TYPE: "Cannot proceed creating the pass because type is missing.", + MISSING_ICON: + "At least one icon file is missing in your bundle. Your pass won't be openable by any Apple Device.", + PERSONALIZATION_REMOVED: + "Personalization file '%s' have been removed from the bundle as the requirements for personalization are not met.", + MISSING_TRANSIT_TYPE: + "Cannot proceed creating the pass because transitType is missing on your boardingPass.", +}; + +export const MODELS = { + DIR_NOT_FOUND: "Cannot import model: directory %s not found.", + FILE_NO_OPEN: "Cannot open model file. %s", +} as const; /** * Creates a message with replaced values - * @param {string} messageName - * @param {any[]} values + * @param messageName + * @param values */ -export default function format( - messageName: ERROR_OR_DEBUG_MESSAGE, - ...values: any[] -) { +export default function format(messageName: string, ...values: any[]) { // reversing because it is better popping than shifting. - let replaceValues = values.reverse(); - return messageName.replace(/%s/g, () => { - let next = replaceValues.pop(); - return next !== undefined ? next : ""; - }); + const replaceValues = values.reverse(); + return messageName.replace(/%s/g, () => replaceValues.pop()); } diff --git a/src/schemas/index.ts b/src/schemas/index.ts index a36fbc6..8782383 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -18,6 +18,8 @@ import { PassFields, TransitType } from "./PassFields"; import { Semantics } from "./SemanticTags"; import { CertificatesSchema } from "./Certificates"; +import formatMessage, * as Messages from "../messages"; + const RGB_COLOR_REGEX = /rgb\(\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*,\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*,\s*(?:[01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])\s*\)/; @@ -194,10 +196,21 @@ export function isValid( export function assertValidity( schema: Joi.ObjectSchema | Joi.StringSchema, data: T, + customErrorMessage?: string, ): void { const validation = schema.validate(data); if (validation.error) { + if (customErrorMessage) { + console.warn(validation.error); + throw new TypeError( + `${validation.error.name} happened. ${formatMessage( + customErrorMessage, + validation.error.message, + )}`, + ); + } + throw new TypeError(validation.error.message); } } @@ -239,7 +252,8 @@ export function filterValid( return source.reduce((acc, current) => { try { return [...acc, validate(schema, current)]; - } catch { + } catch (err) { + console.warn(formatMessage(Messages.FILTER_VALID.INVALID, err)); return [...acc]; } }, []);