diff --git a/Package.swift b/Package.swift index 7a8c575..e111671 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,8 @@ let package = Package( .package(url: "https://github.com/vapor/vapor.git", from: "4.102.0"), .package(url: "https://github.com/vapor/fluent.git", from: "4.11.0"), .package(url: "https://github.com/vapor/apns.git", from: "4.1.0"), + // used in tests + .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.7.4"), ], targets: [ .target( @@ -44,6 +46,7 @@ let package = Package( dependencies: [ .target(name: "Passes"), .product(name: "XCTVapor", package: "vapor"), + .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), ], swiftSettings: swiftSettings ), @@ -52,6 +55,7 @@ let package = Package( dependencies: [ .target(name: "Orders"), .product(name: "XCTVapor", package: "vapor"), + .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), ], swiftSettings: swiftSettings ), diff --git a/Tests/OrdersTests/OrderData.swift b/Tests/OrdersTests/OrderData.swift new file mode 100644 index 0000000..5eb24e8 --- /dev/null +++ b/Tests/OrdersTests/OrderData.swift @@ -0,0 +1,126 @@ +import Fluent +import struct Foundation.UUID +import Orders +import Vapor + +final class OrderData: OrderDataModel, @unchecked Sendable { + static let schema = OrderData.vddMMyyyy.schemaName + + @ID(key: .id) + var id: UUID? + + @Field(key: OrderData.vddMMyyyy.title) + var title: String + + @Parent(key: OrderData.vddMMyyyy.orderID) + var order: Order + + init() { } + + init(id: UUID? = nil, title: String) { + 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 { + func prepare(on database: any Database) async throws { + try await database.schema(OrderData.vddMMyyyy.schemaName) + .id() + .field(OrderData.vddMMyyyy.title, .string, .required) + .field(OrderData.vddMMyyyy.orderID, .uuid, .required, .references(Order.schema, .id, onDelete: .cascade)) + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(OrderData.vddMMyyyy.schemaName).delete() + } +} + +extension OrderData { + enum vddMMyyyy { + static let schemaName = "order_data" + static let title = FieldKey(stringLiteral: "title") + static let orderID = FieldKey(stringLiteral: "order_id") + } +} + +struct OrderJSONData: OrderJSON.Properties { + let schemaVersion = OrderJSON.SchemaVersion.v1 + let orderTypeIdentifier = "order.com.example.pet-store" + let orderIdentifier: String + let orderType = OrderJSON.OrderType.ecommerce + let orderNumber = "HM090772020864" + let createdAt: String + let updatedAt: String + let status = OrderJSON.OrderStatus.open + let merchant: MerchantData + let orderManagementURL = "https://www.example.com/" + let authenticationToken: String + + private let webServiceURL = "https://www.example.com/api/orders/" + + struct MerchantData: OrderJSON.Merchant { + let merchantIdentifier = "com.example.pet-store" + let displayName: String + let url = "https://www.example.com/" + let logo = "pet_store_logo.png" + } + + init(data: OrderData, order: Order) { + self.orderIdentifier = order.id!.uuidString + self.authenticationToken = order.authenticationToken + self.merchant = MerchantData(displayName: data.title) + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = .withInternetDateTime + self.createdAt = dateFormatter.string(from: order.createdAt!) + self.updatedAt = dateFormatter.string(from: order.updatedAt!) + } +} + +struct OrderDataMiddleware: AsyncModelMiddleware { + private unowned let service: OrdersService + + init(service: OrdersService) { + self.service = service + } + + 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()) + try await order.save(on: db) + model.$order.id = try order.requireID() + try await next.create(model, on: db) + } + + 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) + try await next.update(model, on: db) + try await service.sendPushNotifications(for: order, on: db) + } +} diff --git a/Tests/OrdersTests/OrderDelegate.swift b/Tests/OrdersTests/OrderDelegate.swift new file mode 100644 index 0000000..3db3338 --- /dev/null +++ b/Tests/OrdersTests/OrderDelegate.swift @@ -0,0 +1,27 @@ +import Vapor +import Fluent +import Orders + +final class OrderDelegate: OrdersDelegate { + let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true) + + let pemPrivateKeyPassword: String? = "password" + + 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 { + return URL(fileURLWithPath: "Templates/Orders/", isDirectory: true) + } +} diff --git a/Tests/OrdersTests/OrdersTests.swift b/Tests/OrdersTests/OrdersTests.swift index a452f5d..b231bbf 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -1,11 +1,27 @@ import XCTVapor +import Fluent +import FluentSQLiteDriver @testable import Orders final class OrdersTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - //XCTAssertEqual(OrdersService().text, "Hello, World!") + var app: Application! + let orderDelegate = OrderDelegate() + + override func setUp() async throws { + self.app = try await Application.make(.testing) + + app.databases.use(.sqlite(.memory), as: .sqlite) + OrdersService.register(migrations: app.migrations) + app.migrations.add(CreateOrderData()) + let ordersService = try OrdersService(app: app, delegate: orderDelegate) + app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) + + try await app.autoMigrate() + } + + override func tearDown() async throws { + try await app.autoRevert() + try await self.app.asyncShutdown() + self.app = nil } } diff --git a/Tests/PassesTests/PassData.swift b/Tests/PassesTests/PassData.swift new file mode 100644 index 0000000..cb3d7bd --- /dev/null +++ b/Tests/PassesTests/PassData.swift @@ -0,0 +1,157 @@ +import Fluent +import struct Foundation.UUID +import Passes +import Vapor + +final class PassData: PassDataModel, @unchecked Sendable { + static let schema = PassData.vddMMyyyy.schemaName + + @ID(key: .id) + var id: UUID? + + @Field(key: PassData.vddMMyyyy.title) + var title: String + + @Parent(key: PassData.vddMMyyyy.passID) + var pass: PKPass + + init() { } + + init(id: UUID? = nil, title: String) { + 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 { + func prepare(on database: any Database) async throws { + try await database.schema(PassData.vddMMyyyy.schemaName) + .id() + .field(PassData.vddMMyyyy.title, .string, .required) + .field(PassData.vddMMyyyy.passID, .uuid, .required, .references(PKPass.schema, .id, onDelete: .cascade)) + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema(PassData.vddMMyyyy.schemaName).delete() + } +} + +extension PassData { + enum vddMMyyyy { + static let schemaName = "pass_data" + static let title = FieldKey(stringLiteral: "title") + static let passID = FieldKey(stringLiteral: "pass_id") + } +} + +struct PassJSONData: PassJSON.Properties { + let description: String + let formatVersion = PassJSON.FormatVersion.v1 + let organizationName = "vapor-community" + let passTypeIdentifier = "pass.com.vapor-community.pass" + let serialNumber: String + let teamIdentifier = "ABCD1234" + + private let webServiceURL = "https://www.example.com/api/passes/" + private let authenticationToken: String + private let logoText = "Vapor" + 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 { + let format = PassJSON.BarcodeFormat.qr + let message: String + let messageEncoding = "iso-8859-1" + } + + let boardingPass = Boarding(transitType: .air) + struct Boarding: PassJSON.BoardingPass { + let transitType: PassJSON.TransitType + let headerFields: [PassField] + let primaryFields: [PassField] + let secondaryFields: [PassField] + let auxiliaryFields: [PassField] + let backFields: [PassField] + + struct PassField: PassJSON.PassFieldContent { + let key: String + let label: String + let value: String + } + + init(transitType: PassJSON.TransitType) { + self.headerFields = [.init(key: "header", label: "Header", value: "Header")] + self.primaryFields = [.init(key: "primary", label: "Primary", value: "Primary")] + self.secondaryFields = [.init(key: "secondary", label: "Secondary", value: "Secondary")] + self.auxiliaryFields = [.init(key: "auxiliary", label: "Auxiliary", value: "Auxiliary")] + self.backFields = [.init(key: "back", label: "Back", value: "Back")] + self.transitType = transitType + } + } + + init(data: PassData, pass: PKPass) { + self.description = data.title + self.serialNumber = pass.id!.uuidString + self.authenticationToken = pass.authenticationToken + } +} + +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 + + init(service: PassesService) { + self.service = service + } + + func create(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { + let pkPass = PKPass( + passTypeIdentifier: "pass.com.vapor-community.pass", + authenticationToken: Data([UInt8].random(count: 12)).base64EncodedString()) + try await pkPass.save(on: db) + model.$pass.id = try pkPass.requireID() + try await next.create(model, on: db) + } + + func update(model: PassData, on db: any Database, next: any AnyAsyncModelResponder) async throws { + let pkPass = try await model.$pass.get(on: db) + pkPass.updatedAt = Date() + try await pkPass.save(on: db) + try await next.update(model, on: db) + try await service.sendPushNotifications(for: pkPass, on: db) + } +} diff --git a/Tests/PassesTests/PassDelegate.swift b/Tests/PassesTests/PassDelegate.swift new file mode 100644 index 0000000..29e49ed --- /dev/null +++ b/Tests/PassesTests/PassDelegate.swift @@ -0,0 +1,46 @@ +import Vapor +import Fluent +import Passes + +final class PassDelegate: PassesDelegate { + let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Passes/", isDirectory: true) + + let pemPrivateKeyPassword: String? = "password" + + 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 encodePersonalization(for pass: P, db: any Database, encoder: JSONEncoder) async throws -> Data? { + guard let passData = try await PassData.query(on: db) + .filter(\.$pass.$id == pass.id!) + .with(\.$pass) + .first() + else { + throw Abort(.internalServerError) + } + + if try await passData.pass.$userPersonalization.get(on: db) == nil { + guard let data = try? encoder.encode(PersonalizationJSONData()) else { + throw Abort(.internalServerError) + } + return data + } else { return nil } + } + */ + + func template(for pass: P, db: any Database) async throws -> URL { + return URL(fileURLWithPath: "Templates/Passes/", isDirectory: true) + } +} diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index 9f91910..e035a8e 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -1,11 +1,27 @@ import XCTVapor +import Fluent +import FluentSQLiteDriver @testable import Passes final class PassesTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - //XCTAssertEqual(PassesService().text, "Hello, World!") + var app: Application! + let passDelegate = PassDelegate() + + override func setUp() async throws { + self.app = try await Application.make(.testing) + + app.databases.use(.sqlite(.memory), as: .sqlite) + PassesService.register(migrations: app.migrations) + app.migrations.add(CreatePassData()) + let passesService = try PassesService(app: app, delegate: passDelegate) + app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) + + try await app.autoMigrate() + } + + override func tearDown() async throws { + try await app.autoRevert() + try await self.app.asyncShutdown() + self.app = nil } }