diff --git a/Package.swift b/Package.swift index 2271b6b..aa277e4 100644 --- a/Package.swift +++ b/Package.swift @@ -54,18 +54,18 @@ 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: "AppTests", + // dependencies: [ + // .target(name: "App"), + // .target(name: "HtmlSnapshotTesting"), + // .product(name: "XCTVapor", package: "vapor") + // ], + // resources: [ + // .copy("__Snapshots__") + // ], + // swiftSettings: swiftSettings + // ), .testTarget( name: "ViewRouteTests", dependencies: [ @@ -115,13 +115,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing") ] ), - .testTarget( - name: "HtmlSnapshotTestingTests", - dependencies: [ - .target(name: "App"), - .target(name: "HtmlSnapshotTesting") - ] - ), .target( name: "SharedModels", dependencies: [ diff --git a/Sources/App/Controllers/ApiController.swift b/Sources/App/Controllers/ApiController.swift index 5510755..6af4a3e 100644 --- a/Sources/App/Controllers/ApiController.swift +++ b/Sources/App/Controllers/ApiController.swift @@ -7,13 +7,14 @@ import Vapor private let apiMiddleware: [any Middleware] = [ UserPasswordAuthenticator(), UserTokenAuthenticator(), + UserSessionAuthenticator(), User.guardMiddleware() ] extension ApiRoute { var middleware: [any Middleware]? { apiMiddleware } - func handle(request: Request) async throws -> any AsyncResponseEncodable { + func respond(request: Request) async throws -> any AsyncResponseEncodable { switch self { case let .employee(route): return try await route.handleApiRequest(request: request) diff --git a/Sources/App/Extensions/ViewController+respond.swift b/Sources/App/Extensions/ViewController+respond.swift new file mode 100644 index 0000000..0da92e3 --- /dev/null +++ b/Sources/App/Extensions/ViewController+respond.swift @@ -0,0 +1,53 @@ +import Elementary +import SharedModels +import Vapor +import VaporElementary +import ViewController + +extension ViewController { + func respond(route: ViewRoute, request: Vapor.Request) async throws -> any AsyncResponseEncodable { + let html = try await view( + for: route, + isHtmxRequest: request.isHtmxRequest, + logger: request.logger, + authenticate: { request.session.authenticate($0) } + ) + return AnyHTMLResponse(value: html) + } +} + +// Re-adapted from `HTMLResponse` in the VaporElementary package to work with any html types +// returned from the view controller. +struct AnyHTMLResponse: AsyncResponseEncodable { + + public var chunkSize: Int + public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"] + var value: _SendableAnyHTMLBox + + init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: AnySendableHTML) { + self.chunkSize = chunkSize + if additionalHeaders.contains(name: .contentType) { + self.headers = additionalHeaders + } else { + headers.add(contentsOf: additionalHeaders) + } + self.value = .init(value) + } + + func encodeResponse(for request: Request) async throws -> Response { + Response( + status: .ok, + headers: headers, + body: .init(asyncStream: { [value, chunkSize] writer in + guard let html = value.tryTake() else { + assertionFailure("Non-sendable HTML value consumed more than once") + request.logger.error("Non-sendable HTML value consumed more than once") + throw Abort(.internalServerError) + } + try await writer.writeHTML(html, chunkSize: chunkSize) + try await writer.write(.end) + + }) + ) + } +} diff --git a/Sources/App/Middleware/DependenciesMiddleware.swift b/Sources/App/Middleware/DependenciesMiddleware.swift index 0837451..46a66e0 100644 --- a/Sources/App/Middleware/DependenciesMiddleware.swift +++ b/Sources/App/Middleware/DependenciesMiddleware.swift @@ -1,20 +1,23 @@ import DatabaseClientLive import Dependencies import Vapor +import ViewControllerLive // Taken from discussions page on `swift-dependencies`. -// TODO: Pass dependencies to set into this middleware. struct DependenciesMiddleware: AsyncMiddleware { private let values: DependencyValues.Continuation private let database: DatabaseClient + private let viewController: ViewController init( - database: DatabaseClient + database: DatabaseClient, + viewController: ViewController = .liveValue ) { self.values = withEscapedDependencies { $0 } self.database = database + self.viewController = viewController } func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { @@ -22,6 +25,7 @@ struct DependenciesMiddleware: AsyncMiddleware { try await withDependencies { $0.database = database $0.dateFormatter = .liveValue + $0.viewController = viewController } operation: { try await next.respond(to: request) } diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Middleware/ViewRoute+middleware.swift similarity index 96% rename from Sources/App/Controllers/ViewController.swift rename to Sources/App/Middleware/ViewRoute+middleware.swift index 02ed93d..968ffe1 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Middleware/ViewRoute+middleware.swift @@ -16,7 +16,7 @@ extension SharedModels.ViewRoute { var middleware: [any Middleware]? { switch self { - case .index: return viewProtectedMiddleware + // case .index: return viewProtectedMiddleware case let .employee(route): return route.middleware case .login: return nil case let .purchaseOrder(route): return route.middleware diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 0fb71fa..57d12cb 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -48,6 +48,11 @@ public func configure( app.middleware.use(DependenciesMiddleware(database: databaseClient)) + // Redirect the index path to purchase order route. + app.get { req in + req.redirect(to: ViewRoute.router.path(for: .purchaseOrder(.index))) + } + app.mount( SiteRoute.router, middleware: { @@ -79,7 +84,6 @@ extension SiteRoute { case .health: return nil case let .view(route): - // return nil return route.middleware } } @@ -90,62 +94,13 @@ func siteHandler( request: Request, route: SiteRoute ) async throws -> any AsyncResponseEncodable { + @Dependency(\.viewController) var viewController switch route { case let .api(route): - return try await route.handle(request: request) + return try await route.respond(request: request) case .health: return HTTPStatus.ok case let .view(route): - return try await route.respond(request: request) - // return try await route.handle(request: request) - } -} - -extension ViewRoute { - func respond(request: Request) async throws -> any AsyncResponseEncodable { - if self == .index { - return request.redirect(to: ViewRoute.router.path(for: .purchaseOrder(.index))) - } else { - let html = try await view(isHtmxRequest: request.isHtmxRequest, authenticate: { request.auth.login($0) }) - // Delete routes return nil, but are valid routes. - guard let html else { - return HTTPStatus.ok - } - return AnyHTMLResponse(value: html) - } - } -} - -struct AnyHTMLResponse: AsyncResponseEncodable { - - public var chunkSize: Int - public var headers: HTTPHeaders = ["Content-Type": "text/html; charset=utf-8"] - var value: _SendableAnyHTMLBox - - init(chunkSize: Int = 1024, additionalHeaders: HTTPHeaders = [:], value: any HTML & Sendable) { - self.chunkSize = chunkSize - if additionalHeaders.contains(name: .contentType) { - self.headers = additionalHeaders - } else { - headers.add(contentsOf: additionalHeaders) - } - self.value = .init(value) - } - - func encodeResponse(for request: Request) async throws -> Response { - Response( - status: .ok, - headers: headers, - body: .init(asyncStream: { [value, chunkSize] writer in - guard let html = value.tryTake() else { - assertionFailure("Non-sendable HTML value consumed more than once") - request.logger.error("Non-sendable HTML value consumed more than once") - throw Abort(.internalServerError) - } - try await writer.writeHTML(html, chunkSize: chunkSize) - try await writer.write(.end) - - }) - ) + return try await viewController.respond(route: route, request: request) } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift deleted file mode 100644 index fd888ff..0000000 --- a/Sources/App/routes.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Dependencies -import Elementary -import SharedModels -@_exported import ViewController diff --git a/Sources/SharedModels/Routes/ViewRoute.swift b/Sources/SharedModels/Routes/ViewRoute.swift index cd415cc..9b08add 100644 --- a/Sources/SharedModels/Routes/ViewRoute.swift +++ b/Sources/SharedModels/Routes/ViewRoute.swift @@ -2,10 +2,9 @@ import CasePathsCore import Foundation @preconcurrency import URLRouting -// TODO: Remove `delete` routes from views and use api routes. public enum ViewRoute: Sendable, Equatable { - case index + // case index case employee(EmployeeRoute) case login(LoginRoute) case purchaseOrder(PurchaseOrderRoute) @@ -14,9 +13,9 @@ public enum ViewRoute: Sendable, Equatable { case vendorBranch(VendorBranchRoute) public static let router = OneOf { - Route(.case(Self.index)) { - Method.get - } + // Route(.case(Self.index)) { + // Method.get + // } Route(.case(Self.employee)) { EmployeeRoute.router } Route(.case(Self.login)) { LoginRoute.router } Route(.case(Self.purchaseOrder)) { PurchaseOrderRoute.router } @@ -31,7 +30,6 @@ public extension ViewRoute { enum EmployeeRoute: Sendable, Equatable { case create(Employee.Create) - case delete(id: Employee.ID) case form case get(id: Employee.ID) case index @@ -53,10 +51,6 @@ public extension ViewRoute { .map(.memberwise(Employee.Create.init)) } } - Route(.case(Self.delete(id:))) { - Path { rootPath; Employee.ID.parser() } - Method.delete - } Route(.case(Self.get(id:))) { Path { rootPath; Employee.ID.parser() } Method.get @@ -147,7 +141,6 @@ public extension ViewRoute { public extension ViewRoute { enum PurchaseOrderRoute: Sendable, Equatable { case create(PurchaseOrder.Create) - case delete(id: PurchaseOrder.ID) case form case get(id: PurchaseOrder.ID) case index @@ -174,11 +167,6 @@ public extension ViewRoute { .map(.memberwise(PurchaseOrder.Create.init)) } } - Route(.case(Self.delete(id:))) { - Path { rootPath; Digits() } - Method.delete - } - Route(.case(Self.form)) { Path { rootPath; "create" } Method.get @@ -275,7 +263,6 @@ public extension ViewRoute { public extension ViewRoute { enum UserRoute: Sendable, Equatable { case create(User.Create) - case delete(id: User.ID) case form case get(id: User.ID) case index @@ -297,10 +284,6 @@ public extension ViewRoute { .map(.memberwise(User.Create.init)) } } - Route(.case(Self.delete(id:))) { - Path { rootPath; User.ID.parser() } - Method.delete - } Route(.case(Self.form)) { Path { rootPath; "create" } Method.get @@ -335,7 +318,6 @@ public extension ViewRoute { enum VendorRoute: Sendable, Equatable { case create(Vendor.Create) - case delete(id: Vendor.ID) case form case get(id: Vendor.ID) case index @@ -354,10 +336,6 @@ public extension ViewRoute { .map(.memberwise(Vendor.Create.init)) } } - Route(.case(Self.delete(id:))) { - Path { rootPath; Vendor.ID.parser() } - Method.delete - } Route(.case(Self.get(id:))) { Path { rootPath; Vendor.ID.parser() } Method.get @@ -389,7 +367,6 @@ public extension ViewRoute { enum VendorBranchRoute: Sendable, Equatable { case create(VendorBranch.Create) - case delete(id: VendorBranch.ID) case index(for: Vendor.ID? = nil) case select(context: ViewRoute.SelectContext) @@ -405,10 +382,6 @@ public extension ViewRoute { .map(.memberwise(VendorBranch.Create.init)) } } - Route(.case(Self.delete(id:))) { - Path { "vendors"; "branches"; VendorBranch.ID.parser() } - Method.delete - } Route(.case(Self.index(for:))) { Path { "vendors"; "branches" } Method.get diff --git a/Sources/ViewController/ViewController.swift b/Sources/ViewController/ViewController.swift index df3d244..9a28512 100644 --- a/Sources/ViewController/ViewController.swift +++ b/Sources/ViewController/ViewController.swift @@ -1,6 +1,7 @@ import Dependencies import DependenciesMacros import Elementary +import Logging import SharedModels public extension DependencyValues { @@ -10,19 +11,41 @@ public extension DependencyValues { } } +public typealias AnySendableHTML = (any HTML & Sendable) + @DependencyClient public struct ViewController: Sendable { public typealias AuthenticateHandler = @Sendable (User) -> Void - public var view: @Sendable (ViewRoute, Bool, @escaping AuthenticateHandler) async throws -> (any HTML)? + public var view: @Sendable (Request) async throws -> AnySendableHTML @Sendable public func view( for route: ViewRoute, isHtmxRequest: Bool, + logger: Logger, authenticate: @escaping AuthenticateHandler - ) async throws -> (any HTML)? { - try await view(route, isHtmxRequest, authenticate) + ) async throws -> AnySendableHTML { + try await view(.init(route, isHtmxRequest: isHtmxRequest, authenticate: authenticate, logger: logger)) + } + + public struct Request: Sendable { + public let route: ViewRoute + public let isHtmxRequest: Bool + public let authenticate: AuthenticateHandler + public let logger: Logger + + public init( + _ route: ViewRoute, + isHtmxRequest: Bool, + authenticate: @escaping AuthenticateHandler, + logger: Logger + ) { + self.route = route + self.isHtmxRequest = isHtmxRequest + self.authenticate = authenticate + self.logger = logger + } } } diff --git a/Sources/ViewControllerLive/Live.swift b/Sources/ViewControllerLive/Live.swift index b8448b7..71fa230 100644 --- a/Sources/ViewControllerLive/Live.swift +++ b/Sources/ViewControllerLive/Live.swift @@ -5,8 +5,12 @@ import SharedModels extension ViewController: DependencyKey { public static var liveValue: ViewController { - .init(view: { route, isHtmxRequest, authenticate in - try await route.view(isHtmxRequest: isHtmxRequest, authenticate: authenticate) + .init(view: { request in + try await request.route.view( + isHtmxRequest: request.isHtmxRequest, + logger: request.logger, + authenticate: request.authenticate + ) }) } } diff --git a/Sources/ViewControllerLive/Routes+view.swift b/Sources/ViewControllerLive/Routes+view.swift index 7854562..080d46a 100644 --- a/Sources/ViewControllerLive/Routes+view.swift +++ b/Sources/ViewControllerLive/Routes+view.swift @@ -1,22 +1,23 @@ import DatabaseClient import Dependencies import Elementary +import Logging import SharedModels import Vapor - -public typealias AnySendableHTML = (any HTML & Sendable) +import ViewController public extension SharedModels.ViewRoute { func view( isHtmxRequest: Bool, + logger: Logger, authenticate: @escaping @Sendable (User) -> Void - ) async throws -> AnySendableHTML? { + ) async throws -> AnySendableHTML { @Dependency(\.database.users) var users switch self { - case .index: - // return request.redirect(to: Self.router.path(for: .purchaseOrder(.index))) - return nil + // case .index: + // // This get's redirected to purchase-orders route in the app / site handler. + // return nil case let .employee(route): return try await route.view(isHtmxRequest: isHtmxRequest) @@ -31,7 +32,7 @@ public extension SharedModels.ViewRoute { let token = try await users.login(.init(username: login.username, password: login.password)) let user = try await users.get(token.userID)! authenticate(user) - // request.logger.info("Logged in next: \(login.next ?? "N/A")") + logger.info("Logged in next: \(login.next ?? "N/A")") return MainPage.loggedIn(next: login.next) } @@ -65,7 +66,7 @@ extension SharedModels.ViewRoute.EmployeeRoute { } } - func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? { + func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.employees) var employees switch self { @@ -83,17 +84,10 @@ extension SharedModels.ViewRoute.EmployeeRoute { throw Abort(.badRequest, reason: "Employee id not found.") } return try await render(mainPage, isHtmxRequest, EmployeeForm(employee: employee)) - // return try await request.render(mainPage: mainPage) { - // EmployeeForm(employee: employee) - // } case let .create(employee): return try await EmployeeTable.Row(employee: employees.create(employee)) - case let .delete(id: id): - try await employees.delete(id) - return nil - case let .update(id: id, updates: updates): return try await EmployeeTable.Row(employee: employees.update(id, updates)) } @@ -115,7 +109,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute { } } - func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? { + func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.purchaseOrders) var purchaseOrders switch self { @@ -130,10 +124,6 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute { case let .create(purchaseOrder): return try await PurchaseOrderTable.Row(purchaseOrder: purchaseOrders.create(purchaseOrder)) - case let .delete(id: id): - try await purchaseOrders.delete(id) - return nil - case .index: return try await mainPage(PurchaseOrderForm()) @@ -198,7 +188,7 @@ extension SharedModels.ViewRoute.UserRoute { } } - func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? { + func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database.users) var users switch self { @@ -208,10 +198,6 @@ extension SharedModels.ViewRoute.UserRoute { case let .create(user): return try await UserTable.Row(user: users.create(user)) - case let .delete(id: id): - try await users.delete(id) - return nil - case .index: return try await mainPage(UserDetail(user: nil)) @@ -240,16 +226,14 @@ extension SharedModels.ViewRoute.VendorRoute { } } - func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? { + func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database) var database switch self { case .form: - // html = VendorForm(.float(shouldShow: true)) return try await render(mainPage, isHtmxRequest, VendorForm(.float(shouldShow: true))) case .index: - // return VendorForm() return try await mainPage(VendorForm()) case let .create(vendor): @@ -262,10 +246,6 @@ extension SharedModels.ViewRoute.VendorRoute { } } - case let .delete(id: id): - try await database.vendors.delete(id) - return nil - case let .get(id: id): guard let vendor = try await database.vendors.get(id, .withBranches) else { throw Abort(.badRequest, reason: "Vendor not found.") @@ -282,7 +262,7 @@ extension SharedModels.ViewRoute.VendorRoute { extension SharedModels.ViewRoute.VendorBranchRoute { - func view(isHtmxRequest: Bool) async throws -> AnySendableHTML? { + func view(isHtmxRequest: Bool) async throws -> AnySendableHTML { @Dependency(\.database) var database switch self { @@ -300,10 +280,6 @@ extension SharedModels.ViewRoute.VendorBranchRoute { case let .create(branch): return try await VendorBranchList.Row(branch: database.vendorBranches.create(branch)) - - case let .delete(id: id): - try await database.vendorBranches.delete(id) - return nil } } } diff --git a/Sources/ViewControllerLive/Views/HTMXExtensions.swift b/Sources/ViewControllerLive/Views/HTMXExtensions.swift index 7d045d6..0069294 100644 --- a/Sources/ViewControllerLive/Views/HTMXExtensions.swift +++ b/Sources/ViewControllerLive/Views/HTMXExtensions.swift @@ -15,8 +15,8 @@ extension HTMLAttribute.hx { put(SharedModels.ViewRoute.router.path(for: route)) } - static func delete(route: SharedModels.ViewRoute) -> HTMLAttribute { - delete(SharedModels.ViewRoute.router.path(for: route)) + static func delete(route: SharedModels.ApiRoute) -> HTMLAttribute { + delete(SharedModels.ApiRoute.router.path(for: route)) } } diff --git a/Tests/AppTests/ViewSnapshotTests.swift b/Tests/AppTests/ViewSnapshotTests.swift deleted file mode 100644 index bdfcd42..0000000 --- a/Tests/AppTests/ViewSnapshotTests.swift +++ /dev/null @@ -1,341 +0,0 @@ -@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) - // } - // } - - func testUserViews() async throws { - try await withDependencies { - $0.database.users = .mock - } operation: { - @Dependency(\.database) var database - - try await configure(app, makeDatabaseClient: { _ in database }) - - try app.test(.GET, router.path(for: .user(.form))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.POST, router.path(for: .user(.index))) { req in - req.body = ByteBuffer(string: "username=test&email=test@test.com&password=super-secret&confirmPassword=super-secret") - } afterResponse: { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.GET, router.path(for: .user(.index))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.GET, router.path(for: .user(.get(id: UUID(0))))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.PATCH, router.path(for: .user(.update(id: UUID(0), updates: .mock)))) { req in - req.body = .init(string: "username=test&email=test@test.com") - } afterResponse: { res in - assertSnapshot(of: res.body.string, as: .html) - } - } - } - - func testVendorViews() async throws { - try await withDependencies { - $0.database.vendors = .mock - } operation: { - @Dependency(\.database) var database - - try await configure(app, makeDatabaseClient: { _ in database }) - - try app.test(.GET, router.path(for: .vendor(.form))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.POST, router.path(for: .vendor(.index))) { req in - req.body = ByteBuffer(string: "name=Test") - } afterResponse: { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.GET, router.path(for: .vendor(.index))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.GET, router.path(for: .vendor(.get(id: UUID(0))))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - try app.test(.PUT, router.path(for: .vendor(.update(id: UUID(0), updates: .mock)))) { req in - req.body = .init(string: "name=Test") - } afterResponse: { res in - assertSnapshot(of: res.body.string, as: .html) - } - } - } - - func testVendorBranchViews() async throws { - try await withDependencies { - $0.database.vendorBranches = .mock - } operation: { - @Dependency(\.database) var database - - try await configure(app, makeDatabaseClient: { _ in database }) - - try app.test(.GET, router.path(for: .vendorBranch(.index(for: UUID(0))))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - - for context in SharedModels.ViewRoute.SelectContext.allCases { - try app.test(.GET, router.path(for: .vendorBranch(.select(context: context)))) { res in - assertSnapshot(of: res.body.string, as: .html) - } - } - - try app.test(.POST, router.path(for: .vendorBranch(.create(.mock)))) { req in - req.body = .init(string: "name=Test&vendorID=\(UUID(0))") - } afterResponse: { 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 DatabaseClient.Users { - static var mock: Self { - .init( - count: { 1 }, - create: { _ in User.mock }, - delete: { _ in }, - fetchAll: { [User.mock] }, - get: { _ in User.mock }, - login: { _ in User.Token.mock }, - logout: { _ in }, - token: { _ in User.Token.mock }, - update: { _, _ in User.mock } - ) - } -} - -extension DatabaseClient.Vendors { - static var mock: Self { - .init( - create: { _ in Vendor.mock }, - delete: { _ in }, - fetchAll: { _ in [Vendor.mock] }, - get: { _, _ in Vendor.mock }, - update: { _, _, _ in Vendor.mock } - ) - } -} - -extension DatabaseClient.VendorBranches { - static var mock: Self { - .init( - create: { _ in VendorBranch.mock }, - delete: { _ in }, - fetchAll: { _ in [VendorBranch.mock] }, - fetchAllWithDetail: { [VendorBranch.Detail.mock] }, - get: { _ in VendorBranch.mock }, - update: { _, _ in VendorBranch.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 User.Token { - static var mock: Self { - .init(id: UUID(1), userID: UUID(0), value: "test-token") - } -} - -extension User.Update { - static var mock: Self { - User.Update(username: "test", email: "test@test.com") - } -} - -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 Vendor.Update { - 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/testEmployeeViews.1.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.1.html deleted file mode 100644 index d2162e7..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.1.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Employees


