From 20136945c4dc0741ebae6ca1d0cd4885f9d7e243 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 17 Oct 2024 20:10:38 +0200 Subject: [PATCH 01/17] Simplify `PersonalizationJSON` --- Sources/Passes/DTOs/PersonalizationJSON.swift | 54 ++++++++++++------- Sources/Passes/Passes.docc/Personalization.md | 51 +++++------------- Sources/Passes/PassesDelegate.swift | 17 +++--- Sources/Passes/PassesServiceCustom.swift | 23 +++----- Tests/OrdersTests/OrderData.swift | 35 ++---------- .../PassesTests/EncryptedPassesDelegate.swift | 18 +++---- Tests/PassesTests/PassData.swift | 45 ++-------------- Tests/PassesTests/PassesTests.swift | 2 +- Tests/PassesTests/TestPassesDelegate.swift | 18 +++---- 9 files changed, 81 insertions(+), 182 deletions(-) diff --git a/Sources/Passes/DTOs/PersonalizationJSON.swift b/Sources/Passes/DTOs/PersonalizationJSON.swift index 2738be4..7ec005e 100644 --- a/Sources/Passes/DTOs/PersonalizationJSON.swift +++ b/Sources/Passes/DTOs/PersonalizationJSON.swift @@ -1,34 +1,48 @@ -// -// PersonalizationJSON.swift -// PassKit -// -// Created by Francesco Paolo Severino on 04/07/24. -// - /// The structure of a `personalization.json` file. /// /// This file specifies the personal information requested by the signup form. /// It also contains a description of the program and (optionally) the program’s terms and conditions. -public struct PersonalizationJSON { - /// A protocol that defines the structure of a `personalization.json` file. +/// +/// > Tip: See the [documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) to understand the keys. +public struct PersonalizationJSON: Encodable, Sendable { + /// The contents of this array define the data requested from the user. /// - /// > Tip: See the [documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) to understand the keys. - public protocol Properties: Encodable { - /// The contents of this array define the data requested from the user. - /// - /// The signup form’s fields are generated based on these keys. - var requiredPersonalizationFields: [PersonalizationField] { get } + /// The signup form’s fields are generated based on these keys. + var requiredPersonalizationFields: [PersonalizationField] - /// A brief description of the program. - /// - /// This is displayed on the signup sheet, under the personalization logo. - var description: String { get } + /// A brief description of the program. + /// + /// This is displayed on the signup sheet, under the personalization logo. + var description: String + + /// A description of the program’s terms and conditions. + /// + /// This string can contain HTML link tags to external content. + /// + /// If present, this information is displayed after the user enters their personal information and taps the Next button. + /// The user then has the option to agree to the terms, or to cancel out of the signup process. + var termsAndConditions: String? + + /// Initializes a new ``PersonalizationJSON`` instance. + /// + /// - Parameters: + /// - requiredPersonalizationFields: An array of ``PersonalizationField`` values that define the data requested to the user. + /// - description: A brief description of the program. + /// - termsAndConditions: A description of the program’s terms and conditions. + public init( + requiredPersonalizationFields: [PersonalizationField], + description: String, + termsAndConditions: String? = nil + ) { + self.requiredPersonalizationFields = requiredPersonalizationFields + self.description = description + self.termsAndConditions = termsAndConditions } } extension PersonalizationJSON { /// Personal information requested by the signup form. - public enum PersonalizationField: String, Encodable { + public enum PersonalizationField: String, Encodable, Sendable { /// Prompts the user for their name. /// /// `fullName`, `givenName`, and `familyName` are submitted in the personalize request. diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md index 32054c6..d900063 100644 --- a/Sources/Passes/Passes.docc/Personalization.md +++ b/Sources/Passes/Passes.docc/Personalization.md @@ -12,40 +12,17 @@ Pass Personalization lets you create passes, referred to as personalizable passe Personalizable passes can be distributed like any other pass. For information on personalizable passes, see the [Wallet Developer Guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) and [Return a Personalized Pass](https://developer.apple.com/documentation/walletpasses/return_a_personalized_pass). -### Model the personalization.json contents +### Implement the Delegate + +You'll have to make a few changes to ``PassesDelegate`` to support personalizable passes. A personalizable pass is just a standard pass package with the following additional files: - A `personalization.json` file. - A `personalizationLogo@XX.png` file. -Create a `struct` that implements ``PersonalizationJSON/Properties`` which will contain all the fields for the generated `personalization.json` file. -Create an initializer that takes your custom pass data, the ``Pass`` and everything else you may need. - -```swift -import Passes - -struct PersonalizationJSONData: PersonalizationJSON.Properties { - var requiredPersonalizationFields = [ - PersonalizationJSON.PersonalizationField.name, - PersonalizationJSON.PersonalizationField.postalCode, - PersonalizationJSON.PersonalizationField.emailAddress, - PersonalizationJSON.PersonalizationField.phoneNumber - ] - var description: String - - init(data: PassData, pass: Pass) { - self.description = data.title - } -} -``` - -### Implement the Delegate - -You'll have to make a few changes to ``PassesDelegate`` to support personalizable passes. - -Implement the ``PassesDelegate/encodePersonalization(for:db:encoder:)`` method, which gives you the ``Pass`` to encode. -If the pass requires personalization, and if it was not already personalized, encode the ``PersonalizationJSON`` and return it, otherwise return `nil`. +Implement the ``PassesDelegate/personalizationJSON(for:db:)`` method, which gives you the ``Pass`` to encode. +If the pass requires personalization, and if it was not already personalized, create the ``PersonalizationJSON`` struct, which will contain all the fields for the generated `personalization.json` file, and return it, otherwise return `nil`. In the ``PassesDelegate/template(for:db:)`` method, you have to return two different directory URLs, depending on whether the pass has to be personalized or not. If it does, the directory must contain the `personalizationLogo@XX.png` file. @@ -75,7 +52,7 @@ final class PassDelegate: PassesDelegate { return data } - func encodePersonalization(for pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data? { + func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.requireID()) .with(\.$pass) @@ -85,11 +62,11 @@ final class PassDelegate: PassesDelegate { } if try await passData.pass.$userPersonalization.get(on: db) == nil { - // If the pass requires personalization, encode the personalization JSON data. - guard let data = try? encoder.encode(PersonalizationJSONData(data: passData, pass: pass)) else { - throw Abort(.internalServerError) - } - return data + // If the pass requires personalization, create the personalization JSON struct. + return PersonalizationJSON( + requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber], + description: "Hello, World!" + ) } else { // Otherwise, return `nil`. return nil @@ -118,7 +95,7 @@ final class PassDelegate: PassesDelegate { ### Implement the Web Service -After implementing the JSON `struct` and the delegate, there is nothing else you have to do. +After implementing the delegate methods, there is nothing else you have to do. Initializing the ``PassesService`` will automatically set up the endpoints that Apple Wallet expects to exist on your server to handle pass personalization. @@ -130,10 +107,10 @@ Wallet will then send the user personal information to your server, which will b Immediately after that, Wallet will request the updated pass. This updated pass will contain the user personalization data that was previously saved inside the ``Pass/userPersonalization`` field. -> Important: This updated and personalized pass **must not** contain the `personalization.json` file, so make sure that the ``PassesDelegate/encodePersonalization(for:db:encoder:)`` method returns `nil` when the pass has already been personalized. +> Important: This updated and personalized pass **must not** contain the `personalization.json` file, so make sure that the ``PassesDelegate/personalizationJSON(for:db:)`` method returns `nil` when the pass has already been personalized. ## Topics ### Delegate Method -- ``PassesDelegate/encodePersonalization(for:db:encoder:)`` +- ``PassesDelegate/personalizationJSON(for:db:)`` diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift index a571f6f..b629ee1 100644 --- a/Sources/Passes/PassesDelegate.swift +++ b/Sources/Passes/PassesDelegate.swift @@ -72,12 +72,12 @@ public protocol PassesDelegate: AnyObject, Sendable { /// > Tip: See the [`Pass`](https://developer.apple.com/documentation/walletpasses/pass) object to understand the keys. func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data - /// Encode the personalization JSON file. + /// Create the personalization JSON struct. /// - /// This method of the ``PassesDelegate`` should generate the entire personalization JSON file. + /// This method of the ``PassesDelegate`` should generate the entire personalization JSON struct. /// You are provided with the pass data from the SQL database and, /// if the pass in question requires personalization, - /// you should return a properly formatted personalization JSON file. + /// you should return a ``PersonalizationJSON``. /// /// If the pass does not require personalization, you should return `nil`. /// @@ -86,11 +86,8 @@ public protocol PassesDelegate: AnyObject, Sendable { /// - Parameters: /// - pass: The pass data from the SQL server. /// - db: The SQL database to query against. - /// - encoder: The `JSONEncoder` which you should use. - /// - Returns: The encoded personalization JSON data, or `nil` if the pass does not require personalization. - func encodePersonalization( - for pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data? + /// - Returns: A ``PersonalizationJSON`` or `nil` if the pass does not require personalization. + func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? /// Should return a `URL` which points to the template data for the pass. /// @@ -151,9 +148,7 @@ extension PassesDelegate { return false } - public func encodePersonalization( - for pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data? { + public func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { return nil } } diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 334a990..db71d4c 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -30,6 +30,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { private unowned let app: Application private unowned let delegate: any PassesDelegate private let logger: Logger? + private let encoder = JSONEncoder() /// Initializes the service and registers all the routes required for PassKit to work. /// @@ -89,7 +90,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { apnsConfig, eventLoopGroupProvider: .shared(app.eventLoopGroup), responseDecoder: JSONDecoder(), - requestEncoder: JSONEncoder(), + requestEncoder: self.encoder, as: .init(string: "passes"), isDefault: false ) @@ -483,9 +484,7 @@ extension PassesServiceCustom { // MARK: - pkpass file generation extension PassesServiceCustom { - private static func generateManifestFile( - using encoder: JSONEncoder, in root: URL - ) throws -> Data { + private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { var manifest: [String: String] = [:] let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) for relativePath in paths { @@ -578,23 +577,17 @@ extension PassesServiceCustom { try FileManager.default.copyItem(at: templateDirectory, to: root) defer { _ = try? FileManager.default.removeItem(at: root) } - let encoder = JSONEncoder() - try await self.delegate.encode(pass: pass, db: db, encoder: encoder) + + try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) .write(to: root.appendingPathComponent("pass.json")) // Pass Personalization - if let encodedPersonalization = try await self.delegate.encodePersonalization( - for: pass, db: db, encoder: encoder) - { - try encodedPersonalization.write( - to: root.appendingPathComponent("personalization.json")) + 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)) } - try self.generateSignatureFile( - for: Self.generateManifestFile(using: encoder, in: root), - in: root - ) + try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) files.append(URL(fileURLWithPath: "pass.json", relativeTo: root)) files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift index 698c457..6733174 100644 --- a/Tests/OrdersTests/OrderData.swift +++ b/Tests/OrdersTests/OrderData.swift @@ -22,28 +22,6 @@ final class OrderData: OrderDataModel, @unchecked Sendable { self.id = id self.title = title } - - func toDTO() -> OrderDataDTO { - .init( - id: self.id, - title: self.$title.value - ) - } -} - -struct OrderDataDTO: Content { - var id: UUID? - var title: String? - - func toModel() -> OrderData { - let model = OrderData() - - model.id = self.id - if let title = self.title { - model.title = title - } - return model - } } struct CreateOrderData: AsyncMigration { @@ -51,10 +29,7 @@ struct CreateOrderData: AsyncMigration { try await database.schema(OrderData.FieldKeys.schemaName) .id() .field(OrderData.FieldKeys.title, .string, .required) - .field( - OrderData.FieldKeys.orderID, .uuid, .required, - .references(Order.schema, .id, onDelete: .cascade) - ) + .field(OrderData.FieldKeys.orderID, .uuid, .required, .references(Order.schema, .id, onDelete: .cascade)) .create() } @@ -111,9 +86,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware { self.service = service } - func create( - model: OrderData, on db: any Database, next: any AnyAsyncModelResponder - ) async throws { + func create(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let order = Order( orderTypeIdentifier: "order.com.example.pet-store", authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) @@ -122,9 +95,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware { try await next.create(model, on: db) } - func update( - model: OrderData, on db: any Database, next: any AnyAsyncModelResponder - ) async throws { + func update(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let order = try await model.$order.get(on: db) order.updatedAt = Date() try await order.save(on: db) diff --git a/Tests/PassesTests/EncryptedPassesDelegate.swift b/Tests/PassesTests/EncryptedPassesDelegate.swift index 5c887e5..2e666a6 100644 --- a/Tests/PassesTests/EncryptedPassesDelegate.swift +++ b/Tests/PassesTests/EncryptedPassesDelegate.swift @@ -31,9 +31,7 @@ final class EncryptedPassesDelegate: PassesDelegate { return data } - func encodePersonalization( - for pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data? { + func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.id!) @@ -46,20 +44,16 @@ final class EncryptedPassesDelegate: PassesDelegate { if passData.title != "Personalize" { return nil } if try await passData.pass.$userPersonalization.get(on: db) == nil { - guard let data = try? encoder.encode(PersonalizationJSONData()) else { - throw Abort(.internalServerError) - } - return data + return PersonalizationJSON( + requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber], + description: "Hello, World!" + ) } else { return nil } } func template(for pass: P, db: any Database) async throws -> URL { - URL( - fileURLWithPath: - "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", - isDirectory: true - ) + URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", isDirectory: true) } } diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/PassData.swift index bf96087..c2e4ff4 100644 --- a/Tests/PassesTests/PassData.swift +++ b/Tests/PassesTests/PassData.swift @@ -22,28 +22,6 @@ final class PassData: PassDataModel, @unchecked Sendable { self.id = id self.title = title } - - func toDTO() -> PassDataDTO { - .init( - id: self.id, - title: self.$title.value - ) - } -} - -struct PassDataDTO: Content { - var id: UUID? - var title: String? - - func toModel() -> PassData { - let model = PassData() - - model.id = self.id - if let title = self.title { - model.title = title - } - return model - } } struct CreatePassData: AsyncMigration { @@ -51,10 +29,7 @@ struct CreatePassData: AsyncMigration { try await database.schema(PassData.FieldKeys.schemaName) .id() .field(PassData.FieldKeys.title, .string, .required) - .field( - PassData.FieldKeys.passID, .uuid, .required, - .references(Pass.schema, .id, onDelete: .cascade) - ) + .field(PassData.FieldKeys.passID, .uuid, .required, .references(Pass.schema, .id, onDelete: .cascade)) .create() } @@ -125,16 +100,6 @@ struct PassJSONData: PassJSON.Properties { } } -struct PersonalizationJSONData: PersonalizationJSON.Properties { - var requiredPersonalizationFields = [ - PersonalizationJSON.PersonalizationField.name, - PersonalizationJSON.PersonalizationField.postalCode, - PersonalizationJSON.PersonalizationField.emailAddress, - PersonalizationJSON.PersonalizationField.phoneNumber, - ] - var description = "Hello, World!" -} - struct PassDataMiddleware: AsyncModelMiddleware { private unowned let service: PassesService @@ -142,9 +107,7 @@ struct PassDataMiddleware: AsyncModelMiddleware { self.service = service } - func create( - model: PassData, on db: any Database, next: any AnyAsyncModelResponder - ) async throws { + func create(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let pass = Pass( passTypeIdentifier: "pass.com.vapor-community.PassKit", authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) @@ -153,9 +116,7 @@ struct PassDataMiddleware: AsyncModelMiddleware { try await next.create(model, on: db) } - func update( - model: PassData, on db: any Database, next: any AnyAsyncModelResponder - ) async throws { + func update(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let pass = try await model.$pass.get(on: db) pass.updatedAt = Date() try await pass.save(on: db) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 7742d25..c497f8d 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -535,7 +535,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 defaultDelegate.encodePersonalization(for: pass, db: app.db, encoder: JSONEncoder()) + let data = try await defaultDelegate.personalizationJSON(for: pass, db: app.db) #expect(data == nil) } } diff --git a/Tests/PassesTests/TestPassesDelegate.swift b/Tests/PassesTests/TestPassesDelegate.swift index 4b4fc87..e96d99f 100644 --- a/Tests/PassesTests/TestPassesDelegate.swift +++ b/Tests/PassesTests/TestPassesDelegate.swift @@ -29,9 +29,7 @@ final class TestPassesDelegate: PassesDelegate { return data } - func encodePersonalization( - for pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data? { + func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.id!) @@ -44,20 +42,16 @@ final class TestPassesDelegate: PassesDelegate { if passData.title != "Personalize" { return nil } if try await passData.pass.$userPersonalization.get(on: db) == nil { - guard let data = try? encoder.encode(PersonalizationJSONData()) else { - throw Abort(.internalServerError) - } - return data + return PersonalizationJSON( + requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber], + description: "Hello, World!" + ) } else { return nil } } func template(for pass: P, db: any Database) async throws -> URL { - URL( - fileURLWithPath: - "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", - isDirectory: true - ) + URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", isDirectory: true) } } From 920c5ab8c49f41109bf7047231c5d43009aeb094 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 17 Oct 2024 20:36:13 +0200 Subject: [PATCH 02/17] Simplify `PassesDelegate` --- Sources/Passes/Passes.docc/GettingStarted.md | 34 +++--- Sources/Passes/Passes.docc/Personalization.md | 4 - Sources/Passes/PassesDelegate.swift | 53 --------- Sources/Passes/PassesService.swift | 36 ++++-- Sources/Passes/PassesServiceCustom.swift | 111 ++++++++++-------- .../PassesTests/EncryptedPassesDelegate.swift | 10 -- Tests/PassesTests/EncryptedPassesTests.swift | 6 +- Tests/PassesTests/PassesTests.swift | 5 - Tests/PassesTests/TestPassesDelegate.swift | 8 -- Tests/PassesTests/withApp.swift | 5 + 10 files changed, 119 insertions(+), 153 deletions(-) diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index fba044d..563645c 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -135,12 +135,6 @@ struct PassJSONData: PassJSON.Properties { ### Implement the Delegate Create a delegate class that implements ``PassesDelegate``. -In the ``PassesDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `passcertificate.pem` and `passkey.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). - -There are other fields available which have reasonable default values. See ``PassesDelegate``'s documentation. Because the files for your pass' template and the method of encoding might vary by pass type, you'll be provided the ``Pass`` for those methods. In the ``PassesDelegate/encode(pass:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``PassJSON``. @@ -151,10 +145,6 @@ import Fluent import Passes final class PassDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Passes/", isDirectory: true) - - let pemPrivateKeyPassword: String? = Environment.get("PASSES_PEM_PRIVATE_KEY_PASSWORD")! - func encode(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data { // The specific PassData class you use here may vary based on the `pass.passTypeIdentifier` // if you have multiple different types of passes, and thus multiple types of pass data. @@ -177,12 +167,14 @@ final class PassDelegate: PassesDelegate { } ``` -> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``PassesDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead. - ### Initialize the Service 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). ```swift import Fluent @@ -193,7 +185,11 @@ let passDelegate = PassDelegate() public func configure(_ app: Application) async throws { ... - let passesService = try PassesService(app: app, delegate: passDelegate) + let passesService = try PassesService( + app: app, + delegate: passDelegate, + signingFilesDirectory: "Certificates/Passes/" + ) } ``` @@ -223,7 +219,17 @@ let passDelegate = PassDelegate() public func configure(_ app: Application) async throws { ... - let passesService = try PassesServiceCustom(app: app, delegate: passDelegate) + let passesService = try PassesServiceCustom< + MyPassType, + MyUserPersonalizationType, + MyDeviceType, + MyPassesRegistrationType, + MyErrorLogType + >( + app: app, + delegate: passDelegate, + signingFilesDirectory: "Certificates/Passes/" + ) } ``` diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md index d900063..fc6743f 100644 --- a/Sources/Passes/Passes.docc/Personalization.md +++ b/Sources/Passes/Passes.docc/Personalization.md @@ -34,10 +34,6 @@ import Fluent import Passes final class PassDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Passes/", isDirectory: true) - - let pemPrivateKeyPassword: String? = Environment.get("PASSES_PEM_PRIVATE_KEY_PASSWORD")! - func encode(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data { // Here encode the pass JSON data as usual. guard let passData = try await PassData.query(on: db) diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift index b629ee1..55c2e17 100644 --- a/Sources/Passes/PassesDelegate.swift +++ b/Sources/Passes/PassesDelegate.swift @@ -88,62 +88,9 @@ public protocol PassesDelegate: AnyObject, Sendable { /// - db: The SQL database to query against. /// - Returns: A ``PersonalizationJSON`` or `nil` if the pass does not require personalization. func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? - - /// Should return a `URL` which points to the template data for the pass. - /// - /// The URL should point to a directory containing the files specified by these keys: - /// - `wwdrCertificate` - /// - `pemCertificate` - /// - `pemPrivateKey` - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer! - var sslSigningFilesDirectory: URL { get } - - /// The location of the `openssl` command as a file URL. - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor. - var sslBinary: URL { get } - - /// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path. - /// - /// Defaults to `WWDR.pem` - var wwdrCertificate: String { get } - - /// The name of the PEM Certificate for signing the pass as contained in `sslSigningFiles` path. - /// - /// Defaults to `passcertificate.pem` - var pemCertificate: String { get } - - /// The name of the PEM Certificate's private key for signing the pass as contained in `sslSigningFiles` path. - /// - /// Defaults to `passkey.pem` - var pemPrivateKey: String { get } - - /// The password to the private key file. - var pemPrivateKeyPassword: String? { get } } extension PassesDelegate { - public var wwdrCertificate: String { - return "WWDR.pem" - } - - public var pemCertificate: String { - return "passcertificate.pem" - } - - public var pemPrivateKey: String { - return "passkey.pem" - } - - public var pemPrivateKeyPassword: String? { - return nil - } - - public var sslBinary: URL { - return URL(fileURLWithPath: "/usr/bin/openssl") - } - public func generateSignatureFile(in root: URL) -> Bool { return false } diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 4fa593a..4be56a9 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -31,24 +31,44 @@ import Vapor /// The main class that handles PassKit passes. public final class PassesService: Sendable { - private let service: - PassesServiceCustom< - Pass, UserPersonalization, PassesDevice, PassesRegistration, PassesErrorLog - > + private let service: PassesServiceCustom /// Initializes the service and registers all the routes required for PassKit to work. /// /// - 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. public init( - app: Application, delegate: any PassesDelegate, - pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil + 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 ) throws { - service = try .init( - app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger + self.service = try .init( + app: app, + delegate: delegate, + signingFilesDirectory: signingFilesDirectory, + wwdrCertificate: wwdrCertificate, + pemCertificate: pemCertificate, + pemPrivateKey: pemPrivateKey, + pemPrivateKeyPassword: pemPrivateKeyPassword, + sslBinary: sslBinary, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger ) } diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index db71d4c..d95ff59 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -29,6 +29,12 @@ public final class PassesServiceCustom< 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 pemCertificate: String + private let pemPrivateKey: String + private let pemPrivateKeyPassword: String? + private let sslBinary: URL private let logger: Logger? private let encoder = JSONEncoder() @@ -37,32 +43,46 @@ 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. 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 ) throws { self.app = app self.delegate = delegate + self.signingFilesDirectory = URL(fileURLWithPath: signingFilesDirectory, isDirectory: true) + self.wwdrCertificate = wwdrCertificate + self.pemCertificate = pemCertificate + self.pemPrivateKey = pemPrivateKey + self.pemPrivateKeyPassword = pemPrivateKeyPassword + self.sslBinary = URL(fileURLWithPath: sslBinary) self.logger = logger - let privateKeyPath = URL( - fileURLWithPath: delegate.pemPrivateKey, relativeTo: delegate.sslSigningFilesDirectory - ).path + let privateKeyPath = URL(fileURLWithPath: pemPrivateKey, relativeTo: self.signingFilesDirectory).path guard FileManager.default.fileExists(atPath: privateKeyPath) else { throw PassesError.pemPrivateKeyMissing } - let pemPath = URL( - fileURLWithPath: delegate.pemCertificate, relativeTo: delegate.sslSigningFilesDirectory - ).path + let pemPath = URL(fileURLWithPath: pemCertificate, relativeTo: self.signingFilesDirectory).path guard FileManager.default.fileExists(atPath: pemPath) else { throw PassesError.pemCertificateMissing } let apnsConfig: APNSClientConfiguration - if let password = delegate.pemPrivateKeyPassword { + if let password = pemPrivateKeyPassword { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( privateKey: .privateKey( @@ -98,31 +118,26 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { let v1 = app.grouped("api", "passes", "v1") v1.get( "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", - use: { try await self.passesForDevice(req: $0) }) + use: { try await self.passesForDevice(req: $0) } + ) v1.post("log", use: { try await self.logError(req: $0) }) - v1.post( - "passes", ":passTypeIdentifier", ":passSerial", "personalize", - use: { try await self.personalizedPass(req: $0) }) + v1.post("passes", ":passTypeIdentifier", ":passSerial", "personalize", use: { try await self.personalizedPass(req: $0) }) let v1auth = v1.grouped(ApplePassMiddleware

