feat: Adds basic tests for database client.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ db.sqlite
|
|||||||
.env.*
|
.env.*
|
||||||
! .env.example
|
! .env.example
|
||||||
.vscode
|
.vscode
|
||||||
|
.nvim
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ let package = Package(
|
|||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v14)
|
.macOS(.v14)
|
||||||
],
|
],
|
||||||
|
products: [
|
||||||
|
.library(name: "SharedModels", targets: ["SharedModels"]),
|
||||||
|
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||||
|
.library(name: "DatabaseClientLive", targets: ["DatabaseClientLive"])
|
||||||
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
// 💧 A server-side Swift web framework.
|
// 💧 A server-side Swift web framework.
|
||||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
|
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
|
||||||
@@ -62,6 +67,13 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "DatabaseClientTests",
|
||||||
|
dependencies: [
|
||||||
|
"DatabaseClientLive",
|
||||||
|
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SharedModels",
|
name: "SharedModels",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ public extension DatabaseClient.Employees {
|
|||||||
.init { create in
|
.init { create in
|
||||||
let model = try create.toModel()
|
let model = try create.toModel()
|
||||||
try await model.save(on: database)
|
try await model.save(on: database)
|
||||||
return model.toDTO()
|
return try model.toDTO()
|
||||||
} delete: { id in
|
} delete: { id in
|
||||||
guard let model = try await EmployeeModel.find(id, on: database) else {
|
guard let model = try await EmployeeModel.find(id, on: database) else {
|
||||||
throw NotFoundError()
|
throw NotFoundError()
|
||||||
@@ -28,17 +28,17 @@ public extension DatabaseClient.Employees {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return try await query.all().map { $0.toDTO() }
|
return try await query.all().map { try $0.toDTO() }
|
||||||
|
|
||||||
} get: { id in
|
} get: { id in
|
||||||
try await EmployeeModel.find(id, on: database).map { $0.toDTO() }
|
try await EmployeeModel.find(id, on: database).map { try $0.toDTO() }
|
||||||
} update: { id, updates in
|
} update: { id, updates in
|
||||||
guard let model = try await EmployeeModel.find(id, on: database) else {
|
guard let model = try await EmployeeModel.find(id, on: database) else {
|
||||||
throw NotFoundError()
|
throw NotFoundError()
|
||||||
}
|
}
|
||||||
try model.applyUpdate(updates)
|
try model.applyUpdate(updates)
|
||||||
try await model.save(on: database)
|
try await model.save(on: database)
|
||||||
return model.toDTO()
|
return try model.toDTO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,14 +150,14 @@ final class EmployeeModel: Model, @unchecked Sendable {
|
|||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func toDTO() -> Employee {
|
func toDTO() throws -> Employee {
|
||||||
.init(
|
try .init(
|
||||||
id: id,
|
id: requireID(),
|
||||||
active: active,
|
active: active,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt!,
|
||||||
firstName: firstName,
|
firstName: firstName,
|
||||||
lastName: lastName,
|
lastName: lastName,
|
||||||
updatedAt: updatedAt
|
updatedAt: updatedAt!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,32 +10,29 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
|
|||||||
public var updatedAt: Date
|
public var updatedAt: Date
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
id: UUID? = nil,
|
id: UUID,
|
||||||
active: Bool = true,
|
active: Bool = true,
|
||||||
createdAt: Date? = nil,
|
createdAt: Date,
|
||||||
firstName: String,
|
firstName: String,
|
||||||
lastName: String,
|
lastName: String,
|
||||||
updatedAt: Date? = nil
|
updatedAt: Date
|
||||||
) {
|
) {
|
||||||
@Dependency(\.date) var date
|
self.id = id
|
||||||
@Dependency(\.uuid) var uuid
|
|
||||||
|
|
||||||
self.id = id ?? uuid()
|
|
||||||
self.active = active
|
self.active = active
|
||||||
self.createdAt = createdAt ?? date.now
|
self.createdAt = createdAt
|
||||||
self.firstName = firstName
|
self.firstName = firstName
|
||||||
self.lastName = lastName
|
self.lastName = lastName
|
||||||
self.updatedAt = updatedAt ?? date.now
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Employee {
|
// public extension Employee {
|
||||||
static var mocks: [Self] {
|
// static var mocks: [Self] {
|
||||||
[
|
// [
|
||||||
.init(firstName: "Michael", lastName: "Housh"),
|
// .init(firstName: "Michael", lastName: "Housh"),
|
||||||
.init(firstName: "Blob", lastName: "Esquire"),
|
// .init(firstName: "Blob", lastName: "Esquire"),
|
||||||
.init(firstName: "Testy", lastName: "McTestface")
|
// .init(firstName: "Testy", lastName: "McTestface")
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,82 +1,82 @@
|
|||||||
@testable import App
|
// @testable import App
|
||||||
import VaporTesting
|
// import VaporTesting
|
||||||
import Testing
|
// import Testing
|
||||||
import Fluent
|
// import Fluent
|
||||||
|
//
|
||||||
@Suite("App Tests with DB", .serialized)
|
// @Suite("App Tests with DB", .serialized)
|
||||||
struct AppTests {
|
// struct AppTests {
|
||||||
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
// private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||||
let app = try await Application.make(.testing)
|
// let app = try await Application.make(.testing)
|
||||||
do {
|
// do {
|
||||||
try await configure(app)
|
// try await configure(app)
|
||||||
try await app.autoMigrate()
|
// try await app.autoMigrate()
|
||||||
try await test(app)
|
// try await test(app)
|
||||||
try await app.autoRevert()
|
// try await app.autoRevert()
|
||||||
}
|
// }
|
||||||
catch {
|
// catch {
|
||||||
try? await app.autoRevert()
|
// try? await app.autoRevert()
|
||||||
try await app.asyncShutdown()
|
// try await app.asyncShutdown()
|
||||||
throw error
|
// throw error
|
||||||
}
|
// }
|
||||||
try await app.asyncShutdown()
|
// try await app.asyncShutdown()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test("Test Hello World Route")
|
// @Test("Test Hello World Route")
|
||||||
func helloWorld() async throws {
|
// func helloWorld() async throws {
|
||||||
try await withApp { app in
|
// try await withApp { app in
|
||||||
try await app.testing().test(.GET, "hello", afterResponse: { res async in
|
// try await app.testing().test(.GET, "hello", afterResponse: { res async in
|
||||||
#expect(res.status == .ok)
|
// #expect(res.status == .ok)
|
||||||
#expect(res.body.string == "Hello, world!")
|
// #expect(res.body.string == "Hello, world!")
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test("Getting all the Todos")
|
// @Test("Getting all the Todos")
|
||||||
func getAllTodos() async throws {
|
// func getAllTodos() async throws {
|
||||||
try await withApp { app in
|
// try await withApp { app in
|
||||||
let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
|
// let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
|
||||||
try await sampleTodos.create(on: app.db)
|
// try await sampleTodos.create(on: app.db)
|
||||||
|
//
|
||||||
try await app.testing().test(.GET, "todos", afterResponse: { res async throws in
|
// try await app.testing().test(.GET, "todos", afterResponse: { res async throws in
|
||||||
#expect(res.status == .ok)
|
// #expect(res.status == .ok)
|
||||||
#expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
|
// #expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test("Creating a Todo")
|
// @Test("Creating a Todo")
|
||||||
func createTodo() async throws {
|
// func createTodo() async throws {
|
||||||
let newDTO = TodoDTO(id: nil, title: "test")
|
// let newDTO = TodoDTO(id: nil, title: "test")
|
||||||
|
//
|
||||||
try await withApp { app in
|
// try await withApp { app in
|
||||||
try await app.testing().test(.POST, "todos", beforeRequest: { req in
|
// try await app.testing().test(.POST, "todos", beforeRequest: { req in
|
||||||
try req.content.encode(newDTO)
|
// try req.content.encode(newDTO)
|
||||||
}, afterResponse: { res async throws in
|
// }, afterResponse: { res async throws in
|
||||||
#expect(res.status == .ok)
|
// #expect(res.status == .ok)
|
||||||
let models = try await Todo.query(on: app.db).all()
|
// let models = try await Todo.query(on: app.db).all()
|
||||||
#expect(models.map({ $0.toDTO().title }) == [newDTO.title])
|
// #expect(models.map({ $0.toDTO().title }) == [newDTO.title])
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Test("Deleting a Todo")
|
// @Test("Deleting a Todo")
|
||||||
func deleteTodo() async throws {
|
// func deleteTodo() async throws {
|
||||||
let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
|
// let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
|
||||||
|
//
|
||||||
try await withApp { app in
|
// try await withApp { app in
|
||||||
try await testTodos.create(on: app.db)
|
// try await testTodos.create(on: app.db)
|
||||||
|
//
|
||||||
try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
|
// try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
|
||||||
#expect(res.status == .noContent)
|
// #expect(res.status == .noContent)
|
||||||
let model = try await Todo.find(testTodos[0].id, on: app.db)
|
// let model = try await Todo.find(testTodos[0].id, on: app.db)
|
||||||
#expect(model == nil)
|
// #expect(model == nil)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
extension TodoDTO: Equatable {
|
// extension TodoDTO: Equatable {
|
||||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
// public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
lhs.id == rhs.id && lhs.title == rhs.title
|
// lhs.id == rhs.id && lhs.title == rhs.title
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
194
Tests/DatabaseClientTests/DatabaseClientTests.swift
Normal file
194
Tests/DatabaseClientTests/DatabaseClientTests.swift
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
@testable import DatabaseClientLive
|
||||||
|
import Dependencies
|
||||||
|
import FluentSQLiteDriver
|
||||||
|
import Logging
|
||||||
|
import NIO
|
||||||
|
import SharedModels
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("DatabaseClientTests")
|
||||||
|
struct DatabaseClientTests {
|
||||||
|
|
||||||
|
let logger: Logger
|
||||||
|
|
||||||
|
init() async throws {
|
||||||
|
let logger = Logger(label: "database-client-tests")
|
||||||
|
self.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func users() async throws {
|
||||||
|
try await withDatabase(migrations: User.Migrate()) {
|
||||||
|
$0.database.users = .live(database: $1)
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
|
let user = try await users.create(.init(
|
||||||
|
username: "blob",
|
||||||
|
email: "blob@example.com",
|
||||||
|
password: "super-secret",
|
||||||
|
confirmPassword: "super-secret"
|
||||||
|
))
|
||||||
|
#expect(user.username == "blob")
|
||||||
|
#expect(user.email == "blob@example.com")
|
||||||
|
#expect(user.createdAt != nil)
|
||||||
|
#expect(user.updatedAt != nil)
|
||||||
|
|
||||||
|
let allUsers = try await users.fetchAll()
|
||||||
|
#expect(allUsers.count == 1)
|
||||||
|
#expect(allUsers.first == user)
|
||||||
|
|
||||||
|
let fetched = try await users.get(user.id)
|
||||||
|
#expect(fetched != nil)
|
||||||
|
#expect(fetched == user)
|
||||||
|
|
||||||
|
try await users.delete(user.id)
|
||||||
|
let allUsers2 = try await users.fetchAll()
|
||||||
|
#expect(allUsers2.count == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func employees() async throws {
|
||||||
|
try await withDatabase(migrations: Employee.Migrate()) {
|
||||||
|
$0.database.employees = .live(database: $1)
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.database.employees) var employees
|
||||||
|
|
||||||
|
let employee = try await employees.create(.init(
|
||||||
|
firstName: "Blob",
|
||||||
|
lastName: "Esquire"
|
||||||
|
))
|
||||||
|
|
||||||
|
#expect(employee.firstName == "Blob")
|
||||||
|
#expect(employee.lastName == "Esquire")
|
||||||
|
#expect(employee.active)
|
||||||
|
|
||||||
|
let allEmployees = try await employees.fetchAll()
|
||||||
|
#expect(allEmployees.count == 1)
|
||||||
|
#expect(allEmployees.first == employee)
|
||||||
|
|
||||||
|
let inActiveEmployees = try await employees.fetchAll(.inactive)
|
||||||
|
#expect(inActiveEmployees.count == 0)
|
||||||
|
|
||||||
|
let activeEmployees = try await employees.fetchAll(.active)
|
||||||
|
#expect(activeEmployees == allEmployees)
|
||||||
|
|
||||||
|
let fetched = try await employees.get(employee.id)
|
||||||
|
#expect(fetched == employee)
|
||||||
|
|
||||||
|
let updated = try await employees.update(employee.id, Employee.Update(active: false))
|
||||||
|
#expect(updated.active == false)
|
||||||
|
|
||||||
|
let inActiveEmployees2 = try await employees.fetchAll(.inactive)
|
||||||
|
#expect(inActiveEmployees2.count == 1)
|
||||||
|
|
||||||
|
try await employees.delete(employee.id)
|
||||||
|
|
||||||
|
let shouldBeNone = try await employees.fetchAll()
|
||||||
|
#expect(shouldBeNone.count == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func vendors() async throws {
|
||||||
|
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
|
||||||
|
$0.database.vendors = .live(database: $1)
|
||||||
|
$0.database.vendorBranches = .live(database: $1)
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.database.vendorBranches) var branches
|
||||||
|
@Dependency(\.database.vendors) var vendors
|
||||||
|
|
||||||
|
let vendor = try await vendors.create(.init(name: "Corken"))
|
||||||
|
#expect(vendor.name == "Corken")
|
||||||
|
|
||||||
|
let branch = try await branches.create(.init(name: "Monroe", vendorID: vendor.id))
|
||||||
|
#expect(branch.name == "Monroe")
|
||||||
|
|
||||||
|
let all = try await vendors.fetchAll()
|
||||||
|
#expect(all.count == 1)
|
||||||
|
#expect(all.first == vendor)
|
||||||
|
|
||||||
|
let allWithBranches = try await vendors.fetchAll(.withBranches)
|
||||||
|
#expect(allWithBranches.count == 1)
|
||||||
|
#expect(allWithBranches.first!.branches!.first == branch)
|
||||||
|
|
||||||
|
let fetched = try await vendors.get(vendor.id)
|
||||||
|
#expect(fetched == vendor)
|
||||||
|
|
||||||
|
let fetchedWithBranches = try await vendors.get(vendor.id, .withBranches)
|
||||||
|
#expect(fetchedWithBranches!.branches!.first == branch)
|
||||||
|
|
||||||
|
let updated = try await vendors.update(vendor.id, .init(name: "Johnstone"))
|
||||||
|
#expect(updated.name == "Johnstone")
|
||||||
|
|
||||||
|
try await vendors.delete(vendor.id)
|
||||||
|
|
||||||
|
let shouldBeNone = try await vendors.fetchAll()
|
||||||
|
#expect(shouldBeNone.count == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func vendorBranches() async throws {
|
||||||
|
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
|
||||||
|
$0.database.vendors = .live(database: $1)
|
||||||
|
$0.database.vendorBranches = .live(database: $1)
|
||||||
|
} operation: {
|
||||||
|
@Dependency(\.database.vendorBranches) var branches
|
||||||
|
@Dependency(\.database.vendors) var vendors
|
||||||
|
|
||||||
|
let vendor = try await vendors.create(.init(name: "Corken"))
|
||||||
|
#expect(vendor.name == "Corken")
|
||||||
|
|
||||||
|
let branch = try await branches.create(.init(name: "Monroe", vendorID: vendor.id))
|
||||||
|
#expect(branch.name == "Monroe")
|
||||||
|
|
||||||
|
let all = try await branches.fetchAll()
|
||||||
|
#expect(all.count == 1)
|
||||||
|
|
||||||
|
let fetched = try await branches.get(branch.id)
|
||||||
|
#expect(fetched == branch)
|
||||||
|
|
||||||
|
let updated = try await branches.update(branch.id, .init(name: "Covington"))
|
||||||
|
#expect(updated.name == "Covington")
|
||||||
|
|
||||||
|
try await branches.delete(branch.id)
|
||||||
|
|
||||||
|
let shouldBeNone = try await branches.fetchAll()
|
||||||
|
#expect(shouldBeNone.count == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create an in-memory database for testing.
|
||||||
|
func withDatabase(
|
||||||
|
migrations: (any AsyncMigration)...,
|
||||||
|
setupDependencies: (inout DependencyValues, any Database) -> Void,
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async throws {
|
||||||
|
let dbs = Databases(threadPool: NIOThreadPool.singleton, on: MultiThreadedEventLoopGroup.singleton)
|
||||||
|
dbs.use(.sqlite(.memory), as: .sqlite)
|
||||||
|
|
||||||
|
let database = dbs.database(
|
||||||
|
.sqlite,
|
||||||
|
logger: .init(label: "test.sqlite"),
|
||||||
|
on: MultiThreadedEventLoopGroup.singleton.any()
|
||||||
|
)!
|
||||||
|
|
||||||
|
for migration in migrations {
|
||||||
|
try await migration.prepare(on: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
try await withDependencies {
|
||||||
|
setupDependencies(&$0, database)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
for migration in migrations {
|
||||||
|
try await migration.revert(on: database)
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbs.shutdownAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user