Employees are who purchase orders can be issued to.


Name
Testy Mctestface
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.2.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.2.html deleted file mode 100644 index 07f5b73..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.2.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Employees


Employees are who purchase orders can be issued to.


Name
Testy Mctestface
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.3.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.3.html deleted file mode 100644 index df00bd3..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.3.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.4.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.4.html deleted file mode 100644 index e381095..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.4.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.5.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.5.html deleted file mode 100644 index d81307b..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.5.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Employees


Employees are who purchase orders can be issued to.


Name
Testy Mctestface
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.6.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.6.html deleted file mode 100644 index f7f933d..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.6.html +++ /dev/null @@ -1 +0,0 @@ -Testy Mctestface \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.7.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.7.html deleted file mode 100644 index f7f933d..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testEmployeeViews.7.html +++ /dev/null @@ -1 +0,0 @@ -Testy Mctestface \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.1.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.1.html deleted file mode 100644 index 83bc1ed..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.1.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Users


Users are who can login and issue purchase orders for employees.


UsernameEmail
testtest@example.com
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.2.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.2.html deleted file mode 100644 index e5e94b4..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.2.html +++ /dev/null @@ -1 +0,0 @@ -testtest@example.com \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.3.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.3.html deleted file mode 100644 index c4c6b22..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.3.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Users


