diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift index a6640eb..167948f 100644 --- a/Sources/App/Views/Employees/EmployeeForm.swift +++ b/Sources/App/Views/Employees/EmployeeForm.swift @@ -24,9 +24,9 @@ struct EmployeeForm: HTML { employee == nil ? .hx.swap(.beforeEnd.transition(true).swap("0.5s")) : .hx.swap(.outerHTML.transition(true).swap("0.5s")), - .custom( - name: "hx-on::after-request", - value: "if (event.detail.successful) toggleContent('float'); window.location.href='/employees';" + .hx.on( + .afterRequest, + .ifSuccessful(.toggleContent(.float), .setWindowLocation(to: .employee(.index))) ) ) { div(.class("row")) { @@ -53,7 +53,7 @@ struct EmployeeForm: HTML { .attributes( .hx.confirm("Are you sure you want to delete this employee?"), .hx.delete(route: .employee(.delete(id: employee.id))), - .hx.target("#employee_\(employee.id)"), + .hx.target(.id(.employee(.row(id: employee.id)))), .hx.swap(.outerHTML.transition(true).swap("1s")) ) } diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index b5e77a0..5e25ca7 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -28,10 +28,6 @@ struct UserForm: HTML, Sendable { .afterRequest, .ifSuccessful(.resetForm, .toggleContent(.float)) ) - // .custom( - // name: "hx-on::after-request", - // value: "if(event.detail.successful) this.reset(); toggleContent('float');" - // ) ) { if case let .login(next) = context, let next { input(.type(.hidden), .name("next"), .value(next)) diff --git a/Sources/SharedModels/Employee.swift b/Sources/SharedModels/Employee.swift index 5fa6bcb..0550026 100644 --- a/Sources/SharedModels/Employee.swift +++ b/Sources/SharedModels/Employee.swift @@ -31,7 +31,7 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable { } public extension Employee { - struct Create: Codable, Sendable { + struct Create: Codable, Sendable, Equatable { public let firstName: String public let lastName: String public let active: Bool? @@ -47,7 +47,7 @@ public extension Employee { } } - struct Update: Codable, Sendable { + struct Update: Codable, Sendable, Equatable { public let firstName: String? public let lastName: String? public let active: Bool? diff --git a/Sources/SharedModels/PurchaseOrder.swift b/Sources/SharedModels/PurchaseOrder.swift index bac00aa..4fe5e1d 100644 --- a/Sources/SharedModels/PurchaseOrder.swift +++ b/Sources/SharedModels/PurchaseOrder.swift @@ -41,7 +41,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable { public extension PurchaseOrder { - struct Create: Codable, Sendable { + struct Create: Codable, Sendable, Equatable { public let id: Int? public let workOrder: Int? @@ -73,7 +73,7 @@ public extension PurchaseOrder { } } - struct CreateIntermediate: Codable, Sendable { + struct CreateIntermediate: Codable, Sendable, Equatable { public let id: Int? public let workOrder: Int? @@ -115,7 +115,7 @@ public extension PurchaseOrder { } } - enum SearchContext: Sendable { + enum SearchContext: Sendable, Equatable { case customer(String) case vendor(VendorBranch.ID) case employee(Employee.ID) diff --git a/Sources/SharedModels/Routes/ViewRoute.swift b/Sources/SharedModels/Routes/ViewRoute.swift index fb5dc36..32aefeb 100644 --- a/Sources/SharedModels/Routes/ViewRoute.swift +++ b/Sources/SharedModels/Routes/ViewRoute.swift @@ -2,7 +2,8 @@ import CasePathsCore import Foundation @preconcurrency import URLRouting -public enum ViewRoute: Sendable { +// swiftlint:disable file_length +public enum ViewRoute: Sendable, Equatable { case employee(EmployeeRoute) case login(LoginRoute) @@ -24,7 +25,7 @@ public enum ViewRoute: Sendable { public extension ViewRoute { - enum EmployeeRoute: Sendable { + enum EmployeeRoute: Sendable, Equatable { case create(Employee.Create) case delete(id: Employee.ID) case form @@ -43,15 +44,11 @@ public extension ViewRoute { FormData { Field("firstName", .string) Field("lastName", .string) - Field("active") { Optionally { Bool.parser() } } + Optionally { Field("active") { Bool.parser() } } } .map(.memberwise(Employee.Create.init)) } } - Route(.case(Self.index)) { - Path { rootPath } - Method.get - } Route(.case(Self.delete(id:))) { Path { rootPath; Employee.ID.parser() } Method.delete @@ -60,14 +57,22 @@ public extension ViewRoute { Path { rootPath; Employee.ID.parser() } Method.get } + Route(.case(Self.form)) { + Path { rootPath; "create" } + Method.get + } + Route(.case(Self.index)) { + Path { rootPath } + Method.get + } Route(.case(Self.update(id:updates:))) { Path { rootPath; Employee.ID.parser() } Method.put Body { FormData { - Field("firstName") { Optionally { CharacterSet.alphanumerics.map(.string) } } - Field("lastName") { Optionally { CharacterSet.alphanumerics.map(.string) } } - Field("active") { Optionally { Bool.parser() } } + Optionally { Field("firstName") { CharacterSet.alphanumerics.map(.string) } } + Optionally { Field("lastName") { CharacterSet.alphanumerics.map(.string) } } + Optionally { Field("active") { Bool.parser() } } } .map(.memberwise(Employee.Update.init)) } @@ -85,7 +90,7 @@ public extension ViewRoute { public extension ViewRoute { - enum LoginRoute: Sendable { + enum LoginRoute: Sendable, Equatable { case index(next: String? = nil) case post(Request) @@ -96,8 +101,10 @@ public extension ViewRoute { Method.get Path { rootPath } Query { - Field("next", default: nil) { - Optionally { CharacterSet.urlPathAllowed.map(.string) } + Optionally { + Field("next", default: nil) { + CharacterSet.urlPathAllowed.map(.string) + } } } } @@ -108,8 +115,10 @@ public extension ViewRoute { FormData { Field("username", .string) Field("password", .string) - Field("next", default: nil) { - Optionally { CharacterSet.urlPathAllowed.map(.string) } + Optionally { + Field("next", default: nil) { + CharacterSet.urlPathAllowed.map(.string) + } } } .map(.memberwise(Request.init)) @@ -121,12 +130,18 @@ public extension ViewRoute { public let username: String public let password: String public let next: String? + + public init(username: String, password: String, next: String? = nil) { + self.username = username + self.password = password + self.next = next + } } } } public extension ViewRoute { - enum PurchaseOrderRoute: Sendable { + enum PurchaseOrderRoute: Sendable, Equatable { case create(PurchaseOrder.Create) case delete(id: PurchaseOrder.ID) case form @@ -143,11 +158,11 @@ public extension ViewRoute { Method.post Body { FormData { - Field("id") { Optionally { PurchaseOrder.ID.parser() } } - Field("workOrder") { Optionally { Int.parser() } } + Optionally { Field("id") { PurchaseOrder.ID.parser() } } + Optionally { Field("workOrder") { Int.parser() } } Field("materials", .string) Field("customer", .string) - Field("truckStock") { Optionally { Bool.parser() } } + Optionally { Field("truckStock") { Bool.parser() } } Field("createdByID") { User.ID.parser() } Field("createdForID") { Employee.ID.parser() } Field("vendorBranchID") { VendorBranch.ID.parser() } @@ -185,9 +200,9 @@ public extension ViewRoute { } } - public enum Search: Sendable { + public enum Search: Sendable, Equatable { case index(context: Context? = nil, table: Bool? = nil) - case search(Request) + case request(Request) static let rootPath = Path { "purchase-orders"; "search" } @@ -196,24 +211,33 @@ public extension ViewRoute { rootPath Method.get Query { - Field("context", default: .employee) { Optionally { Search.Context.parser() } } - Field("table", default: nil) { Optionally { Bool.parser() } } + Optionally { Field("context", default: .employee) { Search.Context.parser() } } + Optionally { Field("table", default: nil) { Bool.parser() } } } } - Route(.case(Search.search)) { + Route(.case(Search.request)) { rootPath Method.post - Body(.json(Search.Request.self)) + Body { + FormData { + Field("context") { Context.parser() } + Optionally { Field("createdForID") { Employee.ID.parser() } } + Optionally { Field("customerSearch", .string) } + Optionally { Field("vendorBranchID") { VendorBranch.ID.parser() } } + } + .map(.memberwise(Request.init)) + } } } - public enum Context: String, Codable, CaseIterable, Sendable { + public enum Context: String, Codable, CaseIterable, Equatable, Sendable { case employee case customer case vendor } - public struct Request: Codable, Sendable { + // TODO: Create a validation or potentially turn this into an enum with the valid states. + public struct Request: Codable, Equatable, Sendable { public let context: Context public let createdForID: Employee.ID? public let customerSearch: String? @@ -231,14 +255,13 @@ public extension ViewRoute { self.vendorBranchID = vendorBranchID } } - } } } public extension ViewRoute { - enum SelectContext: String, Codable, Sendable, CaseIterable { + enum SelectContext: String, Codable, Equatable, Sendable, CaseIterable { case purchaseOrderForm case purchaseOrderSearch } @@ -246,7 +269,7 @@ public extension ViewRoute { } public extension ViewRoute { - enum UserRoute: Sendable { + enum UserRoute: Sendable, Equatable { case create(User.Create) case delete(id: User.ID) case form @@ -260,7 +283,15 @@ public extension ViewRoute { Route(.case(Self.create)) { Path { rootPath } Method.post - Body(.json(User.Create.self)) + Body { + FormData { + Field("username", .string) + Field("email", .string) + Field("password", .string) + Field("confirmPassword", .string) + } + .map(.memberwise(User.Create.init)) + } } Route(.case(Self.delete(id:))) { Path { rootPath; User.ID.parser() } @@ -283,15 +314,11 @@ public extension ViewRoute { Method.patch Body { FormData { - Field("username") { - Optionally { CharacterSet.alphanumerics.map(.string) } + Optionally { Field("username") { + CharacterSet.alphanumerics.map(.string) } - Field("email") { - Optionally { - // TODO: Not sure if this is correct. - Rest().map(.string) - } } + Optionally { Field("email", .string) } } .map(.memberwise(User.Update.init)) } @@ -302,7 +329,7 @@ public extension ViewRoute { public extension ViewRoute { - enum VendorRoute: Sendable { + enum VendorRoute: Sendable, Equatable { case create(Vendor.Create) case delete(id: Vendor.ID) case form @@ -356,7 +383,7 @@ public extension ViewRoute { public extension ViewRoute { - enum VendorBranchRoute: Sendable { + enum VendorBranchRoute: Sendable, Equatable { case create(VendorBranch.Create) case delete(id: VendorBranch.ID) case select(context: ViewRoute.SelectContext) @@ -387,3 +414,5 @@ public extension ViewRoute { } } } + +// swiftlint:enable file_length diff --git a/Sources/SharedModels/User.swift b/Sources/SharedModels/User.swift index c0bde05..b732d76 100644 --- a/Sources/SharedModels/User.swift +++ b/Sources/SharedModels/User.swift @@ -26,7 +26,7 @@ public struct User: Codable, Equatable, Identifiable, Sendable { public extension User { - struct Create: Codable, Sendable { + struct Create: Codable, Sendable, Equatable { public let username: String public let email: String public let password: String @@ -45,7 +45,7 @@ public extension User { } } - struct Login: Codable, Sendable { + struct Login: Codable, Sendable, Equatable { public let username: String? public let email: String? public let password: String @@ -80,6 +80,11 @@ public extension User { struct Update: Codable, Equatable, Sendable { public let username: String? public let email: String? + + public init(username: String?, email: String?) { + self.username = username + self.email = email + } } } diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index d02c3e9..94f98c6 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -25,7 +25,7 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable { public extension Vendor { - struct Create: Codable, Sendable { + struct Create: Codable, Sendable, Equatable { public let name: String public init(name: String) { @@ -33,7 +33,7 @@ public extension Vendor { } } - struct Update: Codable, Sendable { + struct Update: Codable, Sendable, Equatable { public let name: String public init(name: String) { diff --git a/Sources/SharedModels/VendorBranch.swift b/Sources/SharedModels/VendorBranch.swift index 36f1d67..a82b775 100644 --- a/Sources/SharedModels/VendorBranch.swift +++ b/Sources/SharedModels/VendorBranch.swift @@ -24,7 +24,7 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable { } public extension VendorBranch { - struct Create: Codable, Sendable { + struct Create: Codable, Sendable, Equatable { public let name: String public let vendorID: Vendor.ID @@ -56,7 +56,7 @@ public extension VendorBranch { } } - struct Update: Codable, Sendable { + struct Update: Codable, Sendable, Equatable { public let name: String? public init(name: String?) { diff --git a/Tests/AppTests/EmployeeViewRouteTests.swift b/Tests/AppTests/EmployeeViewRouteTests.swift new file mode 100644 index 0000000..76826a2 --- /dev/null +++ b/Tests/AppTests/EmployeeViewRouteTests.swift @@ -0,0 +1,101 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("EmployeeViewRouteTests") +struct EmployeeViewRouteTests { + let router = ViewRoute.router + + @Test + func employeeCreate() throws { + var request = URLRequestData( + method: "POST", + path: "/employees", + body: .init("firstName=Blob&lastName=Esquire&active=true".utf8) + ) + let route = try router.parse(&request) + #expect( + route == .employee(.create(.init(firstName: "Blob", lastName: "Esquire", active: true))) + ) + } + + @Test + func employeeDelete() throws { + let id = UUID(0) + var request = URLRequestData( + method: "DELETE", + path: "/employees/\(id)" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.delete(id: id)) + ) + } + + @Test + func employeeForm() throws { + var request = URLRequestData( + method: "GET", + path: "/employees/create" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.form) + ) + } + + @Test + func employeeGet() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/employees/\(id)" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.get(id: id)) + ) + } + + @Test + func employeeIndex() throws { + var request = URLRequestData( + method: "GET", + path: "/employees" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.index) + ) + } + + @Test + func employeeUpdate() throws { + let id = UUID(0) + var request = URLRequestData( + method: "PUT", + path: "/employees/\(id)", + body: .init("firstName=Blob&lastName=Esquire&active=true".utf8) + ) + let route = try router.parse(&request) + #expect( + route == .employee(.update( + id: id, + updates: .init(firstName: "Blob", lastName: "Esquire", active: true) + )) + ) + } + + @Test + func employeeSelect() throws { + var request = URLRequestData( + method: "GET", + path: "/employees/select", + query: ["context": ["purchaseOrderForm"]] + ) + let route = try router.parse(&request) + #expect(route == .employee(.select(context: .purchaseOrderForm))) + } +} diff --git a/Tests/AppTests/LoginViewRouteTests.swift b/Tests/AppTests/LoginViewRouteTests.swift new file mode 100644 index 0000000..97b5e43 --- /dev/null +++ b/Tests/AppTests/LoginViewRouteTests.swift @@ -0,0 +1,32 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("LoginViewRouteTests") +struct LoginViewRouteTests { + let router = ViewRoute.router + + @Test + func get() throws { + var request = URLRequestData( + method: "GET", + path: "/login", + query: ["next": ["/users"]] + ) + let route = try router.parse(&request) + #expect(route == .login(.index(next: "/users"))) + } + + @Test + func post() throws { + var request = URLRequestData( + method: "POST", + path: "/login", + body: .init("username=foo&password=super-secret&next=/users".utf8) + ) + let route = try router.parse(&request) + #expect(route == .login(.post(.init(username: "foo", password: "super-secret", next: "/users")))) + } +} diff --git a/Tests/AppTests/PurchaseOrderViewRouteTests.swift b/Tests/AppTests/PurchaseOrderViewRouteTests.swift new file mode 100644 index 0000000..ed4aa9b --- /dev/null +++ b/Tests/AppTests/PurchaseOrderViewRouteTests.swift @@ -0,0 +1,129 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("PurchaseOrderViewRouteTests") +struct PurchaseOrderViewRouteTests { + let router = ViewRoute.router + + @Test + func create() throws { + let id = UUID(0) + var request = URLRequestData( + method: "POST", + path: "/purchase-orders", + body: .init("id=1&workOrder=12345&materials=some&customer=Testy&truckStock=false&createdByID=\(id)&createdForID=\(id)&vendorBranchID=\(id)".utf8) + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.create(.init( + id: 1, + workOrder: 12345, + materials: "some", + customer: "Testy", + truckStock: false, + createdByID: id, + createdForID: id, + vendorBranchID: id + )))) + } + + @Test + func delete() throws { + let id = 1 + var request = URLRequestData( + method: "DELETE", + path: "/purchase-orders/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.delete(id: id))) + } + + @Test + func form() throws { + var request = URLRequestData( + method: "GET", + path: "/purchase-orders/create" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.form)) + } + + @Test + func get() throws { + let id = 1 + var request = URLRequestData( + method: "GET", + path: "/purchase-orders/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.get(id: id))) + } + + @Test + func index() throws { + var request = URLRequestData( + method: "GET", + path: "/purchase-orders" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.index)) + } + + @Test + func page() throws { + var request = URLRequestData( + method: "GET", + path: "/purchase-orders/next" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.page(page: 1, limit: 25))) + + var request2 = URLRequestData( + method: "GET", + path: "/purchase-orders/next", + query: ["page": ["2"], "limit": ["50"]] + ) + let route2 = try router.parse(&request2) + #expect(route2 == .purchaseOrder(.page(page: 2, limit: 50))) + } + + @Test + func searchIndex() throws { + var request = URLRequestData( + method: "GET", + path: "/purchase-orders/search", + query: ["context": ["customer"]] + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.search(.index(context: .customer, table: nil)))) + + var request2 = URLRequestData( + method: "GET", + path: "/purchase-orders/search" + ) + let route2 = try router.parse(&request2) + #expect(route2 == .purchaseOrder(.search(.index(context: .employee, table: nil)))) + + var request3 = URLRequestData( + method: "GET", + path: "/purchase-orders/search", + query: ["context": ["vendor"], "table": ["true"]] + ) + let route3 = try router.parse(&request3) + #expect(route3 == .purchaseOrder(.search(.index(context: .vendor, table: true)))) + } + + @Test + func searchPost() throws { + let id = UUID(0) + var request = URLRequestData( + method: "POST", + path: "/purchase-orders/search", + body: .init("context=employee&createdForID=\(id)".utf8) + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.search(.request(.init(context: .employee, createdForID: id))))) + } +} diff --git a/Tests/AppTests/UserViewRouteTests.swift b/Tests/AppTests/UserViewRouteTests.swift new file mode 100644 index 0000000..6619721 --- /dev/null +++ b/Tests/AppTests/UserViewRouteTests.swift @@ -0,0 +1,81 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("UserViewRouteTests") +struct UserViewRouteTests { + let router = ViewRoute.router + + @Test + func create() throws { + var request = URLRequestData( + method: "POST", + path: "/users", + body: .init("username=foo&email=foo@bar.com&password=super-secret&confirmPassword=super-secret".utf8) + ) + let route = try router.parse(&request) + #expect( + route == .user(.create(.init( + username: "foo", + email: "foo@bar.com", + password: "super-secret", + confirmPassword: "super-secret" + )))) + } + + @Test + func delete() throws { + let id = UUID(0) + var request = URLRequestData( + method: "DELETE", + path: "/users/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .user(.delete(id: id))) + } + + @Test + func form() throws { + var request = URLRequestData( + method: "GET", + path: "/users/create" + ) + let route = try router.parse(&request) + #expect(route == .user(.form)) + } + + @Test + func get() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/users/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .user(.get(id: id))) + } + + @Test + func index() throws { + var request = URLRequestData( + method: "GET", + path: "/users" + ) + let route = try router.parse(&request) + #expect(route == .user(.index)) + } + + @Test + func update() throws { + let id = UUID(0) + var request = URLRequestData( + method: "PATCH", + path: "/users/\(id)", + body: .init("username=bar&email=bar@foo.com".utf8) + ) + let route = try router.parse(&request) + #expect(route == .user(.update(id: id, updates: .init(username: "bar", email: "bar@foo.com")))) + } +} diff --git a/Tests/AppTests/VendorBranchViewRouteTests.swift b/Tests/AppTests/VendorBranchViewRouteTests.swift new file mode 100644 index 0000000..a0db28b --- /dev/null +++ b/Tests/AppTests/VendorBranchViewRouteTests.swift @@ -0,0 +1,45 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("VendorBranchViewRouteTests") +struct VendorBranchViewRouteTests { + let router = ViewRoute.router + + @Test + func create() throws { + let id = UUID(0) + var request = URLRequestData( + method: "POST", + path: "/vendors/branches", + body: .init("name=Test&vendorID=\(id)".utf8) + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.create(.init(name: "Test", vendorID: id)))) + } + + @Test + func delete() throws { + let id = UUID(0) + var request = URLRequestData( + method: "DELETE", + path: "/vendors/branches/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.delete(id: id))) + } + + @Test + func select() throws { + var request = URLRequestData( + method: "GET", + path: "/vendors/branches/select", + query: ["context": ["purchaseOrderForm"]] + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.select(context: .purchaseOrderForm))) + } + +} diff --git a/Tests/AppTests/VendorViewRouteTests.swift b/Tests/AppTests/VendorViewRouteTests.swift new file mode 100644 index 0000000..09a773c --- /dev/null +++ b/Tests/AppTests/VendorViewRouteTests.swift @@ -0,0 +1,75 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("VendorViewRouteTests") +struct VendorViewRouteTests { + let router = ViewRoute.router + + @Test + func create() throws { + var request = URLRequestData( + method: "POST", + path: "/vendors", + body: .init("name=Test".utf8) + ) + let route = try router.parse(&request) + #expect(route == .vendor(.create(.init(name: "Test")))) + } + + @Test + func delete() throws { + let id = UUID(0) + var request = URLRequestData( + method: "DELETE", + path: "/vendors/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.delete(id: id))) + } + + @Test + func get() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/vendors/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.get(id: id))) + } + + @Test + func form() throws { + var request = URLRequestData( + method: "GET", + path: "/vendors/create" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.form)) + } + + @Test + func index() throws { + var request = URLRequestData( + method: "GET", + path: "/vendors" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.index)) + } + + @Test + func update() throws { + let id = UUID(0) + var request = URLRequestData( + method: "PUT", + path: "/vendors/\(id)", + body: .init("name=Test".utf8) + ) + let route = try router.parse(&request) + #expect(route == .vendor(.update(id: id, updates: .init(name: "Test")))) + } +}