diff --git a/Package.swift b/Package.swift index 42e7337..7ab52e8 100644 --- a/Package.swift +++ b/Package.swift @@ -50,12 +50,25 @@ let package = Package( ], swiftSettings: swiftSettings ), + .testTarget( + name: "AppTests", + dependencies: [ + .target(name: "App"), + .target(name: "HtmlSnapshotTesting"), + .product(name: "XCTVapor", package: "vapor") + ], + resources: [ + .copy("__Snapshots__") + ], + swiftSettings: swiftSettings + ), .testTarget( name: "ViewRouteTests", dependencies: [ .target(name: "App"), .product(name: "VaporTesting", package: "vapor") ], + swiftSettings: swiftSettings ), .testTarget( diff --git a/Sources/App/Extensions/Request+extensions.swift b/Sources/App/Extensions/Request+extensions.swift index f6d2907..ad805d7 100644 --- a/Sources/App/Extensions/Request+extensions.swift +++ b/Sources/App/Extensions/Request+extensions.swift @@ -1,3 +1,4 @@ +import Dependencies import Elementary import Vapor import VaporElementary diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift index 3d5d8e3..0837451 100644 --- a/Sources/App/Middleware/DependenciesMiddleware.swift +++ b/Sources/App/Middleware/DependenciesMiddleware.swift @@ -8,15 +8,19 @@ import Vapor struct DependenciesMiddleware: AsyncMiddleware { private let values: DependencyValues.Continuation + private let database: DatabaseClient - init() { + init( + database: DatabaseClient + ) { self.values = withEscapedDependencies { $0 } + self.database = database } func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { try await values.yield { try await withDependencies { - $0.database = .live(database: request.db) + $0.database = database $0.dateFormatter = .liveValue } operation: { try await next.respond(to: request) diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index c416d06..601694e 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -8,7 +8,10 @@ import Vapor @preconcurrency import VaporRouting // configures your application -public func configure(_ app: Application) async throws { +public func configure( + _ app: Application, + makeDatabaseClient: @escaping (any Database) -> DatabaseClient = { .live(database: $0) } +) async throws { // cors middleware should come before default error middleware using `at: .beginning` let corsConfiguration = CORSMiddleware.Configuration( allowedOrigin: .all, @@ -21,7 +24,6 @@ public func configure(_ app: Application) async throws { app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(app.sessions.middleware) - app.middleware.use(DependenciesMiddleware()) #if DEBUG app.lifecycle.use(BrowserSyncHandler()) @@ -35,8 +37,13 @@ public func configure(_ app: Application) async throws { app.databases.use(DatabaseConfigurationFactory.sqlite(.memory), as: .sqlite) } - let databaseClient = DatabaseClient.live(database: app.db) - try await app.migrations.add(databaseClient.migrations()) + let databaseClient = makeDatabaseClient(app.db) + + if app.environment != .testing { + try await app.migrations.add(databaseClient.migrations()) + } + + app.middleware.use(DependenciesMiddleware(database: databaseClient)) app.mount( SiteRoute.router, @@ -50,7 +57,9 @@ public func configure(_ app: Application) async throws { use: siteHandler ) - try await app.autoMigrate() + if app.environment != .testing { + try await app.autoMigrate() + } #if DEBUG app.asyncCommands.use(SeedCommand(), as: "seed") diff --git a/Sources/HtmlSnapshotTesting/HtmlSnapshotTesting.swift b/Sources/HtmlSnapshotTesting/HtmlSnapshotTesting.swift index 48d034f..4c82945 100644 --- a/Sources/HtmlSnapshotTesting/HtmlSnapshotTesting.swift +++ b/Sources/HtmlSnapshotTesting/HtmlSnapshotTesting.swift @@ -10,3 +10,13 @@ public extension Snapshotting where Value == (any HTML), Format == String { return snapshotting } } + +public extension Snapshotting where Value == String, Format == String { + static var html: Snapshotting { + var snapshotting = SimplySnapshotting.lines + .pullback { $0 } + + snapshotting.pathExtension = "html" + return snapshotting + } +} diff --git a/Sources/SharedModels/Employee.swift b/Sources/SharedModels/Employee.swift index 0550026..b0e9159 100644 --- a/Sources/SharedModels/Employee.swift +++ b/Sources/SharedModels/Employee.swift @@ -65,29 +65,6 @@ public extension Employee { } #if DEBUG - // - // public extension Employee { - // - // static func generateMocks(count: Int = 10) -> [Self] { - // @Dependency(\.date.now) var now - // @Dependency(\.uuid) var uuid - // - // var output = [Self]() - // - // for _ in 0 ... count { - // output.append(.init( - // id: uuid(), - // active: Bool.random(), - // createdAt: now, - // firstName: RandomNames.firstNames.randomElement()!, - // lastName: RandomNames.lastNames.randomElement()!, - // updatedAt: now - // )) - // } - // - // return output - // } - // } public extension Employee.Create { @@ -103,13 +80,3 @@ public extension Employee { } #endif - -// public extension Employee { -// static var mocks: [Self] { -// [ -// .init(firstName: "Michael", lastName: "Housh"), -// .init(firstName: "Blob", lastName: "Esquire"), -// .init(firstName: "Testy", lastName: "McTestface") -// ] -// } -// } diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index 94f98c6..dce4341 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -43,29 +43,6 @@ public extension Vendor { } #if DEBUG - // - // public extension Vendor { - // - // static func generateMocks(count: Int = 20) -> [Self] { - // @Dependency(\.date.now) var now - // @Dependency(\.uuid) var uuid - // - // var output = [Self]() - // - // for _ in 0 ... count { - // output.append(.init( - // id: uuid(), - // name: RandomNames.companyNames.randomElement()!, - // branches: nil, - // createdAt: now, - // updatedAt: now - // )) - // } - // - // return output - // } - // } - public extension Vendor.Create { static func generateMocks(count: Int = 5) -> [Self] { (0 ... count).reduce(into: [Self]()) { array, _ in diff --git a/Tests/AppTests/ViewSnapshotTests.swift b/Tests/AppTests/ViewSnapshotTests.swift new file mode 100644 index 0000000..bddcab4 --- /dev/null +++ b/Tests/AppTests/ViewSnapshotTests.swift @@ -0,0 +1,188 @@ +@testable import App +import DatabaseClient +import Dependencies +import HtmlSnapshotTesting +import SharedModels +import SnapshotTesting +import Vapor +import XCTVapor + +final class ViewSnapshotTests: XCTestCase { + + var app: Application! + let router = ViewRoute.router + + override func setUp() { + app = Application(.testing) + } + + override func invokeTest() { + withSnapshotTesting(record: .missing) { + super.invokeTest() + } + } + + override func tearDown() { + app.shutdown() + } + + func testEmployeeViews() async throws { + try await withDependencies { + $0.database.employees = .mock + } operation: { + @Dependency(\.database) var database + + try await configure(app, makeDatabaseClient: { _ in database }) + + try await app.test(.GET, router.path(for: .employee(.index))) { res in + assertSnapshot(of: res.body.string, as: .html) + } + + try await app.test(.GET, router.path(for: .employee(.form))) { res in + assertSnapshot(of: res.body.string, as: .html) + } + + for context in SharedModels.ViewRoute.SelectContext.allCases { + try app.test(.GET, router.path(for: .employee(.select(context: context)))) { res in + assertSnapshot(of: res.body.string, as: .html) + } + } + + try app.test(.GET, router.path(for: .employee(.get(id: UUID(0))))) { res in + assertSnapshot(of: res.body.string, as: .html) + } + + try app.test(.POST, router.path(for: .employee(.index)), beforeRequest: { req in + req.body = ByteBuffer(string: "firstName=Testy&lastName=McTestface") + }, afterResponse: { res in + assertSnapshot(of: res.body.string, as: .html) + }) + + try app.test(.PUT, router.path(for: .employee(.update(id: UUID(0), updates: .mock))), beforeRequest: { req in + req.body = ByteBuffer(string: "firstName=Testy&lastName=McTestface") + }, afterResponse: { res in + assertSnapshot(of: res.body.string, as: .html) + }) + } + } + + // TODO: These need to come after mocks are generated. + // func testPurchaseOrderIndex() async throws { + // try await configure(app) + // try await app.test(.GET, router.path(for: .purchaseOrder(.index))) { res in + // assertSnapshot(of: res.body.string, as: .html) + // } + // } +} + +extension DatabaseClient.Employees { + static var mock: Self { + .init( + create: { _ in .mock }, + delete: { _ in }, + fetchAll: { _ in [Employee.mock] }, + get: { _ in Employee.mock }, + update: { _, _ in Employee.mock } + ) + } +} + +extension Date { + static var mock: Self { + Date(timeIntervalSince1970: 1_234_567_890) + } +} + +extension Employee { + static var mock: Self { + Employee( + id: UUID(0), + createdAt: Date(timeIntervalSince1970: 1_234_567_890), + firstName: "Testy", + lastName: "McTestface", + updatedAt: Date(timeIntervalSince1970: 1_234_567_890) + ) + } +} + +extension Employee.Create { + static var mock: Self { + .init(firstName: "Testy", lastName: "McTestface") + } + + func employeeMock() -> Employee { + @Dependency(\.date.now) var now + return .init( + id: UUID(0), + createdAt: Date(timeIntervalSince1970: 1_234_567_890), + firstName: firstName, + lastName: lastName, + updatedAt: Date(timeIntervalSince1970: 1_234_567_890) + ) + } +} + +extension Employee.Update { + static var mock: Self { + .init(firstName: "Testy", lastName: "McTestface", active: false) + } +} + +extension User { + static var mock: Self { + .init(id: UUID(0), email: "test@example.com", username: "test") + } +} + +extension User.Create { + static var mock: Self { + .init(username: "test", email: "test@example.com", password: "super-secret", confirmPassword: "super-secret") + } +} + +extension Vendor { + static var mock: Self { + .init(id: UUID(0), name: "Test", branches: nil, createdAt: .mock, updatedAt: .mock) + } +} + +extension Vendor.Create { + static var mock: Self { + .init(name: "Test") + } +} + +extension VendorBranch { + static var mock: Self { + .init(id: UUID(1), name: "Mock", vendorID: UUID(0), createdAt: .mock, updatedAt: .mock) + } +} + +extension VendorBranch.Create { + static var mock: Self { + .init(name: "Mock", vendorID: UUID(0)) + } +} + +extension VendorBranch.Detail { + static var mock: Self { + .init(id: UUID(1), name: "Mock", vendor: .mock, createdAt: .mock, updatedAt: .mock) + } +} + +extension PurchaseOrder { + static var mock: Self { + .init( + id: 1, + workOrder: 12245, + materials: "foo", + customer: "Testy McTestface", + truckStock: true, + createdBy: .mock, + createdFor: .mock, + vendorBranch: .mock, + createdAt: .mock, + updatedAt: .mock + ) + } +} diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeCreate.1.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeCreate.1.html new file mode 100644 index 0000000..f7f933d --- /dev/null +++ b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeCreate.1.html @@ -0,0 +1 @@ +
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
Employees are who purchase orders can be issued to.
| Name | |
|---|---|
| Testy Mctestface |
| PO | Work Order | Customer | Vendor | Materials | Created For |
|---|