From 410bbae1c845f91084efc88a31a29ff87e46757a Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 20 Jan 2025 16:44:12 -0500 Subject: [PATCH] feat: Adds api route tests. Tested user interface works as expected, still needs some work on vendors form. --- Package.swift | 11 ++- Sources/App/Controllers/ViewController.swift | 10 +- Sources/App/Views/HTMXExtensions.swift | 2 + Sources/App/Views/Main.swift | 12 ++- .../PurchaseOrders/PurchaseOrderTable.swift | 9 +- Sources/App/Views/Users/UserForm.swift | 8 +- Sources/App/Views/Utils/Select.swift | 19 ++-- Sources/App/Views/Vendors/VendorDetail.swift | 6 +- Sources/App/Views/Vendors/VendorForm.swift | 4 +- Sources/SharedModels/Routes/ApiRoute.swift | 22 ++--- .../ApiRouteTests/EmployeeApiRouteTests.swift | 93 +++++++++++++++++++ Tests/ApiRouteTests/LoginApiRouteTests.swift | 32 +++++++ .../PurchaseOrderApiRouteTests.swift | 93 +++++++++++++++++++ Tests/ApiRouteTests/UserApiRouteTests.swift | 85 +++++++++++++++++ Tests/ApiRouteTests/VendorApiRouteTests.swift | 83 +++++++++++++++++ .../VendorBranchApiRouteTests.swift | 87 +++++++++++++++++ Tests/AppTests/AppTests.swift | 82 ---------------- .../EmployeeViewRouteTests.swift | 0 .../LoginViewRouteTests.swift | 0 .../PurchaseOrderViewRouteTests.swift | 0 .../UserViewRouteTests.swift | 0 .../VendorBranchViewRouteTests.swift | 0 .../VendorViewRouteTests.swift | 0 23 files changed, 537 insertions(+), 121 deletions(-) create mode 100644 Tests/ApiRouteTests/EmployeeApiRouteTests.swift create mode 100644 Tests/ApiRouteTests/LoginApiRouteTests.swift create mode 100644 Tests/ApiRouteTests/PurchaseOrderApiRouteTests.swift create mode 100644 Tests/ApiRouteTests/UserApiRouteTests.swift create mode 100644 Tests/ApiRouteTests/VendorApiRouteTests.swift create mode 100644 Tests/ApiRouteTests/VendorBranchApiRouteTests.swift delete mode 100644 Tests/AppTests/AppTests.swift rename Tests/{AppTests => ViewRouteTests}/EmployeeViewRouteTests.swift (100%) rename Tests/{AppTests => ViewRouteTests}/LoginViewRouteTests.swift (100%) rename Tests/{AppTests => ViewRouteTests}/PurchaseOrderViewRouteTests.swift (100%) rename Tests/{AppTests => ViewRouteTests}/UserViewRouteTests.swift (100%) rename Tests/{AppTests => ViewRouteTests}/VendorBranchViewRouteTests.swift (100%) rename Tests/{AppTests => ViewRouteTests}/VendorViewRouteTests.swift (100%) diff --git a/Package.swift b/Package.swift index 10648e8..b8677f0 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ let package = Package( .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), // 🪶 Fluent driver for SQLite. .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), - // 🔵 Non-blocking, event-driven networking for Swift. Used for, custom executors + // 🔵 Non-blocking, event-driven networking f Swift. Used for, custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), .package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"), @@ -49,13 +49,20 @@ let package = Package( swiftSettings: swiftSettings ), .testTarget( - name: "AppTests", + name: "ViewRouteTests", dependencies: [ .target(name: "App"), .product(name: "VaporTesting", package: "vapor") ], swiftSettings: swiftSettings ), + .testTarget( + name: "ApiRouteTests", + dependencies: [ + .target(name: "SharedModels") + ], + swiftSettings: swiftSettings + ), .target( name: "DatabaseClient", dependencies: [ diff --git a/Sources/App/Controllers/ViewController.swift b/Sources/App/Controllers/ViewController.swift index 45b58f1..a40209b 100644 --- a/Sources/App/Controllers/ViewController.swift +++ b/Sources/App/Controllers/ViewController.swift @@ -8,7 +8,7 @@ private let viewProtectedMiddleware: [any Middleware] = [ UserPasswordAuthenticator(), UserSessionAuthenticator(), User.redirectMiddleware { req in - "/login?next=\(req.url)" + "/login?next=\(req.url.string)" } ] @@ -43,6 +43,7 @@ extension SharedModels.ViewRoute { let token = try await users.login(.init(username: login.username, password: login.password)) let user = try await users.get(token.userID)! request.session.authenticate(user) + request.logger.info("Logged in next: \(login.next ?? "N/A")") return await request.render { MainPage.loggedIn(next: login.next) } @@ -197,7 +198,10 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { MainPage(displayNav: true, route: .purchaseOrders) { div(.class("container"), .id("purchase-order-content")) { search - PurchaseOrderTable(page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0))) + PurchaseOrderTable( + page: .init(items: [], metadata: .init(page: 0, per: 50, total: 0)), + context: .search + ) } } } @@ -212,7 +216,7 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search { } return await request.render { html } - case let .search(context): + case let .request(context): let results = try await database.purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25)) return await request.render { PurchaseOrderTable(page: results, context: .search) diff --git a/Sources/App/Views/HTMXExtensions.swift b/Sources/App/Views/HTMXExtensions.swift index 005c909..52f8a5e 100644 --- a/Sources/App/Views/HTMXExtensions.swift +++ b/Sources/App/Views/HTMXExtensions.swift @@ -169,10 +169,12 @@ enum IDKey: CustomStringConvertible { } enum Vendor: CustomStringConvertible { + case form case row(id: SharedModels.Vendor.ID) var description: String { switch self { + case .form: return "form" case let .row(id): return "\(id)" } } diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift index 75d12f5..ef30cd3 100644 --- a/Sources/App/Views/Main.swift +++ b/Sources/App/Views/Main.swift @@ -52,15 +52,23 @@ struct LoggedIn: HTML { let next: String? var content: some HTML { div( - .hx.get(next ?? ViewRoute.router.path(for: .purchaseOrder(.index))), + .hx.get(nextRoute ?? ViewRoute.router.path(for: .purchaseOrder(.index))), .hx.pushURL(true), - .hx.target("body"), + .hx.target(.body), .hx.trigger(.event(.revealed)), .hx.indicator(".hx-indicator") ) { Img.spinner().attributes(.class("hx-indicator")) } } + + // HACK: to get search route to work after login. + var nextRoute: String? { + if let next, next.contains("search") { + return ViewRoute.router.path(for: .purchaseOrder(.search(.index(context: .employee, table: true)))) + } + return next + } } struct RouteHeaderView: HTML { diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift index 7e688dc..f143b21 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -45,7 +45,7 @@ struct PurchaseOrderTable: HTML { if context != .search { Button.add() .attributes( - .hx.get(route: .purchaseOrder(.index)), .hx.target(.id(.float)), + .hx.get(route: .purchaseOrder(.form)), .hx.target(.id(.float)), .hx.swap(.outerHTML), .hx.pushURL(true) ) } @@ -79,9 +79,12 @@ struct PurchaseOrderTable: HTML { for purchaseOrder in page.items { Row(purchaseOrder: purchaseOrder) } - if page.metadata.pageCount > page.metadata.page { + // We set page to 0 when we're on search, but have not completed the search + // form yet, so don't add the infinite scroll row / trigger otherwise it will + // load the first page, which is not what we want, but we need the empty table + // to be available once the search form is completed. + if page.metadata.page > 0, page.metadata.pageCount > page.metadata.page { tr( - // .hx.get("/purchase-orders/next?page=\(page.metadata.page + 1)&limit=\(page.metadata.per)"), .hx.get(route: .purchaseOrder(.page(page: page.metadata.page + 1, limit: page.metadata.per))), .hx.trigger(.event(.revealed)), .hx.swap(.outerHTML.transition(true).swap("1s")), diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index 5e25ca7..4ce7c95 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -23,7 +23,7 @@ struct UserForm: HTML, Sendable { .hx.post(context.targetURL), .hx.pushURL(context.pushURL), .hx.target(context.target), - .hx.swap(.outerHTML), + .hx.swap(context == .create ? .afterBegin.transition(true).swap("0.5s") : .outerHTML), .hx.on( .afterRequest, .ifSuccessful(.resetForm, .toggleContent(.float)) @@ -92,12 +92,12 @@ struct UserForm: HTML, Sendable { } } - var target: String { + var target: HXTarget { switch self { case .create: - return "next table" + return .id(.user(.table)) case .login: - return "body" + return .body } } diff --git a/Sources/App/Views/Utils/Select.swift b/Sources/App/Views/Utils/Select.swift index b7b1f2c..0e9bb90 100644 --- a/Sources/App/Views/Utils/Select.swift +++ b/Sources/App/Views/Utils/Select.swift @@ -6,7 +6,7 @@ import Vapor struct EmployeeSelect: HTML { let employees: [Employee]? - let context: SelectContext + let context: ViewRoute.SelectContext var content: some HTML { if let employees { @@ -18,7 +18,7 @@ struct EmployeeSelect: HTML { .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch) } else { div( - .hx.get("/select/employee?context=\(context.rawValue)"), + .hx.get(route: .employee(.select(context: context))), .hx.target("this"), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.indicator("next .hx-indicator"), @@ -42,11 +42,11 @@ struct EmployeeSelect: HTML { struct VendorBranchSelect: HTML { let branches: [VendorBranch.Detail]? - let context: SelectContext + let context: ViewRoute.SelectContext var content: some HTML { if let branches { - select(.name("vendorBranchID"), .class(context.classString)) { + select(.name("vendorBranchID"), .class("col-4")) { for branch in branches { option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" } } @@ -54,8 +54,8 @@ struct VendorBranchSelect: HTML { .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch) } else { div( - .hx.get("/select/vendor-branches?context=\(context.rawValue)"), - .hx.target("this"), + .hx.get(route: .vendorBranch(.select(context: context))), + .hx.target(.this), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.indicator("next .hx-indicator"), .hx.trigger(.event(.revealed)), @@ -75,10 +75,11 @@ struct VendorBranchSelect: HTML { } } -enum SelectContext: String, Codable, Content { - case purchaseOrderForm - case purchaseOrderSearch +// enum SelectContext: String, Codable, Content { +// case purchaseOrderForm +// case purchaseOrderSearch +extension ViewRoute.SelectContext { var classString: String { switch self { case .purchaseOrderForm: return "col-3" diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 13180ef..b9dc4dc 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -26,15 +26,15 @@ struct VendorDetail: HTML { // TODO: What route for here?? var branchForm: some HTML { - // TODO: Add hidden input field with vendor id. form( - .id("branch-form"), - .hx.post("/vendors/\(vendor.id)/branches"), + .id(.branch(.form)), + .hx.post("/vendors/branches"), .hx.target("#branches"), .hx.swap(.beforeEnd), .hx.on(.afterRequest, .ifSuccessful(.resetForm)) // .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") ) { + input(.type(.hidden), .name("vendorID"), .value(vendor.id.uuidString)) input( .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, // FIX: route diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift index 1b8d8b9..f140200 100644 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -40,7 +40,7 @@ struct VendorForm: HTML { func makeForm(vendor: Vendor?) -> some HTML { form( - .id("vendor-form"), + .id(.vendor(.form)), vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), .hx.target("#content"), .hx.swap(.outerHTML) @@ -63,7 +63,7 @@ struct VendorForm: HTML { .style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"), .hx.delete(route: .vendor(.delete(id: vendor.id))), .hx.confirm("Are you sure you want to delete this vendor?"), - .hx.target("#vendor_\(vendor.id)"), + .hx.target(.id(.vendor(.row(id: vendor.id)))), .hx.swap(.outerHTML.transition(true).swap("1s")), .custom( name: "hx-on::after-request", diff --git a/Sources/SharedModels/Routes/ApiRoute.swift b/Sources/SharedModels/Routes/ApiRoute.swift index eac496e..e7f0602 100644 --- a/Sources/SharedModels/Routes/ApiRoute.swift +++ b/Sources/SharedModels/Routes/ApiRoute.swift @@ -2,9 +2,7 @@ import CasePathsCore import Foundation @preconcurrency import URLRouting -// TODO: Switch shared to be on API routes not view routes?? - -public enum ApiRoute: Sendable { +public enum ApiRoute: Sendable, Equatable { case employee(EmployeeApiRoute) case purchaseOrder(PurchaseOrderApiRoute) @@ -37,7 +35,7 @@ public enum ApiRoute: Sendable { } } - public enum EmployeeApiRoute: Sendable { + public enum EmployeeApiRoute: Sendable, Equatable { case create(Employee.Create) case delete(id: Employee.ID) case get(id: Employee.ID) @@ -72,7 +70,7 @@ public enum ApiRoute: Sendable { } } - public enum PurchaseOrderApiRoute: Sendable { + public enum PurchaseOrderApiRoute: Sendable, Equatable { case create(PurchaseOrder.Create) case delete(id: PurchaseOrder.ID) case get(id: PurchaseOrder.ID) @@ -111,7 +109,7 @@ public enum ApiRoute: Sendable { } // TODO: Add login / logout. - public enum UserApiRoute: Sendable { + public enum UserApiRoute: Sendable, Equatable { case create(User.Create) case delete(id: User.ID) case get(id: User.ID) @@ -146,7 +144,7 @@ public enum ApiRoute: Sendable { } } - public enum VendorApiRoute: Sendable { + public enum VendorApiRoute: Sendable, Equatable { case index(withBranches: Bool? = nil) case create(Vendor.Create) case delete(id: Vendor.ID) @@ -173,8 +171,10 @@ public enum ApiRoute: Sendable { Path { rootPath } Method.get Query { - Field("branches", default: nil) { - Optionally { Bool.parser() } + Optionally { + Field("branches", default: nil) { + Bool.parser() + } } } } @@ -186,7 +186,7 @@ public enum ApiRoute: Sendable { } } - public enum VendorBranchApiRoute: Sendable { + public enum VendorBranchApiRoute: Sendable, Equatable { case create(VendorBranch.Create) case delete(id: VendorBranch.ID) case get(id: VendorBranch.ID) @@ -212,7 +212,7 @@ public enum ApiRoute: Sendable { Path { "vendors"; "branches" } Method.get Query { - Field("vendorID", default: nil) { Optionally { VendorBranch.ID.parser() } } + Optionally { Field("vendorID", default: nil) { VendorBranch.ID.parser() } } } } Route(.case(Self.update(id:updates:))) { diff --git a/Tests/ApiRouteTests/EmployeeApiRouteTests.swift b/Tests/ApiRouteTests/EmployeeApiRouteTests.swift new file mode 100644 index 0000000..12fcfde --- /dev/null +++ b/Tests/ApiRouteTests/EmployeeApiRouteTests.swift @@ -0,0 +1,93 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("EmployeeApiRouteTests") +struct EmployeeApiRouteTests { + let router = ApiRoute.router + + @Test + func employeeCreate() throws { + let json = """ + { + \"firstName\": \"Blob\", + \"lastName\": \"Esquire\", + \"active\": true + } + """ + var request = URLRequestData( + method: "POST", + path: "/api/v1/employees", + body: .init(json.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: "/api/v1/employees/\(id)" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.delete(id: id)) + ) + } + + @Test + func employeeGet() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/api/v1/employees/\(id)" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.get(id: id)) + ) + } + + @Test + func employeeIndex() throws { + var request = URLRequestData( + method: "GET", + path: "/api/v1/employees" + ) + let route = try router.parse(&request) + #expect( + route == .employee(.index) + ) + } + + @Test + func employeeUpdate() throws { + let id = UUID(0) + let json = """ + { + \"firstName\": \"Blob\", + \"lastName\": \"Esquire\", + \"active\": true + } + """ + var request = URLRequestData( + method: "PUT", + path: "/api/v1/employees/\(id)", + body: .init(json.utf8) + ) + let route = try router.parse(&request) + #expect( + route == .employee(.update( + id: id, + updates: .init(firstName: "Blob", lastName: "Esquire", active: true) + )) + ) + } + +} diff --git a/Tests/ApiRouteTests/LoginApiRouteTests.swift b/Tests/ApiRouteTests/LoginApiRouteTests.swift new file mode 100644 index 0000000..e70304b --- /dev/null +++ b/Tests/ApiRouteTests/LoginApiRouteTests.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/ApiRouteTests/PurchaseOrderApiRouteTests.swift b/Tests/ApiRouteTests/PurchaseOrderApiRouteTests.swift new file mode 100644 index 0000000..05d51f2 --- /dev/null +++ b/Tests/ApiRouteTests/PurchaseOrderApiRouteTests.swift @@ -0,0 +1,93 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("PurchaseOrderApiRouteTests") +struct PurchaseOrderApiRouteTests { + let router = ApiRoute.router + + @Test + func create() throws { + let id = UUID(0) + let json = """ + { + \"id\": 1, + \"workOrder\": 12345, + \"materials\": \"some\", + \"customer\": \"Testy\", + \"truckStock\": false, + \"createdByID\": \"\(id)\", + "createdForID\": \"\(id)\", + "vendorBranchID\": \"\(id)\" + } + """ + var request = URLRequestData( + method: "POST", + path: "/api/v1/purchase-orders", + body: .init(json.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: "/api/v1/purchase-orders/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.delete(id: id))) + } + + @Test + func get() throws { + let id = 1 + var request = URLRequestData( + method: "GET", + path: "/api/v1/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: "/api/v1/purchase-orders" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.index)) + } + + @Test + func page() throws { + var request = URLRequestData( + method: "GET", + path: "/api/v1/purchase-orders/next" + ) + let route = try router.parse(&request) + #expect(route == .purchaseOrder(.page(page: 1, limit: 25))) + + var request2 = URLRequestData( + method: "GET", + path: "/api/v1/purchase-orders/next", + query: ["page": ["2"], "limit": ["50"]] + ) + let route2 = try router.parse(&request2) + #expect(route2 == .purchaseOrder(.page(page: 2, limit: 50))) + } +} diff --git a/Tests/ApiRouteTests/UserApiRouteTests.swift b/Tests/ApiRouteTests/UserApiRouteTests.swift new file mode 100644 index 0000000..30669a3 --- /dev/null +++ b/Tests/ApiRouteTests/UserApiRouteTests.swift @@ -0,0 +1,85 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("UserApiRouteTests") +struct UserApiRouteTests { + let router = ApiRoute.router + + @Test + func create() throws { + let json = """ + { + \"username\": \"foo\", + \"email\": \"foo@bar.com\", + \"password\": \"super-secret\", + \"confirmPassword\": \"super-secret\" + } + """ + var request = URLRequestData( + method: "POST", + path: "/api/v1/users", + body: .init(json.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: "/api/v1/users/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .user(.delete(id: id))) + } + + @Test + func get() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/api/v1/users/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .user(.get(id: id))) + } + + @Test + func index() throws { + var request = URLRequestData( + method: "GET", + path: "/api/v1/users" + ) + let route = try router.parse(&request) + #expect(route == .user(.index)) + } + + @Test + func update() throws { + let id = UUID(0) + let json = """ + { + \"username\": \"bar\", + \"email\": \"bar@foo.com\" + } + """ + var request = URLRequestData( + method: "PATCH", + path: "/api/v1/users/\(id)", + body: .init(json.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/ApiRouteTests/VendorApiRouteTests.swift b/Tests/ApiRouteTests/VendorApiRouteTests.swift new file mode 100644 index 0000000..61a41c8 --- /dev/null +++ b/Tests/ApiRouteTests/VendorApiRouteTests.swift @@ -0,0 +1,83 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("VendorApiRouteTests") +struct VendorApiRouteTests { + let router = ApiRoute.router + + @Test + func create() throws { + let json = """ + { + \"name\": \"Test\" + } + """ + var request = URLRequestData( + method: "POST", + path: "/api/v1/vendors", + body: .init(json.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: "/api/v1/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: "/api/v1/vendors/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.get(id: id))) + } + + @Test + func index() throws { + var request = URLRequestData( + method: "GET", + path: "/api/v1/vendors" + ) + let route = try router.parse(&request) + #expect(route == .vendor(.index())) + + var request2 = URLRequestData( + method: "GET", + path: "/api/v1/vendors", + query: ["branches": ["true"]] + ) + let route2 = try router.parse(&request2) + #expect(route2 == .vendor(.index(withBranches: true))) + } + + @Test + func update() throws { + let id = UUID(0) + let json = """ + { + \"name\": \"Test\" + } + """ + var request = URLRequestData( + method: "PUT", + path: "/api/v1/vendors/\(id)", + body: .init(json.utf8) + ) + let route = try router.parse(&request) + #expect(route == .vendor(.update(id: id, updates: .init(name: "Test")))) + } +} diff --git a/Tests/ApiRouteTests/VendorBranchApiRouteTests.swift b/Tests/ApiRouteTests/VendorBranchApiRouteTests.swift new file mode 100644 index 0000000..91dd20b --- /dev/null +++ b/Tests/ApiRouteTests/VendorBranchApiRouteTests.swift @@ -0,0 +1,87 @@ +import Dependencies +import Foundation +import SharedModels +import Testing +import URLRouting + +@Suite("VendorBranchApiRouteTests") +struct VendorBranchApiRouteTests { + let router = ApiRoute.router + + @Test + func create() throws { + let id = UUID(0) + let json = """ + { + \"name\": \"Test\", + \"vendorID\": \"\(id)\" + } + """ + var request = URLRequestData( + method: "POST", + path: "/api/v1/vendors/branches", + body: .init(json.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: "/api/v1/vendors/branches/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.delete(id: id))) + } + + @Test + func get() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/api/v1/vendors/branches/\(id)" + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.get(id: id))) + } + + @Test + func index() throws { + let id = UUID(0) + var request = URLRequestData( + method: "GET", + path: "/api/v1/vendors/branches" + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.index())) + + var request2 = URLRequestData( + method: "GET", + path: "/api/v1/vendors/branches", + query: ["vendorID": ["\(id)"]] + ) + let route2 = try router.parse(&request2) + #expect(route2 == .vendorBranch(.index(for: id))) + } + + @Test + func update() throws { + let id = UUID(0) + let json = """ + { + \"name\": \"Test\" + } + """ + var request = URLRequestData( + method: "PUT", + path: "/api/v1/vendors/branches/\(id)", + body: .init(json.utf8) + ) + let route = try router.parse(&request) + #expect(route == .vendorBranch(.update(id: id, updates: .init(name: "Test")))) + } + +} diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift deleted file mode 100644 index 219654a..0000000 --- a/Tests/AppTests/AppTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// @testable import App -// import VaporTesting -// import Testing -// import Fluent -// -// @Suite("App Tests with DB", .serialized) -// struct AppTests { -// private func withApp(_ test: (Application) async throws -> ()) async throws { -// let app = try await Application.make(.testing) -// do { -// try await configure(app) -// try await app.autoMigrate() -// try await test(app) -// try await app.autoRevert() -// } -// catch { -// try? await app.autoRevert() -// try await app.asyncShutdown() -// throw error -// } -// try await app.asyncShutdown() -// } -// -// @Test("Test Hello World Route") -// func helloWorld() async throws { -// try await withApp { app in -// try await app.testing().test(.GET, "hello", afterResponse: { res async in -// #expect(res.status == .ok) -// #expect(res.body.string == "Hello, world!") -// }) -// } -// } -// -// @Test("Getting all the Todos") -// func getAllTodos() async throws { -// try await withApp { app in -// let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")] -// try await sampleTodos.create(on: app.db) -// -// try await app.testing().test(.GET, "todos", afterResponse: { res async throws in -// #expect(res.status == .ok) -// #expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} ) -// }) -// } -// } -// -// @Test("Creating a Todo") -// func createTodo() async throws { -// let newDTO = TodoDTO(id: nil, title: "test") -// -// try await withApp { app in -// try await app.testing().test(.POST, "todos", beforeRequest: { req in -// try req.content.encode(newDTO) -// }, afterResponse: { res async throws in -// #expect(res.status == .ok) -// let models = try await Todo.query(on: app.db).all() -// #expect(models.map({ $0.toDTO().title }) == [newDTO.title]) -// }) -// } -// } -// -// @Test("Deleting a Todo") -// func deleteTodo() async throws { -// let testTodos = [Todo(title: "test1"), Todo(title: "test2")] -// -// try await withApp { app in -// try await testTodos.create(on: app.db) -// -// try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in -// #expect(res.status == .noContent) -// let model = try await Todo.find(testTodos[0].id, on: app.db) -// #expect(model == nil) -// }) -// } -// } -// } -// -// extension TodoDTO: Equatable { -// public static func == (lhs: Self, rhs: Self) -> Bool { -// lhs.id == rhs.id && lhs.title == rhs.title -// } -// } diff --git a/Tests/AppTests/EmployeeViewRouteTests.swift b/Tests/ViewRouteTests/EmployeeViewRouteTests.swift similarity index 100% rename from Tests/AppTests/EmployeeViewRouteTests.swift rename to Tests/ViewRouteTests/EmployeeViewRouteTests.swift diff --git a/Tests/AppTests/LoginViewRouteTests.swift b/Tests/ViewRouteTests/LoginViewRouteTests.swift similarity index 100% rename from Tests/AppTests/LoginViewRouteTests.swift rename to Tests/ViewRouteTests/LoginViewRouteTests.swift diff --git a/Tests/AppTests/PurchaseOrderViewRouteTests.swift b/Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift similarity index 100% rename from Tests/AppTests/PurchaseOrderViewRouteTests.swift rename to Tests/ViewRouteTests/PurchaseOrderViewRouteTests.swift diff --git a/Tests/AppTests/UserViewRouteTests.swift b/Tests/ViewRouteTests/UserViewRouteTests.swift similarity index 100% rename from Tests/AppTests/UserViewRouteTests.swift rename to Tests/ViewRouteTests/UserViewRouteTests.swift diff --git a/Tests/AppTests/VendorBranchViewRouteTests.swift b/Tests/ViewRouteTests/VendorBranchViewRouteTests.swift similarity index 100% rename from Tests/AppTests/VendorBranchViewRouteTests.swift rename to Tests/ViewRouteTests/VendorBranchViewRouteTests.swift diff --git a/Tests/AppTests/VendorViewRouteTests.swift b/Tests/ViewRouteTests/VendorViewRouteTests.swift similarity index 100% rename from Tests/AppTests/VendorViewRouteTests.swift rename to Tests/ViewRouteTests/VendorViewRouteTests.swift