()) v1auth.post( - "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", - ":passSerial", use: { try await self.registerDevice(req: $0) }) - v1auth.get( - "passes", ":passTypeIdentifier", ":passSerial", - use: { try await self.latestVersionOfPass(req: $0) }) + "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", ":passSerial", + use: { try await self.registerDevice(req: $0) } + ) + v1auth.get("passes", ":passTypeIdentifier", ":passSerial", use: { try await self.latestVersionOfPass(req: $0) }) v1auth.delete( - "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", - ":passSerial", use: { try await self.unregisterDevice(req: $0) }) + "devices", ":deviceLibraryIdentifier", "registrations", ":passTypeIdentifier", ":passSerial", + use: { try await self.unregisterDevice(req: $0) } + ) if let pushRoutesMiddleware { let pushAuth = v1.grouped(pushRoutesMiddleware) - pushAuth.post( - "push", ":passTypeIdentifier", ":passSerial", - use: { try await self.pushUpdatesForPass(req: $0) }) - pushAuth.get( - "push", ":passTypeIdentifier", ":passSerial", - use: { try await self.tokensForPassUpdate(req: $0) }) + pushAuth.post("push", ":passTypeIdentifier", ":passSerial", use: { try await self.pushUpdatesForPass(req: $0) }) + pushAuth.get("push", ":passTypeIdentifier", ":passSerial", use: { try await self.tokensForPassUpdate(req: $0) }) } } } @@ -339,8 +354,8 @@ extension PassesServiceCustom { throw Abort(.internalServerError) } let signature: Data - if let password = delegate.pemPrivateKeyPassword { - let sslBinary = delegate.sslBinary + if let password = self.pemPrivateKeyPassword { + let sslBinary: URL = self.sslBinary guard FileManager.default.fileExists(atPath: sslBinary.path) else { throw PassesError.opensslBinaryMissing } @@ -349,13 +364,13 @@ extension PassesServiceCustom { try token.write(to: tokenURL) let proc = Process() - proc.currentDirectoryURL = delegate.sslSigningFilesDirectory + proc.currentDirectoryURL = self.signingFilesDirectory proc.executableURL = sslBinary proc.arguments = [ "smime", "-binary", "-sign", - "-certfile", delegate.wwdrCertificate, - "-signer", delegate.pemCertificate, - "-inkey", delegate.pemPrivateKey, + "-certfile", self.wwdrCertificate, + "-signer", self.pemCertificate, + "-inkey", self.pemPrivateKey, "-in", tokenURL.path, "-out", root.appendingPathComponent("signature").path, "-outform", "DER", @@ -371,21 +386,21 @@ extension PassesServiceCustom { additionalIntermediateCertificates: [ Certificate( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.wwdrCertificate) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.wwdrCertificate) ) ) ], certificate: Certificate( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.pemCertificate) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.pemCertificate) ) ), privateKey: .init( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.pemPrivateKey) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.pemPrivateKey) ) ), signingTime: Date() @@ -504,20 +519,20 @@ extension PassesServiceCustom { if delegate.generateSignatureFile(in: root) { return } // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. - if let password = delegate.pemPrivateKeyPassword { - let sslBinary = delegate.sslBinary + if let password = self.pemPrivateKeyPassword { + let sslBinary = self.sslBinary guard FileManager.default.fileExists(atPath: sslBinary.path) else { throw PassesError.opensslBinaryMissing } let proc = Process() - proc.currentDirectoryURL = delegate.sslSigningFilesDirectory + proc.currentDirectoryURL = self.signingFilesDirectory proc.executableURL = sslBinary proc.arguments = [ "smime", "-binary", "-sign", - "-certfile", delegate.wwdrCertificate, - "-signer", delegate.pemCertificate, - "-inkey", delegate.pemPrivateKey, + "-certfile", self.wwdrCertificate, + "-signer", self.pemCertificate, + "-inkey", self.pemPrivateKey, "-in", root.appendingPathComponent("manifest.json").path, "-out", root.appendingPathComponent("signature").path, "-outform", "DER", @@ -534,21 +549,21 @@ extension PassesServiceCustom { additionalIntermediateCertificates: [ Certificate( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.wwdrCertificate) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.wwdrCertificate) ) ) ], certificate: Certificate( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.pemCertificate) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.pemCertificate) ) ), privateKey: .init( pemEncoded: String( - contentsOf: delegate.sslSigningFilesDirectory - .appendingPathComponent(delegate.pemPrivateKey) + contentsOf: self.signingFilesDirectory + .appendingPathComponent(self.pemPrivateKey) ) ), signingTime: Date() diff --git a/Tests/PassesTests/EncryptedPassesDelegate.swift b/Tests/PassesTests/EncryptedPassesDelegate.swift index 2e666a6..89b1365 100644 --- a/Tests/PassesTests/EncryptedPassesDelegate.swift +++ b/Tests/PassesTests/EncryptedPassesDelegate.swift @@ -3,16 +3,6 @@ import Passes import Vapor final class EncryptedPassesDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL( - fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - isDirectory: true - ) - - let pemCertificate = "encryptedcert.pem" - let pemPrivateKey = "encryptedkey.pem" - - let pemPrivateKeyPassword: String? = "password" - func encode( pass: P, db: any Database, encoder: JSONEncoder ) async throws -> Data { diff --git a/Tests/PassesTests/EncryptedPassesTests.swift b/Tests/PassesTests/EncryptedPassesTests.swift index f533c6e..8dea8c9 100644 --- a/Tests/PassesTests/EncryptedPassesTests.swift +++ b/Tests/PassesTests/EncryptedPassesTests.swift @@ -12,7 +12,7 @@ struct EncryptedPassesTests { @Test("Pass Generation") func passGeneration() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -40,7 +40,7 @@ struct EncryptedPassesTests { @Test("Personalizable Pass Apple Wallet API") func personalizationAPI() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in let passData = PassData(title: "Personalize") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -87,7 +87,7 @@ struct EncryptedPassesTests { @Test("APNS Client") func apnsClient() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in #expect(app.apns.client(.init(string: "passes")) != nil) let passData = PassData(title: "Test Pass") diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index c497f8d..4717ace 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -524,11 +524,6 @@ struct PassesTests { @Test("Default PassesDelegate Properties") func defaultDelegate() async throws { let defaultDelegate = DefaultPassesDelegate() - #expect(defaultDelegate.wwdrCertificate == "WWDR.pem") - #expect(defaultDelegate.pemCertificate == "passcertificate.pem") - #expect(defaultDelegate.pemPrivateKey == "passkey.pem") - #expect(defaultDelegate.pemPrivateKeyPassword == nil) - #expect(defaultDelegate.sslBinary == URL(fileURLWithPath: "/usr/bin/openssl")) #expect(!defaultDelegate.generateSignatureFile(in: URL(fileURLWithPath: ""))) try await withApp(delegate: delegate) { app, passesService in diff --git a/Tests/PassesTests/TestPassesDelegate.swift b/Tests/PassesTests/TestPassesDelegate.swift index e96d99f..cd2af50 100644 --- a/Tests/PassesTests/TestPassesDelegate.swift +++ b/Tests/PassesTests/TestPassesDelegate.swift @@ -3,14 +3,6 @@ import Passes import Vapor final class TestPassesDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL( - fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - isDirectory: true - ) - - let pemCertificate = "certificate.pem" - let pemPrivateKey = "key.pem" - func encode( pass: P, db: any Database, encoder: JSONEncoder ) async throws -> Data { diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index 6ff4a1c..86e1a58 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -8,6 +8,7 @@ import Zip func withApp( delegate: some PassesDelegate, + useEncryptedKey: Bool = false, _ body: (Application, PassesService) async throws -> Void ) async throws { let app = try await Application.make(.testing) @@ -21,6 +22,10 @@ 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 ) From 8a6cfc5be3f6728c5e473204ae6fb2a81dfc7763 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 17 Oct 2024 20:48:17 +0200 Subject: [PATCH 03/17] Simplify `OrdersDelegate` --- Sources/Orders/Orders.docc/GettingStarted.md | 33 ++++++----- Sources/Orders/OrdersDelegate.swift | 53 ----------------- Sources/Orders/OrdersService.swift | 31 ++++++++-- Sources/Orders/OrdersServiceCustom.swift | 58 +++++++++++++------ .../OrdersTests/EncryptedOrdersDelegate.swift | 10 ---- Tests/OrdersTests/OrdersTests.swift | 8 +-- Tests/OrdersTests/TestOrdersDelegate.swift | 8 --- Tests/OrdersTests/withApp.swift | 11 +++- 8 files changed, 94 insertions(+), 118 deletions(-) diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index ce70e86..664a06c 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -111,12 +111,6 @@ struct OrderJSONData: OrderJSON.Properties { ### Implement the Delegate Create a delegate class that implements ``OrdersDelegate``. -In the ``OrdersDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `ordercertificate.pem` and `orderkey.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. - -There are other fields available which have reasonable default values. See ``OrdersDelegate``'s documentation. Because the files for your order's template and the method of encoding might vary by order type, you'll be provided the ``Order`` for those methods. In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``. @@ -127,10 +121,6 @@ import Fluent import Orders final class OrderDelegate: OrdersDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true) - - let pemPrivateKeyPassword: String? = Environment.get("ORDERS_PEM_PRIVATE_KEY_PASSWORD")! - func encode(order: O, db: Database, encoder: JSONEncoder) async throws -> Data { // The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier` // if you have multiple different types of orders, and thus multiple types of order data. @@ -153,12 +143,14 @@ final class OrderDelegate: OrdersDelegate { } ``` -> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead. - ### Initialize the Service 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. ```swift import Fluent @@ -169,7 +161,11 @@ let orderDelegate = OrderDelegate() public func configure(_ app: Application) async throws { ... - let ordersService = try OrdersService(app: app, delegate: orderDelegate) + let ordersService = try OrdersService( + app: app, + delegate: orderDelegate, + signingFilesDirectory: "Certificates/Orders/" + ) } ``` @@ -199,7 +195,16 @@ let orderDelegate = OrderDelegate() public func configure(_ app: Application) async throws { ... - let ordersService = try OrdersServiceCustom(app: app, delegate: orderDelegate) + let ordersService = try OrdersServiceCustom< + MyOrderType, + MyDeviceType, + MyOrdersRegistrationType, + MyErrorLogType + >( + app: app, + delegate: orderDelegate, + signingFilesDirectory: "Certificates/Orders/" + ) } ``` diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift index f0b974b..cdbe8d2 100644 --- a/Sources/Orders/OrdersDelegate.swift +++ b/Sources/Orders/OrdersDelegate.swift @@ -51,62 +51,9 @@ public protocol OrdersDelegate: AnyObject, Sendable { func encode( order: O, db: any Database, encoder: JSONEncoder ) async throws -> Data - - /// Should return a `URL` which points to the template data for the order. - /// - /// The URL should point to a directory containing the files specified by these keys: - /// - `wwdrCertificate` - /// - `pemCertificate` - /// - `pemPrivateKey` - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer! - var sslSigningFilesDirectory: URL { get } - - /// The location of the `openssl` command as a file URL. - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor. - var sslBinary: URL { get } - - /// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path. - /// - /// Defaults to `WWDR.pem` - var wwdrCertificate: String { get } - - /// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path. - /// - /// Defaults to `ordercertificate.pem` - var pemCertificate: String { get } - - /// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path. - /// - /// Defaults to `orderkey.pem` - var pemPrivateKey: String { get } - - /// The password to the private key file. - var pemPrivateKeyPassword: String? { get } } extension OrdersDelegate { - public var wwdrCertificate: String { - return "WWDR.pem" - } - - public var pemCertificate: String { - return "ordercertificate.pem" - } - - public var pemPrivateKey: String { - return "orderkey.pem" - } - - public var pemPrivateKeyPassword: String? { - return nil - } - - public var sslBinary: URL { - return URL(fileURLWithPath: "/usr/bin/openssl") - } - public func generateSignatureFile(in root: URL) -> Bool { return false } diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index 2577513..016c134 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -17,14 +17,37 @@ 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. public init( - app: Application, delegate: any OrdersDelegate, - pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil + 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 ) throws { - service = try .init( - app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger + self.service = try .init( + app: app, + delegate: delegate, + signingFilesDirectory: signingFilesDirectory, + wwdrCertificate: wwdrCertificate, + pemCertificate: pemCertificate, + pemPrivateKey: pemPrivateKey, + pemPrivateKeyPassword: pemPrivateKeyPassword, + sslBinary: sslBinary, + pushRoutesMiddleware: pushRoutesMiddleware, + logger: logger ) } diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index a650869..0905e38 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -26,6 +26,12 @@ public final class OrdersServiceCustom( order: O, db: any Database, encoder: JSONEncoder ) async throws -> Data { diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index c7d1557..98a5317 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -397,13 +397,7 @@ struct OrdersTests { @Test("Default OrdersDelegate Properties") func defaultDelegate() { - let delegate = DefaultOrdersDelegate() - #expect(delegate.wwdrCertificate == "WWDR.pem") - #expect(delegate.pemCertificate == "ordercertificate.pem") - #expect(delegate.pemPrivateKey == "orderkey.pem") - #expect(delegate.pemPrivateKeyPassword == nil) - #expect(delegate.sslBinary == URL(fileURLWithPath: "/usr/bin/openssl")) - #expect(!delegate.generateSignatureFile(in: URL(fileURLWithPath: ""))) + #expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: ""))) } } diff --git a/Tests/OrdersTests/TestOrdersDelegate.swift b/Tests/OrdersTests/TestOrdersDelegate.swift index ae36748..d6fa266 100644 --- a/Tests/OrdersTests/TestOrdersDelegate.swift +++ b/Tests/OrdersTests/TestOrdersDelegate.swift @@ -3,14 +3,6 @@ import Orders import Vapor final class TestOrdersDelegate: OrdersDelegate { - let sslSigningFilesDirectory = URL( - fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/", - isDirectory: true - ) - - let pemCertificate = "certificate.pem" - let pemPrivateKey = "key.pem" - func encode( order: O, db: any Database, encoder: JSONEncoder ) async throws -> Data { diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index ab5df1d..0185293 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -8,6 +8,7 @@ import Zip func withApp( delegate: some OrdersDelegate, + useEncryptedKey: Bool = false, _ body: (Application, OrdersService) async throws -> Void ) async throws { let app = try await Application.make(.testing) @@ -18,20 +19,24 @@ func withApp( OrdersService.register(migrations: app.migrations) app.migrations.add(CreateOrderData()) - let passesService = try OrdersService( + 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 ) - app.databases.middleware.use(OrderDataMiddleware(service: passesService), on: .sqlite) + app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) try await app.autoMigrate() Zip.addCustomFileExtension("order") - try await body(app, passesService) + try await body(app, ordersService) try await app.autoRevert() try await app.asyncShutdown() From ec3de27e5e9c7da2ba2d91dad24e579bab209a36 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 17 Oct 2024 20:54:11 +0200 Subject: [PATCH 04/17] Make the linter happy --- Sources/Passes/DTOs/PersonalizationJSON.swift | 8 ++++---- Sources/Passes/PassesServiceCustom.swift | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/Passes/DTOs/PersonalizationJSON.swift b/Sources/Passes/DTOs/PersonalizationJSON.swift index 7ec005e..e73e5aa 100644 --- a/Sources/Passes/DTOs/PersonalizationJSON.swift +++ b/Sources/Passes/DTOs/PersonalizationJSON.swift @@ -2,7 +2,7 @@ /// /// This file specifies the personal information requested by the signup form. /// It also contains a description of the program and (optionally) the program’s terms and conditions. -/// +/// /// > Tip: See the [documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) to understand the keys. public struct PersonalizationJSON: Encodable, Sendable { /// The contents of this array define the data requested from the user. @@ -16,15 +16,15 @@ public struct PersonalizationJSON: Encodable, Sendable { var description: String /// A description of the program’s terms and conditions. - /// + /// /// This string can contain HTML link tags to external content. - /// + /// /// If present, this information is displayed after the user enters their personal information and taps the Next button. /// The user then has the option to agree to the terms, or to cancel out of the signup process. var termsAndConditions: String? /// Initializes a new ``PersonalizationJSON`` instance. - /// + /// /// - Parameters: /// - requiredPersonalizationFields: An array of ``PersonalizationField`` values that define the data requested to the user. /// - description: A brief description of the program. diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index d95ff59..2cbe831 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -592,7 +592,6 @@ extension PassesServiceCustom { try FileManager.default.copyItem(at: templateDirectory, to: root) defer { _ = try? FileManager.default.removeItem(at: root) } - try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) .write(to: root.appendingPathComponent("pass.json")) From 6b4ded08520761c04f62e5a3af5eff82dadbcbe7 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 18 Oct 2024 16:16:19 +0200 Subject: [PATCH 05/17] Simplify tests --- .../OrdersTests/EncryptedOrdersDelegate.swift | 31 ---- Tests/OrdersTests/EncryptedOrdersTests.swift | 92 ------------ Tests/OrdersTests/OrderData.swift | 3 +- Tests/OrdersTests/OrdersTests.swift | 38 +++-- Tests/OrdersTests/TestOrdersDelegate.swift | 13 +- Tests/OrdersTests/withApp.swift | 3 +- .../PassesTests/EncryptedPassesDelegate.swift | 49 ------ Tests/PassesTests/EncryptedPassesTests.swift | 139 ------------------ Tests/PassesTests/PassData.swift | 3 +- Tests/PassesTests/PassesTests.swift | 51 ++++--- Tests/PassesTests/TestPassesDelegate.swift | 7 +- Tests/PassesTests/withApp.swift | 3 +- 12 files changed, 56 insertions(+), 376 deletions(-) delete mode 100644 Tests/OrdersTests/EncryptedOrdersDelegate.swift delete mode 100644 Tests/OrdersTests/EncryptedOrdersTests.swift delete mode 100644 Tests/PassesTests/EncryptedPassesDelegate.swift delete mode 100644 Tests/PassesTests/EncryptedPassesTests.swift diff --git a/Tests/OrdersTests/EncryptedOrdersDelegate.swift b/Tests/OrdersTests/EncryptedOrdersDelegate.swift deleted file mode 100644 index 0db2b01..0000000 --- a/Tests/OrdersTests/EncryptedOrdersDelegate.swift +++ /dev/null @@ -1,31 +0,0 @@ -import FluentKit -import Orders -import Vapor - -final class EncryptedOrdersDelegate: OrdersDelegate { - func encode( - order: O, db: any Database, encoder: JSONEncoder - ) async throws -> Data { - guard - let orderData = try await OrderData.query(on: db) - .filter(\.$order.$id == order.requireID()) - .with(\.$order) - .first() - else { - throw Abort(.internalServerError) - } - guard let data = try? encoder.encode(OrderJSONData(data: orderData, order: orderData.order)) - else { - throw Abort(.internalServerError) - } - return data - } - - func template(for: O, db: any Database) async throws -> URL { - URL( - fileURLWithPath: - "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/", - isDirectory: true - ) - } -} diff --git a/Tests/OrdersTests/EncryptedOrdersTests.swift b/Tests/OrdersTests/EncryptedOrdersTests.swift deleted file mode 100644 index 368b7dc..0000000 --- a/Tests/OrdersTests/EncryptedOrdersTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -import FluentKit -import PassKit -import Testing -import XCTVapor -import Zip - -@testable import Orders - -@Suite("Orders Tests with Encrypted PEM Key") -struct EncryptedOrdersTests { - let delegate = EncryptedOrdersDelegate() - let ordersURI = "/api/orders/v1/" - - @Test("Order Generation") - func orderGeneration() async throws { - try await withApp(delegate: delegate) { app, ordersService in - 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 orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.order") - try data.write(to: orderURL) - let orderFolder = try Zip.quickUnzipFile(orderURL) - - #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) - - let passJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) - let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] - #expect(passJSON["authenticationToken"] as? String == order.authenticationToken) - let orderID = try order.requireID().uuidString - #expect(passJSON["orderIdentifier"] as? String == orderID) - - let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) - let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] - let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) - let iconHash = Array(SHA256.hash(data: iconData)).hex - #expect(manifestJSON["icon.png"] as? String == iconHash) - } - } - - @Test("APNS Client") - func apnsClient() async throws { - try await withApp(delegate: delegate) { app, ordersService in - #expect(app.apns.client(.init(string: "orders")) != nil) - - let orderData = OrderData(title: "Test Order") - 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.orderTypeIdentifier, on: app.db) - - let deviceLibraryIdentifier = "abcdefg" - let pushToken = "1234567890" - - try await app.test( - .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", - headers: ["X-Secret": "foo"], - afterResponse: { res async throws in - #expect(res.status == .noContent) - } - ) - - try await app.test( - .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", - headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], - beforeRequest: { req async throws in - try req.content.encode(RegistrationDTO(pushToken: pushToken)) - }, - afterResponse: { res async throws in - #expect(res.status == .created) - } - ) - - try await app.test( - .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", - headers: ["X-Secret": "foo"], - afterResponse: { res async throws in - #expect(res.status == .internalServerError) - } - ) - - // Test `OrderDataMiddleware` update method - orderData.title = "Test Order 2" - do { - try await orderData.update(on: app.db) - } catch {} - } - } -} diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift index 6733174..d51c128 100644 --- a/Tests/OrdersTests/OrderData.swift +++ b/Tests/OrdersTests/OrderData.swift @@ -89,7 +89,8 @@ struct OrderDataMiddleware: AsyncModelMiddleware { func create(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let order = Order( orderTypeIdentifier: "order.com.example.pet-store", - authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) + authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString() + ) try await order.save(on: db) model.$order.id = try order.requireID() try await next.create(model, on: db) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 98a5317..fc8cad1 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -8,12 +8,11 @@ import Zip @Suite("Orders Tests") struct OrdersTests { - let delegate = TestOrdersDelegate() let ordersURI = "/api/orders/v1/" - @Test("Order Generation") - func orderGeneration() async throws { - try await withApp(delegate: delegate) { app, ordersService in + @Test("Order Generation", arguments: [true, false]) + func orderGeneration(useEncryptedKey: Bool) async throws { + try await withApp(useEncryptedKey: useEncryptedKey) { app, ordersService in let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) @@ -41,7 +40,7 @@ struct OrdersTests { @Test("Getting Order from Apple Wallet API") func getOrderFromAPI() async throws { - try await withApp(delegate: delegate) { app, ordersService in + try await withApp { app, ordersService in let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) @@ -117,7 +116,7 @@ struct OrdersTests { @Test("Device Registration API") func apiDeviceRegistration() async throws { - try await withApp(delegate: delegate) { app, ordersService in + try await withApp { app, ordersService in let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) @@ -270,7 +269,7 @@ struct OrdersTests { @Test("Error Logging") func errorLog() async throws { - try await withApp(delegate: delegate) { app, ordersService in + try await withApp { app, ordersService in let log1 = "Error 1" let log2 = "Error 2" @@ -313,9 +312,9 @@ struct OrdersTests { } } - @Test("APNS Client") - func apnsClient() async throws { - try await withApp(delegate: delegate) { app, ordersService in + @Test("APNS Client", arguments: [true, false]) + func apnsClient(useEncryptedKey: Bool) async throws { + try await withApp(useEncryptedKey: useEncryptedKey) { app, ordersService in #expect(app.apns.client(.init(string: "orders")) != nil) let orderData = OrderData(title: "Test Order") @@ -397,16 +396,15 @@ struct OrdersTests { @Test("Default OrdersDelegate Properties") func defaultDelegate() { - #expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: ""))) - } -} + final class DefaultOrdersDelegate: OrdersDelegate { + func template(for order: O, db: any Database) async throws -> URL { + URL(fileURLWithPath: "") + } + func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { + Data() + } + } -final class DefaultOrdersDelegate: OrdersDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "", isDirectory: true) - func template(for order: O, db: any Database) async throws -> URL { - URL(fileURLWithPath: "") - } - func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { - Data() + #expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: ""))) } } diff --git a/Tests/OrdersTests/TestOrdersDelegate.swift b/Tests/OrdersTests/TestOrdersDelegate.swift index d6fa266..7fa2485 100644 --- a/Tests/OrdersTests/TestOrdersDelegate.swift +++ b/Tests/OrdersTests/TestOrdersDelegate.swift @@ -3,9 +3,7 @@ import Orders import Vapor final class TestOrdersDelegate: OrdersDelegate { - func encode( - order: O, db: any Database, encoder: JSONEncoder - ) async throws -> Data { + func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { guard let orderData = try await OrderData.query(on: db) .filter(\.$order.$id == order.requireID()) @@ -14,18 +12,13 @@ final class TestOrdersDelegate: OrdersDelegate { else { throw Abort(.internalServerError) } - guard let data = try? encoder.encode(OrderJSONData(data: orderData, order: orderData.order)) - else { + guard let data = try? encoder.encode(OrderJSONData(data: orderData, order: orderData.order)) else { throw Abort(.internalServerError) } return data } func template(for: O, db: any Database) async throws -> URL { - URL( - fileURLWithPath: - "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/", - isDirectory: true - ) + URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/", isDirectory: true) } } diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index 0185293..7f89cc2 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -7,7 +7,6 @@ import Vapor import Zip func withApp( - delegate: some OrdersDelegate, useEncryptedKey: Bool = false, _ body: (Application, OrdersService) async throws -> Void ) async throws { @@ -17,6 +16,8 @@ func withApp( app.databases.use(.sqlite(.memory), as: .sqlite) + let delegate = TestOrdersDelegate() + OrdersService.register(migrations: app.migrations) app.migrations.add(CreateOrderData()) let ordersService = try OrdersService( diff --git a/Tests/PassesTests/EncryptedPassesDelegate.swift b/Tests/PassesTests/EncryptedPassesDelegate.swift deleted file mode 100644 index 89b1365..0000000 --- a/Tests/PassesTests/EncryptedPassesDelegate.swift +++ /dev/null @@ -1,49 +0,0 @@ -import FluentKit -import Passes -import Vapor - -final class EncryptedPassesDelegate: PassesDelegate { - func encode( - pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data { - guard - let passData = try await PassData.query(on: db) - .filter(\.$pass.$id == pass.requireID()) - .with(\.$pass) - .first() - else { - throw Abort(.internalServerError) - } - guard let data = try? encoder.encode(PassJSONData(data: passData, pass: passData.pass)) - else { - throw Abort(.internalServerError) - } - return data - } - - func personalizationJSON(for pass: P, db: any Database) async throws -> PersonalizationJSON? { - guard - let passData = try await PassData.query(on: db) - .filter(\.$pass.$id == pass.id!) - .with(\.$pass) - .first() - else { - throw Abort(.internalServerError) - } - - if passData.title != "Personalize" { return nil } - - if try await passData.pass.$userPersonalization.get(on: db) == nil { - return PersonalizationJSON( - requiredPersonalizationFields: [.name, .postalCode, .emailAddress, .phoneNumber], - description: "Hello, World!" - ) - } else { - return nil - } - } - - func template(for pass: P, db: any Database) async throws -> URL { - URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", isDirectory: true) - } -} diff --git a/Tests/PassesTests/EncryptedPassesTests.swift b/Tests/PassesTests/EncryptedPassesTests.swift deleted file mode 100644 index 8dea8c9..0000000 --- a/Tests/PassesTests/EncryptedPassesTests.swift +++ /dev/null @@ -1,139 +0,0 @@ -import PassKit -import Testing -import XCTVapor -import Zip - -@testable import Passes - -@Suite("Passes Tests with Encrypted PEM Key") -struct EncryptedPassesTests { - let delegate = EncryptedPassesDelegate() - let passesURI = "/api/passes/v1/" - - @Test("Pass Generation") - func passGeneration() async throws { - try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in - 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 passURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.pkpass") - try data.write(to: passURL) - let passFolder = try Zip.quickUnzipFile(passURL) - - #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) - - let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) - let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] - #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) - let passID = try pass.requireID().uuidString - #expect(passJSON["serialNumber"] as? String == passID) - #expect(passJSON["description"] as? String == passData.title) - - let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) - let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] - let iconData = try Data(contentsOf: passFolder.appendingPathComponent("/icon.png")) - let iconHash = Array(Insecure.SHA1.hash(data: iconData)).hex - #expect(manifestJSON["icon.png"] as? String == iconHash) - } - } - - @Test("Personalizable Pass Apple Wallet API") - func personalizationAPI() async throws { - try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in - let passData = PassData(title: "Personalize") - try await passData.create(on: app.db) - let pass = try await passData.$pass.get(on: app.db) - let personalizationDict = PersonalizationDictionaryDTO( - personalizationToken: "1234567890", - requiredPersonalizationInfo: .init( - emailAddress: "test@example.com", - familyName: "Doe", - fullName: "John Doe", - givenName: "John", - isoCountryCode: "US", - phoneNumber: "1234567890", - postalCode: "12345" - ) - ) - - try await app.test( - .POST, - "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())/personalize", - headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], - beforeRequest: { req async throws in - try req.content.encode(personalizationDict) - }, - afterResponse: { res async throws in - #expect(res.status == .ok) - #expect(res.body != nil) - #expect(res.headers.contentType?.description == "application/octet-stream") - } - ) - - let personalizationQuery = try await UserPersonalization.query(on: app.db).all() - #expect(personalizationQuery.count == 1) - let passPersonalizationID = try await Pass.query(on: app.db).first()?._$userPersonalization.get(on: app.db)?.requireID() - #expect(personalizationQuery[0]._$id.value == passPersonalizationID) - #expect(personalizationQuery[0]._$emailAddress.value == personalizationDict.requiredPersonalizationInfo.emailAddress) - #expect(personalizationQuery[0]._$familyName.value == personalizationDict.requiredPersonalizationInfo.familyName) - #expect(personalizationQuery[0]._$fullName.value == personalizationDict.requiredPersonalizationInfo.fullName) - #expect(personalizationQuery[0]._$givenName.value == personalizationDict.requiredPersonalizationInfo.givenName) - #expect(personalizationQuery[0]._$isoCountryCode.value == personalizationDict.requiredPersonalizationInfo.isoCountryCode) - #expect(personalizationQuery[0]._$phoneNumber.value == personalizationDict.requiredPersonalizationInfo.phoneNumber) - #expect(personalizationQuery[0]._$postalCode.value == personalizationDict.requiredPersonalizationInfo.postalCode) - } - } - - @Test("APNS Client") - func apnsClient() async throws { - try await withApp(delegate: delegate, useEncryptedKey: true) { app, passesService in - #expect(app.apns.client(.init(string: "passes")) != nil) - - let passData = PassData(title: "Test Pass") - 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.passTypeIdentifier, on: app.db) - - let deviceLibraryIdentifier = "abcdefg" - let pushToken = "1234567890" - - try await app.test( - .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", - headers: ["X-Secret": "foo"], - afterResponse: { res async throws in - #expect(res.status == .noContent) - } - ) - - try await app.test( - .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", - headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], - beforeRequest: { req async throws in - try req.content.encode(RegistrationDTO(pushToken: pushToken)) - }, - afterResponse: { res async throws in - #expect(res.status == .created) - } - ) - - try await app.test( - .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", - headers: ["X-Secret": "foo"], - afterResponse: { res async throws in - #expect(res.status == .internalServerError) - } - ) - - // Test `PassDataMiddleware` update method - passData.title = "Test Pass 2" - do { - try await passData.update(on: app.db) - } catch {} - } - } -} diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/PassData.swift index c2e4ff4..313d479 100644 --- a/Tests/PassesTests/PassData.swift +++ b/Tests/PassesTests/PassData.swift @@ -110,7 +110,8 @@ struct PassDataMiddleware: AsyncModelMiddleware { func create(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let pass = Pass( passTypeIdentifier: "pass.com.vapor-community.PassKit", - authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) + authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString() + ) try await pass.save(on: db) model.$pass.id = try pass.requireID() try await next.create(model, on: db) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 4717ace..c50921c 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -8,12 +8,11 @@ import Zip @Suite("Passes Tests") struct PassesTests { - let delegate = TestPassesDelegate() let passesURI = "/api/passes/v1/" - @Test("Pass Generation") - func passGeneration() async throws { - try await withApp(delegate: delegate) { app, passesService in + @Test("Pass Generation", arguments: [true, false]) + func passGeneration(useEncryptedKey: Bool) async throws { + try await withApp(useEncryptedKey: useEncryptedKey) { app, passesService in let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -43,7 +42,7 @@ struct PassesTests { @Test("Generating Multiple Passes") func passesGeneration() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let passData1 = PassData(title: "Test Pass 1") try await passData1.create(on: app.db) let pass1 = try await passData1.$pass.get(on: app.db) @@ -66,7 +65,7 @@ struct PassesTests { @Test("Personalizable Passes") func personalization() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let passData = PassData(title: "Personalize") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -96,7 +95,7 @@ struct PassesTests { @Test("Getting Pass from Apple Wallet API") func getPassFromAPI() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -170,9 +169,9 @@ struct PassesTests { } } - @Test("Personalizable Pass Apple Wallet API") - func personalizationAPI() async throws { - try await withApp(delegate: delegate) { app, passesService in + @Test("Personalizable Pass Apple Wallet API", arguments: [true, false]) + func personalizationAPI(useEncryptedKey: Bool) async throws { + try await withApp(useEncryptedKey: useEncryptedKey) { app, passesService in let passData = PassData(title: "Personalize") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -242,7 +241,7 @@ struct PassesTests { @Test("Device Registration API") func apiDeviceRegistration() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -395,7 +394,7 @@ struct PassesTests { @Test("Error Logging") func errorLog() async throws { - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let log1 = "Error 1" let log2 = "Error 2" @@ -438,9 +437,9 @@ struct PassesTests { } } - @Test("APNS Client") - func apnsClient() async throws { - try await withApp(delegate: delegate) { app, passesService in + @Test("APNS Client", arguments: [true, false]) + func apnsClient(useEncryptedKey: Bool) async throws { + try await withApp(useEncryptedKey: useEncryptedKey) { app, passesService in #expect(app.apns.client(.init(string: "passes")) != nil) let passData = PassData(title: "Test Pass") @@ -523,10 +522,20 @@ struct PassesTests { @Test("Default PassesDelegate Properties") func defaultDelegate() async throws { + final class DefaultPassesDelegate: PassesDelegate { + let sslSigningFilesDirectory = URL(fileURLWithPath: "", isDirectory: true) + func template(for pass: P, db: any Database) async throws -> URL { + URL(fileURLWithPath: "") + } + func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { + Data() + } + } + let defaultDelegate = DefaultPassesDelegate() #expect(!defaultDelegate.generateSignatureFile(in: URL(fileURLWithPath: ""))) - try await withApp(delegate: delegate) { app, passesService in + try await withApp { app, passesService in let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) @@ -535,13 +544,3 @@ struct PassesTests { } } } - -final class DefaultPassesDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "", isDirectory: true) - func template(for pass: P, db: any Database) async throws -> URL { - URL(fileURLWithPath: "") - } - func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { - Data() - } -} diff --git a/Tests/PassesTests/TestPassesDelegate.swift b/Tests/PassesTests/TestPassesDelegate.swift index cd2af50..2d556bf 100644 --- a/Tests/PassesTests/TestPassesDelegate.swift +++ b/Tests/PassesTests/TestPassesDelegate.swift @@ -3,9 +3,7 @@ import Passes import Vapor final class TestPassesDelegate: PassesDelegate { - func encode( - pass: P, db: any Database, encoder: JSONEncoder - ) async throws -> Data { + func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.requireID()) @@ -14,8 +12,7 @@ final class TestPassesDelegate: PassesDelegate { else { throw Abort(.internalServerError) } - guard let data = try? encoder.encode(PassJSONData(data: passData, pass: passData.pass)) - else { + guard let data = try? encoder.encode(PassJSONData(data: passData, pass: passData.pass)) else { throw Abort(.internalServerError) } return data diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index 86e1a58..bb48255 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -7,7 +7,6 @@ import Vapor import Zip func withApp( - delegate: some PassesDelegate, useEncryptedKey: Bool = false, _ body: (Application, PassesService) async throws -> Void ) async throws { @@ -17,6 +16,8 @@ func withApp( app.databases.use(.sqlite(.memory), as: .sqlite) + let delegate = TestPassesDelegate() + PassesService.register(migrations: app.migrations) app.migrations.add(CreatePassData()) let passesService = try PassesService( From 028df3f8dbb20fc25c8f6ee027e6500a9fdd2fed Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 18 Oct 2024 20:49:58 +0200 Subject: [PATCH 06/17] Disable flaky tests --- Tests/OrdersTests/OrdersTests.swift | 5 +++++ Tests/OrdersTests/withApp.swift | 10 +++------- Tests/PassesTests/PassesTests.swift | 10 ++++++++++ Tests/PassesTests/withApp.swift | 10 +++------- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index fc8cad1..b9dcdb2 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -23,11 +23,14 @@ struct OrdersTests { #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/order.json"))) + /* TODO: Fix this test let passJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == order.authenticationToken) let orderID = try order.requireID().uuidString #expect(passJSON["orderIdentifier"] as? String == orderID) + */ let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] @@ -377,12 +380,14 @@ struct OrdersTests { ) // Test `OrderDataMiddleware` update method + /* TODO: Fix this test orderData.title = "Test Order 2" do { try await orderData.update(on: app.db) } catch let error as HTTPClientError { #expect(error.self == .remoteConnectionClosed) } + */ } } diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index 7f89cc2..01af823 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -11,15 +11,14 @@ func withApp( _ body: (Application, OrdersService) async throws -> Void ) async throws { let app = try await Application.make(.testing) - try #require(isLoggingConfigured) app.databases.use(.sqlite(.memory), as: .sqlite) - - let delegate = TestOrdersDelegate() - OrdersService.register(migrations: app.migrations) app.migrations.add(CreateOrderData()) + try await app.autoMigrate() + + let delegate = TestOrdersDelegate() let ordersService = try OrdersService( app: app, delegate: delegate, @@ -30,11 +29,8 @@ func withApp( pushRoutesMiddleware: SecretMiddleware(secret: "foo"), logger: app.logger ) - app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) - try await app.autoMigrate() - Zip.addCustomFileExtension("order") try await body(app, ordersService) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index c50921c..4374607 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -23,12 +23,15 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) + /* TODO: Fix this test let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) let passID = try pass.requireID().uuidString #expect(passJSON["serialNumber"] as? String == passID) #expect(passJSON["description"] as? String == passData.title) + */ let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] @@ -74,12 +77,17 @@ struct PassesTests { try data.write(to: passURL) let passFolder = try Zip.quickUnzipFile(passURL) + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) + + #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) + /* TODO: Fix this test let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) let passID = try pass.requireID().uuidString #expect(passJSON["serialNumber"] as? String == passID) #expect(passJSON["description"] as? String == passData.title) + */ let personalizationJSONData = try String(contentsOfFile: passFolder.path.appending("/personalization.json")).data(using: .utf8) let personalizationJSON = try JSONSerialization.jsonObject(with: personalizationJSONData!) as! [String: Any] @@ -502,12 +510,14 @@ struct PassesTests { ) // Test `PassDataMiddleware` update method + /* TODO: Fix this test passData.title = "Test Pass 2" do { try await passData.update(on: app.db) } catch let error as HTTPClientError { #expect(error.self == .remoteConnectionClosed) } + */ } } diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index bb48255..cc32e01 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -11,15 +11,14 @@ func withApp( _ body: (Application, PassesService) async throws -> Void ) async throws { let app = try await Application.make(.testing) - try #require(isLoggingConfigured) app.databases.use(.sqlite(.memory), as: .sqlite) - - let delegate = TestPassesDelegate() - PassesService.register(migrations: app.migrations) app.migrations.add(CreatePassData()) + try await app.autoMigrate() + + let delegate = TestPassesDelegate() let passesService = try PassesService( app: app, delegate: delegate, @@ -30,11 +29,8 @@ func withApp( pushRoutesMiddleware: SecretMiddleware(secret: "foo"), logger: app.logger ) - app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) - try await app.autoMigrate() - Zip.addCustomFileExtension("pkpass") try await body(app, passesService) From b319928600cf0cf4b630e05343b6fa4914d6e440 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 20 Oct 2024 12:08:27 +0200 Subject: [PATCH 07/17] Try with test serialization --- Tests/OrdersTests/OrdersTests.swift | 4 +--- Tests/PassesTests/PassesTests.swift | 6 +----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index b9dcdb2..e3e93a7 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -10,7 +10,7 @@ import Zip struct OrdersTests { let ordersURI = "/api/orders/v1/" - @Test("Order Generation", arguments: [true, false]) + @Test("Order Generation", .serialized, arguments: [true, false]) func orderGeneration(useEncryptedKey: Bool) async throws { try await withApp(useEncryptedKey: useEncryptedKey) { app, ordersService in let orderData = OrderData(title: "Test Order") @@ -24,13 +24,11 @@ struct OrdersTests { #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/order.json"))) - /* TODO: Fix this test let passJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == order.authenticationToken) let orderID = try order.requireID().uuidString #expect(passJSON["orderIdentifier"] as? String == orderID) - */ let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 4374607..65689e0 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -10,7 +10,7 @@ import Zip struct PassesTests { let passesURI = "/api/passes/v1/" - @Test("Pass Generation", arguments: [true, false]) + @Test("Pass Generation", .serialized, arguments: [true, false]) func passGeneration(useEncryptedKey: Bool) async throws { try await withApp(useEncryptedKey: useEncryptedKey) { app, passesService in let passData = PassData(title: "Test Pass") @@ -24,14 +24,12 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) - /* TODO: Fix this test let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) let passID = try pass.requireID().uuidString #expect(passJSON["serialNumber"] as? String == passID) #expect(passJSON["description"] as? String == passData.title) - */ let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] @@ -80,14 +78,12 @@ struct PassesTests { #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/pass.json"))) - /* TODO: Fix this test let passJSONData = try String(contentsOfFile: passFolder.path.appending("/pass.json")).data(using: .utf8) let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) let passID = try pass.requireID().uuidString #expect(passJSON["serialNumber"] as? String == passID) #expect(passJSON["description"] as? String == passData.title) - */ let personalizationJSONData = try String(contentsOfFile: passFolder.path.appending("/personalization.json")).data(using: .utf8) let personalizationJSON = try JSONSerialization.jsonObject(with: personalizationJSONData!) as! [String: Any] From 4fd2980a32d7a9be6f9eaad9dce4201b18278ac9 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 20 Oct 2024 12:32:49 +0200 Subject: [PATCH 08/17] Simplify `template` delegate method --- Sources/Orders/Orders.docc/GettingStarted.md | 4 ++-- Sources/Orders/OrdersDelegate.swift | 11 ++++----- Sources/Orders/OrdersServiceCustom.swift | 17 ++++--------- Sources/Passes/Passes.docc/GettingStarted.md | 4 ++-- Sources/Passes/Passes.docc/Personalization.md | 12 +++++----- Sources/Passes/PassesDelegate.swift | 11 ++++----- Sources/Passes/PassesServiceCustom.swift | 13 ++++------ Tests/OrdersTests/OrdersTests.swift | 23 +++++++----------- Tests/OrdersTests/TestOrdersDelegate.swift | 4 ++-- Tests/PassesTests/PassesTests.swift | 24 +++++++------------ Tests/PassesTests/TestPassesDelegate.swift | 4 ++-- 11 files changed, 52 insertions(+), 75 deletions(-) diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index 664a06c..2628281 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -136,9 +136,9 @@ final class OrderDelegate: OrdersDelegate { return data } - func template(for order: O, db: Database) async throws -> URL { + func template(for order: O, db: Database) async throws -> String { // The location might vary depending on the type of order. - URL(fileURLWithPath: "Templates/Orders/", isDirectory: true) + "Templates/Orders/" } } ``` diff --git a/Sources/Orders/OrdersDelegate.swift b/Sources/Orders/OrdersDelegate.swift index cdbe8d2..dc40cdc 100644 --- a/Sources/Orders/OrdersDelegate.swift +++ b/Sources/Orders/OrdersDelegate.swift @@ -10,9 +10,10 @@ import Foundation /// The delegate which is responsible for generating the order files. public protocol OrdersDelegate: AnyObject, Sendable { - /// Should return a `URL` which points to the template data for the order. + /// Should return a URL path which points to the template data for the order. /// - /// The URL should point to a directory containing all the images and localizations for the generated `.order` archive but should *not* contain any of these items: + /// The path should point to a directory containing all the images and localizations for the generated `.order` archive + /// but should *not* contain any of these items: /// - `manifest.json` /// - `order.json` /// - `signature` @@ -21,10 +22,8 @@ public protocol OrdersDelegate: AnyObject, Sendable { /// - order: The order data from the SQL server. /// - db: The SQL database to query against. /// - /// - Returns: A `URL` which points to the template data for the order. - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor. - func template(for order: O, db: any Database) async throws -> URL + /// - 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. /// diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 0905e38..3e7b201 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -371,13 +371,10 @@ extension OrdersServiceCustom { /// - order: The order to send the notifications for. /// - db: The `Database` to use. public func sendPushNotifications(for order: O, on db: any Database) async throws { - try await sendPushNotificationsForOrder( - id: order.requireID(), of: order.orderTypeIdentifier, on: db) + try await sendPushNotificationsForOrder(id: order.requireID(), of: order.orderTypeIdentifier, on: db) } - static func registrationsForOrder( - id: UUID, of orderTypeIdentifier: String, on db: any Database - ) async throws -> [R] { + static func registrationsForOrder(id: UUID, of orderTypeIdentifier: String, 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) @@ -474,7 +471,7 @@ extension OrdersServiceCustom { /// - 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 delegate.template(for: order, db: db) + let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: order, db: db), isDirectory: true) guard (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { @@ -490,13 +487,9 @@ extension OrdersServiceCustom { try await self.delegate.encode(order: order, db: db, encoder: encoder) .write(to: root.appendingPathComponent("order.json")) - try self.generateSignatureFile( - for: Self.generateManifestFile(using: encoder, in: root), - in: root - ) + try self.generateSignatureFile(for: Self.generateManifestFile(using: encoder, in: root), in: root) - var files = try FileManager.default.contentsOfDirectory( - at: templateDirectory, includingPropertiesForKeys: nil) + 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)) diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index 563645c..5f2d701 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -160,9 +160,9 @@ final class PassDelegate: PassesDelegate { return data } - func template(for pass: P, db: Database) async throws -> URL { + func template(for pass: P, db: Database) async throws -> String { // The location might vary depending on the type of pass. - URL(fileURLWithPath: "Templates/Passes/", isDirectory: true) + "Templates/Passes/" } } ``` diff --git a/Sources/Passes/Passes.docc/Personalization.md b/Sources/Passes/Passes.docc/Personalization.md index fc6743f..c828d2c 100644 --- a/Sources/Passes/Passes.docc/Personalization.md +++ b/Sources/Passes/Passes.docc/Personalization.md @@ -24,7 +24,7 @@ A personalizable pass is just a standard pass package with the following additio Implement the ``PassesDelegate/personalizationJSON(for:db:)`` method, which gives you the ``Pass`` to encode. If the pass requires personalization, and if it was not already personalized, create the ``PersonalizationJSON`` struct, which will contain all the fields for the generated `personalization.json` file, and return it, otherwise return `nil`. -In the ``PassesDelegate/template(for:db:)`` method, you have to return two different directory URLs, depending on whether the pass has to be personalized or not. If it does, the directory must contain the `personalizationLogo@XX.png` file. +In the ``PassesDelegate/template(for:db:)`` method, you have to return two different directory paths, depending on whether the pass has to be personalized or not. If it does, the directory must contain the `personalizationLogo@XX.png` file. Finally, you have to implement the ``PassesDelegate/encode(pass:db:encoder:)`` method as usual, but remember to use in the ``PassJSON`` initializer the user info that will be saved inside ``Pass/userPersonalization`` after the pass has been personalized. @@ -69,7 +69,7 @@ final class PassDelegate: PassesDelegate { } } - func template(for pass: P, db: Database) async throws -> URL { + func template(for pass: P, db: Database) async throws -> String { guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.requireID()) .first() @@ -78,12 +78,12 @@ final class PassDelegate: PassesDelegate { } if passData.requiresPersonalization { - // If the pass requires personalization, return the URL to the personalization template, + // If the pass requires personalization, return the URL path to the personalization template, // which must contain the `personalizationLogo@XX.png` file. - return URL(fileURLWithPath: "Templates/Passes/Personalization/", isDirectory: true) + return "Templates/Passes/Personalization/" } else { - // Otherwise, return the URL to the standard pass template. - return URL(fileURLWithPath: "Templates/Passes/Standard/", isDirectory: true) + // Otherwise, return the URL path to the standard pass template. + return "Templates/Passes/Standard/" } } } diff --git a/Sources/Passes/PassesDelegate.swift b/Sources/Passes/PassesDelegate.swift index 55c2e17..ca3464b 100644 --- a/Sources/Passes/PassesDelegate.swift +++ b/Sources/Passes/PassesDelegate.swift @@ -31,9 +31,10 @@ import Foundation /// The delegate which is responsible for generating the pass files. public protocol PassesDelegate: AnyObject, Sendable { - /// Should return a `URL` which points to the template data for the pass. + /// Should return a URL path which points to the template data for the pass. /// - /// The URL should point to a directory containing all the images and localizations for the generated `.pkpass` archive but should *not* contain any of these items: + /// The path should point to a directory containing all the images and localizations for the generated `.pkpass` archive + /// but should *not* contain any of these items: /// - `manifest.json` /// - `pass.json` /// - `personalization.json` @@ -43,10 +44,8 @@ public protocol PassesDelegate: AnyObject, Sendable { /// - pass: The pass data from the SQL server. /// - db: The SQL database to query against. /// - /// - Returns: A `URL` which points to the template data for the pass. - /// - /// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor. - func template(for pass: P, db: any Database) async throws -> URL + /// - 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. /// diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index 2cbe831..dd8aa02 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -477,13 +477,10 @@ extension PassesServiceCustom { /// - 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.passTypeIdentifier, on: db) + try await sendPushNotificationsForPass(id: pass.requireID(), of: pass.passTypeIdentifier, on: db) } - static func registrationsForPass( - id: UUID, of passTypeIdentifier: String, on db: any Database - ) async throws -> [R] { + static func registrationsForPass(id: UUID, of passTypeIdentifier: String, 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) @@ -578,14 +575,12 @@ extension PassesServiceCustom { /// - 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 delegate.template(for: pass, db: db) + let templateDirectory = try await URL(fileURLWithPath: delegate.template(for: pass, db: db), isDirectory: true) guard (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { throw PassesError.templateNotDirectory } - var files = try FileManager.default.contentsOfDirectory( - at: templateDirectory, includingPropertiesForKeys: nil) let tmp = FileManager.default.temporaryDirectory let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) @@ -595,6 +590,8 @@ extension PassesServiceCustom { try await self.delegate.encode(pass: pass, db: db, encoder: self.encoder) .write(to: root.appendingPathComponent("pass.json")) + var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) + // 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")) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index e3e93a7..616a477 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -378,14 +378,13 @@ struct OrdersTests { ) // Test `OrderDataMiddleware` update method - /* TODO: Fix this test - orderData.title = "Test Order 2" - do { - try await orderData.update(on: app.db) - } catch let error as HTTPClientError { - #expect(error.self == .remoteConnectionClosed) - } - */ + // TODO: Fix this test + // orderData.title = "Test Order 2" + // do { + // try await orderData.update(on: app.db) + // } catch let error as HTTPClientError { + // #expect(error.self == .remoteConnectionClosed) + // } } } @@ -400,12 +399,8 @@ struct OrdersTests { @Test("Default OrdersDelegate Properties") func defaultDelegate() { final class DefaultOrdersDelegate: OrdersDelegate { - func template(for order: O, db: any Database) async throws -> URL { - URL(fileURLWithPath: "") - } - func encode(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data { - Data() - } + 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/TestOrdersDelegate.swift b/Tests/OrdersTests/TestOrdersDelegate.swift index 7fa2485..b5b45bb 100644 --- a/Tests/OrdersTests/TestOrdersDelegate.swift +++ b/Tests/OrdersTests/TestOrdersDelegate.swift @@ -18,7 +18,7 @@ final class TestOrdersDelegate: OrdersDelegate { return data } - func template(for: O, db: any Database) async throws -> URL { - URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/", isDirectory: true) + func template(for: O, db: any Database) async throws -> String { + "\(FileManager.default.currentDirectoryPath)/Tests/OrdersTests/Templates/" } } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 65689e0..c0d1147 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -506,14 +506,13 @@ struct PassesTests { ) // Test `PassDataMiddleware` update method - /* TODO: Fix this test - passData.title = "Test Pass 2" - do { - try await passData.update(on: app.db) - } catch let error as HTTPClientError { - #expect(error.self == .remoteConnectionClosed) - } - */ + // TODO: Fix this test + // passData.title = "Test Pass 2" + // do { + // try await passData.update(on: app.db) + // } catch let error as HTTPClientError { + // #expect(error.self == .remoteConnectionClosed) + // } } } @@ -529,13 +528,8 @@ struct PassesTests { @Test("Default PassesDelegate Properties") func defaultDelegate() async throws { final class DefaultPassesDelegate: PassesDelegate { - let sslSigningFilesDirectory = URL(fileURLWithPath: "", isDirectory: true) - func template(for pass: P, db: any Database) async throws -> URL { - URL(fileURLWithPath: "") - } - func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { - Data() - } + func template(for pass: P, db: any Database) async throws -> String { "" } + func encode(pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data { Data() } } let defaultDelegate = DefaultPassesDelegate() diff --git a/Tests/PassesTests/TestPassesDelegate.swift b/Tests/PassesTests/TestPassesDelegate.swift index 2d556bf..c323c8d 100644 --- a/Tests/PassesTests/TestPassesDelegate.swift +++ b/Tests/PassesTests/TestPassesDelegate.swift @@ -40,7 +40,7 @@ final class TestPassesDelegate: PassesDelegate { } } - func template(for pass: P, db: any Database) async throws -> URL { - URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/", isDirectory: true) + func template(for pass: P, db: any Database) async throws -> String { + "\(FileManager.default.currentDirectoryPath)/Tests/PassesTests/Templates/" } } From 168b6d5a8fa64e29deb9a30ba6fbab67587376e7 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 22 Oct 2024 18:05:55 +0200 Subject: [PATCH 09/17] Serialize all tests --- .github/workflows/test.yml | 1 + Tests/OrdersTests/OrdersTests.swift | 2 +- Tests/PassesTests/PassesTests.swift | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2054aea..4560c53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,5 +11,6 @@ jobs: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main with: with_linting: true + extra_flags: --no-parallel secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 616a477..fb89ead 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -10,7 +10,7 @@ import Zip struct OrdersTests { let ordersURI = "/api/orders/v1/" - @Test("Order Generation", .serialized, arguments: [true, false]) + @Test("Order Generation", arguments: [true, false]) func orderGeneration(useEncryptedKey: Bool) async throws { try await withApp(useEncryptedKey: useEncryptedKey) { app, ordersService in let orderData = OrderData(title: "Test Order") diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index c0d1147..c806e3f 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -10,7 +10,7 @@ import Zip struct PassesTests { let passesURI = "/api/passes/v1/" - @Test("Pass Generation", .serialized, arguments: [true, false]) + @Test("Pass Generation", arguments: [true, false]) func passGeneration(useEncryptedKey: Bool) async throws { try await withApp(useEncryptedKey: useEncryptedKey) { app, passesService in let passData = PassData(title: "Test Pass") From 839cc5d71724c437c1a079b3f15dc21d275e9f61 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 22 Oct 2024 18:16:06 +0200 Subject: [PATCH 10/17] Handle errors in tests --- Tests/OrdersTests/OrdersTests.swift | 17 ++++++----- Tests/OrdersTests/withApp.swift | 45 ++++++++++++++++------------- Tests/PassesTests/PassesTests.swift | 17 ++++++----- Tests/PassesTests/withApp.swift | 45 ++++++++++++++++------------- 4 files changed, 68 insertions(+), 56 deletions(-) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index fb89ead..e2214ac 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -377,14 +377,15 @@ struct OrdersTests { } ) - // Test `OrderDataMiddleware` update method - // TODO: Fix this test - // orderData.title = "Test Order 2" - // do { - // try await orderData.update(on: app.db) - // } catch let error as HTTPClientError { - // #expect(error.self == .remoteConnectionClosed) - // } + if !useEncryptedKey { + // Test `OrderDataMiddleware` update method + orderData.title = "Test Order 2" + do { + try await orderData.update(on: app.db) + } catch let error as HTTPClientError { + #expect(error.self == .remoteConnectionClosed) + } + } } } diff --git a/Tests/OrdersTests/withApp.swift b/Tests/OrdersTests/withApp.swift index 01af823..5075c87 100644 --- a/Tests/OrdersTests/withApp.swift +++ b/Tests/OrdersTests/withApp.swift @@ -11,30 +11,35 @@ func withApp( _ body: (Application, OrdersService) async throws -> Void ) async throws { let app = try await Application.make(.testing) - try #require(isLoggingConfigured) + do { + try #require(isLoggingConfigured) - app.databases.use(.sqlite(.memory), as: .sqlite) - OrdersService.register(migrations: app.migrations) - app.migrations.add(CreateOrderData()) - try await app.autoMigrate() + app.databases.use(.sqlite(.memory), as: .sqlite) + OrdersService.register(migrations: app.migrations) + app.migrations.add(CreateOrderData()) + try await app.autoMigrate() - let delegate = TestOrdersDelegate() - 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 - ) - app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) + let delegate = TestOrdersDelegate() + 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 + ) + app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) - Zip.addCustomFileExtension("order") + Zip.addCustomFileExtension("order") - try await body(app, ordersService) + try await body(app, ordersService) - try await app.autoRevert() + try await app.autoRevert() + } catch { + try await app.asyncShutdown() + throw error + } try await app.asyncShutdown() } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index c806e3f..06c8519 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -505,14 +505,15 @@ struct PassesTests { } ) - // Test `PassDataMiddleware` update method - // TODO: Fix this test - // passData.title = "Test Pass 2" - // do { - // try await passData.update(on: app.db) - // } catch let error as HTTPClientError { - // #expect(error.self == .remoteConnectionClosed) - // } + if !useEncryptedKey { + // Test `PassDataMiddleware` update method + passData.title = "Test Pass 2" + do { + try await passData.update(on: app.db) + } catch let error as HTTPClientError { + #expect(error.self == .remoteConnectionClosed) + } + } } } diff --git a/Tests/PassesTests/withApp.swift b/Tests/PassesTests/withApp.swift index cc32e01..52a051f 100644 --- a/Tests/PassesTests/withApp.swift +++ b/Tests/PassesTests/withApp.swift @@ -11,30 +11,35 @@ func withApp( _ body: (Application, PassesService) async throws -> Void ) async throws { let app = try await Application.make(.testing) - try #require(isLoggingConfigured) + do { + try #require(isLoggingConfigured) - app.databases.use(.sqlite(.memory), as: .sqlite) - PassesService.register(migrations: app.migrations) - app.migrations.add(CreatePassData()) - try await app.autoMigrate() + app.databases.use(.sqlite(.memory), as: .sqlite) + PassesService.register(migrations: app.migrations) + app.migrations.add(CreatePassData()) + try await app.autoMigrate() - let delegate = TestPassesDelegate() - 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 - ) - app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) + let delegate = TestPassesDelegate() + 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 + ) + app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) - Zip.addCustomFileExtension("pkpass") + Zip.addCustomFileExtension("pkpass") - try await body(app, passesService) + try await body(app, passesService) - try await app.autoRevert() + try await app.autoRevert() + } catch { + try await app.asyncShutdown() + throw error + } try await app.asyncShutdown() } From c93f073e330bc85ba424434edc49b24dbb9c3458 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Tue, 22 Oct 2024 18:26:43 +0200 Subject: [PATCH 11/17] Fix CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4560c53..dbddd19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,6 @@ jobs: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main with: with_linting: true - extra_flags: --no-parallel + test_filter: --no-parallel secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From d390bc7c941ebbbf84f914188b73ea40ba70e0ae Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 26 Oct 2024 15:55:38 +0200 Subject: [PATCH 12/17] Get rid of `JSONSerialization` --- Sources/Passes/DTOs/PersonalizationJSON.swift | 4 +-- Tests/OrdersTests/OrderData.swift | 20 +++++++++++-- Tests/OrdersTests/OrdersTests.swift | 13 +++++---- Tests/PassesTests/PassData.swift | 27 +++++++++++++---- Tests/PassesTests/PassesTests.swift | 29 ++++++++++--------- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/Sources/Passes/DTOs/PersonalizationJSON.swift b/Sources/Passes/DTOs/PersonalizationJSON.swift index e73e5aa..08fd2a5 100644 --- a/Sources/Passes/DTOs/PersonalizationJSON.swift +++ b/Sources/Passes/DTOs/PersonalizationJSON.swift @@ -4,7 +4,7 @@ /// It also contains a description of the program and (optionally) the program’s terms and conditions. /// /// > Tip: See the [documentation](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/PassPersonalization.html#//apple_ref/doc/uid/TP40012195-CH12-SW2) to understand the keys. -public struct PersonalizationJSON: Encodable, Sendable { +public struct PersonalizationJSON: Codable, Sendable { /// The contents of this array define the data requested from the user. /// /// The signup form’s fields are generated based on these keys. @@ -42,7 +42,7 @@ public struct PersonalizationJSON: Encodable, Sendable { extension PersonalizationJSON { /// Personal information requested by the signup form. - public enum PersonalizationField: String, Encodable, Sendable { + public enum PersonalizationField: String, Codable, Sendable { /// Prompts the user for their name. /// /// `fullName`, `givenName`, and `familyName` are submitted in the personalize request. diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift index d51c128..32e4dc9 100644 --- a/Tests/OrdersTests/OrderData.swift +++ b/Tests/OrdersTests/OrderData.swift @@ -46,7 +46,11 @@ extension OrderData { } } -struct OrderJSONData: OrderJSON.Properties { +extension OrderJSON.SchemaVersion: Decodable {} +extension OrderJSON.OrderType: Decodable {} +extension OrderJSON.OrderStatus: Decodable {} + +struct OrderJSONData: OrderJSON.Properties, Decodable { let schemaVersion = OrderJSON.SchemaVersion.v1 let orderTypeIdentifier = "order.com.example.pet-store" let orderIdentifier: String @@ -61,11 +65,23 @@ struct OrderJSONData: OrderJSON.Properties { private let webServiceURL = "https://www.example.com/api/orders/" - struct MerchantData: OrderJSON.Merchant { + enum CodingKeys: String, CodingKey { + case schemaVersion + case orderTypeIdentifier, orderIdentifier, orderType, orderNumber + case createdAt, updatedAt + case status, merchant + case orderManagementURL, authenticationToken, webServiceURL + } + + struct MerchantData: OrderJSON.Merchant, Decodable { let merchantIdentifier = "com.example.pet-store" let displayName: String let url = "https://www.example.com/" let logo = "pet_store_logo.png" + + enum CodingKeys: String, CodingKey { + case merchantIdentifier, displayName, url, logo + } } init(data: OrderData, order: Order) { diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index e2214ac..5c2156d 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -9,6 +9,7 @@ import Zip @Suite("Orders Tests") struct OrdersTests { let ordersURI = "/api/orders/v1/" + let decoder = JSONDecoder() @Test("Order Generation", arguments: [true, false]) func orderGeneration(useEncryptedKey: Bool) async throws { @@ -24,17 +25,17 @@ struct OrdersTests { #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/order.json"))) - let passJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) - let passJSON = try JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] - #expect(passJSON["authenticationToken"] as? String == order.authenticationToken) + let orderJSONData = try String(contentsOfFile: orderFolder.path.appending("/order.json")).data(using: .utf8) + let orderJSON = try decoder.decode(OrderJSONData.self, from: orderJSONData!) + #expect(orderJSON.authenticationToken == order.authenticationToken) let orderID = try order.requireID().uuidString - #expect(passJSON["orderIdentifier"] as? String == orderID) + #expect(orderJSON.orderIdentifier == orderID) let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) - let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] + let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) let iconHash = Array(SHA256.hash(data: iconData)).hex - #expect(manifestJSON["icon.png"] as? String == iconHash) + #expect(manifestJSON["icon.png"] == iconHash) #expect(manifestJSON["pet_store_logo.png"] != nil) } } diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/PassData.swift index 313d479..38614f5 100644 --- a/Tests/PassesTests/PassData.swift +++ b/Tests/PassesTests/PassData.swift @@ -46,7 +46,11 @@ extension PassData { } } -struct PassJSONData: PassJSON.Properties { +extension PassJSON.FormatVersion: Decodable {} +extension PassJSON.BarcodeFormat: Decodable {} +extension PassJSON.TransitType: Decodable {} + +struct PassJSONData: PassJSON.Properties, Decodable { let description: String let formatVersion = PassJSON.FormatVersion.v1 let organizationName = "vapor-community" @@ -55,21 +59,25 @@ struct PassJSONData: PassJSON.Properties { let teamIdentifier = "K6512ZA2S5" private let webServiceURL = "https://www.example.com/api/passes/" - private let authenticationToken: String + let authenticationToken: String private let logoText = "Vapor Community" private let sharingProhibited = true let backgroundColor = "rgb(207, 77, 243)" let foregroundColor = "rgb(255, 255, 255)" let barcodes = Barcode(message: "test") - struct Barcode: PassJSON.Barcodes { + struct Barcode: PassJSON.Barcodes, Decodable { let format = PassJSON.BarcodeFormat.qr let message: String let messageEncoding = "iso-8859-1" + + enum CodingKeys: String, CodingKey { + case format, message, messageEncoding + } } let boardingPass = Boarding(transitType: .air) - struct Boarding: PassJSON.BoardingPass { + struct Boarding: PassJSON.BoardingPass, Decodable { let transitType: PassJSON.TransitType let headerFields: [PassField] let primaryFields: [PassField] @@ -77,7 +85,7 @@ struct PassJSONData: PassJSON.Properties { let auxiliaryFields: [PassField] let backFields: [PassField] - struct PassField: PassJSON.PassFieldContent { + struct PassField: PassJSON.PassFieldContent, Decodable { let key: String let label: String let value: String @@ -93,6 +101,15 @@ struct PassJSONData: PassJSON.Properties { } } + enum CodingKeys: String, CodingKey { + case description + case formatVersion + case organizationName, passTypeIdentifier, serialNumber, teamIdentifier + case webServiceURL, authenticationToken + case logoText, sharingProhibited, backgroundColor, foregroundColor + case barcodes, boardingPass + } + init(data: PassData, pass: Pass) { self.description = data.title self.serialNumber = pass.id!.uuidString diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 06c8519..9da4885 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -9,6 +9,7 @@ import Zip @Suite("Passes Tests") struct PassesTests { let passesURI = "/api/passes/v1/" + let decoder = JSONDecoder() @Test("Pass Generation", arguments: [true, false]) func passGeneration(useEncryptedKey: Bool) async throws { @@ -25,17 +26,17 @@ struct PassesTests { #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 JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] - #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) + let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) + #expect(passJSON.authenticationToken == pass.authenticationToken) let passID = try pass.requireID().uuidString - #expect(passJSON["serialNumber"] as? String == passID) - #expect(passJSON["description"] as? String == passData.title) + #expect(passJSON.serialNumber == passID) + #expect(passJSON.description == passData.title) let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) - let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] + let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) let iconData = try Data(contentsOf: passFolder.appendingPathComponent("/icon.png")) let iconHash = Array(Insecure.SHA1.hash(data: iconData)).hex - #expect(manifestJSON["icon.png"] as? String == iconHash) + #expect(manifestJSON["icon.png"] == iconHash) #expect(manifestJSON["logo.png"] != nil) #expect(manifestJSON["personalizationLogo.png"] != nil) } @@ -79,21 +80,21 @@ struct PassesTests { #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 JSONSerialization.jsonObject(with: passJSONData!) as! [String: Any] - #expect(passJSON["authenticationToken"] as? String == pass.authenticationToken) + let passJSON = try decoder.decode(PassJSONData.self, from: passJSONData!) + #expect(passJSON.authenticationToken == pass.authenticationToken) let passID = try pass.requireID().uuidString - #expect(passJSON["serialNumber"] as? String == passID) - #expect(passJSON["description"] as? String == passData.title) + #expect(passJSON.serialNumber == passID) + #expect(passJSON.description == passData.title) let personalizationJSONData = try String(contentsOfFile: passFolder.path.appending("/personalization.json")).data(using: .utf8) - let personalizationJSON = try JSONSerialization.jsonObject(with: personalizationJSONData!) as! [String: Any] - #expect(personalizationJSON["description"] as? String == "Hello, World!") + let personalizationJSON = try decoder.decode(PersonalizationJSON.self, from: personalizationJSONData!) + #expect(personalizationJSON.description == "Hello, World!") let manifestJSONData = try String(contentsOfFile: passFolder.path.appending("/manifest.json")).data(using: .utf8) - let manifestJSON = try JSONSerialization.jsonObject(with: manifestJSONData!) as! [String: Any] + let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) let iconData = try Data(contentsOf: passFolder.appendingPathComponent("/personalizationLogo.png")) let iconHash = Array(Insecure.SHA1.hash(data: iconData)).hex - #expect(manifestJSON["personalizationLogo.png"] as? String == iconHash) + #expect(manifestJSON["personalizationLogo.png"] == iconHash) } } From c86ee9b900866db1e786b6fd4469daa8cc48c777 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 31 Oct 2024 15:17:52 +0100 Subject: [PATCH 13/17] Small fixes --- Package.swift | 4 ++-- Sources/Orders/OrdersServiceCustom.swift | 11 +++++------ Sources/Passes/PassesServiceCustom.swift | 3 +-- Tests/OrdersTests/OrdersTests.swift | 2 +- Tests/PassesTests/PassesTests.swift | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Package.swift b/Package.swift index 4ce018b..2a5323a 100644 --- a/Package.swift +++ b/Package.swift @@ -11,11 +11,11 @@ let package = Package( .library(name: "Orders", targets: ["Orders"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.106.0"), + .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/apple/swift-certificates.git", from: "1.5.0"), + .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/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 3e7b201..eb49ca0 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -33,6 +33,7 @@ where O == R.OrderType, D == R.DeviceType { private let pemPrivateKeyPassword: String? private let sslBinary: URL private let logger: Logger? + private let encoder = JSONEncoder() /// Initializes the service and registers all the routes required for Apple Wallet to work. /// @@ -106,7 +107,7 @@ where O == R.OrderType, D == R.DeviceType { apnsConfig, eventLoopGroupProvider: .shared(app.eventLoopGroup), responseDecoder: JSONDecoder(), - requestEncoder: JSONEncoder(), + requestEncoder: self.encoder, as: .init(string: "orders"), isDefault: false ) @@ -478,16 +479,14 @@ extension OrdersServiceCustom { throw OrdersError.templateNotDirectory } - let tmp = FileManager.default.temporaryDirectory - let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) + 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 encoder = JSONEncoder() - try await self.delegate.encode(order: order, db: db, encoder: encoder) + try await self.delegate.encode(order: order, db: db, encoder: self.encoder) .write(to: root.appendingPathComponent("order.json")) - try self.generateSignatureFile(for: Self.generateManifestFile(using: encoder, in: root), in: root) + try self.generateSignatureFile(for: Self.generateManifestFile(using: self.encoder, in: root), in: root) var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) files.append(URL(fileURLWithPath: "order.json", relativeTo: root)) diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index dd8aa02..e1d83b0 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -582,8 +582,7 @@ extension PassesServiceCustom { throw PassesError.templateNotDirectory } - let tmp = FileManager.default.temporaryDirectory - let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) + 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) } diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 5c2156d..e653da4 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -6,7 +6,7 @@ import Zip @testable import Orders -@Suite("Orders Tests") +@Suite("Orders Tests", .serialized) struct OrdersTests { let ordersURI = "/api/orders/v1/" let decoder = JSONDecoder() diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 9da4885..60d8f0c 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -6,7 +6,7 @@ import Zip @testable import Passes -@Suite("Passes Tests") +@Suite("Passes Tests", .serialized) struct PassesTests { let passesURI = "/api/passes/v1/" let decoder = JSONDecoder() From 99ba8761cf603406c5fb1a881e42ec095d721247 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 31 Oct 2024 15:58:07 +0100 Subject: [PATCH 14/17] Fix hashing --- Sources/Orders/OrdersServiceCustom.swift | 10 ++++------ Sources/Passes/PassesServiceCustom.swift | 6 +++--- Tests/OrdersTests/OrdersTests.swift | 2 +- Tests/PassesTests/PassesTests.swift | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index eb49ca0..057ee44 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -391,18 +391,16 @@ extension OrdersServiceCustom { // MARK: - order file generation extension OrdersServiceCustom { - private static func generateManifestFile( - using encoder: JSONEncoder, in root: URL - ) throws -> Data { + private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { var manifest: [String: String] = [:] let paths = try FileManager.default.subpathsOfDirectory(atPath: root.path) for relativePath in paths { let file = URL(fileURLWithPath: relativePath, relativeTo: root) guard !file.hasDirectoryPath else { continue } - let data = try Data(contentsOf: file) - let hash = SHA256.hash(data: data) - manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() + manifest[relativePath] = try SHA256.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 diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index e1d83b0..b3893bf 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -502,10 +502,10 @@ extension PassesServiceCustom { for relativePath in paths { let file = URL(fileURLWithPath: relativePath, relativeTo: root) guard !file.hasDirectoryPath else { continue } - let data = try Data(contentsOf: file) - let hash = Insecure.SHA1.hash(data: data) - manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() + 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 diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index e653da4..247e4de 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -34,7 +34,7 @@ struct OrdersTests { let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) - let iconHash = Array(SHA256.hash(data: iconData)).hex + let iconHash = SHA256.hash(data: iconData).hex #expect(manifestJSON["icon.png"] == iconHash) #expect(manifestJSON["pet_store_logo.png"] != nil) } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 60d8f0c..7f87345 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -35,7 +35,7 @@ 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("/icon.png")) - let iconHash = Array(Insecure.SHA1.hash(data: iconData)).hex + let iconHash = Insecure.SHA1.hash(data: iconData).hex #expect(manifestJSON["icon.png"] == iconHash) #expect(manifestJSON["logo.png"] != nil) #expect(manifestJSON["personalizationLogo.png"] != nil) @@ -93,7 +93,7 @@ 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")) - let iconHash = Array(Insecure.SHA1.hash(data: iconData)).hex + let iconHash = Insecure.SHA1.hash(data: iconData).hex #expect(manifestJSON["personalizationLogo.png"] == iconHash) } } From f9339d30e2af32dad0f8233fcb4dd7478b4819bb Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 31 Oct 2024 23:17:38 +0100 Subject: [PATCH 15/17] Fix testing --- Sources/Orders/OrdersServiceCustom.swift | 23 +++++++++--------- Sources/Passes/PassesServiceCustom.swift | 31 +++++++++++------------- Tests/OrdersTests/OrdersTests.swift | 8 +++--- Tests/PassesTests/PassesTests.swift | 16 ++++++------ 4 files changed, 37 insertions(+), 41 deletions(-) diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 057ee44..664ae80 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -83,8 +83,8 @@ where O == R.OrderType, D == R.DeviceType { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( privateKey: .privateKey( - NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { closure in - closure(password.utf8) + NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { passphraseCallback in + passphraseCallback(password.utf8) }), certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) @@ -143,7 +143,7 @@ where O == R.OrderType, D == R.DeviceType { // MARK: - API Routes extension OrdersServiceCustom { - func latestVersionOfOrder(req: Request) async throws -> Response { + fileprivate func latestVersionOfOrder(req: Request) async throws -> Response { logger?.debug("Called latestVersionOfOrder") var ifModifiedSince: TimeInterval = 0 @@ -180,7 +180,7 @@ extension OrdersServiceCustom { ) } - func registerDevice(req: Request) async throws -> HTTPStatus { + fileprivate func registerDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called register device") let pushToken: String @@ -236,7 +236,7 @@ extension OrdersServiceCustom { return .created } - func ordersForDevice(req: Request) async throws -> OrdersForDeviceDTO { + fileprivate func ordersForDevice(req: Request) async throws -> OrdersForDeviceDTO { logger?.debug("Called ordersForDevice") let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")! @@ -268,7 +268,7 @@ extension OrdersServiceCustom { return OrdersForDeviceDTO(with: orderIdentifiers, maxDate: maxDate) } - func logError(req: Request) async throws -> HTTPStatus { + fileprivate func logError(req: Request) async throws -> HTTPStatus { logger?.debug("Called logError") let body: ErrorLogDTO @@ -286,7 +286,7 @@ extension OrdersServiceCustom { return .ok } - func unregisterDevice(req: Request) async throws -> HTTPStatus { + fileprivate func unregisterDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called unregisterDevice") guard let orderIdentifier = req.parameters.get("orderIdentifier", as: UUID.self) else { @@ -310,7 +310,7 @@ extension OrdersServiceCustom { } // MARK: - Push Routes - func pushUpdatesForOrder(req: Request) async throws -> HTTPStatus { + fileprivate func pushUpdatesForOrder(req: Request) async throws -> HTTPStatus { logger?.debug("Called pushUpdatesForOrder") guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else { @@ -322,7 +322,7 @@ extension OrdersServiceCustom { return .noContent } - func tokensForOrderUpdate(req: Request) async throws -> [String] { + fileprivate func tokensForOrderUpdate(req: Request) async throws -> [String] { logger?.debug("Called tokensForOrderUpdate") guard let id = req.parameters.get("orderIdentifier", as: UUID.self) else { @@ -330,8 +330,7 @@ extension OrdersServiceCustom { } let orderTypeIdentifier = req.parameters.get("orderTypeIdentifier")! - return try await Self.registrationsForOrder(id: id, of: orderTypeIdentifier, on: req.db) - .map { $0.device.pushToken } + return try await Self.registrationsForOrder(id: id, of: orderTypeIdentifier, on: req.db).map { $0.device.pushToken } } } @@ -375,7 +374,7 @@ extension OrdersServiceCustom { try await sendPushNotificationsForOrder(id: order.requireID(), of: order.orderTypeIdentifier, on: db) } - static func registrationsForOrder(id: UUID, of orderTypeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrationsForOrder(id: UUID, of orderTypeIdentifier: String, 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) diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index b3893bf..a447be6 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -86,8 +86,8 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { apnsConfig = APNSClientConfiguration( authenticationMethod: try .tls( privateKey: .privateKey( - NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { closure in - closure(password.utf8) + NIOSSLPrivateKey(file: privateKeyPath, format: .pem) { passphraseCallback in + passphraseCallback(password.utf8) }), certificateChain: NIOSSLCertificate.fromPEMFile(pemPath).map { .certificate($0) @@ -144,7 +144,7 @@ where P == R.PassType, D == R.DeviceType, U == P.UserPersonalizationType { // MARK: - API Routes extension PassesServiceCustom { - func registerDevice(req: Request) async throws -> HTTPStatus { + fileprivate func registerDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called register device") let pushToken: String @@ -203,7 +203,7 @@ extension PassesServiceCustom { return .created } - func passesForDevice(req: Request) async throws -> PassesForDeviceDTO { + fileprivate func passesForDevice(req: Request) async throws -> PassesForDeviceDTO { logger?.debug("Called passesForDevice") let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! @@ -235,7 +235,7 @@ extension PassesServiceCustom { return PassesForDeviceDTO(with: serialNumbers, maxDate: maxDate) } - func latestVersionOfPass(req: Request) async throws -> Response { + fileprivate func latestVersionOfPass(req: Request) async throws -> Response { logger?.debug("Called latestVersionOfPass") var ifModifiedSince: TimeInterval = 0 @@ -272,7 +272,7 @@ extension PassesServiceCustom { ) } - func unregisterDevice(req: Request) async throws -> HTTPStatus { + fileprivate func unregisterDevice(req: Request) async throws -> HTTPStatus { logger?.debug("Called unregisterDevice") guard let passId = req.parameters.get("passSerial", as: UUID.self) else { @@ -295,7 +295,7 @@ extension PassesServiceCustom { return .ok } - func logError(req: Request) async throws -> HTTPStatus { + fileprivate func logError(req: Request) async throws -> HTTPStatus { logger?.debug("Called logError") let body: ErrorLogDTO @@ -313,7 +313,7 @@ extension PassesServiceCustom { return .ok } - func personalizedPass(req: Request) async throws -> Response { + fileprivate func personalizedPass(req: Request) async throws -> Response { logger?.debug("Called personalizedPass") guard let passTypeIdentifier = req.parameters.get("passTypeIdentifier"), @@ -345,8 +345,7 @@ extension PassesServiceCustom { pass._$userPersonalization.id = try userPersonalization.requireID() try await pass.update(on: req.db) - let tmp = FileManager.default.temporaryDirectory - let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) + 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) } @@ -415,7 +414,7 @@ extension PassesServiceCustom { } // MARK: - Push Routes - func pushUpdatesForPass(req: Request) async throws -> HTTPStatus { + fileprivate func pushUpdatesForPass(req: Request) async throws -> HTTPStatus { logger?.debug("Called pushUpdatesForPass") guard let id = req.parameters.get("passSerial", as: UUID.self) else { @@ -427,7 +426,7 @@ extension PassesServiceCustom { return .noContent } - func tokensForPassUpdate(req: Request) async throws -> [String] { + fileprivate func tokensForPassUpdate(req: Request) async throws -> [String] { logger?.debug("Called tokensForPassUpdate") guard let id = req.parameters.get("passSerial", as: UUID.self) else { @@ -435,8 +434,7 @@ extension PassesServiceCustom { } let passTypeIdentifier = req.parameters.get("passTypeIdentifier")! - return try await Self.registrationsForPass(id: id, of: passTypeIdentifier, on: req.db) - .map { $0.device.pushToken } + return try await Self.registrationsForPass(id: id, of: passTypeIdentifier, on: req.db).map { $0.device.pushToken } } } @@ -480,7 +478,7 @@ extension PassesServiceCustom { try await sendPushNotificationsForPass(id: pass.requireID(), of: pass.passTypeIdentifier, on: db) } - static func registrationsForPass(id: UUID, of passTypeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrationsForPass(id: UUID, of passTypeIdentifier: String, 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) @@ -620,8 +618,7 @@ extension PassesServiceCustom { throw PassesError.invalidNumberOfPasses } - let tmp = FileManager.default.temporaryDirectory - let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) + 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) } diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 247e4de..24ab490 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -18,9 +18,10 @@ struct OrdersTests { 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 orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.order") + let orderURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).order") try data.write(to: orderURL) - let orderFolder = try Zip.quickUnzipFile(orderURL) + let orderFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try Zip.unzipFile(orderURL, destination: orderFolder) #expect(FileManager.default.fileExists(atPath: orderFolder.path.appending("/signature"))) @@ -34,8 +35,7 @@ struct OrdersTests { let manifestJSONData = try String(contentsOfFile: orderFolder.path.appending("/manifest.json")).data(using: .utf8) let manifestJSON = try decoder.decode([String: String].self, from: manifestJSONData!) let iconData = try Data(contentsOf: orderFolder.appendingPathComponent("/icon.png")) - let iconHash = SHA256.hash(data: iconData).hex - #expect(manifestJSON["icon.png"] == iconHash) + #expect(manifestJSON["icon.png"] == SHA256.hash(data: iconData).hex) #expect(manifestJSON["pet_store_logo.png"] != nil) } } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 7f87345..27c525b 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -18,9 +18,10 @@ struct PassesTests { 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 passURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.pkpass") + let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) - let passFolder = try Zip.quickUnzipFile(passURL) + let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try Zip.unzipFile(passURL, destination: passFolder) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) @@ -35,8 +36,7 @@ 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("/icon.png")) - let iconHash = Insecure.SHA1.hash(data: iconData).hex - #expect(manifestJSON["icon.png"] == iconHash) + #expect(manifestJSON["icon.png"] == Insecure.SHA1.hash(data: iconData).hex) #expect(manifestJSON["logo.png"] != nil) #expect(manifestJSON["personalizationLogo.png"] != nil) } @@ -72,9 +72,10 @@ struct PassesTests { 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 passURL = FileManager.default.temporaryDirectory.appendingPathComponent("test.pkpass") + let passURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).pkpass") try data.write(to: passURL) - let passFolder = try Zip.quickUnzipFile(passURL) + let passFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try Zip.unzipFile(passURL, destination: passFolder) #expect(FileManager.default.fileExists(atPath: passFolder.path.appending("/signature"))) @@ -93,8 +94,7 @@ 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")) - let iconHash = Insecure.SHA1.hash(data: iconData).hex - #expect(manifestJSON["personalizationLogo.png"] == iconHash) + #expect(manifestJSON["personalizationLogo.png"] == Insecure.SHA1.hash(data: iconData).hex) } } From d03686bae4bfe3dc75b9bb0b82387a84dfdaf5da Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 31 Oct 2024 23:58:14 +0100 Subject: [PATCH 16/17] Change `xxxTypeIdentifier` to `typeIdentifier` --- .../Orders/Models/Concrete Models/Order.swift | 12 ++-- Sources/Orders/Models/OrderModel.swift | 12 ++-- .../Models/OrdersRegistrationModel.swift | 6 +- Sources/Orders/Orders.docc/GettingStarted.md | 4 +- Sources/Orders/OrdersService.swift | 8 +-- Sources/Orders/OrdersServiceCustom.swift | 59 +++++++++---------- .../Passes/Models/Concrete Models/Pass.swift | 12 ++-- Sources/Passes/Models/PassModel.swift | 16 ++--- .../Models/PassesRegistrationModel.swift | 6 +- Sources/Passes/Passes.docc/GettingStarted.md | 4 +- Sources/Passes/PassesService.swift | 8 +-- Sources/Passes/PassesServiceCustom.swift | 39 ++++++------ Tests/OrdersTests/OrderData.swift | 2 +- Tests/OrdersTests/OrdersTests.swift | 44 +++++++------- Tests/PassesTests/PassData.swift | 2 +- Tests/PassesTests/PassesTests.swift | 48 +++++++-------- 16 files changed, 133 insertions(+), 149 deletions(-) diff --git a/Sources/Orders/Models/Concrete Models/Order.swift b/Sources/Orders/Models/Concrete Models/Order.swift index 2430a2d..8cf29ef 100644 --- a/Sources/Orders/Models/Concrete Models/Order.swift +++ b/Sources/Orders/Models/Concrete Models/Order.swift @@ -28,8 +28,8 @@ final public class Order: OrderModel, @unchecked Sendable { public var updatedAt: Date? /// An identifier for the order type associated with the order. - @Field(key: Order.FieldKeys.orderTypeIdentifier) - public var orderTypeIdentifier: String + @Field(key: Order.FieldKeys.typeIdentifier) + public var typeIdentifier: String /// The authentication token supplied to your web service. @Field(key: Order.FieldKeys.authenticationToken) @@ -37,8 +37,8 @@ final public class Order: OrderModel, @unchecked Sendable { public required init() {} - public required init(orderTypeIdentifier: String, authenticationToken: String) { - self.orderTypeIdentifier = orderTypeIdentifier + public required init(typeIdentifier: String, authenticationToken: String) { + self.typeIdentifier = typeIdentifier self.authenticationToken = authenticationToken } } @@ -49,7 +49,7 @@ extension Order: AsyncMigration { .id() .field(Order.FieldKeys.createdAt, .datetime, .required) .field(Order.FieldKeys.updatedAt, .datetime, .required) - .field(Order.FieldKeys.orderTypeIdentifier, .string, .required) + .field(Order.FieldKeys.typeIdentifier, .string, .required) .field(Order.FieldKeys.authenticationToken, .string, .required) .create() } @@ -64,7 +64,7 @@ extension Order { static let schemaName = "orders" static let createdAt = FieldKey(stringLiteral: "created_at") static let updatedAt = FieldKey(stringLiteral: "updated_at") - static let orderTypeIdentifier = FieldKey(stringLiteral: "order_type_identifier") + static let typeIdentifier = FieldKey(stringLiteral: "type_identifier") static let authenticationToken = FieldKey(stringLiteral: "authentication_token") } } diff --git a/Sources/Orders/Models/OrderModel.swift b/Sources/Orders/Models/OrderModel.swift index a9045fd..92cbd4f 100644 --- a/Sources/Orders/Models/OrderModel.swift +++ b/Sources/Orders/Models/OrderModel.swift @@ -13,7 +13,7 @@ import Foundation /// Uses a UUID so people can't easily guess order IDs. public protocol OrderModel: Model where IDValue == UUID { /// An identifier for the order type associated with the order. - var orderTypeIdentifier: String { get set } + var typeIdentifier: String { get set } /// The date and time when the customer created the order. var createdAt: Date? { get set } @@ -36,14 +36,14 @@ extension OrderModel { return id } - var _$orderTypeIdentifier: Field { - guard let mirror = Mirror(reflecting: self).descendant("_orderTypeIdentifier"), - let orderTypeIdentifier = mirror as? Field + var _$typeIdentifier: Field { + guard let mirror = Mirror(reflecting: self).descendant("_typeIdentifier"), + let typeIdentifier = mirror as? Field else { - fatalError("orderTypeIdentifier property must be declared using @Field") + fatalError("typeIdentifier property must be declared using @Field") } - return orderTypeIdentifier + return typeIdentifier } var _$updatedAt: Timestamp { diff --git a/Sources/Orders/Models/OrdersRegistrationModel.swift b/Sources/Orders/Models/OrdersRegistrationModel.swift index 6550627..64e41e3 100644 --- a/Sources/Orders/Models/OrdersRegistrationModel.swift +++ b/Sources/Orders/Models/OrdersRegistrationModel.swift @@ -41,15 +41,13 @@ extension OrdersRegistrationModel { return order } - static func `for`( - deviceLibraryIdentifier: String, orderTypeIdentifier: String, on db: any Database - ) -> QueryBuilder { + static func `for`(deviceLibraryIdentifier: String, typeIdentifier: String, on db: any Database) -> QueryBuilder { Self.query(on: db) .join(parent: \._$order) .join(parent: \._$device) .with(\._$order) .with(\._$device) - .filter(OrderType.self, \._$orderTypeIdentifier == orderTypeIdentifier) + .filter(OrderType.self, \._$typeIdentifier == typeIdentifier) .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) } } diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index 2628281..9870283 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -122,7 +122,7 @@ import Orders final class OrderDelegate: OrdersDelegate { func encode(order: O, db: Database, encoder: JSONEncoder) async throws -> Data { - // The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier` + // The specific OrderData class you use here may vary based on the `order.typeIdentifier` // if you have multiple different types of orders, and thus multiple types of order data. guard let orderData = try await OrderData.query(on: db) .filter(\.$order.$id == order.requireID()) @@ -239,7 +239,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware { // Create the `Order` and add it to the `OrderData` automatically at creation func create(model: OrderData, on db: Database, next: AnyAsyncModelResponder) async throws { let order = Order( - orderTypeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!, + typeIdentifier: Environment.get("ORDER_TYPE_IDENTIFIER")!, authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) try await order.save(on: db) model.$order.id = try order.requireID() diff --git a/Sources/Orders/OrdersService.swift b/Sources/Orders/OrdersService.swift index 016c134..0a9d1e0 100644 --- a/Sources/Orders/OrdersService.swift +++ b/Sources/Orders/OrdersService.swift @@ -75,12 +75,10 @@ public final class OrdersService: Sendable { /// /// - Parameters: /// - id: The `UUID` of the order to send the notifications for. - /// - orderTypeIdentifier: The type identifier of the order. + /// - typeIdentifier: The type identifier of the order. /// - db: The `Database` to use. - public func sendPushNotificationsForOrder( - id: UUID, of orderTypeIdentifier: String, on db: any Database - ) async throws { - try await service.sendPushNotificationsForOrder(id: id, of: orderTypeIdentifier, on: db) + 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. diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 664ae80..f174720 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -113,30 +113,24 @@ where O == R.OrderType, D == R.DeviceType { ) let v1 = app.grouped("api", "orders", "v1") - v1.get( - "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", - use: { try await self.ordersForDevice(req: $0) }) + v1.get("devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", use: { try await self.ordersForDevice(req: $0) }) v1.post("log", use: { try await self.logError(req: $0) }) let v1auth = v1.grouped(AppleOrderMiddleware()) v1auth.post( - "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", - ":orderIdentifier", use: { try await self.registerDevice(req: $0) }) - v1auth.get( - "orders", ":orderTypeIdentifier", ":orderIdentifier", - use: { try await self.latestVersionOfOrder(req: $0) }) + "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", ":orderIdentifier", + use: { try await self.registerDevice(req: $0) } + ) + v1auth.get("orders", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.latestVersionOfOrder(req: $0) }) v1auth.delete( - "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", - ":orderIdentifier", use: { try await self.unregisterDevice(req: $0) }) + "devices", ":deviceIdentifier", "registrations", ":orderTypeIdentifier", ":orderIdentifier", + use: { try await self.unregisterDevice(req: $0) } + ) if let pushRoutesMiddleware { let pushAuth = v1.grouped(pushRoutesMiddleware) - pushAuth.post( - "push", ":orderTypeIdentifier", ":orderIdentifier", - use: { try await self.pushUpdatesForOrder(req: $0) }) - pushAuth.get( - "push", ":orderTypeIdentifier", ":orderIdentifier", - use: { try await self.tokensForOrderUpdate(req: $0) }) + pushAuth.post("push", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.pushUpdatesForOrder(req: $0) }) + pushAuth.get("push", ":orderTypeIdentifier", ":orderIdentifier", use: { try await self.tokensForOrderUpdate(req: $0) }) } } } @@ -159,7 +153,7 @@ extension OrdersServiceCustom { guard let order = try await O.query(on: req.db) .filter(\._$id == id) - .filter(\._$orderTypeIdentifier == orderTypeIdentifier) + .filter(\._$typeIdentifier == orderTypeIdentifier) .first() else { throw Abort(.notFound) @@ -198,7 +192,7 @@ extension OrdersServiceCustom { guard let order = try await O.query(on: req.db) .filter(\._$id == orderIdentifier) - .filter(\._$orderTypeIdentifier == orderTypeIdentifier) + .filter(\._$typeIdentifier == orderTypeIdentifier) .first() else { throw Abort(.notFound) @@ -222,7 +216,8 @@ extension OrdersServiceCustom { ) async throws -> HTTPStatus { let r = try await R.for( deviceLibraryIdentifier: device.deviceLibraryIdentifier, - orderTypeIdentifier: order.orderTypeIdentifier, on: db + typeIdentifier: order.typeIdentifier, + on: db ) .filter(O.self, \._$id == order.requireID()) .first() @@ -243,8 +238,10 @@ extension OrdersServiceCustom { let deviceIdentifier = req.parameters.get("deviceIdentifier")! var query = R.for( - deviceLibraryIdentifier: deviceIdentifier, orderTypeIdentifier: orderTypeIdentifier, - on: req.db) + deviceLibraryIdentifier: deviceIdentifier, + typeIdentifier: orderTypeIdentifier, + on: req.db + ) if let since: TimeInterval = req.query["ordersModifiedSince"] { let when = Date(timeIntervalSince1970: since) query = query.filter(O.self, \._$updatedAt > when) @@ -297,7 +294,8 @@ extension OrdersServiceCustom { guard let r = try await R.for( - deviceLibraryIdentifier: deviceIdentifier, orderTypeIdentifier: orderTypeIdentifier, + deviceLibraryIdentifier: deviceIdentifier, + typeIdentifier: orderTypeIdentifier, on: req.db ) .filter(O.self, \._$id == orderIdentifier) @@ -340,17 +338,14 @@ extension OrdersServiceCustom { /// /// - Parameters: /// - id: The `UUID` of the order to send the notifications for. - /// - orderTypeIdentifier: The type identifier of the order. + /// - typeIdentifier: The type identifier of the order. /// - db: The `Database` to use. - public func sendPushNotificationsForOrder( - id: UUID, of orderTypeIdentifier: String, on db: any Database - ) async throws { - let registrations = try await Self.registrationsForOrder( - id: id, of: orderTypeIdentifier, on: db) + public func sendPushNotificationsForOrder(id: UUID, of typeIdentifier: String, on db: any Database) async throws { + let registrations = try await Self.registrationsForOrder(id: id, of: typeIdentifier, on: db) for reg in registrations { let backgroundNotification = APNSBackgroundNotification( expiration: .immediately, - topic: reg.order.orderTypeIdentifier, + topic: reg.order.typeIdentifier, payload: EmptyPayload() ) do { @@ -371,10 +366,10 @@ extension OrdersServiceCustom { /// - order: The order to send the notifications for. /// - db: The `Database` to use. public func sendPushNotifications(for order: O, on db: any Database) async throws { - try await sendPushNotificationsForOrder(id: order.requireID(), of: order.orderTypeIdentifier, on: db) + try await sendPushNotificationsForOrder(id: order.requireID(), of: order.typeIdentifier, on: db) } - private static func registrationsForOrder(id: UUID, of orderTypeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrationsForOrder(id: UUID, of typeIdentifier: String, 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) @@ -382,7 +377,7 @@ extension OrdersServiceCustom { .join(parent: \._$device) .with(\._$order) .with(\._$device) - .filter(O.self, \._$orderTypeIdentifier == orderTypeIdentifier) + .filter(O.self, \._$typeIdentifier == typeIdentifier) .filter(O.self, \._$id == id) .all() } diff --git a/Sources/Passes/Models/Concrete Models/Pass.swift b/Sources/Passes/Models/Concrete Models/Pass.swift index 87a3212..e723208 100644 --- a/Sources/Passes/Models/Concrete Models/Pass.swift +++ b/Sources/Passes/Models/Concrete Models/Pass.swift @@ -29,8 +29,8 @@ final public class Pass: PassModel, @unchecked Sendable { public var updatedAt: Date? /// The pass type identifier that’s registered with Apple. - @Field(key: Pass.FieldKeys.passTypeIdentifier) - public var passTypeIdentifier: String + @Field(key: Pass.FieldKeys.typeIdentifier) + public var typeIdentifier: String /// The authentication token to use with the web service in the `webServiceURL` key. @Field(key: Pass.FieldKeys.authenticationToken) @@ -42,8 +42,8 @@ final public class Pass: PassModel, @unchecked Sendable { public required init() {} - public required init(passTypeIdentifier: String, authenticationToken: String) { - self.passTypeIdentifier = passTypeIdentifier + public required init(typeIdentifier: String, authenticationToken: String) { + self.typeIdentifier = typeIdentifier self.authenticationToken = authenticationToken } } @@ -53,7 +53,7 @@ extension Pass: AsyncMigration { try await database.schema(Self.schema) .id() .field(Pass.FieldKeys.updatedAt, .datetime, .required) - .field(Pass.FieldKeys.passTypeIdentifier, .string, .required) + .field(Pass.FieldKeys.typeIdentifier, .string, .required) .field(Pass.FieldKeys.authenticationToken, .string, .required) .field( Pass.FieldKeys.userPersonalizationID, .int, @@ -72,7 +72,7 @@ extension Pass { enum FieldKeys { static let schemaName = "passes" static let updatedAt = FieldKey(stringLiteral: "updated_at") - static let passTypeIdentifier = FieldKey(stringLiteral: "pass_type_identifier") + static let typeIdentifier = FieldKey(stringLiteral: "type_identifier") static let authenticationToken = FieldKey(stringLiteral: "authentication_token") static let userPersonalizationID = FieldKey(stringLiteral: "user_personalization_id") } diff --git a/Sources/Passes/Models/PassModel.swift b/Sources/Passes/Models/PassModel.swift index ca6dd12..e4fe6a8 100644 --- a/Sources/Passes/Models/PassModel.swift +++ b/Sources/Passes/Models/PassModel.swift @@ -36,7 +36,7 @@ public protocol PassModel: Model where IDValue == UUID { associatedtype UserPersonalizationType: UserPersonalizationModel /// The pass type identifier that’s registered with Apple. - var passTypeIdentifier: String { get set } + var typeIdentifier: String { get set } /// The last time the pass was modified. var updatedAt: Date? { get set } @@ -49,9 +49,9 @@ public protocol PassModel: Model where IDValue == UUID { /// The designated initializer. /// - Parameters: - /// - passTypeIdentifier: The pass type identifier that’s registered with Apple. + /// - typeIdentifier: The pass type identifier that’s registered with Apple. /// - authenticationToken: The authentication token to use with the web service in the `webServiceURL` key. - init(passTypeIdentifier: String, authenticationToken: String) + init(typeIdentifier: String, authenticationToken: String) } extension PassModel { @@ -65,14 +65,14 @@ extension PassModel { return id } - var _$passTypeIdentifier: Field { - guard let mirror = Mirror(reflecting: self).descendant("_passTypeIdentifier"), - let passTypeIdentifier = mirror as? Field + var _$typeIdentifier: Field { + guard let mirror = Mirror(reflecting: self).descendant("_typeIdentifier"), + let typeIdentifier = mirror as? Field else { - fatalError("passTypeIdentifier property must be declared using @Field") + fatalError("typeIdentifier property must be declared using @Field") } - return passTypeIdentifier + return typeIdentifier } var _$updatedAt: Timestamp { diff --git a/Sources/Passes/Models/PassesRegistrationModel.swift b/Sources/Passes/Models/PassesRegistrationModel.swift index 4eb7e39..cb35fcb 100644 --- a/Sources/Passes/Models/PassesRegistrationModel.swift +++ b/Sources/Passes/Models/PassesRegistrationModel.swift @@ -62,15 +62,13 @@ extension PassesRegistrationModel { return pass } - static func `for`( - deviceLibraryIdentifier: String, passTypeIdentifier: String, on db: any Database - ) -> QueryBuilder { + static func `for`(deviceLibraryIdentifier: String, typeIdentifier: String, on db: any Database) -> QueryBuilder { Self.query(on: db) .join(parent: \._$pass) .join(parent: \._$device) .with(\._$pass) .with(\._$device) - .filter(PassType.self, \._$passTypeIdentifier == passTypeIdentifier) + .filter(PassType.self, \._$typeIdentifier == typeIdentifier) .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) } } diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index 5f2d701..f7c679e 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -146,7 +146,7 @@ import Passes final class PassDelegate: PassesDelegate { func encode(pass: P, db: Database, encoder: JSONEncoder) async throws -> Data { - // The specific PassData class you use here may vary based on the `pass.passTypeIdentifier` + // The specific PassData class you use here may vary based on the `pass.typeIdentifier` // if you have multiple different types of passes, and thus multiple types of pass data. guard let passData = try await PassData.query(on: db) .filter(\.$pass.$id == pass.requireID()) @@ -264,7 +264,7 @@ struct PassDataMiddleware: AsyncModelMiddleware { // Create the `Pass` and add it to the `PassData` automatically at creation func create(model: PassData, on db: Database, next: AnyAsyncModelResponder) async throws { let pass = Pass( - passTypeIdentifier: Environment.get("PASS_TYPE_IDENTIFIER")!, + typeIdentifier: Environment.get("PASS_TYPE_IDENTIFIER")!, authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) try await pass.save(on: db) model.$pass.id = try pass.requireID() diff --git a/Sources/Passes/PassesService.swift b/Sources/Passes/PassesService.swift index 4be56a9..572f07f 100644 --- a/Sources/Passes/PassesService.swift +++ b/Sources/Passes/PassesService.swift @@ -111,12 +111,10 @@ public final class PassesService: Sendable { /// /// - Parameters: /// - id: The `UUID` of the pass to send the notifications for. - /// - passTypeIdentifier: The type identifier of the pass. + /// - typeIdentifier: The type identifier of the pass. /// - db: The `Database` to use. - public func sendPushNotificationsForPass( - id: UUID, of passTypeIdentifier: String, on db: any Database - ) async throws { - try await service.sendPushNotificationsForPass(id: id, of: passTypeIdentifier, on: db) + 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. diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index a447be6..eb79390 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -161,7 +161,7 @@ extension PassesServiceCustom { let deviceLibraryIdentifier = req.parameters.get("deviceLibraryIdentifier")! guard let pass = try await P.query(on: req.db) - .filter(\._$passTypeIdentifier == passTypeIdentifier) + .filter(\._$typeIdentifier == passTypeIdentifier) .filter(\._$id == serial) .first() else { @@ -182,14 +182,11 @@ extension PassesServiceCustom { } } - private static func createRegistration( - device: D, - pass: P, - db: any Database - ) async throws -> HTTPStatus { + private static func createRegistration(device: D, pass: P, db: any Database) async throws -> HTTPStatus { let r = try await R.for( deviceLibraryIdentifier: device.deviceLibraryIdentifier, - passTypeIdentifier: pass.passTypeIdentifier, on: db + typeIdentifier: pass.typeIdentifier, + on: db ) .filter(P.self, \._$id == pass.requireID()) .first() @@ -211,7 +208,9 @@ extension PassesServiceCustom { var query = R.for( deviceLibraryIdentifier: deviceLibraryIdentifier, - passTypeIdentifier: passTypeIdentifier, on: req.db) + typeIdentifier: passTypeIdentifier, + on: req.db + ) if let since: TimeInterval = req.query["passesUpdatedSince"] { let when = Date(timeIntervalSince1970: since) query = query.filter(P.self, \._$updatedAt > when) @@ -251,7 +250,7 @@ extension PassesServiceCustom { guard let pass = try await P.query(on: req.db) .filter(\._$id == id) - .filter(\._$passTypeIdentifier == passTypeIdentifier) + .filter(\._$typeIdentifier == passTypeIdentifier) .first() else { throw Abort(.notFound) @@ -284,7 +283,8 @@ extension PassesServiceCustom { guard let r = try await R.for( deviceLibraryIdentifier: deviceLibraryIdentifier, - passTypeIdentifier: passTypeIdentifier, on: req.db + typeIdentifier: passTypeIdentifier, + on: req.db ) .filter(P.self, \._$id == passId) .first() @@ -324,7 +324,7 @@ extension PassesServiceCustom { guard let pass = try await P.query(on: req.db) .filter(\._$id == id) - .filter(\._$passTypeIdentifier == passTypeIdentifier) + .filter(\._$typeIdentifier == passTypeIdentifier) .first() else { throw Abort(.notFound) @@ -444,17 +444,14 @@ extension PassesServiceCustom { /// /// - Parameters: /// - id: The `UUID` of the pass to send the notifications for. - /// - passTypeIdentifier: The type identifier of the pass. + /// - typeIdentifier: The type identifier of the pass. /// - db: The `Database` to use. - public func sendPushNotificationsForPass( - id: UUID, of passTypeIdentifier: String, on db: any Database - ) async throws { - let registrations = try await Self.registrationsForPass( - id: id, of: passTypeIdentifier, on: db) + 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) for reg in registrations { let backgroundNotification = APNSBackgroundNotification( expiration: .immediately, - topic: reg.pass.passTypeIdentifier, + topic: reg.pass.typeIdentifier, payload: EmptyPayload() ) do { @@ -475,10 +472,10 @@ extension PassesServiceCustom { /// - 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.passTypeIdentifier, on: db) + try await sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: db) } - private static func registrationsForPass(id: UUID, of passTypeIdentifier: String, on db: any Database) async throws -> [R] { + private static func registrationsForPass(id: UUID, of typeIdentifier: String, 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) @@ -486,7 +483,7 @@ extension PassesServiceCustom { .join(parent: \._$device) .with(\._$pass) .with(\._$device) - .filter(P.self, \._$passTypeIdentifier == passTypeIdentifier) + .filter(P.self, \._$typeIdentifier == typeIdentifier) .filter(P.self, \._$id == id) .all() } diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift index 32e4dc9..627ecd3 100644 --- a/Tests/OrdersTests/OrderData.swift +++ b/Tests/OrdersTests/OrderData.swift @@ -104,7 +104,7 @@ struct OrderDataMiddleware: AsyncModelMiddleware { func create(model: OrderData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let order = Order( - orderTypeIdentifier: "order.com.example.pet-store", + typeIdentifier: "order.com.example.pet-store", authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString() ) try await order.save(on: db) diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index 24ab490..b5afb84 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -49,7 +49,7 @@ struct OrdersTests { try await app.test( .GET, - "\(ordersURI)orders/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)orders/\(order.typeIdentifier)/\(order.requireID())", headers: [ "Authorization": "AppleOrder \(order.authenticationToken)", "If-Modified-Since": "0", @@ -65,7 +65,7 @@ struct OrdersTests { // Test call with invalid authentication token try await app.test( .GET, - "\(ordersURI)orders/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)orders/\(order.typeIdentifier)/\(order.requireID())", headers: [ "Authorization": "AppleOrder invalidToken", "If-Modified-Since": "0", @@ -78,7 +78,7 @@ struct OrdersTests { // Test distant future `If-Modified-Since` date try await app.test( .GET, - "\(ordersURI)orders/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)orders/\(order.typeIdentifier)/\(order.requireID())", headers: [ "Authorization": "AppleOrder \(order.authenticationToken)", "If-Modified-Since": "2147483647", @@ -91,7 +91,7 @@ struct OrdersTests { // Test call with invalid order ID try await app.test( .GET, - "\(ordersURI)orders/\(order.orderTypeIdentifier)/invalidID", + "\(ordersURI)orders/\(order.typeIdentifier)/invalidID", headers: [ "Authorization": "AppleOrder \(order.authenticationToken)", "If-Modified-Since": "0", @@ -127,7 +127,7 @@ struct OrdersTests { try await app.test( .GET, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)?ordersModifiedSince=0", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)?ordersModifiedSince=0", afterResponse: { res async throws in #expect(res.status == .noContent) } @@ -135,7 +135,7 @@ struct OrdersTests { try await app.test( .DELETE, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .notFound) @@ -145,7 +145,7 @@ struct OrdersTests { // Test registration without authentication token try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, @@ -170,7 +170,7 @@ struct OrdersTests { // Test call without DTO try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -180,7 +180,7 @@ struct OrdersTests { // Test call with invalid UUID try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\("not-a-uuid")", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\("not-a-uuid")", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -192,7 +192,7 @@ struct OrdersTests { try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -205,7 +205,7 @@ struct OrdersTests { // Test registration of an already registered device try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -217,7 +217,7 @@ struct OrdersTests { try await app.test( .GET, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)?ordersModifiedSince=0", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)?ordersModifiedSince=0", afterResponse: { res async throws in let orders = try res.content.decode(OrdersForDeviceDTO.self) #expect(orders.orderIdentifiers.count == 1) @@ -229,7 +229,7 @@ struct OrdersTests { try await app.test( .GET, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)push/\(order.typeIdentifier)/\(order.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in let pushTokens = try res.content.decode([String].self) @@ -241,7 +241,7 @@ struct OrdersTests { // Test call with invalid UUID try await app.test( .GET, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\("not-a-uuid")", + "\(ordersURI)push/\(order.typeIdentifier)/\("not-a-uuid")", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -251,7 +251,7 @@ struct OrdersTests { // Test call with invalid UUID try await app.test( .DELETE, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\("not-a-uuid")", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\("not-a-uuid")", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -260,7 +260,7 @@ struct OrdersTests { try await app.test( .DELETE, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .ok) @@ -323,7 +323,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.orderTypeIdentifier, on: app.db) + try await ordersService.sendPushNotificationsForOrder(id: order.requireID(), of: order.typeIdentifier, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" @@ -331,7 +331,7 @@ struct OrdersTests { // Test call with incorrect secret try await app.test( .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)push/\(order.typeIdentifier)/\(order.requireID())", headers: ["X-Secret": "bar"], afterResponse: { res async throws in #expect(res.status == .unauthorized) @@ -340,7 +340,7 @@ struct OrdersTests { try await app.test( .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)push/\(order.typeIdentifier)/\(order.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .noContent) @@ -349,7 +349,7 @@ struct OrdersTests { try await app.test( .POST, - "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.typeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -361,7 +361,7 @@ struct OrdersTests { try await app.test( .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", + "\(ordersURI)push/\(order.typeIdentifier)/\(order.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .internalServerError) @@ -371,7 +371,7 @@ struct OrdersTests { // Test call with invalid UUID try await app.test( .POST, - "\(ordersURI)push/\(order.orderTypeIdentifier)/\("not-a-uuid")", + "\(ordersURI)push/\(order.typeIdentifier)/\("not-a-uuid")", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .badRequest) diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/PassData.swift index 38614f5..c5388c4 100644 --- a/Tests/PassesTests/PassData.swift +++ b/Tests/PassesTests/PassData.swift @@ -126,7 +126,7 @@ struct PassDataMiddleware: AsyncModelMiddleware { func create(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { let pass = Pass( - passTypeIdentifier: "pass.com.vapor-community.PassKit", + typeIdentifier: "pass.com.vapor-community.PassKit", authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString() ) try await pass.save(on: db) diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 27c525b..5bccc26 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -107,7 +107,7 @@ struct PassesTests { try await app.test( .GET, - "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)passes/\(pass.typeIdentifier)/\(pass.requireID())", headers: [ "Authorization": "ApplePass \(pass.authenticationToken)", "If-Modified-Since": "0", @@ -123,7 +123,7 @@ struct PassesTests { // Test call with invalid authentication token try await app.test( .GET, - "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)passes/\(pass.typeIdentifier)/\(pass.requireID())", headers: [ "Authorization": "ApplePass invalid-token", "If-Modified-Since": "0", @@ -136,7 +136,7 @@ struct PassesTests { // Test distant future `If-Modified-Since` date try await app.test( .GET, - "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)passes/\(pass.typeIdentifier)/\(pass.requireID())", headers: [ "Authorization": "ApplePass \(pass.authenticationToken)", "If-Modified-Since": "2147483647", @@ -149,7 +149,7 @@ struct PassesTests { // Test call with invalid pass ID try await app.test( .GET, - "\(passesURI)passes/\(pass.passTypeIdentifier)/invalid-uuid", + "\(passesURI)passes/\(pass.typeIdentifier)/invalid-uuid", headers: [ "Authorization": "ApplePass \(pass.authenticationToken)", "If-Modified-Since": "0", @@ -195,7 +195,7 @@ struct PassesTests { try await app.test( .POST, - "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())/personalize", + "\(passesURI)passes/\(pass.typeIdentifier)/\(pass.requireID())/personalize", beforeRequest: { req async throws in try req.content.encode(personalizationDict) }, @@ -221,7 +221,7 @@ struct PassesTests { // Test call with invalid pass ID try await app.test( .POST, - "\(passesURI)passes/\(pass.passTypeIdentifier)/invalid-uuid/personalize", + "\(passesURI)passes/\(pass.typeIdentifier)/invalid-uuid/personalize", beforeRequest: { req async throws in try req.content.encode(personalizationDict) }, @@ -255,7 +255,7 @@ struct PassesTests { try await app.test( .GET, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)?passesUpdatedSince=0", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)?passesUpdatedSince=0", afterResponse: { res async throws in #expect(res.status == .noContent) } @@ -263,7 +263,7 @@ struct PassesTests { try await app.test( .DELETE, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .notFound) @@ -273,7 +273,7 @@ struct PassesTests { // Test registration without authentication token try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, @@ -298,7 +298,7 @@ struct PassesTests { // Test call without DTO try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -308,7 +308,7 @@ struct PassesTests { // Test call with invalid UUID try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\("not-a-uuid")", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\("not-a-uuid")", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -320,7 +320,7 @@ struct PassesTests { try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -333,7 +333,7 @@ struct PassesTests { // Test registration of an already registered device try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -345,7 +345,7 @@ struct PassesTests { try await app.test( .GET, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)?passesUpdatedSince=0", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)?passesUpdatedSince=0", afterResponse: { res async throws in let passes = try res.content.decode(PassesForDeviceDTO.self) #expect(passes.serialNumbers.count == 1) @@ -357,7 +357,7 @@ struct PassesTests { try await app.test( .GET, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)push/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in let pushTokens = try res.content.decode([String].self) @@ -369,7 +369,7 @@ struct PassesTests { // Test call with invalid UUID try await app.test( .GET, - "\(passesURI)push/\(pass.passTypeIdentifier)/\("not-a-uuid")", + "\(passesURI)push/\(pass.typeIdentifier)/\("not-a-uuid")", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -379,7 +379,7 @@ struct PassesTests { // Test call with invalid UUID try await app.test( .DELETE, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\("not-a-uuid")", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\("not-a-uuid")", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .badRequest) @@ -388,7 +388,7 @@ struct PassesTests { try await app.test( .DELETE, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], afterResponse: { res async throws in #expect(res.status == .ok) @@ -451,7 +451,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.passTypeIdentifier, on: app.db) + try await passesService.sendPushNotificationsForPass(id: pass.requireID(), of: pass.typeIdentifier, on: app.db) let deviceLibraryIdentifier = "abcdefg" let pushToken = "1234567890" @@ -459,7 +459,7 @@ struct PassesTests { // Test call with incorrect secret try await app.test( .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)push/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["X-Secret": "bar"], afterResponse: { res async throws in #expect(res.status == .unauthorized) @@ -468,7 +468,7 @@ struct PassesTests { try await app.test( .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)push/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .noContent) @@ -477,7 +477,7 @@ struct PassesTests { try await app.test( .POST, - "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], beforeRequest: { req async throws in try req.content.encode(RegistrationDTO(pushToken: pushToken)) @@ -489,7 +489,7 @@ struct PassesTests { try await app.test( .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", + "\(passesURI)push/\(pass.typeIdentifier)/\(pass.requireID())", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .internalServerError) @@ -499,7 +499,7 @@ struct PassesTests { // Test call with invalid UUID try await app.test( .POST, - "\(passesURI)push/\(pass.passTypeIdentifier)/\("not-a-uuid")", + "\(passesURI)push/\(pass.typeIdentifier)/\("not-a-uuid")", headers: ["X-Secret": "foo"], afterResponse: { res async throws in #expect(res.status == .badRequest) From 351e88a10f883929f4b2a85a038ce338f66b5570 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 1 Nov 2024 00:09:19 +0100 Subject: [PATCH 17/17] Change `deviceLibraryIdentifier` to `libraryIdentifier` --- .../Models/Concrete Models/OrdersDevice.swift | 16 +++++++--------- .../Orders/Models/OrdersRegistrationModel.swift | 2 +- Sources/Orders/OrdersServiceCustom.swift | 6 +++--- Sources/PassKit/Models/DeviceModel.swift | 16 ++++++++-------- .../Models/Concrete Models/PassesDevice.swift | 16 +++++++--------- .../Passes/Models/PassesRegistrationModel.swift | 2 +- Sources/Passes/PassesServiceCustom.swift | 7 +++---- 7 files changed, 30 insertions(+), 35 deletions(-) diff --git a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift index eab4e47..b739e94 100644 --- a/Sources/Orders/Models/Concrete Models/OrdersDevice.swift +++ b/Sources/Orders/Models/Concrete Models/OrdersDevice.swift @@ -21,11 +21,11 @@ final public class OrdersDevice: DeviceModel, @unchecked Sendable { public var pushToken: String /// The identifier Apple Wallet provides for the device. - @Field(key: OrdersDevice.FieldKeys.deviceLibraryIdentifier) - public var deviceLibraryIdentifier: String + @Field(key: OrdersDevice.FieldKeys.libraryIdentifier) + public var libraryIdentifier: String - public init(deviceLibraryIdentifier: String, pushToken: String) { - self.deviceLibraryIdentifier = deviceLibraryIdentifier + public init(libraryIdentifier: String, pushToken: String) { + self.libraryIdentifier = libraryIdentifier self.pushToken = pushToken } @@ -37,10 +37,8 @@ extension OrdersDevice: AsyncMigration { try await database.schema(Self.schema) .field(.id, .int, .identifier(auto: true)) .field(OrdersDevice.FieldKeys.pushToken, .string, .required) - .field(OrdersDevice.FieldKeys.deviceLibraryIdentifier, .string, .required) - .unique( - on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.deviceLibraryIdentifier - ) + .field(OrdersDevice.FieldKeys.libraryIdentifier, .string, .required) + .unique(on: OrdersDevice.FieldKeys.pushToken, OrdersDevice.FieldKeys.libraryIdentifier) .create() } @@ -53,6 +51,6 @@ extension OrdersDevice { enum FieldKeys { static let schemaName = "orders_devices" static let pushToken = FieldKey(stringLiteral: "push_token") - static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier") + static let libraryIdentifier = FieldKey(stringLiteral: "library_identifier") } } diff --git a/Sources/Orders/Models/OrdersRegistrationModel.swift b/Sources/Orders/Models/OrdersRegistrationModel.swift index 64e41e3..22a8708 100644 --- a/Sources/Orders/Models/OrdersRegistrationModel.swift +++ b/Sources/Orders/Models/OrdersRegistrationModel.swift @@ -48,6 +48,6 @@ extension OrdersRegistrationModel { .with(\._$order) .with(\._$device) .filter(OrderType.self, \._$typeIdentifier == typeIdentifier) - .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) + .filter(DeviceType.self, \._$libraryIdentifier == deviceLibraryIdentifier) } } diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index f174720..4f741f7 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -199,13 +199,13 @@ extension OrdersServiceCustom { } let device = try await D.query(on: req.db) - .filter(\._$deviceLibraryIdentifier == deviceIdentifier) + .filter(\._$libraryIdentifier == deviceIdentifier) .filter(\._$pushToken == pushToken) .first() if let device = device { return try await Self.createRegistration(device: device, order: order, db: req.db) } else { - let newDevice = D(deviceLibraryIdentifier: deviceIdentifier, pushToken: pushToken) + let newDevice = D(libraryIdentifier: deviceIdentifier, pushToken: pushToken) try await newDevice.create(on: req.db) return try await Self.createRegistration(device: newDevice, order: order, db: req.db) } @@ -215,7 +215,7 @@ extension OrdersServiceCustom { device: D, order: O, db: any Database ) async throws -> HTTPStatus { let r = try await R.for( - deviceLibraryIdentifier: device.deviceLibraryIdentifier, + deviceLibraryIdentifier: device.libraryIdentifier, typeIdentifier: order.typeIdentifier, on: db ) diff --git a/Sources/PassKit/Models/DeviceModel.swift b/Sources/PassKit/Models/DeviceModel.swift index 19ad199..6062737 100644 --- a/Sources/PassKit/Models/DeviceModel.swift +++ b/Sources/PassKit/Models/DeviceModel.swift @@ -34,13 +34,13 @@ public protocol DeviceModel: Model where IDValue == Int { var pushToken: String { get set } /// The identifier PassKit provides for the device. - var deviceLibraryIdentifier: String { get set } + var libraryIdentifier: String { get set } /// The designated initializer. /// - Parameters: - /// - deviceLibraryIdentifier: The device identifier as provided during registration. + /// - libraryIdentifier: The device identifier as provided during registration. /// - pushToken: The push token to use when sending updates via push notifications. - init(deviceLibraryIdentifier: String, pushToken: String) + init(libraryIdentifier: String, pushToken: String) } extension DeviceModel { @@ -54,13 +54,13 @@ extension DeviceModel { return pushToken } - package var _$deviceLibraryIdentifier: Field { - guard let mirror = Mirror(reflecting: self).descendant("_deviceLibraryIdentifier"), - let deviceLibraryIdentifier = mirror as? Field + package var _$libraryIdentifier: Field { + guard let mirror = Mirror(reflecting: self).descendant("_libraryIdentifier"), + let libraryIdentifier = mirror as? Field else { - fatalError("deviceLibraryIdentifier property must be declared using @Field") + fatalError("libraryIdentifier property must be declared using @Field") } - return deviceLibraryIdentifier + return libraryIdentifier } } diff --git a/Sources/Passes/Models/Concrete Models/PassesDevice.swift b/Sources/Passes/Models/Concrete Models/PassesDevice.swift index c3cd02b..593b62f 100644 --- a/Sources/Passes/Models/Concrete Models/PassesDevice.swift +++ b/Sources/Passes/Models/Concrete Models/PassesDevice.swift @@ -21,11 +21,11 @@ final public class PassesDevice: DeviceModel, @unchecked Sendable { public var pushToken: String /// The identifier PassKit provides for the device. - @Field(key: PassesDevice.FieldKeys.deviceLibraryIdentifier) - public var deviceLibraryIdentifier: String + @Field(key: PassesDevice.FieldKeys.libraryIdentifier) + public var libraryIdentifier: String - public init(deviceLibraryIdentifier: String, pushToken: String) { - self.deviceLibraryIdentifier = deviceLibraryIdentifier + public init(libraryIdentifier: String, pushToken: String) { + self.libraryIdentifier = libraryIdentifier self.pushToken = pushToken } @@ -37,10 +37,8 @@ extension PassesDevice: AsyncMigration { try await database.schema(Self.schema) .field(.id, .int, .identifier(auto: true)) .field(PassesDevice.FieldKeys.pushToken, .string, .required) - .field(PassesDevice.FieldKeys.deviceLibraryIdentifier, .string, .required) - .unique( - on: PassesDevice.FieldKeys.pushToken, PassesDevice.FieldKeys.deviceLibraryIdentifier - ) + .field(PassesDevice.FieldKeys.libraryIdentifier, .string, .required) + .unique(on: PassesDevice.FieldKeys.pushToken, PassesDevice.FieldKeys.libraryIdentifier) .create() } @@ -53,6 +51,6 @@ extension PassesDevice { enum FieldKeys { static let schemaName = "passes_devices" static let pushToken = FieldKey(stringLiteral: "push_token") - static let deviceLibraryIdentifier = FieldKey(stringLiteral: "device_library_identifier") + static let libraryIdentifier = FieldKey(stringLiteral: "library_identifier") } } diff --git a/Sources/Passes/Models/PassesRegistrationModel.swift b/Sources/Passes/Models/PassesRegistrationModel.swift index cb35fcb..c23accc 100644 --- a/Sources/Passes/Models/PassesRegistrationModel.swift +++ b/Sources/Passes/Models/PassesRegistrationModel.swift @@ -69,6 +69,6 @@ extension PassesRegistrationModel { .with(\._$pass) .with(\._$device) .filter(PassType.self, \._$typeIdentifier == typeIdentifier) - .filter(DeviceType.self, \._$deviceLibraryIdentifier == deviceLibraryIdentifier) + .filter(DeviceType.self, \._$libraryIdentifier == deviceLibraryIdentifier) } } diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index eb79390..56db552 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -169,14 +169,13 @@ extension PassesServiceCustom { } let device = try await D.query(on: req.db) - .filter(\._$deviceLibraryIdentifier == deviceLibraryIdentifier) + .filter(\._$libraryIdentifier == deviceLibraryIdentifier) .filter(\._$pushToken == pushToken) .first() if let device = device { return try await Self.createRegistration(device: device, pass: pass, db: req.db) } else { - let newDevice = D( - deviceLibraryIdentifier: deviceLibraryIdentifier, pushToken: pushToken) + let newDevice = D(libraryIdentifier: deviceLibraryIdentifier, pushToken: pushToken) try await newDevice.create(on: req.db) return try await Self.createRegistration(device: newDevice, pass: pass, db: req.db) } @@ -184,7 +183,7 @@ extension PassesServiceCustom { private static func createRegistration(device: D, pass: P, db: any Database) async throws -> HTTPStatus { let r = try await R.for( - deviceLibraryIdentifier: device.deviceLibraryIdentifier, + deviceLibraryIdentifier: device.libraryIdentifier, typeIdentifier: pass.typeIdentifier, on: db )