feat: Adds api route tests. Tested user interface works as expected, still needs some work on vendors form.

This commit is contained in:
2025-01-20 16:44:12 -05:00
parent affd9b5d81
commit 410bbae1c8
23 changed files with 537 additions and 121 deletions

View File

@@ -19,7 +19,7 @@ let package = Package(
.package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"),
// 🪶 Fluent driver for SQLite. // 🪶 Fluent driver for SQLite.
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), .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/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/pointfreeco/swift-dependencies.git", from: "1.6.3"),
.package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"), .package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"),
@@ -49,13 +49,20 @@ let package = Package(
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.testTarget( .testTarget(
name: "AppTests", name: "ViewRouteTests",
dependencies: [ dependencies: [
.target(name: "App"), .target(name: "App"),
.product(name: "VaporTesting", package: "vapor") .product(name: "VaporTesting", package: "vapor")
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),
.testTarget(
name: "ApiRouteTests",
dependencies: [
.target(name: "SharedModels")
],
swiftSettings: swiftSettings
),
.target( .target(
name: "DatabaseClient", name: "DatabaseClient",
dependencies: [ dependencies: [

View File

@@ -8,7 +8,7 @@ private let viewProtectedMiddleware: [any Middleware] = [
UserPasswordAuthenticator(), UserPasswordAuthenticator(),
UserSessionAuthenticator(), UserSessionAuthenticator(),
User.redirectMiddleware { req in 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 token = try await users.login(.init(username: login.username, password: login.password))
let user = try await users.get(token.userID)! let user = try await users.get(token.userID)!
request.session.authenticate(user) request.session.authenticate(user)
request.logger.info("Logged in next: \(login.next ?? "N/A")")
return await request.render { return await request.render {
MainPage.loggedIn(next: login.next) MainPage.loggedIn(next: login.next)
} }
@@ -197,7 +198,10 @@ extension SharedModels.ViewRoute.PurchaseOrderRoute.Search {
MainPage(displayNav: true, route: .purchaseOrders) { MainPage(displayNav: true, route: .purchaseOrders) {
div(.class("container"), .id("purchase-order-content")) { div(.class("container"), .id("purchase-order-content")) {
search 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 } 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)) let results = try await database.purchaseOrders.search(context.toDatabaseQuery(), .init(page: 1, per: 25))
return await request.render { return await request.render {
PurchaseOrderTable(page: results, context: .search) PurchaseOrderTable(page: results, context: .search)

View File

@@ -169,10 +169,12 @@ enum IDKey: CustomStringConvertible {
} }
enum Vendor: CustomStringConvertible { enum Vendor: CustomStringConvertible {
case form
case row(id: SharedModels.Vendor.ID) case row(id: SharedModels.Vendor.ID)
var description: String { var description: String {
switch self { switch self {
case .form: return "form"
case let .row(id): return "\(id)" case let .row(id): return "\(id)"
} }
} }

View File

@@ -52,15 +52,23 @@ struct LoggedIn: HTML {
let next: String? let next: String?
var content: some HTML { var content: some HTML {
div( div(
.hx.get(next ?? ViewRoute.router.path(for: .purchaseOrder(.index))), .hx.get(nextRoute ?? ViewRoute.router.path(for: .purchaseOrder(.index))),
.hx.pushURL(true), .hx.pushURL(true),
.hx.target("body"), .hx.target(.body),
.hx.trigger(.event(.revealed)), .hx.trigger(.event(.revealed)),
.hx.indicator(".hx-indicator") .hx.indicator(".hx-indicator")
) { ) {
Img.spinner().attributes(.class("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 { struct RouteHeaderView: HTML {

View File

@@ -45,7 +45,7 @@ struct PurchaseOrderTable: HTML {
if context != .search { if context != .search {
Button.add() Button.add()
.attributes( .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) .hx.swap(.outerHTML), .hx.pushURL(true)
) )
} }
@@ -79,9 +79,12 @@ struct PurchaseOrderTable: HTML {
for purchaseOrder in page.items { for purchaseOrder in page.items {
Row(purchaseOrder: purchaseOrder) 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( 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.get(route: .purchaseOrder(.page(page: page.metadata.page + 1, limit: page.metadata.per))),
.hx.trigger(.event(.revealed)), .hx.trigger(.event(.revealed)),
.hx.swap(.outerHTML.transition(true).swap("1s")), .hx.swap(.outerHTML.transition(true).swap("1s")),

View File

@@ -23,7 +23,7 @@ struct UserForm: HTML, Sendable {
.hx.post(context.targetURL), .hx.post(context.targetURL),
.hx.pushURL(context.pushURL), .hx.pushURL(context.pushURL),
.hx.target(context.target), .hx.target(context.target),
.hx.swap(.outerHTML), .hx.swap(context == .create ? .afterBegin.transition(true).swap("0.5s") : .outerHTML),
.hx.on( .hx.on(
.afterRequest, .afterRequest,
.ifSuccessful(.resetForm, .toggleContent(.float)) .ifSuccessful(.resetForm, .toggleContent(.float))
@@ -92,12 +92,12 @@ struct UserForm: HTML, Sendable {
} }
} }
var target: String { var target: HXTarget {
switch self { switch self {
case .create: case .create:
return "next table" return .id(.user(.table))
case .login: case .login:
return "body" return .body
} }
} }

View File

@@ -6,7 +6,7 @@ import Vapor
struct EmployeeSelect: HTML { struct EmployeeSelect: HTML {
let employees: [Employee]? let employees: [Employee]?
let context: SelectContext let context: ViewRoute.SelectContext
var content: some HTML { var content: some HTML {
if let employees { if let employees {
@@ -18,7 +18,7 @@ struct EmployeeSelect: HTML {
.attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch) .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch)
} else { } else {
div( div(
.hx.get("/select/employee?context=\(context.rawValue)"), .hx.get(route: .employee(.select(context: context))),
.hx.target("this"), .hx.target("this"),
.hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.swap(.outerHTML.transition(true).swap("0.5s")),
.hx.indicator("next .hx-indicator"), .hx.indicator("next .hx-indicator"),
@@ -42,11 +42,11 @@ struct EmployeeSelect: HTML {
struct VendorBranchSelect: HTML { struct VendorBranchSelect: HTML {
let branches: [VendorBranch.Detail]? let branches: [VendorBranch.Detail]?
let context: SelectContext let context: ViewRoute.SelectContext
var content: some HTML { var content: some HTML {
if let branches { if let branches {
select(.name("vendorBranchID"), .class(context.classString)) { select(.name("vendorBranchID"), .class("col-4")) {
for branch in branches { for branch in branches {
option(.value(branch.id.uuidString)) { "\(branch.vendor.name) - \(branch.name)" } 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) .attributes(.style("margin-left: 15px;"), when: context == .purchaseOrderSearch)
} else { } else {
div( div(
.hx.get("/select/vendor-branches?context=\(context.rawValue)"), .hx.get(route: .vendorBranch(.select(context: context))),
.hx.target("this"), .hx.target(.this),
.hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.swap(.outerHTML.transition(true).swap("0.5s")),
.hx.indicator("next .hx-indicator"), .hx.indicator("next .hx-indicator"),
.hx.trigger(.event(.revealed)), .hx.trigger(.event(.revealed)),
@@ -75,10 +75,11 @@ struct VendorBranchSelect: HTML {
} }
} }
enum SelectContext: String, Codable, Content { // enum SelectContext: String, Codable, Content {
case purchaseOrderForm // case purchaseOrderForm
case purchaseOrderSearch // case purchaseOrderSearch
extension ViewRoute.SelectContext {
var classString: String { var classString: String {
switch self { switch self {
case .purchaseOrderForm: return "col-3" case .purchaseOrderForm: return "col-3"

View File

@@ -26,15 +26,15 @@ struct VendorDetail: HTML {
// TODO: What route for here?? // TODO: What route for here??
var branchForm: some HTML { var branchForm: some HTML {
// TODO: Add hidden input field with vendor id.
form( form(
.id("branch-form"), .id(.branch(.form)),
.hx.post("/vendors/\(vendor.id)/branches"), .hx.post("/vendors/branches"),
.hx.target("#branches"), .hx.target("#branches"),
.hx.swap(.beforeEnd), .hx.swap(.beforeEnd),
.hx.on(.afterRequest, .ifSuccessful(.resetForm)) .hx.on(.afterRequest, .ifSuccessful(.resetForm))
// .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") // .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();")
) { ) {
input(.type(.hidden), .name("vendorID"), .value(vendor.id.uuidString))
input( input(
.type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required,
// FIX: route // FIX: route

View File

@@ -40,7 +40,7 @@ struct VendorForm: HTML {
func makeForm(vendor: Vendor?) -> some HTML { func makeForm(vendor: Vendor?) -> some HTML {
form( form(
.id("vendor-form"), .id(.vendor(.form)),
vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL), vendor != nil ? .hx.put(route: targetURL) : .hx.post(route: targetURL),
.hx.target("#content"), .hx.target("#content"),
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
@@ -63,7 +63,7 @@ struct VendorForm: HTML {
.style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"), .style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"),
.hx.delete(route: .vendor(.delete(id: vendor.id))), .hx.delete(route: .vendor(.delete(id: vendor.id))),
.hx.confirm("Are you sure you want to delete this vendor?"), .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")), .hx.swap(.outerHTML.transition(true).swap("1s")),
.custom( .custom(
name: "hx-on::after-request", name: "hx-on::after-request",

View File

@@ -2,9 +2,7 @@ import CasePathsCore
import Foundation import Foundation
@preconcurrency import URLRouting @preconcurrency import URLRouting
// TODO: Switch shared to be on API routes not view routes?? public enum ApiRoute: Sendable, Equatable {
public enum ApiRoute: Sendable {
case employee(EmployeeApiRoute) case employee(EmployeeApiRoute)
case purchaseOrder(PurchaseOrderApiRoute) 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 create(Employee.Create)
case delete(id: Employee.ID) case delete(id: Employee.ID)
case get(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 create(PurchaseOrder.Create)
case delete(id: PurchaseOrder.ID) case delete(id: PurchaseOrder.ID)
case get(id: PurchaseOrder.ID) case get(id: PurchaseOrder.ID)
@@ -111,7 +109,7 @@ public enum ApiRoute: Sendable {
} }
// TODO: Add login / logout. // TODO: Add login / logout.
public enum UserApiRoute: Sendable { public enum UserApiRoute: Sendable, Equatable {
case create(User.Create) case create(User.Create)
case delete(id: User.ID) case delete(id: User.ID)
case get(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 index(withBranches: Bool? = nil)
case create(Vendor.Create) case create(Vendor.Create)
case delete(id: Vendor.ID) case delete(id: Vendor.ID)
@@ -173,8 +171,10 @@ public enum ApiRoute: Sendable {
Path { rootPath } Path { rootPath }
Method.get Method.get
Query { Query {
Field("branches", default: nil) { Optionally {
Optionally { Bool.parser() } 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 create(VendorBranch.Create)
case delete(id: VendorBranch.ID) case delete(id: VendorBranch.ID)
case get(id: VendorBranch.ID) case get(id: VendorBranch.ID)
@@ -212,7 +212,7 @@ public enum ApiRoute: Sendable {
Path { "vendors"; "branches" } Path { "vendors"; "branches" }
Method.get Method.get
Query { Query {
Field("vendorID", default: nil) { Optionally { VendorBranch.ID.parser() } } Optionally { Field("vendorID", default: nil) { VendorBranch.ID.parser() } }
} }
} }
Route(.case(Self.update(id:updates:))) { Route(.case(Self.update(id:updates:))) {

View File

@@ -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)
))
)
}
}

View File

@@ -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"))))
// }
// }

View File

@@ -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)))
}
}

View File

@@ -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"))))
}
}

View File

@@ -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"))))
}
}

View File

@@ -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"))))
}
}

View File

@@ -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
// }
// }