Users are who can login and issue purchase orders for employees.


UsernameEmail
testtest@example.com
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.4.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.4.html deleted file mode 100644 index db9aadc..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.4.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
xPurchase OrdersUsersEmployeesVendors
Logout

Users


Users are who can login and issue purchase orders for employees.


Created:Updated:
UsernameEmail
testtest@example.com
\ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.5.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.5.html deleted file mode 100644 index e5e94b4..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testUserViews.5.html +++ /dev/null @@ -1 +0,0 @@ -testtest@example.com \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.1.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.1.html deleted file mode 100644 index 0deb531..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.1.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.2.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.2.html deleted file mode 100644 index a384bfd..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.2.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.3.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.3.html deleted file mode 100644 index f4496f8..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.3.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.4.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.4.html deleted file mode 100644 index 72e7a3a..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorBranchViews.4.html +++ /dev/null @@ -1 +0,0 @@ -
  • Mock
  • \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.1.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.1.html deleted file mode 100644 index 2d99558..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.1.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
    xPurchase OrdersUsersEmployeesVendors
    Logout

    Vendors


    Vendors are where purchase orders can be issued to.


    NameBranches
    Test(0) Branches
    \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.2.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.2.html deleted file mode 100644 index e7e2c3f..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.2.html +++ /dev/null @@ -1 +0,0 @@ -

    Branches

    NameBranches
    Test(0) Branches
    \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.3.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.3.html deleted file mode 100644 index 870e4d4..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.3.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
    xPurchase OrdersUsersEmployeesVendors
    Logout

    Vendors


    Vendors are where purchase orders can be issued to.


    NameBranches
    Test(0) Branches
    \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.4.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.4.html deleted file mode 100644 index 99883ed..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.4.html +++ /dev/null @@ -1 +0,0 @@ -Purchase Orders
    xPurchase OrdersUsersEmployeesVendors
    Logout

    Vendors


    Vendors are where purchase orders can be issued to.


    Branches

    NameBranches
    Test(0) Branches
    \ No newline at end of file diff --git a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.5.html b/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.5.html deleted file mode 100644 index f48c3e8..0000000 --- a/Tests/AppTests/__Snapshots__/ViewSnapshotTests/testVendorViews.5.html +++ /dev/null @@ -1 +0,0 @@ -

    Branches

    \ No newline at end of file diff --git a/Tests/HtmlSnapshotTestingTests/HtmlSnapshotTestingTests.swift b/Tests/HtmlSnapshotTestingTests/HtmlSnapshotTestingTests.swift deleted file mode 100644 index 028a2a8..0000000 --- a/Tests/HtmlSnapshotTestingTests/HtmlSnapshotTestingTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -@testable import App -import Elementary -import HtmlSnapshotTesting -import SnapshotTesting -import XCTest - -final class SnapshotTestingTests: XCTestCase { - func testSimple() { - let doc = MainPage.loggedIn(next: nil) - assertSnapshot(of: doc, as: .html) - } -} diff --git a/Tests/HtmlSnapshotTestingTests/__Snapshots__/HtmlSnapshotTestingTests/testSimple.1.html b/Tests/HtmlSnapshotTestingTests/__Snapshots__/HtmlSnapshotTestingTests/testSimple.1.html deleted file mode 100644 index 7a99021..0000000 --- a/Tests/HtmlSnapshotTestingTests/__Snapshots__/HtmlSnapshotTestingTests/testSimple.1.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - Purchase Orders - - - - - - - -
    - -
    - xPurchase OrdersUsersEmployeesVendors -
    - Logout -
    - -
    -
    -

    Purchase Orders

    -
    -

    -
    -
    -
    - -
    - - \ No newline at end of file diff --git a/Tests/ViewControllerTests/ViewControllerTests.swift b/Tests/ViewControllerTests/ViewControllerTests.swift index 600e1ee..8b1fbb0 100644 --- a/Tests/ViewControllerTests/ViewControllerTests.swift +++ b/Tests/ViewControllerTests/ViewControllerTests.swift @@ -182,7 +182,12 @@ struct ViewControllerTests { extension ViewController { func render(_ route: ViewRoute) async throws -> String { - guard let html = try await view(for: route, isHtmxRequest: true, authenticate: { _ in }) else { + guard let html = try await view( + for: route, + isHtmxRequest: true, + logger: .init(label: "tests"), + authenticate: { _ in } + ) else { throw TestError() } return html.renderFormatted() @@ -380,6 +385,12 @@ extension PurchaseOrder { extension PurchaseOrder.Create { static var mock: Self { - .init(materials: "bar", customer: "Testy McTestface", createdByID: UUID(0), createdForID: UUID(0), vendorBranchID: UUID(0)) + .init( + materials: "bar", + customer: "Testy McTestface", + createdByID: UUID(0), + createdForID: UUID(0), + vendorBranchID: UUID(0) + ) } }