From 398127e283a58e5e8855ab95c6a8d4501b8e4ecd Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 14 Aug 2024 15:50:37 +0200 Subject: [PATCH] Cleanup and add more tests --- Sources/Orders/Orders.docc/GettingStarted.md | 7 +- Sources/Orders/OrdersServiceCustom.swift | 12 ++- Sources/Passes/Passes.docc/GettingStarted.md | 14 +++- Sources/Passes/PassesServiceCustom.swift | 14 ++-- Tests/OrdersTests/OrdersTests.swift | 38 +++++++++- Tests/PassesTests/PassesTests.swift | 80 +++++++++++++++++++- 6 files changed, 141 insertions(+), 24 deletions(-) diff --git a/Sources/Orders/Orders.docc/GettingStarted.md b/Sources/Orders/Orders.docc/GettingStarted.md index 17ce424..b2b39e6 100644 --- a/Sources/Orders/Orders.docc/GettingStarted.md +++ b/Sources/Orders/Orders.docc/GettingStarted.md @@ -292,12 +292,17 @@ fileprivate func orderHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + let bundle = try await ordersService.generateOrderContent(for: orderData.order, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.order") headers.add(name: .contentDisposition, value: "attachment; filename=name.order") - headers.add(name: .lastModified, value: String(orderData.order.updatedAt?.timeIntervalSince1970 ?? 0)) + headers.add(name: .lastModified, value: dateFormatter.string(from: order.updatedAt ?? Date.distantPast)) headers.add(name: .contentTransferEncoding, value: "binary") return Response(status: .ok, headers: headers, body: body) } diff --git a/Sources/Orders/OrdersServiceCustom.swift b/Sources/Orders/OrdersServiceCustom.swift index 02f2b8a..46f1171 100644 --- a/Sources/Orders/OrdersServiceCustom.swift +++ b/Sources/Orders/OrdersServiceCustom.swift @@ -104,8 +104,10 @@ extension OrdersServiceCustom { func latestVersionOfOrder(req: Request) async throws -> Response { logger?.debug("Called latestVersionOfOrder") - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = .withInternetDateTime + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" var ifModifiedSince = Date.distantPast if let header = req.headers[.ifModifiedSince].first, let ims = dateFormatter.date(from: header) { ifModifiedSince = ims @@ -353,7 +355,6 @@ extension OrdersServiceCustom { extension OrdersServiceCustom { private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath()) try paths.forEach { relativePath in let file = URL(fileURLWithPath: relativePath, relativeTo: root) @@ -362,7 +363,6 @@ extension OrdersServiceCustom { let hash = SHA256.hash(data: data) manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } - let data = try encoder.encode(manifest) try data.write(to: root.appendingPathComponent("manifest.json")) return data @@ -436,11 +436,9 @@ extension OrdersServiceCustom { guard (try? templateDirectory.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { throw OrdersError.templateNotDirectory } - var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) let tmp = FileManager.default.temporaryDirectory let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.copyItem(at: templateDirectory, to: root) defer { _ = try? FileManager.default.removeItem(at: root) } @@ -453,10 +451,10 @@ extension OrdersServiceCustom { in: root ) + var files = try FileManager.default.contentsOfDirectory(at: templateDirectory, includingPropertiesForKeys: nil) files.append(URL(fileURLWithPath: "order.json", relativeTo: root)) files.append(URL(fileURLWithPath: "manifest.json", relativeTo: root)) files.append(URL(fileURLWithPath: "signature", relativeTo: root)) - return try Data(contentsOf: Zip.quickZipFiles(files, fileName: UUID().uuidString)) } } diff --git a/Sources/Passes/Passes.docc/GettingStarted.md b/Sources/Passes/Passes.docc/GettingStarted.md index 4b3370b..d3b6161 100644 --- a/Sources/Passes/Passes.docc/GettingStarted.md +++ b/Sources/Passes/Passes.docc/GettingStarted.md @@ -316,12 +316,17 @@ fileprivate func passHandler(_ req: Request) async throws -> Response { throw Abort(.notFound) } + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + let bundle = try await passesService.generatePassContent(for: passData.pass, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpass") headers.add(name: .contentDisposition, value: "attachment; filename=name.pkpass") - headers.add(name: .lastModified, value: String(passData.pass.updatedAt?.timeIntervalSince1970 ?? 0)) + headers.add(name: .lastModified, value: dateFormatter.string(from: pass.updatedAt ?? Date.distantPast)) headers.add(name: .contentTransferEncoding, value: "binary") return Response(status: .ok, headers: headers, body: body) } @@ -341,12 +346,17 @@ fileprivate func passesHandler(_ req: Request) async throws -> Response { let passesData = try await PassData.query(on: req.db).with(\.$pass).all() let passes = passesData.map { $0.pass } + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + let bundle = try await passesService.generatePassesContent(for: passes, on: req.db) let body = Response.Body(data: bundle) var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpasses") headers.add(name: .contentDisposition, value: "attachment; filename=name.pkpasses") - headers.add(name: .lastModified, value: String(Date().timeIntervalSince1970)) + headers.add(name: .lastModified, value: dateFormatter.string(from: Date())) headers.add(name: .contentTransferEncoding, value: "binary") return Response(status: .ok, headers: headers, body: body) } diff --git a/Sources/Passes/PassesServiceCustom.swift b/Sources/Passes/PassesServiceCustom.swift index e342a48..fb12705 100644 --- a/Sources/Passes/PassesServiceCustom.swift +++ b/Sources/Passes/PassesServiceCustom.swift @@ -186,8 +186,12 @@ extension PassesServiceCustom { func latestVersionOfPass(req: Request) async throws -> Response { logger?.debug("Called latestVersionOfPass") - var ifModifiedSince: TimeInterval = 0 - if let header = req.headers[.ifModifiedSince].first, let ims = TimeInterval(header) { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" + var ifModifiedSince = Date.distantPast + if let header = req.headers[.ifModifiedSince].first, let ims = dateFormatter.date(from: header) { ifModifiedSince = ims } @@ -203,13 +207,13 @@ extension PassesServiceCustom { throw Abort(.notFound) } - guard ifModifiedSince < pass.updatedAt?.timeIntervalSince1970 ?? 0 else { + guard ifModifiedSince < pass.updatedAt ?? Date.distantPast else { throw Abort(.notModified) } var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/vnd.apple.pkpass") - headers.add(name: .lastModified, value: String(pass.updatedAt?.timeIntervalSince1970 ?? 0)) + headers.add(name: .lastModified, value: dateFormatter.string(from: pass.updatedAt ?? Date.distantPast)) headers.add(name: .contentTransferEncoding, value: "binary") return try await Response( status: .ok, @@ -450,7 +454,6 @@ extension PassesServiceCustom { extension PassesServiceCustom { private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws -> Data { var manifest: [String: String] = [:] - let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath()) try paths.forEach { relativePath in let file = URL(fileURLWithPath: relativePath, relativeTo: root) @@ -459,7 +462,6 @@ extension PassesServiceCustom { let hash = Insecure.SHA1.hash(data: data) manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() } - 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 6e5554a..3d02546 100644 --- a/Tests/OrdersTests/OrdersTests.swift +++ b/Tests/OrdersTests/OrdersTests.swift @@ -16,7 +16,12 @@ final class OrdersTests: XCTestCase { OrdersService.register(migrations: app.migrations) app.migrations.add(CreateOrderData()) - ordersService = try OrdersService(app: app, delegate: orderDelegate, pushRoutesMiddleware: SecretMiddleware(secret: "foo")) + ordersService = try OrdersService( + app: app, + delegate: orderDelegate, + pushRoutesMiddleware: SecretMiddleware(secret: "foo"), + logger: app.logger + ) app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite) try await app.autoMigrate() @@ -30,6 +35,25 @@ final class OrdersTests: XCTestCase { XCTAssertNotNil(data) } + // Tests the API Apple Wallet calls to get orders + func testGetOrderFromAPI() async throws { + 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 app.test( + .GET, + "\(ordersURI)orders/\(order.orderTypeIdentifier)/\(order.requireID())", + headers: ["Authorization": "AppleOrder \(order.authenticationToken)", "If-Modified-Since": "0"], + afterResponse: { res async throws in + XCTAssertEqual(res.status, .ok) + XCTAssertNotNil(res.body) + XCTAssertEqual(res.headers.contentType?.description, "application/vnd.apple.order") + XCTAssertNotNil(res.headers.lastModified) + } + ) + } + func testAPIDeviceRegistration() async throws { let orderData = OrderData(title: "Test Order") try await orderData.create(on: app.db) @@ -45,7 +69,6 @@ final class OrdersTests: XCTestCase { try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .created) } ) @@ -58,7 +81,6 @@ final class OrdersTests: XCTestCase { try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .ok) } ) @@ -93,7 +115,6 @@ final class OrdersTests: XCTestCase { "\(ordersURI)devices/\(deviceLibraryIdentifier)/registrations/\(order.orderTypeIdentifier)/\(order.requireID())", headers: ["Authorization": "AppleOrder \(order.authenticationToken)"], afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .ok) } ) @@ -126,5 +147,14 @@ final class OrdersTests: XCTestCase { try await orderData.create(on: app.db) let order = try await orderData.$order.get(on: app.db) try await ordersService.sendPushNotifications(for: order, on: app.db) + + try await app.test( + .POST, + "\(ordersURI)push/\(order.orderTypeIdentifier)/\(order.requireID())", + headers: ["X-Secret": "foo"], + afterResponse: { res async throws in + XCTAssertEqual(res.status, .noContent) + } + ) } } diff --git a/Tests/PassesTests/PassesTests.swift b/Tests/PassesTests/PassesTests.swift index bd489a5..cad1ec3 100644 --- a/Tests/PassesTests/PassesTests.swift +++ b/Tests/PassesTests/PassesTests.swift @@ -16,7 +16,12 @@ final class PassesTests: XCTestCase { PassesService.register(migrations: app.migrations) app.migrations.add(CreatePassData()) - passesService = try PassesService(app: app, delegate: passDelegate, pushRoutesMiddleware: SecretMiddleware(secret: "foo")) + passesService = try PassesService( + app: app, + delegate: passDelegate, + pushRoutesMiddleware: SecretMiddleware(secret: "foo"), + logger: app.logger + ) app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite) try await app.autoMigrate() @@ -57,6 +62,67 @@ final class PassesTests: XCTestCase { XCTAssertGreaterThan(dataPersonalize.count, data.count) } + // Tests the API Apple Wallet calls to get passes + func testGetPassFromAPI() async throws { + 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 app.test( + .GET, + "\(passesURI)passes/\(pass.passTypeIdentifier)/\(pass.requireID())", + headers: ["Authorization": "ApplePass \(pass.authenticationToken)", "If-Modified-Since": "0"], + afterResponse: { res async throws in + XCTAssertEqual(res.status, .ok) + XCTAssertNotNil(res.body) + XCTAssertEqual(res.headers.contentType?.description, "application/vnd.apple.pkpass") + XCTAssertNotNil(res.headers.lastModified) + } + ) + } + + func testPersonalizationAPI() async throws { + 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 + XCTAssertEqual(res.status, .ok) + XCTAssertNotNil(res.body) + XCTAssertEqual(res.headers.contentType?.description, "application/octet-stream") + } + ) + + let personalizationQuery = try await UserPersonalization.query(on: app.db).all() + XCTAssertEqual(personalizationQuery.count, 1) + XCTAssertEqual(personalizationQuery[0].emailAddress, personalizationDict.requiredPersonalizationInfo.emailAddress) + XCTAssertEqual(personalizationQuery[0].familyName, personalizationDict.requiredPersonalizationInfo.familyName) + XCTAssertEqual(personalizationQuery[0].fullName, personalizationDict.requiredPersonalizationInfo.fullName) + XCTAssertEqual(personalizationQuery[0].givenName, personalizationDict.requiredPersonalizationInfo.givenName) + XCTAssertEqual(personalizationQuery[0].ISOCountryCode, personalizationDict.requiredPersonalizationInfo.ISOCountryCode) + XCTAssertEqual(personalizationQuery[0].phoneNumber, personalizationDict.requiredPersonalizationInfo.phoneNumber) + XCTAssertEqual(personalizationQuery[0].postalCode, personalizationDict.requiredPersonalizationInfo.postalCode) + } + func testAPIDeviceRegistration() async throws { let passData = PassData(title: "Test Pass") try await passData.create(on: app.db) @@ -72,7 +138,6 @@ final class PassesTests: XCTestCase { try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .created) } ) @@ -85,7 +150,6 @@ final class PassesTests: XCTestCase { try req.content.encode(RegistrationDTO(pushToken: pushToken)) }, afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .ok) } ) @@ -118,7 +182,6 @@ final class PassesTests: XCTestCase { "\(passesURI)devices/\(deviceLibraryIdentifier)/registrations/\(pass.passTypeIdentifier)/\(pass.requireID())", headers: ["Authorization": "ApplePass \(pass.authenticationToken)"], afterResponse: { res async throws in - XCTAssertNotEqual(res.status, .unauthorized) XCTAssertEqual(res.status, .ok) } ) @@ -151,5 +214,14 @@ final class PassesTests: XCTestCase { try await passData.create(on: app.db) let pass = try await passData.$pass.get(on: app.db) try await passesService.sendPushNotifications(for: pass, on: app.db) + + try await app.test( + .POST, + "\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())", + headers: ["X-Secret": "foo"], + afterResponse: { res async throws in + XCTAssertEqual(res.status, .noContent) + } + ) } }