diff --git a/.spi.yml b/.spi.yml index eaf12d0..ad36f6c 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [PassKit, Passes, Orders] - swift_version: 6.0 \ No newline at end of file + - documentation_targets: [PassKit, Passes, Orders] \ No newline at end of file diff --git a/Package.swift b/Package.swift index 2a5323a..4bfb12f 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( .package(url: "https://github.com/vapor/vapor.git", from: "4.106.1"), .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), .package(url: "https://github.com/vapor/apns.git", from: "4.2.0"), - .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.3"), + .package(url: "https://github.com/vapor-community/Zip.git", from: "2.2.4"), .package(url: "https://github.com/apple/swift-certificates.git", from: "1.6.1"), // used in tests .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"), diff --git a/Sources/Orders/Orders.docc/Extensions/OrdersService.md b/Sources/Orders/Orders.docc/Extensions/OrdersService.md index d59b09c..5716063 100644 --- a/Sources/Orders/Orders.docc/Extensions/OrdersService.md +++ b/Sources/Orders/Orders.docc/Extensions/OrdersService.md @@ -4,10 +4,9 @@ ### Essentials -- ``generateOrderContent(for:on:)`` +- ``build(order:on:)`` - ``register(migrations:)`` ### Push Notifications - ``sendPushNotifications(for:on:)`` -- ``sendPushNotificationsForOrder(id:of:on:)`` diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index 9870283..ec20edf 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -147,8 +147,6 @@ final class OrderDelegate: OrdersDelegate { Next, initialize the ``OrdersService`` inside the `configure.swift` file. This will implement all of the routes that Apple Wallet expects to exist on your server. -In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files. -If they are named like that you're good to go, otherwise you have to specify the custom name. > Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders. @@ -164,7 +162,9 @@ public func configure(_ app: Application) async throws { let ordersService = try OrdersService( app: app, delegate: orderDelegate, - signingFilesDirectory: "Certificates/Orders/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -203,7 +203,9 @@ public func configure(_ app: Application) async throws { >( app: app, delegate: orderDelegate, - signingFilesDirectory: "Certificates/Orders/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -284,7 +286,7 @@ struct OrdersController: RouteCollection { > Note: You'll have to register the `OrdersController` in the `configure.swift` file, in order to pass it the ``OrdersService`` object. -Then use the object inside your route handlers to generate the order bundle with the ``OrdersService/generateOrderContent(for:on:)`` method and distribute it with the "`application/vnd.apple.order`" MIME type. +Then use the object inside your route handlers to generate the order bundle with the ``OrdersService/build(order:on:)`` method and distribute it with the "`application/vnd.apple.order`" MIME type. ```swift fileprivate func orderHandler(_ req: Request) async throws -> Response { @@ -297,7 +299,7 @@ fileprivate func orderHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } - let bundle = try await ordersService.generateOrderContent(for: orderData.order, on: req.db) + let bundle = try await ordersService.build(order: orderData.order, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.order") diff --git a/Sources/Orders/Orders.docc/Orders.md b/Sources/Orders/Orders.docc/Orders.md index 9311362..952a12b 100644 --- a/Sources/Orders/Orders.docc/Orders.md +++ b/Sources/Orders/Orders.docc/Orders.md @@ -34,7 +34,3 @@ For information on Apple Wallet orders, see the [Apple Developer Documentation]( - ``OrderModel`` - ``OrdersRegistrationModel`` - ``OrderDataModel`` - -### Errors - -- ``OrdersError`` \ No newline at end of file diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift index dc40cdc..e63979c 100644 --- a/Sources/Orders/OrdersDelegate.swift +++ b/Sources/Orders/OrdersDelegate.swift @@ -25,15 +25,6 @@ public protocol OrdersDelegate: AnyObject, Sendable { /// - Returns: A URL path which points to the template data for the order. func template(for order: O, db: any Database) async throws -> String - /// Generates the SSL `signature` file. - /// - /// If you need to implement custom S/Mime signing you can use this - /// method to do so. You must generate a detached DER signature of the `manifest.json` file. - /// - /// - Parameter root: The location of the `manifest.json` and where to write the `signature` to. - /// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`. - func generateSignatureFile(in root: URL) -> Bool - /// Encode the order into JSON. /// /// This method should generate the entire order JSON. You are provided with @@ -51,9 +42,3 @@ public protocol OrdersDelegate: AnyObject, Sendable { order: O, db: any Database, encoder: JSONEncoder ) async throws -> Data } - -extension OrdersDelegate { - public func generateSignatureFile(in root: URL) -> Bool { - return false - } -} diff --git a/Sources/Orders/OrdersError.swift b/Sources/Orders/OrdersError.swift deleted file mode 100644 index 3c827b9..0000000 --- a/Sources/Orders/OrdersError.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// OrdersError.swift -// PassKit -// -// Created by Francesco Paolo Severino on 04/07/24. -// - -/// Errors that can be thrown by Apple Wallet orders. -public struct OrdersError: Error, Sendable { - /// The type of the errors that can be thrown by Apple Wallet orders. - public struct ErrorType: Sendable, Hashable, CustomStringConvertible { - enum Base: String, Sendable { - case templateNotDirectory - case pemCertificateMissing - case pemPrivateKeyMissing - case opensslBinaryMissing - } - - let base: Base - - private init(_ base: Base) { - self.base = base - } - - /// The template path is not a directory. - public static let templateNotDirectory = Self(.templateNotDirectory) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(.pemCertificateMissing) - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(.pemPrivateKeyMissing) - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(.opensslBinaryMissing) - - /// A textual representation of this error. - public var description: String { - base.rawValue - } - } - - private struct Backing: Sendable { - fileprivate let errorType: ErrorType - - init(errorType: ErrorType) { - self.errorType = errorType - } - } - - private var backing: Backing - - /// The type of this error. - public var errorType: ErrorType { backing.errorType } - - private init(errorType: ErrorType) { - self.backing = .init(errorType: errorType) - } - - /// The template path is not a directory. - public static let templateNotDirectory = Self(errorType: .templateNotDirectory) - - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(errorType: .pemCertificateMissing) - - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(errorType: .pemPrivateKeyMissing) - - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(errorType: .opensslBinaryMissing) -} - -extension OrdersError: CustomStringConvertible { - public var description: String { - "OrdersError(errorType: \(self.errorType))" - } -} diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index 0a9d1e0..e915472 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -17,37 +17,34 @@ public final class OrdersService: Sendable { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``OrdersDelegate`` to use for order generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing orders. + /// - pemPrivateKey: The PEM Certificate's private key for signing orders. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any OrdersDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLPath: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, delegate: delegate, - signingFilesDirectory: signingFilesDirectory, - wwdrCertificate: wwdrCertificate, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger, + pemWWDRCertificate: pemWWDRCertificate, pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - sslBinary: sslBinary, - pushRoutesMiddleware: pushRoutesMiddleware, - logger: logger + openSSLPath: openSSLPath ) } @@ -56,9 +53,10 @@ public final class OrdersService: Sendable { /// - Parameters: /// - order: The order to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated order content. - public func generateOrderContent(for order: Order, on db: any Database) async throws -> Data { - try await service.generateOrderContent(for: order, on: db) + public func build(order: Order, on db: any Database) async throws -> Data { + try await service.build(order: order, on: db) } /// Adds the migrations for Wallet orders models. @@ -71,16 +69,6 @@ public final class OrdersService: Sendable { migrations.add(OrdersErrorLog()) } - /// Sends push notifications for a given order. - /// - /// - Parameters: - /// - id: The `UUID` of the order to send the notifications for. - /// - typeIdentifier: The type identifier of the order. - /// - db: The `Database` to use. - public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - try await service.sendPushNotificationsForOrder(id: id, of: typeIdentifier, on: db) - } - /// Sends push notifications for a given order. /// /// - Parameters: diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 4f741f7..3dde8f0 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -26,13 +26,14 @@ public final class OrdersServiceCustom [R] { + private static func registrations(for order: O, on db: any Database) async throws -> [R] { // This could be done by enforcing the caller to have a Siblings property wrapper, // but there's not really any value to forcing that on them when we can just do the query ourselves like this. try await R.query(on: db) @@ -377,84 +375,77 @@ extension OrdersServiceCustom { .join(parent: \._$device) .with(\._$order) .with(\._$device) - .filter(O.self, \._$typeIdentifier == typeIdentifier) - .filter(O.self, \._$id == id) + .filter(O.self, \._$typeIdentifier == order._$typeIdentifier.value!) + .filter(O.self, \._$id == order.requireID()) .all() } } // MARK: - order file generation extension OrdersServiceCustom { - private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { + private func manifest(for directory: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: root) - guard !file.hasDirectoryPath else { continue } - manifest[relativePath] = try SHA256.hash(data: Data(contentsOf: file)).hex + let file = URL(fileURLWithPath: relativePath, relativeTo: directory) + guard !file.hasDirectoryPath else { + continue + } + + let hash = try SHA256.hash(data: Data(contentsOf: file)) + manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } - // Write the manifest file to the root directory - // and return the data for using it in signing. - let data = try encoder.encode(manifest) - try data.write(to: root.appendingPathComponent("manifest.json")) - return data - } - private func generateSignatureFile(for manifest: Data, in root: URL) throws { - // If the caller's delegate generated a file we don't have to do it. - if delegate.generateSignatureFile(in: root) { return } + return try encoder.encode(manifest) + } + private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. - if let password = self.pemPrivateKeyPassword { - let sslBinary = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw OrdersError.opensslBinaryMissing + if let pemPrivateKeyPassword { + guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { + throw WalletError.noOpenSSLExecutable } - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: dir) } + + try manifest.write(to: dir.appendingPathComponent("manifest.json")) + try self.pemWWDRCertificate.write(to: dir.appendingPathComponent("wwdr.pem"), atomically: true, encoding: .utf8) + try self.pemCertificate.write(to: dir.appendingPathComponent("certificate.pem"), atomically: true, encoding: .utf8) + try self.pemPrivateKey.write(to: dir.appendingPathComponent("private.pem"), atomically: true, encoding: .utf8) + + let process = Process() + process.currentDirectoryURL = dir + process.executableURL = self.openSSLURL + process.arguments = [ "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", root.appendingPathComponent("manifest.json").path, - "-out", root.appendingPathComponent("signature").path, + "-certfile", dir.appendingPathComponent("wwdr.pem").path, + "-signer", dir.appendingPathComponent("certificate.pem").path, + "-inkey", dir.appendingPathComponent("private.pem").path, + "-in", dir.appendingPathComponent("manifest.json").path, + "-out", dir.appendingPathComponent("signature").path, "-outform", "DER", - "-passin", "pass:\(password)", + "-passin", "pass:\(pemPrivateKeyPassword)", ] - try proc.run() - proc.waitUntilExit() - return - } + try process.run() + process.waitUntilExit() - let signature = try CMS.sign( - manifest, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - try Data(signature).write(to: root.appendingPathComponent("signature")) + return try Data(contentsOf: dir.appendingPathComponent("signature")) + } else { + let signature = try CMS.sign( + manifest, + signatureAlgorithm: .sha256WithRSAEncryption, + additionalIntermediateCertificates: [ + Certificate(pemEncoded: self.pemWWDRCertificate) + ], + certificate: Certificate(pemEncoded: self.pemCertificate), + privateKey: .init(pemEncoded: self.pemPrivateKey), + signingTime: Date() + ) + return Data(signature) + } } /// Generates the order content bundle for a given order. @@ -462,28 +453,42 @@ extension OrdersServiceCustom { /// - Parameters: /// - order: The order to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated order content as `Data`. - public func generateOrderContent(for order: O, on db: any Database) async throws -> Data { - let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true) + public func build(order: O, on db: any Database) async throws -> Data { + let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true) guard - (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw OrdersError.templateNotDirectory + throw WalletError.noSourceFiles } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.copyItem(at: templateDirectory, to: root) - defer { _ = try? FileManager.default.removeItem(at: root) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.copyItem(at: filesDirectory, to: tempDir) + defer { try? FileManager.default.removeItem(at: tempDir) } + + var files: [ArchiveFile] = [] - try await self.delegate.encode(order: order, db: db, encoder: self.encoder) - .write(to: root.appendingPathComponent("order.json")) + let orderJSON = try await self.delegate.encode(order: order, db: db, encoder: self.encoder) + try orderJSON.write(to: tempDir.appendingPathComponent("order.json")) + files.append(ArchiveFile(filename: "order.json", data: orderJSON)) - try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) + let manifest = try self.manifest(for: tempDir) + files.append(ArchiveFile(filename: "manifest.json", data: manifest)) + try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path) + for relativePath in paths { + let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + guard !file.hasDirectoryPath else { + continue + } + + try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file))) + } - var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) - files.append(URL(fileURLWithPath: "order.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "signature", relativeTo: root)) - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).order") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } } diff --git a/Sources/PassKit/Testing/TestCertificate.swift b/Sources/PassKit/Testing/TestCertificate.swift new file mode 100644 index 0000000..73e1cc4 --- /dev/null +++ b/Sources/PassKit/Testing/TestCertificate.swift @@ -0,0 +1,115 @@ +package enum TestCertificate { + package static let pemWWDRCertificate = """ + -----BEGIN CERTIFICATE----- + MIIEVTCCAz2gAwIBAgIUE9x3lVJx5T3GMujM/+Uh88zFztIwDQYJKoZIhvcNAQEL + BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsT + HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBS + b290IENBMB4XDTIwMTIxNjE5MzYwNFoXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UE + Aww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNh + dGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc0MRMwEQYDVQQKDApBcHBsZSBJbmMu + MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAf + eKp6JzKwRl/nF3bYoJ0OKY6tPTKlxGs3yeRBkWq3eXFdDDQEYHX3rkOPR8SGHgjo + v9Y5Ui8eZ/xx8YJtPH4GUnadLLzVQ+mxtLxAOnhRXVGhJeG+bJGdayFZGEHVD41t + QSo5SiHgkJ9OE0/QjJoyuNdqkh4laqQyziIZhQVg3AJK8lrrd3kCfcCXVGySjnYB + 5kaP5eYq+6KwrRitbTOFOCOL6oqW7Z+uZk+jDEAnbZXQYojZQykn/e2kv1MukBVl + PNkuYmQzHWxq3Y4hqqRfFcYw7V/mjDaSlLfcOQIA+2SM1AyB8j/VNJeHdSbCb64D + YyEMe9QbsWLFApy9/a8CAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8G + A1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0 + BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJv + b3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290 + LmNybDAdBgNVHQ4EFgQUW9n6HeeaGgujmXYiUIY+kchbd6gwDgYDVR0PAQH/BAQD + AgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA/Vj2e5bbD + eeZFIGi9v3OLLBKeAuOugCKMBB7DUshwgKj7zqew1UJEggOCTwb8O0kU+9h0UoWv + p50h5wESA5/NQFjQAde/MoMrU1goPO6cn1R2PWQnxn6NHThNLa6B5rmluJyJlPef + x4elUWY0GzlxOSTjh2fvpbFoe4zuPfeutnvi0v/fYcZqdUmVIkSoBPyUuAsuORFJ + EtHlgepZAE9bPFo22noicwkJac3AfOriJP6YRLj477JxPxpd1F1+M02cHSS+APCQ + A1iZQT0xWmJArzmoUUOSqwSonMJNsUvSq3xKX+udO7xPiEAGE/+QF4oIRynoYpgp + pU8RBWk6z/Kf + -----END CERTIFICATE----- + """ + + package static let pemCertificate = """ + -----BEGIN CERTIFICATE----- + MIIC8TCCAdmgAwIBAgICbE8wDQYJKoZIhvcNAQENBQAwGDEWMBQGA1UEAwwNUHVz + aHlUZXN0Um9vdDAgFw0xNzA0MTcwMDUzMzBaGA8yMTE3MDMyNDAwNTMzMFowHzEd + MBsGA1UEAwwUY29tLnJlbGF5cmlkZXMucHVzaHkwggEiMA0GCSqGSIb3DQEBAQUA + A4IBDwAwggEKAoIBAQDHZkZBnDKM4Gt+WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c36 + 41/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTkKljDGe8tuDncT1qSrp/UuikgdIAAiCXA + /vClWPYqZcHAUc9/OcfRiyK5AmJdzz+UbY803ArSPHjz3+Mk6C9tnzBXzG8oJq9o + EKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7U + j8wRstdr2xWhPg1fdIVHzudYubJ7M/h95JQFKtwqEevtLUa4BJgi8SKvRX5NnkGE + QMui1ercRuklVURTeoGDQYENiFnzTyI0J2tw3T+dAgMBAAGjPDA6MAkGA1UdEwQC + MAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD + ATANBgkqhkiG9w0BAQ0FAAOCAQEAnHHYMvBWglQLOUmNOalCMopmk9yKHM7+Sc9h + KsTWJW+YohF5zkRhnwUFxW85Pc63rRVA0qyI5zHzRtwYlcZHU57KttJyDGe1rm/0 + ZUqXharurJzyI09jcwRpDY8EGktrGirE1iHDqQTHNDHyS8iMVU6aPCo0xur63G5y + XzoIVhQXsBuwoU4VKb3n5CrxKEVcmE/nYF/Tk0rTtCrZF7TR3y/oxrp359goJ1b2 + /OjXN4dlqND41SbVTTL0FyXU3ebaS4DALA3pyVa1Rijw7vgEbFabsuMaAbdvlprn + RwUjsrRVu3Tx7sp/NqmeBLVru5nH/yHStDjSdvQtI2ipNGK/9w== + -----END CERTIFICATE----- + """ + + package static let pemPrivateKey = """ + -----BEGIN PRIVATE KEY----- + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHZkZBnDKM4Gt+ + WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c3641/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTk + KljDGe8tuDncT1qSrp/UuikgdIAAiCXA/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+U + bY803ArSPHjz3+Mk6C9tnzBXzG8oJq9oEKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69 + m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7Uj8wRstdr2xWhPg1fdIVHzudYubJ7M/h9 + 5JQFKtwqEevtLUa4BJgi8SKvRX5NnkGEQMui1ercRuklVURTeoGDQYENiFnzTyI0 + J2tw3T+dAgMBAAECggEBAMOsIZWQ6ipEsDe1R+vuq9Z6XeP8nwb7C2FXaKGji0Gz + 78YcCruln7KsHKkkD3UVw0Wa2Q1S8Kbf6A9fXutWL9f1yRHg7Ui0BDSE2ob2zAW5 + lRLnGs+nlSnV4WQQ5EY9NVDz8IcNR+o2znWhbb65kATvQuJO+l/lWWWBqbb+7rW+ + RHy43p7U8cK63nXJy9eHZ7eOgGGUMUX+Yg0g47RGYxlIeSDrtPCXlNuwwAJY7Ecp + LVltCAyCJEaLVwQpz61PTdmkb9HCvkwiuL6cnjtpoAdXCWX7tV61UNweNkvALIWR + kMukFFE/H6JlAkcbw4na1KwQ3glWIIB2H/vZyMNdnyECgYEA78VEXo+iAQ6or4bY + xUQFd/hIibIYMzq8PxDMOmD86G78u5Ho0ytetht5Xk1xmhv402FZCL1LsAEWpCBs + a9LUwo30A23KaTA7Oy5oo5Md1YJejSNOCR+vs5wAo0SZov5tQaxVMoj3vZZqnJzJ + 3A+XUgYZddIFkn8KJjgU/QVapTMCgYEA1OV1okYF2u5VW90RkVdvQONNcUvlKEL4 + UMSF3WJnORmtUL3Dt8AFt9b7pfz6WtVr0apT5SSIFA1+305PTpjjaw25m1GftL3U + 5QwkmgTKxnPD/YPC6tImp+OUXHmk+iTgmQ9HaBpEplcyjD0EP2LQsIc6qiku/P2n + OT8ArOkk5+8CgYEA7B98wRL6G8hv3swRVdMy/36HEPNOWcUR9Zl5RlSVO+FxCtca + Tjt7viM4VuI1aer6FFDd+XlRvDaWMXOs0lKCLEbXczkACK7y5clCSzRqQQVuT9fg + 1aNayKptBlxcYOPmfLJWBLpWH2KuAyV0tT61apWPJTR7QFXTjOfV44cOSXkCgYAH + CvAxRg+7hlbcixuhqzrK8roFHXWfN1fvlBC5mh/AC9Fn8l8fHQMTadE5VH0TtCu0 + 6+WKlwLJZwjjajvFZdlgGTwinzihSgZY7WXoknAC0KGTKWCxU/Jja2vlA0Ep5T5o + 0dCS6QuMVSYe7YXOcv5kWJTgPCyJwfpeMm9bSPsnkQKBgQChy4vU3J6CxGzwuvd/ + 011kszao+cHn1DdMTyUhvA/O/paB+BAVktHm+o/i+kOk4OcPjhRqewzZZdf7ie5U + hUC8kIraXM4aZt69ThQkAIER89wlhxsFXUmGf7ZMXm8f7pvM6/MDaMW3mEsfbL0U + Y3jy0E30W5s1XCW3gmZ1Vg2xAg== + -----END PRIVATE KEY----- + """ + + package static let encryptedPemCertificate = """ + -----BEGIN CERTIFICATE----- + MIICdDCCAVwCCQCtBOr7dtQS6zANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJB + VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 + cyBQdHkgTHRkMRAwDgYDVQQDDAdQYXNza2l0MSIwIAYJKoZIhvcNAQkBFhNub3Jl + cGx5QGV4YW1wbGUuY29tMB4XDTE5MDEzMTE2NTYzNloXDTI0MDEzMDE2NTYzNlow + RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu + dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC1 + +NQj0QzX5Vu9JMZVntP8i+JYAfOxzeP+MWUL/VaOxGaRp7DSiWAOd8bXDjJZjET0 + 4SPZzKvy0a6Suk9aIxCfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHd5jKTM4smJ + b4CoVY2PwYogb+bI4AtpUBV1QlsDrb1xMBHQ6zLf+JhRMya2MqJR+5hDKywrN4bC + j3LY87ir5aJFFaBMs9h0sCEoQKs0cnksf6Gq2pVJF9G+Aki4UF9r7jxoQwXjbtS3 + m6ptezzKYvMcw5rKKhtZRgDT1uuy5hgOCapZrV1s0byRv3W6IcdzOD3cWZEuxz2D + AVZCwIvqThqMaAs3Fvs3L3aQsDiOJpZ65gNnBU6j21liMZ3q7txD3eCzuXWMLPI5 + O7C4Sxy+LF4XAfd1/0nmHC2HBgA6CSMgncEzU6PLRR6bXH1daKWlcMAvF+STbLUJ + 79kQMXh2OCs= + -----END CERTIFICATE----- + """ + + package static let encryptedPemPrivateKey = """ + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMBNCLGhiuR4CAggA + MBQGCCqGSIb3DQMHBAjXF3m+2fdMRgSCAWAaGMyNREsNYTuTE0Zf/GIORBQH1Vjc + pNTvxV0B/YUHfzthOkotQjL8mfbbCWVixEdDE41Rn66WVrVmgFDVIKoGhjsMLGYd + angmocOnZ77ZYXi0f0/8fZYuQF2dF/zOfsdxyNl2gi4MGbKqt8m9vDcFAWEZsN/r + 5l1QJYNpF4OXKwNg4dnf7Ugo3PMWrVxKzKn+KtUvQd+mdYJ3xBjr1yLjLacbCXh0 + 4Kh1kWeV6yyaZswYPPItyAeg4smLdDTEqFI+GHIT7NFQ0GIojIqz2Ug8KWZaMwZs + iRCYXHDECkC7zqgcxJKRtjDmCJIxfIFcnwJ8DmMf7bpawtowcfM/z7TGSzAUeptA + bH9rS4Zf5/5Sx/yFRr2esClwli5BJG1uISQBpA0DePhePTiW6LesvAt3YZ3p2BCI + OE0HwdbAr24Nw7LRCuobRsTKFnBmM+uqtGyJhKE6hC1q4CPjZ09F8njX + -----END ENCRYPTED PRIVATE KEY----- + """ +} diff --git a/Sources/PassKit/WalletError.swift b/Sources/PassKit/WalletError.swift new file mode 100644 index 0000000..6209a7a --- /dev/null +++ b/Sources/PassKit/WalletError.swift @@ -0,0 +1,76 @@ +// +// WalletError.swift +// PassKit +// +// Created by Francesco Paolo Severino on 04/07/24. +// + +/// Errors that can be thrown by Apple Wallet passes and orders. +public struct WalletError: Error, Sendable, Equatable { + /// The type of the errors that can be thrown by Apple Wallet passes and orders. + public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { + enum Base: String, Sendable, Equatable { + case noSourceFiles + case noOpenSSLExecutable + case invalidNumberOfPasses + } + + let base: Base + + private init(_ base: Base) { + self.base = base + } + + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(.noSourceFiles) + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(.noOpenSSLExecutable) + /// The number of passes to bundle is invalid. + public static let invalidNumberOfPasses = Self(.invalidNumberOfPasses) + + /// A textual representation of this error. + public var description: String { + base.rawValue + } + } + + private struct Backing: Sendable, Equatable { + fileprivate let errorType: ErrorType + + init(errorType: ErrorType) { + self.errorType = errorType + } + + static func == (lhs: WalletError.Backing, rhs: WalletError.Backing) -> Bool { + lhs.errorType == rhs.errorType + } + } + + private var backing: Backing + + /// The type of this error. + public var errorType: ErrorType { backing.errorType } + + private init(errorType: ErrorType) { + self.backing = .init(errorType: errorType) + } + + /// The path for the source files is not a directory. + public static let noSourceFiles = Self(errorType: .noSourceFiles) + + /// The `openssl` executable is missing. + public static let noOpenSSLExecutable = Self(errorType: .noOpenSSLExecutable) + + /// The number of passes to bundle is invalid. + public static let invalidNumberOfPasses = Self(errorType: .invalidNumberOfPasses) + + public static func == (lhs: WalletError, rhs: WalletError) -> Bool { + lhs.backing == rhs.backing + } +} + +extension WalletError: CustomStringConvertible { + public var description: String { + "WalletError(errorType: \(self.errorType))" + } +} diff --git a/Sources/Passes/Passes.docc/Extensions/PassesService.md b/Sources/Passes/Passes.docc/Extensions/PassesService.md index 8fe801a..6073ecd 100644 --- a/Sources/Passes/Passes.docc/Extensions/PassesService.md +++ b/Sources/Passes/Passes.docc/Extensions/PassesService.md @@ -4,11 +4,10 @@ ### Essentials -- ``generatePassContent(for:on:)`` -- ``generatePassesContent(for:on:)`` +- ``build(pass:on:)`` +- ``build(passes:on:)`` - ``register(migrations:)`` ### Push Notifications - ``sendPushNotifications(for:on:)`` -- ``sendPushNotificationsForPass(id:of:on:)`` diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index f7c679e..d56b243 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -171,8 +171,6 @@ final class PassDelegate: PassesDelegate { Next, initialize the ``PassesService`` inside the `configure.swift` file. This will implement all of the routes that Apple Wallet expects to exist on your server. -In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files. -If they are named like that you're good to go, otherwise you have to specify the custom name. > Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). @@ -188,7 +186,9 @@ public func configure(_ app: Application) async throws { let passesService = try PassesService( app: app, delegate: passDelegate, - signingFilesDirectory: "Certificates/Passes/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -228,7 +228,9 @@ public func configure(_ app: Application) async throws { >( app: app, delegate: passDelegate, - signingFilesDirectory: "Certificates/Passes/" + pemWWDRCertificate: Environment.get("PEM_WWDR_CERTIFICATE")!, + pemCertificate: Environment.get("PEM_CERTIFICATE")!, + pemPrivateKey: Environment.get("PEM_PRIVATE_KEY")! ) } ``` @@ -309,7 +311,7 @@ struct PassesController: RouteCollection { > Note: You'll have to register the `PassesController` in the `configure.swift` file, in order to pass it the ``PassesService`` object. -Then use the object inside your route handlers to generate the pass bundle with the ``PassesService/generatePassContent(for:on:)`` method and distribute it with the "`application/vnd.apple.pkpass`" MIME type. +Then use the object inside your route handlers to generate the pass bundle with the ``PassesService/build(pass:on:)`` method and distribute it with the "`application/vnd.apple.pkpass`" MIME type. ```swift fileprivate func passHandler(_ req: Request) async throws -> Response { @@ -322,7 +324,7 @@ fileprivate func passHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } - let bundle = try await passesService.generatePassContent(for: passData.pass, on: req.db) + let bundle = try await passesService.build(pass: passData.pass, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpass") @@ -336,7 +338,7 @@ fileprivate func passHandler(_ req: Request) async throws -> Response { ### Create a Bundle of Passes You can also create a bundle of passes to enable your user to download multiple passes at once. -Use the ``PassesService/generatePassesContent(for:on:)`` method to generate the bundle and serve it to the user. +Use the ``PassesService/build(passes:on:)`` method to generate the bundle and serve it to the user. The MIME type for a bundle of passes is "`application/vnd.apple.pkpasses`". > Note: You can have up to 10 passes or 150 MB for a bundle of passes. @@ -347,7 +349,7 @@ fileprivate func passesHandler(_ req: Request) async throws -> Response { let passesData = try await PassData.query(on: req.db).with(\.$pass).all() let passes = passesData.map { $0.pass } - let bundle = try await passesService.generatePassesContent(for: passes, on: req.db) + let bundle = try await passesService.build(passes: passes, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpasses") diff --git a/Sources/Passes/Passes.docc/Passes.md b/Sources/Passes/Passes.docc/Passes.md index 19d5cab..3d3ed98 100644 --- a/Sources/Passes/Passes.docc/Passes.md +++ b/Sources/Passes/Passes.docc/Passes.md @@ -43,10 +43,6 @@ For information on Apple Wallet passes, see the [Apple Developer Documentation]( - ``PassesRegistrationModel`` - ``PassDataModel`` -### Errors - -- ``PassesError`` - ### Personalized Passes (⚠️ WIP) - diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md index c828d2c..7756781 100644 --- a/Sources/Passes/Passes.docc/Personalization.md +++ b/Sources/Passes/Passes.docc/Personalization.md @@ -97,7 +97,7 @@ Initializing the ``PassesService`` will automatically set up the endpoints that Adding the ``PassesService/register(migrations:)`` method to your `configure.swift` file will automatically set up the database table that stores the user personalization data. -Generate the pass bundle with ``PassesService/generatePassContent(for:on:)`` as usual and distribute it. +Generate the pass bundle with ``PassesService/build(pass:on:)`` as usual and distribute it. The user will be prompted to provide the required personal information when they add the pass. Wallet will then send the user personal information to your server, which will be saved in the ``UserPersonalization`` table. Immediately after that, Wallet will request the updated pass. diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift index ca3464b..7b9c46d 100644 --- a/Sources/Passes/PassesDelegate.swift +++ b/Sources/Passes/PassesDelegate.swift @@ -47,15 +47,6 @@ public protocol PassesDelegate: AnyObject, Sendable { /// - Returns: A URL path which points to the template data for the pass. func template(for pass: P, db: any Database) async throws -> String - /// Generates the SSL `signature` file. - /// - /// If you need to implement custom S/Mime signing you can use this - /// method to do so. You must generate a detached DER signature of the `manifest.json` file. - /// - /// - Parameter root: The location of the `manifest.json` and where to write the `signature` to. - /// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`. - func generateSignatureFile(in root: URL) -> Bool - /// Encode the pass into JSON. /// /// This method should generate the entire pass JSON. You are provided with @@ -90,10 +81,6 @@ public protocol PassesDelegate: AnyObject, Sendable { } extension PassesDelegate { - public func generateSignatureFile(in root: URL) -> Bool { - return false - } - public func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { return nil } diff --git a/Sources/Passes/PassesError.swift b/Sources/Passes/PassesError.swift deleted file mode 100644 index 2bacc68..0000000 --- a/Sources/Passes/PassesError.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// PassesError.swift -// PassKit -// -// Created by Francesco Paolo Severino on 04/07/24. -// - -/// Errors that can be thrown by PassKit passes. -public struct PassesError: Error, Sendable, Equatable { - /// The type of the errors that can be thrown by PassKit passes. - public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { - enum Base: String, Sendable, Equatable { - case templateNotDirectory - case pemCertificateMissing - case pemPrivateKeyMissing - case opensslBinaryMissing - case invalidNumberOfPasses - } - - let base: Base - - private init(_ base: Base) { - self.base = base - } - - /// The template path is not a directory. - public static let templateNotDirectory = Self(.templateNotDirectory) - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(.pemCertificateMissing) - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(.pemPrivateKeyMissing) - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(.opensslBinaryMissing) - /// The number of passes to bundle is invalid. - public static let invalidNumberOfPasses = Self(.invalidNumberOfPasses) - - /// A textual representation of this error. - public var description: String { - base.rawValue - } - } - - private struct Backing: Sendable, Equatable { - fileprivate let errorType: ErrorType - - init(errorType: ErrorType) { - self.errorType = errorType - } - - static func == (lhs: PassesError.Backing, rhs: PassesError.Backing) -> Bool { - lhs.errorType == rhs.errorType - } - } - - private var backing: Backing - - /// The type of this error. - public var errorType: ErrorType { backing.errorType } - - private init(errorType: ErrorType) { - self.backing = .init(errorType: errorType) - } - - /// The template path is not a directory. - public static let templateNotDirectory = Self(errorType: .templateNotDirectory) - - /// The `pemCertificate` file is missing. - public static let pemCertificateMissing = Self(errorType: .pemCertificateMissing) - - /// The `pemPrivateKey` file is missing. - public static let pemPrivateKeyMissing = Self(errorType: .pemPrivateKeyMissing) - - /// The path to the `openssl` binary is incorrect. - public static let opensslBinaryMissing = Self(errorType: .opensslBinaryMissing) - - /// The number of passes to bundle is invalid. - public static let invalidNumberOfPasses = Self(errorType: .invalidNumberOfPasses) - - public static func == (lhs: PassesError, rhs: PassesError) -> Bool { - lhs.backing == rhs.backing - } -} - -extension PassesError: CustomStringConvertible { - public var description: String { - "PassesError(errorType: \(self.errorType))" - } -} diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 572f07f..d32f433 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -38,37 +38,34 @@ public final class PassesService: Sendable { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``PassesDelegate`` to use for pass generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing passes. + /// - pemPrivateKey: The PEM Certificate's private key for signing passes. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLPath: String = "/usr/bin/openssl" ) throws { self.service = try .init( app: app, delegate: delegate, - signingFilesDirectory: signingFilesDirectory, - wwdrCertificate: wwdrCertificate, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger, + pemWWDRCertificate: pemWWDRCertificate, pemCertificate: pemCertificate, pemPrivateKey: pemPrivateKey, pemPrivateKeyPassword: pemPrivateKeyPassword, - sslBinary: sslBinary, - pushRoutesMiddleware: pushRoutesMiddleware, - logger: logger + openSSLPath: openSSLPath ) } @@ -77,9 +74,10 @@ public final class PassesService: Sendable { /// - Parameters: /// - pass: The pass to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated pass content as `Data`. - public func generatePassContent(for pass: Pass, on db: any Database) async throws -> Data { - try await service.generatePassContent(for: pass, on: db) + public func build(pass: Pass, on db: any Database) async throws -> Data { + try await service.build(pass: pass, on: db) } /// Generates a bundle of passes to enable your user to download multiple passes at once. @@ -91,9 +89,10 @@ public final class PassesService: Sendable { /// - Parameters: /// - passes: The passes to include in the bundle. /// - db: The `Database` to use. + /// /// - Returns: The bundle of passes as `Data`. - public func generatePassesContent(for passes: [Pass], on db: any Database) async throws -> Data { - try await service.generatePassesContent(for: passes, on: db) + public func build(passes: [Pass], on db: any Database) async throws -> Data { + try await service.build(passes: passes, on: db) } /// Adds the migrations for PassKit passes models. @@ -107,16 +106,6 @@ public final class PassesService: Sendable { migrations.add(PassesErrorLog()) } - /// Sends push notifications for a given pass. - /// - /// - Parameters: - /// - id: The `UUID` of the pass to send the notifications for. - /// - typeIdentifier: The type identifier of the pass. - /// - db: The `Database` to use. - public func sendPushNotificationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - try await service.sendPushNotificationsForPass(id: id, of: typeIdentifier, on: db) - } - /// Sends push notifications for a given pass. /// /// - Parameters: diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 56db552..6c6fb31 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -23,19 +23,18 @@ import Zip /// - Device Type /// - Registration Type /// - Error Log Type -public final class PassesServiceCustom< - P, U, D, R: PassesRegistrationModel, E: ErrorLogModel ->: Sendable +public final class PassesServiceCustom: Sendable where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { private unowned let app: Application private unowned let delegate: any PassesDelegate - private let signingFilesDirectory: URL - private let wwdrCertificate: String + private let logger: Logger? + + private let pemWWDRCertificate: String private let pemCertificate: String private let pemPrivateKey: String private let pemPrivateKeyPassword: String? - private let sslBinary: URL - private let logger: Logger? + private let openSSLURL: URL + private let encoder = JSONEncoder() /// Initializes the service and registers all the routes required for PassKit to work. @@ -43,65 +42,54 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { /// - Parameters: /// - app: The `Vapor.Application` to use in route handlers and APNs. /// - delegate: The ``PassesDelegate`` to use for pass generation. - /// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located. - /// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`. - /// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`. - /// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`. - /// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`. - /// - sslBinary: The location of the `openssl` command as a file path. /// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered. /// - logger: The `Logger` to use. + /// - pemWWDRCertificate: Apple's WWDR.pem certificate in PEM format. + /// - pemCertificate: The PEM Certificate for signing passes. + /// - pemPrivateKey: The PEM Certificate's private key for signing passes. + /// - pemPrivateKeyPassword: The password to the private key. If the key is not encrypted it must be `nil`. Defaults to `nil`. + /// - openSSLPath: The location of the `openssl` command as a file path. public init( app: Application, delegate: any PassesDelegate, - signingFilesDirectory: String, - wwdrCertificate: String = "WWDR.pem", - pemCertificate: String = "certificate.pem", - pemPrivateKey: String = "key.pem", - pemPrivateKeyPassword: String? = nil, - sslBinary: String = "/usr/bin/openssl", pushRoutesMiddleware: (any Middleware)? = nil, - logger: Logger? = nil + logger: Logger? = nil, + pemWWDRCertificate: String, + pemCertificate: String, + pemPrivateKey: String, + pemPrivateKeyPassword: String? = nil, + openSSLPath: String = "/usr/bin/openssl" ) throws { self.app = app self.delegate = delegate - self.signingFilesDirectory = URL(fileURLWithPath: signingFilesDirectory, isDirectory: true) - self.wwdrCertificate = wwdrCertificate + self.logger = logger + + self.pemWWDRCertificate = pemWWDRCertificate self.pemCertificate = pemCertificate self.pemPrivateKey = pemPrivateKey self.pemPrivateKeyPassword = pemPrivateKeyPassword - self.sslBinary = URL(fileURLWithPath: sslBinary) - self.logger = logger + self.openSSLURL = URL(fileURLWithPath: openSSLPath) - let privateKeyPath = URL(fileURLWithPath: pemPrivateKey, relativeTo: self.signingFilesDirectory).path - guard FileManager.default.fileExists(atPath: privateKeyPath) else { - throw PassesError.pemPrivateKeyMissing - } - let pemPath = URL(fileURLWithPath: pemCertificate, relativeTo: self.signingFilesDirectory).path - guard FileManager.default.fileExists(atPath: pemPath) else { - throw PassesError.pemCertificateMissing - } + let privateKeyBytes = pemPrivateKey.data(using: .utf8)!.map { UInt8($0) } + let certificateBytes = pemCertificate.data(using: .utf8)!.map { UInt8($0) } let apnsConfig: APNSClientConfiguration - if let password = pemPrivateKeyPassword { + if let pemPrivateKeyPassword { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( privateKey: .privateKey( - NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { passphraseCallback in - passphraseCallback(password.utf8) - }), - certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { - .certificate($0) - } + NIOSSLPrivateKey(bytes: privateKeyBytes, format: .pem) { passphraseCallback in + passphraseCallback(pemPrivateKeyPassword.utf8) + } + ), + certificateChain: NIOSSLCertificate.fromPEMBytes(certificateBytes).map { .certificate($0) } ), environment: .production ) } else { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( - privateKey: .privateKey(NIOSSLPrivateKey(file: privateKeyPath, format: .pem)), - certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { - .certificate($0) - } + privateKey: .privateKey(NIOSSLPrivateKey(bytes: privateKeyBytes, format: .pem)), + certificateChain: NIOSSLCertificate.fromPEMBytes(certificateBytes).map { .certificate($0) } ), environment: .production ) @@ -266,7 +254,7 @@ extension PassesServiceCustom { return try await Response( status: .ok, headers: headers, - body: Response.Body(data: self.generatePassContent(for: pass, on: req.db)) + body: Response.Body(data: self.build(pass: pass, on: req.db)) ) } @@ -344,72 +332,14 @@ extension PassesServiceCustom { pass._$userPersonalization.id = try userPersonalization.requireID() try await pass.update(on: req.db) - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { _ = try? FileManager.default.removeItem(at: root) } - guard let token = userInfo.personalizationToken.data(using: .utf8) else { throw Abort(.internalServerError) } - let signature: Data - if let password = self.pemPrivateKeyPassword { - let sslBinary: URL = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw PassesError.opensslBinaryMissing - } - - let tokenURL = root.appendingPathComponent("personalizationToken") - try token.write(to: tokenURL) - - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ - "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", tokenURL.path, - "-out", root.appendingPathComponent("signature").path, - "-outform", "DER", - "-passin", "pass:\(password)", - ] - try proc.run() - proc.waitUntilExit() - signature = try Data(contentsOf: root.appendingPathComponent("signature")) - } else { - let signatureBytes = try CMS.sign( - token, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - signature = Data(signatureBytes) - } var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/octet-stream") headers.add(name: .contentTransferEncoding, value: "binary") - return Response(status: .ok, headers: headers, body: Response.Body(data: signature)) + return try Response(status: .ok, headers: headers, body: Response.Body(data: self.signature(for: token))) } // MARK: - Push Routes @@ -421,7 +351,16 @@ extension PassesServiceCustom { } let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! - try await sendPushNotificationsForPass(id: id, of: passTypeIdentifier, on: req.db) + guard + let pass = try await P.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == passTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + try await sendPushNotifications(for: pass, on: req.db) return .noContent } @@ -433,7 +372,16 @@ extension PassesServiceCustom { } let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! - return try await Self.registrationsForPass(id: id, of: passTypeIdentifier, on: req.db).map { $0.device.pushToken } + guard + let pass = try await P.query(on: req.db) + .filter(\._$id == id) + .filter(\._$typeIdentifier == passTypeIdentifier) + .first() + else { + throw Abort(.notFound) + } + + return try await Self.registrations(for: pass, on: req.db).map { $0.device.pushToken } } } @@ -442,11 +390,10 @@ extension PassesServiceCustom { /// Sends push notifications for a given pass. /// /// - Parameters: - /// - id: The `UUID` of the pass to send the notifications for. - /// - typeIdentifier: The type identifier of the pass. + /// - pass: The pass to send the notifications for. /// - db: The `Database` to use. - public func sendPushNotificationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws { - let registrations = try await Self.registrationsForPass(id: id, of: typeIdentifier, on: db) + public func sendPushNotifications(for pass: P, on db: any Database) async throws { + let registrations = try await Self.registrations(for: pass, on: db) for reg in registrations { let backgroundNotification = APNSBackgroundNotification( expiration: .immediately, @@ -465,16 +412,7 @@ extension PassesServiceCustom { } } - /// Sends push notifications for a given pass. - /// - /// - Parameters: - /// - pass: The pass to send the notifications for. - /// - db: The `Database` to use. - public func sendPushNotifications(for pass: P, on db: any Database) async throws { - try await sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: db) - } - - private static func registrationsForPass(id: UUID, of typeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrations(for pass: P, on db: any Database) async throws -> [R] { // This could be done by enforcing the caller to have a Siblings property wrapper, // but there's not really any value to forcing that on them when we can just do the query ourselves like this. try await R.query(on: db) @@ -482,84 +420,78 @@ extension PassesServiceCustom { .join(parent: \._$device) .with(\._$pass) .with(\._$device) - .filter(P.self, \._$typeIdentifier == typeIdentifier) - .filter(P.self, \._$id == id) + .filter(P.self, \._$typeIdentifier == pass._$typeIdentifier.value!) + .filter(P.self, \._$id == pass.requireID()) .all() } } // MARK: - pkpass file generation extension PassesServiceCustom { - private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { + private func manifest(for directory: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: root) - guard !file.hasDirectoryPath else { continue } - manifest[relativePath] = try Insecure.SHA1.hash(data: Data(contentsOf: file)).hex - } - // Write the manifest file to the root directory - // and return the data for using it in signing. - let data = try encoder.encode(manifest) - try data.write(to: root.appendingPathComponent("manifest.json")) - return data - } + let file = URL(fileURLWithPath: relativePath, relativeTo: directory) + guard !file.hasDirectoryPath else { + continue + } - private func generateSignatureFile(for manifest: Data, in root: URL) throws { - // If the caller's delegate generated a file we don't have to do it. - if delegate.generateSignatureFile(in: root) { return } + let hash = try Insecure.SHA1.hash(data: Data(contentsOf: file)) + manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() + } + + return try encoder.encode(manifest) + } + // We use this function to sign the personalization token too. + private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. - if let password = self.pemPrivateKeyPassword { - let sslBinary = self.sslBinary - guard FileManager.default.fileExists(atPath: sslBinary.path) else { - throw PassesError.opensslBinaryMissing + if let pemPrivateKeyPassword { + guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { + throw WalletError.noOpenSSLExecutable } - let proc = Process() - proc.currentDirectoryURL = self.signingFilesDirectory - proc.executableURL = sslBinary - proc.arguments = [ + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: dir) } + + try manifest.write(to: dir.appendingPathComponent("manifest.json")) + try self.pemWWDRCertificate.write(to: dir.appendingPathComponent("wwdr.pem"), atomically: true, encoding: .utf8) + try self.pemCertificate.write(to: dir.appendingPathComponent("certificate.pem"), atomically: true, encoding: .utf8) + try self.pemPrivateKey.write(to: dir.appendingPathComponent("private.pem"), atomically: true, encoding: .utf8) + + let process = Process() + process.currentDirectoryURL = dir + process.executableURL = self.openSSLURL + process.arguments = [ "smime", "-binary", "-sign", - "-certfile", self.wwdrCertificate, - "-signer", self.pemCertificate, - "-inkey", self.pemPrivateKey, - "-in", root.appendingPathComponent("manifest.json").path, - "-out", root.appendingPathComponent("signature").path, + "-certfile", dir.appendingPathComponent("wwdr.pem").path, + "-signer", dir.appendingPathComponent("certificate.pem").path, + "-inkey", dir.appendingPathComponent("private.pem").path, + "-in", dir.appendingPathComponent("manifest.json").path, + "-out", dir.appendingPathComponent("signature").path, "-outform", "DER", - "-passin", "pass:\(password)", + "-passin", "pass:\(pemPrivateKeyPassword)", ] - try proc.run() - proc.waitUntilExit() - return - } - - let signature = try CMS.sign( - manifest, - signatureAlgorithm: .sha256WithRSAEncryption, - additionalIntermediateCertificates: [ - Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.wwdrCertificate) - ) - ) - ], - certificate: Certificate( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemCertificate) - ) - ), - privateKey: .init( - pemEncoded: String( - contentsOf: self.signingFilesDirectory - .appendingPathComponent(self.pemPrivateKey) - ) - ), - signingTime: Date() - ) - try Data(signature).write(to: root.appendingPathComponent("signature")) + try process.run() + process.waitUntilExit() + + return try Data(contentsOf: dir.appendingPathComponent("signature")) + } else { + let signature = try CMS.sign( + manifest, + signatureAlgorithm: .sha256WithRSAEncryption, + additionalIntermediateCertificates: [ + Certificate(pemEncoded: self.pemWWDRCertificate) + ], + certificate: Certificate(pemEncoded: self.pemCertificate), + privateKey: .init(pemEncoded: self.pemPrivateKey), + signingTime: Date() + ) + return Data(signature) + } } /// Generates the pass content bundle for a given pass. @@ -567,36 +499,50 @@ extension PassesServiceCustom { /// - Parameters: /// - pass: The pass to generate the content for. /// - db: The `Database` to use. + /// /// - Returns: The generated pass content as `Data`. - public func generatePassContent(for pass: P, on db: any Database) async throws -> Data { - let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true) + public func build(pass: P, on db: any Database) async throws -> Data { + let filesDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true) guard - (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false + (try? filesDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { - throw PassesError.templateNotDirectory + throw WalletError.noSourceFiles } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.copyItem(at: templateDirectory, to: root) - defer { _ = try? FileManager.default.removeItem(at: root) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.copyItem(at: filesDirectory, to: tempDir) + defer { try? FileManager.default.removeItem(at: tempDir) } - try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) - .write(to: root.appendingPathComponent("pass.json")) + var files: [ArchiveFile] = [] - var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) + let passJSON = try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) + try passJSON.write(to: tempDir.appendingPathComponent("pass.json")) + files.append(ArchiveFile(filename: "pass.json", data: passJSON)) // Pass Personalization if let personalizationJSON = try await self.delegate.personalizationJSON(for: pass, db: db) { - try self.encoder.encode(personalizationJSON).write(to: root.appendingPathComponent("personalization.json")) - files.append(URL(fileURLWithPath: "personalization.json", relativeTo: root)) + let personalizationJSONData = try self.encoder.encode(personalizationJSON) + try personalizationJSONData.write(to: tempDir.appendingPathComponent("personalization.json")) + files.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData)) } - try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) + let manifest = try self.manifest(for: tempDir) + files.append(ArchiveFile(filename: "manifest.json", data: manifest)) + try files.append(ArchiveFile(filename: "signature", data: self.signature(for: manifest))) + + let paths = try FileManager.default.subpathsOfDirectory(atPath: filesDirectory.path) + for relativePath in paths { + let file = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + guard !file.hasDirectoryPath else { + continue + } + + try files.append(ArchiveFile(filename: relativePath, data: Data(contentsOf: file))) + } - files.append(URL(fileURLWithPath: "pass.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) - files.append(URL(fileURLWithPath: "signature", relativeTo: root)) - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } /// Generates a bundle of passes to enable your user to download multiple passes at once. @@ -608,23 +554,20 @@ extension PassesServiceCustom { /// - Parameters: /// - passes: The passes to include in the bundle. /// - db: The `Database` to use. + /// /// - Returns: The bundle of passes as `Data`. - public func generatePassesContent(for passes: [P], on db: any Database) async throws -> Data { + public func build(passes: [P], on db: any Database) async throws -> Data { guard passes.count > 1 && passes.count <= 10 else { - throw PassesError.invalidNumberOfPasses + throw WalletError.invalidNumberOfPasses } - let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { _ = try? FileManager.default.removeItem(at: root) } - - var files: [URL] = [] + var files: [ArchiveFile] = [] for (i, pass) in passes.enumerated() { - let name = "pass\(i).pkpass" - try await self.generatePassContent(for: pass, on: db) - .write(to: root.appendingPathComponent(name)) - files.append(URL(fileURLWithPath: name, relativeTo: root)) + try await files.append(ArchiveFile(filename: "pass\(i).pkpass", data: self.build(pass: pass, on: db))) } - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) + + let zipFile = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") + try Zip.zipData(archiveFiles: files, zipFilePath: zipFile) + return try Data(contentsOf: zipFile) } } diff --git a/Tests/Certificates/WWDR.pem b/Tests/Certificates/WWDR.pem deleted file mode 100644 index 7202de0..0000000 --- a/Tests/Certificates/WWDR.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEVTCCAz2gAwIBAgIUE9x3lVJx5T3GMujM/+Uh88zFztIwDQYJKoZIhvcNAQEL -BQAwYjELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsT -HUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBS -b290IENBMB4XDTIwMTIxNjE5MzYwNFoXDTMwMTIxMDAwMDAwMFowdTFEMEIGA1UE -Aww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkxCzAJBgNVBAsMAkc0MRMwEQYDVQQKDApBcHBsZSBJbmMu -MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAf -eKp6JzKwRl/nF3bYoJ0OKY6tPTKlxGs3yeRBkWq3eXFdDDQEYHX3rkOPR8SGHgjo -v9Y5Ui8eZ/xx8YJtPH4GUnadLLzVQ+mxtLxAOnhRXVGhJeG+bJGdayFZGEHVD41t -QSo5SiHgkJ9OE0/QjJoyuNdqkh4laqQyziIZhQVg3AJK8lrrd3kCfcCXVGySjnYB -5kaP5eYq+6KwrRitbTOFOCOL6oqW7Z+uZk+jDEAnbZXQYojZQykn/e2kv1MukBVl -PNkuYmQzHWxq3Y4hqqRfFcYw7V/mjDaSlLfcOQIA+2SM1AyB8j/VNJeHdSbCb64D -YyEMe9QbsWLFApy9/a8CAwEAAaOB7zCB7DASBgNVHRMBAf8ECDAGAQH/AgEAMB8G -A1UdIwQYMBaAFCvQaUeUdgn+9GuNLkCm90dNfwheMEQGCCsGAQUFBwEBBDgwNjA0 -BggrBgEFBQcwAYYoaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy1hcHBsZXJv -b3RjYTAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vY3JsLmFwcGxlLmNvbS9yb290 -LmNybDAdBgNVHQ4EFgQUW9n6HeeaGgujmXYiUIY+kchbd6gwDgYDVR0PAQH/BAQD -AgEGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQA/Vj2e5bbD -eeZFIGi9v3OLLBKeAuOugCKMBB7DUshwgKj7zqew1UJEggOCTwb8O0kU+9h0UoWv -p50h5wESA5/NQFjQAde/MoMrU1goPO6cn1R2PWQnxn6NHThNLa6B5rmluJyJlPef -x4elUWY0GzlxOSTjh2fvpbFoe4zuPfeutnvi0v/fYcZqdUmVIkSoBPyUuAsuORFJ -EtHlgepZAE9bPFo22noicwkJac3AfOriJP6YRLj477JxPxpd1F1+M02cHSS+APCQ -A1iZQT0xWmJArzmoUUOSqwSonMJNsUvSq3xKX+udO7xPiEAGE/+QF4oIRynoYpgp -pU8RBWk6z/Kf ------END CERTIFICATE----- diff --git a/Tests/Certificates/certificate.pem b/Tests/Certificates/certificate.pem deleted file mode 100644 index c300118..0000000 --- a/Tests/Certificates/certificate.pem +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIC8TCCAdmgAwIBAgICbE8wDQYJKoZIhvcNAQENBQAwGDEWMBQGA1UEAwwNUHVz -aHlUZXN0Um9vdDAgFw0xNzA0MTcwMDUzMzBaGA8yMTE3MDMyNDAwNTMzMFowHzEd -MBsGA1UEAwwUY29tLnJlbGF5cmlkZXMucHVzaHkwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQDHZkZBnDKM4Gt+WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c36 -41/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTkKljDGe8tuDncT1qSrp/UuikgdIAAiCXA -/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+UbY803ArSPHjz3+Mk6C9tnzBXzG8oJq9o -EKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7U -j8wRstdr2xWhPg1fdIVHzudYubJ7M/h95JQFKtwqEevtLUa4BJgi8SKvRX5NnkGE -QMui1ercRuklVURTeoGDQYENiFnzTyI0J2tw3T+dAgMBAAGjPDA6MAkGA1UdEwQC -MAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD -ATANBgkqhkiG9w0BAQ0FAAOCAQEAnHHYMvBWglQLOUmNOalCMopmk9yKHM7+Sc9h -KsTWJW+YohF5zkRhnwUFxW85Pc63rRVA0qyI5zHzRtwYlcZHU57KttJyDGe1rm/0 -ZUqXharurJzyI09jcwRpDY8EGktrGirE1iHDqQTHNDHyS8iMVU6aPCo0xur63G5y -XzoIVhQXsBuwoU4VKb3n5CrxKEVcmE/nYF/Tk0rTtCrZF7TR3y/oxrp359goJ1b2 -/OjXN4dlqND41SbVTTL0FyXU3ebaS4DALA3pyVa1Rijw7vgEbFabsuMaAbdvlprn -RwUjsrRVu3Tx7sp/NqmeBLVru5nH/yHStDjSdvQtI2ipNGK/9w== ------END CERTIFICATE----- diff --git a/Tests/Certificates/encryptedcert.pem b/Tests/Certificates/encryptedcert.pem deleted file mode 100644 index 7a9d7cc..0000000 --- a/Tests/Certificates/encryptedcert.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICdDCCAVwCCQCtBOr7dtQS6zANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRAwDgYDVQQDDAdQYXNza2l0MSIwIAYJKoZIhvcNAQkBFhNub3Jl -cGx5QGV4YW1wbGUuY29tMB4XDTE5MDEzMTE2NTYzNloXDTI0MDEzMDE2NTYzNlow -RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu -dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC1 -+NQj0QzX5Vu9JMZVntP8i+JYAfOxzeP+MWUL/VaOxGaRp7DSiWAOd8bXDjJZjET0 -4SPZzKvy0a6Suk9aIxCfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHd5jKTM4smJ -b4CoVY2PwYogb+bI4AtpUBV1QlsDrb1xMBHQ6zLf+JhRMya2MqJR+5hDKywrN4bC -j3LY87ir5aJFFaBMs9h0sCEoQKs0cnksf6Gq2pVJF9G+Aki4UF9r7jxoQwXjbtS3 -m6ptezzKYvMcw5rKKhtZRgDT1uuy5hgOCapZrV1s0byRv3W6IcdzOD3cWZEuxz2D -AVZCwIvqThqMaAs3Fvs3L3aQsDiOJpZ65gNnBU6j21liMZ3q7txD3eCzuXWMLPI5 -O7C4Sxy+LF4XAfd1/0nmHC2HBgA6CSMgncEzU6PLRR6bXH1daKWlcMAvF+STbLUJ -79kQMXh2OCs= ------END CERTIFICATE----- diff --git a/Tests/Certificates/encryptedkey.pem b/Tests/Certificates/encryptedkey.pem deleted file mode 100644 index 615dcc0..0000000 --- a/Tests/Certificates/encryptedkey.pem +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIMBNCLGhiuR4CAggA -MBQGCCqGSIb3DQMHBAjXF3m+2fdMRgSCAWAaGMyNREsNYTuTE0Zf/GIORBQH1Vjc -pNTvxV0B/YUHfzthOkotQjL8mfbbCWVixEdDE41Rn66WVrVmgFDVIKoGhjsMLGYd -angmocOnZ77ZYXi0f0/8fZYuQF2dF/zOfsdxyNl2gi4MGbKqt8m9vDcFAWEZsN/r -5l1QJYNpF4OXKwNg4dnf7Ugo3PMWrVxKzKn+KtUvQd+mdYJ3xBjr1yLjLacbCXh0 -4Kh1kWeV6yyaZswYPPItyAeg4smLdDTEqFI+GHIT7NFQ0GIojIqz2Ug8KWZaMwZs -iRCYXHDECkC7zqgcxJKRtjDmCJIxfIFcnwJ8DmMf7bpawtowcfM/z7TGSzAUeptA -bH9rS4Zf5/5Sx/yFRr2esClwli5BJG1uISQBpA0DePhePTiW6LesvAt3YZ3p2BCI -OE0HwdbAr24Nw7LRCuobRsTKFnBmM+uqtGyJhKE6hC1q4CPjZ09F8njX ------END ENCRYPTED PRIVATE KEY----- diff --git a/Tests/Certificates/key.pem b/Tests/Certificates/key.pem deleted file mode 100644 index db02c80..0000000 --- a/Tests/Certificates/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHZkZBnDKM4Gt+ -WZwTc5h2GuT1Di7TfUE8SxDhw5wn3c3641/6lnrTj1Sh5tAsed8N2FDrD+Hp9zTk -KljDGe8tuDncT1qSrp/UuikgdIAAiCXA/vClWPYqZcHAUc9/OcfRiyK5AmJdzz+U -bY803ArSPHjz3+Mk6C9tnzBXzG8oJq9oEKJhwUYX+7l8+m0omtZXhMCOrbmZ2s69 -m6hTwHJKdC0mEngdyeiYIsbHaoSwxR7Uj8wRstdr2xWhPg1fdIVHzudYubJ7M/h9 -5JQFKtwqEevtLUa4BJgi8SKvRX5NnkGEQMui1ercRuklVURTeoGDQYENiFnzTyI0 -J2tw3T+dAgMBAAECggEBAMOsIZWQ6ipEsDe1R+vuq9Z6XeP8nwb7C2FXaKGji0Gz -78YcCruln7KsHKkkD3UVw0Wa2Q1S8Kbf6A9fXutWL9f1yRHg7Ui0BDSE2ob2zAW5 -lRLnGs+nlSnV4WQQ5EY9NVDz8IcNR+o2znWhbb65kATvQuJO+l/lWWWBqbb+7rW+ -RHy43p7U8cK63nXJy9eHZ7eOgGGUMUX+Yg0g47RGYxlIeSDrtPCXlNuwwAJY7Ecp -LVltCAyCJEaLVwQpz61PTdmkb9HCvkwiuL6cnjtpoAdXCWX7tV61UNweNkvALIWR -kMukFFE/H6JlAkcbw4na1KwQ3glWIIB2H/vZyMNdnyECgYEA78VEXo+iAQ6or4bY -xUQFd/hIibIYMzq8PxDMOmD86G78u5Ho0ytetht5Xk1xmhv402FZCL1LsAEWpCBs -a9LUwo30A23KaTA7Oy5oo5Md1YJejSNOCR+vs5wAo0SZov5tQaxVMoj3vZZqnJzJ -3A+XUgYZddIFkn8KJjgU/QVapTMCgYEA1OV1okYF2u5VW90RkVdvQONNcUvlKEL4 -UMSF3WJnORmtUL3Dt8AFt9b7pfz6WtVr0apT5SSIFA1+305PTpjjaw25m1GftL3U -5QwkmgTKxnPD/YPC6tImp+OUXHmk+iTgmQ9HaBpEplcyjD0EP2LQsIc6qiku/P2n -OT8ArOkk5+8CgYEA7B98wRL6G8hv3swRVdMy/36HEPNOWcUR9Zl5RlSVO+FxCtca -Tjt7viM4VuI1aer6FFDd+XlRvDaWMXOs0lKCLEbXczkACK7y5clCSzRqQQVuT9fg -1aNayKptBlxcYOPmfLJWBLpWH2KuAyV0tT61apWPJTR7QFXTjOfV44cOSXkCgYAH -CvAxRg+7hlbcixuhqzrK8roFHXWfN1fvlBC5mh/AC9Fn8l8fHQMTadE5VH0TtCu0 -6+WKlwLJZwjjajvFZdlgGTwinzihSgZY7WXoknAC0KGTKWCxU/Jja2vlA0Ep5T5o -0dCS6QuMVSYe7YXOcv5kWJTgPCyJwfpeMm9bSPsnkQKBgQChy4vU3J6CxGzwuvd/ -011kszao+cHn1DdMTyUhvA/O/paB+BAVktHm+o/i+kOk4OcPjhRqewzZZdf7ie5U -hUC8kIraXM4aZt69ThQkAIER89wlhxsFXUmGf7ZMXm8f7pvM6/MDaMW3mEsfbL0U -Y3jy0E30W5s1XCW3gmZ1Vg2xAg== ------END PRIVATE KEY----- diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index b5afb84..4d41358 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -17,7 +17,7 @@ struct OrdersTests { let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) - let data = try await ordersService.generateOrderContent(for: order, on: app.db) + let data = try await ordersService.build(order: order, on: app.db) let orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).order") try data.write(to: orderURL) let orderFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -25,6 +25,9 @@ struct OrdersTests { #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/pet_store_logo.png"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/it-IT.lproj/pet_store_logo.png"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/order.json"))) let orderJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) let orderJSON = try decoder.decode(OrderJSONData.self, from: orderJSONData!) @@ -37,6 +40,7 @@ struct OrdersTests { let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) #expect(manifestJSON["icon.png"] == SHA256.hash(data: iconData).hex) #expect(manifestJSON["pet_store_logo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/pet_store_logo.png"] != nil) } } @@ -323,7 +327,7 @@ struct OrdersTests { try await orderData.create(on: app.db) let order = try await orderData._$order.get(on: app.db) - try await ordersService.sendPushNotificationsForOrder(id: order.requireID(), of: order.typeIdentifier, on: app.db) + try await ordersService.sendPushNotifications(for: order, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" @@ -389,22 +393,4 @@ struct OrdersTests { } } } - - @Test("OrdersError") - func ordersError() { - #expect(OrdersError.templateNotDirectory.description == "OrdersError(errorType: templateNotDirectory)") - #expect(OrdersError.pemCertificateMissing.description == "OrdersError(errorType: pemCertificateMissing)") - #expect(OrdersError.pemPrivateKeyMissing.description == "OrdersError(errorType: pemPrivateKeyMissing)") - #expect(OrdersError.opensslBinaryMissing.description == "OrdersError(errorType: opensslBinaryMissing)") - } - - @Test("Default OrdersDelegate Properties") - func defaultDelegate() { - final class DefaultOrdersDelegate: OrdersDelegate { - func template(for order: O, db: any Database) async throws -> String { "" } - func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { Data() } - } - - #expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: ""))) - } } diff --git a/Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png b/Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png new file mode 100644 index 0000000..b0a0dc1 Binary files /dev/null and b/Tests/OrdersTests/Templates/it-IT.lproj/pet_store_logo.png differ diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index 5075c87..8928800 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -23,12 +23,12 @@ func withApp( let ordersService = try OrdersService( app: app, delegate: delegate, - signingFilesDirectory: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - pemCertificate: useEncryptedKey ? "encryptedcert.pem" : "certificate.pem", - pemPrivateKey: useEncryptedKey ? "encryptedkey.pem" : "key.pem", - pemPrivateKeyPassword: useEncryptedKey ? "password" : nil, pushRoutesMiddleware: SecretMiddleware(secret: "foo"), - logger: app.logger + logger: app.logger, + pemWWDRCertificate: TestCertificate.pemWWDRCertificate, + pemCertificate: useEncryptedKey ? TestCertificate.encryptedPemCertificate : TestCertificate.pemCertificate, + pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey, + pemPrivateKeyPassword: useEncryptedKey ? "password" : nil ) app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 5bccc26..9bc3637 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -17,7 +17,7 @@ struct PassesTests { let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) - let data = try await passesService.generatePassContent(for: pass, on: app.db) + let data = try await passesService.build(pass: pass, on: app.db) let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -25,6 +25,11 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) @@ -39,6 +44,8 @@ struct PassesTests { #expect(manifestJSON["icon.png"] == Insecure.SHA1.hash(data: iconData).hex) #expect(manifestJSON["logo.png"] != nil) #expect(manifestJSON["personalizationLogo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/logo.png"] != nil) + #expect(manifestJSON["it-IT.lproj/personalizationLogo.png"] != nil) } } @@ -53,13 +60,13 @@ struct PassesTests { try await passData2.create(on: app.db) let pass2 = try await passData2._$pass.get(on: app.db) - let data = try await passesService.generatePassesContent(for: [pass1, pass2], on: app.db) + let data = try await passesService.build(passes: [pass1, pass2], on: app.db) #expect(data != nil) do { - let data = try await passesService.generatePassesContent(for: [pass1], on: app.db) + let data = try await passesService.build(passes: [pass1], on: app.db) Issue.record("Expected error, got \(data)") - } catch let error as PassesError { + } catch let error as WalletError { #expect(error == .invalidNumberOfPasses) } } @@ -71,7 +78,7 @@ struct PassesTests { let passData = PassData(title: "Personalize") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) - let data = try await passesService.generatePassContent(for: pass, on: app.db) + let data = try await passesService.build(pass: pass, on: app.db) let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -79,6 +86,11 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/logo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/it-IT.lproj/personalizationLogo.png"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) @@ -93,8 +105,10 @@ struct PassesTests { let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) - let iconData = try Data(contentsOf: passFolder.appendingPathComponent("/personalizationLogo.png")) - #expect(manifestJSON["personalizationLogo.png"] == Insecure.SHA1.hash(data: iconData).hex) + let personalizationLogoData = try Data(contentsOf: passFolder.appendingPathComponent("/personalizationLogo.png")) + let personalizationLogoHash = Insecure.SHA1.hash(data: personalizationLogoData).hex + #expect(manifestJSON["personalizationLogo.png"] == personalizationLogoHash) + #expect(manifestJSON["it-IT.lproj/personalizationLogo.png"] == personalizationLogoHash) } } @@ -451,7 +465,7 @@ struct PassesTests { try await passData.create(on: app.db) let pass = try await passData._$pass.get(on: app.db) - try await passesService.sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: app.db) + try await passesService.sendPushNotifications(for: pass, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" @@ -518,13 +532,14 @@ struct PassesTests { } } - @Test("PassesError") - func passesError() { - #expect(PassesError.templateNotDirectory.description == "PassesError(errorType: templateNotDirectory)") - #expect(PassesError.pemCertificateMissing.description == "PassesError(errorType: pemCertificateMissing)") - #expect(PassesError.pemPrivateKeyMissing.description == "PassesError(errorType: pemPrivateKeyMissing)") - #expect(PassesError.opensslBinaryMissing.description == "PassesError(errorType: opensslBinaryMissing)") - #expect(PassesError.invalidNumberOfPasses.description == "PassesError(errorType: invalidNumberOfPasses)") + @Test("WalletError") + func walletError() { + #expect(WalletError.noSourceFiles.description == "WalletError(errorType: noSourceFiles)") + #expect(WalletError.noOpenSSLExecutable.description == "WalletError(errorType: noOpenSSLExecutable)") + #expect(WalletError.invalidNumberOfPasses.description == "WalletError(errorType: invalidNumberOfPasses)") + + #expect(WalletError.noSourceFiles == WalletError.noSourceFiles) + #expect(WalletError.noOpenSSLExecutable != WalletError.invalidNumberOfPasses) } @Test("Default PassesDelegate Properties") @@ -535,7 +550,6 @@ struct PassesTests { } let defaultDelegate = DefaultPassesDelegate() - #expect(!defaultDelegate.generateSignatureFile(in: URL(fileURLWithPath: ""))) try await withApp { app, passesService in let passData = PassData(title: "Test Pass") diff --git a/Tests/PassesTests/Templates/it-IT.lproj/logo.png b/Tests/PassesTests/Templates/it-IT.lproj/logo.png new file mode 100644 index 0000000..0b96c31 Binary files /dev/null and b/Tests/PassesTests/Templates/it-IT.lproj/logo.png differ diff --git a/Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png b/Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png new file mode 100644 index 0000000..0b96c31 Binary files /dev/null and b/Tests/PassesTests/Templates/it-IT.lproj/personalizationLogo.png differ diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index 52a051f..4681421 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -23,12 +23,12 @@ func withApp( let passesService = try PassesService( app: app, delegate: delegate, - signingFilesDirectory: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - pemCertificate: useEncryptedKey ? "encryptedcert.pem" : "certificate.pem", - pemPrivateKey: useEncryptedKey ? "encryptedkey.pem" : "key.pem", - pemPrivateKeyPassword: useEncryptedKey ? "password" : nil, pushRoutesMiddleware: SecretMiddleware(secret: "foo"), - logger: app.logger + logger: app.logger, + pemWWDRCertificate: TestCertificate.pemWWDRCertificate, + pemCertificate: useEncryptedKey ? TestCertificate.encryptedPemCertificate : TestCertificate.pemCertificate, + pemPrivateKey: useEncryptedKey ? TestCertificate.encryptedPemPrivateKey : TestCertificate.pemPrivateKey, + pemPrivateKeyPassword: useEncryptedKey ? "password" : nil ) app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite)