Skip to content

Commit

Permalink
Update to Swift Testing for PassesTests
Browse files Browse the repository at this point in the history
  • Loading branch information
fpseverino committed Oct 11, 2024
1 parent 939492b commit 805a194
Show file tree
Hide file tree
Showing 11 changed files with 661 additions and 725 deletions.
11 changes: 2 additions & 9 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@ on:
push: { branches: [ main ] }

jobs:
lint:
runs-on: ubuntu-latest
container: swift:noble
steps:
- name: Check out PassKit
uses: actions/checkout@v4
- name: Run format lint check
run: swift format lint --strict --recursive --parallel .

unit-tests:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
with_linting: true
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lineBreakBeforeControlFlowKeywords": false,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeEachGenericRequirement": false,
"lineLength": 100,
"lineLength": 140,
"maximumBlankLines": 1,
"multiElementCollectionTrailingCommas": true,
"noAssignmentInExpressions": {
Expand Down
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ let package = Package(
.library(name: "Orders", targets: ["Orders"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.105.2"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.11.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.106.0"),
.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"),
// used in tests
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.7.4"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.8.0"),
],
targets: [
.target(
Expand Down
3 changes: 1 addition & 2 deletions Sources/Orders/OrdersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import Vapor

/// The main class that handles Wallet orders.
public final class OrdersService: Sendable {
private let service:
OrdersServiceCustom<Order, OrdersDevice, OrdersRegistration, OrdersErrorLog>
private let service: OrdersServiceCustom<Order, OrdersDevice, OrdersRegistration, OrdersErrorLog>

/// Initializes the service and registers all the routes required for Apple Wallet to work.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Vapor

struct SecretMiddleware: AsyncMiddleware {
package struct SecretMiddleware: AsyncMiddleware {
let secret: String

func respond(
to request: Request, chainingTo next: any AsyncResponder
) async throws -> Response {
package init(secret: String) {
self.secret = secret
}

package func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
guard request.headers.first(name: "X-Secret") == secret else {
throw Abort(.unauthorized, reason: "Incorrect X-Secret header.")
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/PassKit/Testing/isLoggingConfigured.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Vapor

package let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = .debug
return handler
}
return true
}()
3 changes: 1 addition & 2 deletions Sources/Passes/PassesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ public final class PassesService: Sendable {
/// - passes: The passes to include in the bundle.
/// - db: The `Database` to use.
/// - Returns: The bundle of passes as `Data`.
public func generatePassesContent(for passes: [Pass], on db: any Database) async throws -> Data
{
public func generatePassesContent(for passes: [Pass], on db: any Database) async throws -> Data {
try await service.generatePassesContent(for: passes, on: db)
}

Expand Down
278 changes: 118 additions & 160 deletions Tests/PassesTests/EncryptedPassesTests.swift
Original file line number Diff line number Diff line change
@@ -1,177 +1,135 @@
import Fluent
import FluentSQLiteDriver
import PassKit
import Testing
import XCTVapor
import Zip

@testable import Passes

final class EncryptedPassesTests: XCTestCase {
struct EncryptedPassesTests {
let delegate = EncryptedPassesDelegate()
let passesURI = "/api/passes/v1/"
var passesService: PassesService!
var app: Application!

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())
passesService = try PassesService(
app: app,
delegate: delegate,
pushRoutesMiddleware: SecretMiddleware(secret: "foo"),
logger: app.logger
)
app.databases.middleware.use(PassDataMiddleware(service: passesService), on: .sqlite)

try await app.autoMigrate()

Zip.addCustomFileExtension("pkpass")
}

override func tearDown() async throws {
try await app.autoRevert()
try await self.app.asyncShutdown()
self.app = nil
@Test func passGeneration() async throws {
try await withApp(delegate: delegate) { 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)
}
}

func testPassGeneration() 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)
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)

XCTAssert(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]
XCTAssertEqual(passJSON["authenticationToken"] as? String, pass.authenticationToken)
try XCTAssertEqual(passJSON["serialNumber"] as? String, pass.requireID().uuidString)
XCTAssertEqual(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
XCTAssertEqual(manifestJSON["icon.png"] as? String, iconHash)
}
@Test func personalizationAPI() async throws {
try await withApp(delegate: delegate) { 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: "[email protected]",
familyName: "Doe",
fullName: "John Doe",
givenName: "John",
isoCountryCode: "US",
phoneNumber: "1234567890",
postalCode: "12345"
)
)

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: "[email protected]",
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")
}
)
)

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)
let passPersonalizationID = try await Pass.query(on: app.db).first()?
._$userPersonalization.get(on: app.db)?
.requireID()
XCTAssertEqual(personalizationQuery[0]._$id.value, passPersonalizationID)
XCTAssertEqual(
personalizationQuery[0]._$emailAddress.value,
personalizationDict.requiredPersonalizationInfo.emailAddress)
XCTAssertEqual(
personalizationQuery[0]._$familyName.value,
personalizationDict.requiredPersonalizationInfo.familyName)
XCTAssertEqual(
personalizationQuery[0]._$fullName.value,
personalizationDict.requiredPersonalizationInfo.fullName)
XCTAssertEqual(
personalizationQuery[0]._$givenName.value,
personalizationDict.requiredPersonalizationInfo.givenName)
XCTAssertEqual(
personalizationQuery[0]._$isoCountryCode.value,
personalizationDict.requiredPersonalizationInfo.isoCountryCode)
XCTAssertEqual(
personalizationQuery[0]._$phoneNumber.value,
personalizationDict.requiredPersonalizationInfo.phoneNumber)
XCTAssertEqual(
personalizationQuery[0]._$postalCode.value,
personalizationDict.requiredPersonalizationInfo.postalCode)

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)
}
}

func testAPNSClient() async throws {
XCTAssertNotNil(app.apns.client(.init(string: "passes")))

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
XCTAssertEqual(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
XCTAssertEqual(res.status, .created)
}
)

try await app.test(
.POST,
"\(passesURI)push/\(pass.passTypeIdentifier)/\(pass.requireID())",
headers: ["X-Secret": "foo"],
afterResponse: { res async throws in
XCTAssertEqual(res.status, .internalServerError)
}
)

// Test `PassDataMiddleware` update method
passData.title = "Test Pass 2"
do {
try await passData.update(on: app.db)
} catch {}
@Test func apnsClient() async throws {
try await withApp(delegate: delegate) { 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 {}
}
}
}
Loading

0 comments on commit 805a194

Please sign in to comment.