Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift Testing #14

Merged
merged 7 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
"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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/codecov/c/github/vapor-community/PassKit?style=plastic&logo=codecov&label=codecov">
</a>
<a href="https://swift.org">
<img src="https://design.vapor.codes/images/swift510up.svg" alt="Swift 5.10+">
<img src="https://design.vapor.codes/images/swift60up.svg" alt="Swift 6.0+">
</a>
</div>
<br>
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
182 changes: 79 additions & 103 deletions Tests/OrdersTests/EncryptedOrdersTests.swift
Original file line number Diff line number Diff line change
@@ -1,116 +1,92 @@
import Fluent
import FluentSQLiteDriver
import FluentKit
import PassKit
import Testing
import XCTVapor
import Zip

@testable import Orders

final class EncryptedOrdersTests: XCTestCase {
@Suite("Orders Tests with Encrypted PEM Key")
struct EncryptedOrdersTests {
let delegate = EncryptedOrdersDelegate()
let ordersURI = "/api/orders/v1/"
var ordersService: OrdersService!
var app: Application!

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

try await app.autoMigrate()

Zip.addCustomFileExtension("order")
}

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

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

XCTAssert(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]
XCTAssertEqual(passJSON["authenticationToken"] as? String, order.authenticationToken)
try XCTAssertEqual(passJSON["orderIdentifier"] as? String, order.requireID().uuidString)

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
XCTAssertEqual(manifestJSON["icon.png"] as? String, iconHash)
}

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

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

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

// Test `OrderDataMiddleware` update method
orderData.title = "Test Order 2"
do {
try await orderData.update(on: app.db)
} catch {}
@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 {}
}
}
}
Loading
Loading