feat: Adds project database tests.
All checks were successful
CI / Linux Tests (push) Successful in 5m24s
All checks were successful
CI / Linux Tests (push) Successful in 5m24s
This commit is contained in:
@@ -21,28 +21,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
|||||||
try await model.delete(on: database)
|
try await model.delete(on: database)
|
||||||
},
|
},
|
||||||
detail: { id in
|
detail: { id in
|
||||||
guard
|
let model = try await ProjectModel.fetchDetail(for: id, on: database)
|
||||||
let model = try await ProjectModel.query(on: database)
|
|
||||||
.with(\.$componentLosses)
|
|
||||||
.with(\.$equipment)
|
|
||||||
.with(\.$equivalentLengths)
|
|
||||||
.with(\.$rooms)
|
|
||||||
.with(
|
|
||||||
\.$trunks,
|
|
||||||
{ trunk in
|
|
||||||
trunk.with(
|
|
||||||
\.$rooms,
|
|
||||||
{
|
|
||||||
$0.with(\.$room)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.filter(\.$id == id)
|
|
||||||
.first()
|
|
||||||
else {
|
|
||||||
throw NotFoundError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Different error ??
|
// TODO: Different error ??
|
||||||
guard let equipmentInfo = model.equipment else { return nil }
|
guard let equipmentInfo = model.equipment else { return nil }
|
||||||
@@ -62,44 +41,25 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
|||||||
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
||||||
},
|
},
|
||||||
getCompletedSteps: { id in
|
getCompletedSteps: { id in
|
||||||
let roomsCount = try await RoomModel.query(on: database)
|
let model = try await ProjectModel.fetchDetail(for: id, on: database)
|
||||||
.with(\.$project)
|
|
||||||
.filter(\.$project.$id == id)
|
|
||||||
.count()
|
|
||||||
|
|
||||||
let equivalentLengths = try await EffectiveLengthModel.query(on: database)
|
|
||||||
.with(\.$project)
|
|
||||||
.filter(\.$project.$id == id)
|
|
||||||
.all()
|
|
||||||
|
|
||||||
var equivalentLengthsCompleted = false
|
var equivalentLengthsCompleted = false
|
||||||
|
|
||||||
if equivalentLengths.filter({ $0.type == "supply" }).first != nil,
|
if model.equivalentLengths.filter({ $0.type == "supply" }).first != nil,
|
||||||
equivalentLengths.filter({ $0.type == "return" }).first != nil
|
model.equivalentLengths.filter({ $0.type == "return" }).first != nil
|
||||||
{
|
{
|
||||||
equivalentLengthsCompleted = true
|
equivalentLengthsCompleted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let componentLosses = try await ComponentLossModel.query(on: database)
|
|
||||||
.with(\.$project)
|
|
||||||
.filter(\.$project.$id == id)
|
|
||||||
.count()
|
|
||||||
|
|
||||||
let equipmentInfo = try await EquipmentModel.query(on: database)
|
|
||||||
.with(\.$project)
|
|
||||||
.filter(\.$project.$id == id)
|
|
||||||
.first()
|
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
equipmentInfo: equipmentInfo != nil,
|
equipmentInfo: model.equipment != nil,
|
||||||
rooms: roomsCount > 0,
|
rooms: model.rooms.count > 0,
|
||||||
equivalentLength: equivalentLengthsCompleted,
|
equivalentLength: equivalentLengthsCompleted,
|
||||||
frictionRate: componentLosses > 0
|
frictionRate: model.componentLosses.count > 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
getSensibleHeatRatio: { id in
|
getSensibleHeatRatio: { id in
|
||||||
guard
|
guard
|
||||||
let shr = try await ProjectModel.query(on: database)
|
let model = try await ProjectModel.query(on: database)
|
||||||
.field(\.$id)
|
.field(\.$id)
|
||||||
.field(\.$sensibleHeatRatio)
|
.field(\.$sensibleHeatRatio)
|
||||||
.filter(\.$id == id)
|
.filter(\.$id == id)
|
||||||
@@ -107,7 +67,7 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
|||||||
else {
|
else {
|
||||||
throw NotFoundError()
|
throw NotFoundError()
|
||||||
}
|
}
|
||||||
return shr.sensibleHeatRatio
|
return model.sensibleHeatRatio
|
||||||
},
|
},
|
||||||
fetch: { userID, request in
|
fetch: { userID, request in
|
||||||
try await ProjectModel.query(on: database)
|
try await ProjectModel.query(on: database)
|
||||||
@@ -227,7 +187,7 @@ extension Project {
|
|||||||
.field("sensibleHeatRatio", .double)
|
.field("sensibleHeatRatio", .double)
|
||||||
.field("createdAt", .datetime)
|
.field("createdAt", .datetime)
|
||||||
.field("updatedAt", .datetime)
|
.field("updatedAt", .datetime)
|
||||||
.field("userID", .uuid, .required, .references(UserModel.schema, "id"))
|
.field("userID", .uuid, .required, .references(UserModel.schema, "id", onDelete: .cascade))
|
||||||
.unique(on: "userID", "name")
|
.unique(on: "userID", "name")
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
@@ -350,4 +310,35 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
self.sensibleHeatRatio = sensibleHeatRatio
|
self.sensibleHeatRatio = sensibleHeatRatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a ``ProjectModel`` with all the relations eagerly loaded.
|
||||||
|
static func fetchDetail(
|
||||||
|
for projectID: Project.ID,
|
||||||
|
on database: any Database
|
||||||
|
) async throws -> ProjectModel {
|
||||||
|
guard
|
||||||
|
let model =
|
||||||
|
try await ProjectModel.query(on: database)
|
||||||
|
.with(\.$componentLosses)
|
||||||
|
.with(\.$equipment)
|
||||||
|
.with(\.$equivalentLengths)
|
||||||
|
.with(\.$rooms)
|
||||||
|
.with(
|
||||||
|
\.$trunks,
|
||||||
|
{ trunk in
|
||||||
|
trunk.with(
|
||||||
|
\.$rooms,
|
||||||
|
{
|
||||||
|
$0.with(\.$room)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.filter(\.$id == projectID)
|
||||||
|
.first()
|
||||||
|
else {
|
||||||
|
throw NotFoundError()
|
||||||
|
}
|
||||||
|
return model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ extension DatabaseClient.Users: TestDependencyKey {
|
|||||||
throw NotFoundError()
|
throw NotFoundError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the password matches the user's hashed password.
|
||||||
|
guard try user.verifyPassword(request.password) else {
|
||||||
|
throw Abort(.unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
let token: User.Token
|
let token: User.Token
|
||||||
|
|
||||||
// Check if there's a user token
|
// Check if there's a user token
|
||||||
|
|||||||
@@ -160,16 +160,16 @@ extension EquivalentLength {
|
|||||||
/// The longest return equivalent length.
|
/// The longest return equivalent length.
|
||||||
public let `return`: EquivalentLength?
|
public let `return`: EquivalentLength?
|
||||||
|
|
||||||
|
public init(supply: EquivalentLength? = nil, return: EquivalentLength? = nil) {
|
||||||
|
self.supply = supply
|
||||||
|
self.return = `return`
|
||||||
|
}
|
||||||
|
|
||||||
public var totalEquivalentLength: Double? {
|
public var totalEquivalentLength: Double? {
|
||||||
guard let supply else { return nil }
|
guard let supply else { return nil }
|
||||||
guard let `return` else { return nil }
|
guard let `return` else { return nil }
|
||||||
return supply.totalEquivalentLength + `return`.totalEquivalentLength
|
return supply.totalEquivalentLength + `return`.totalEquivalentLength
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(supply: EquivalentLength? = nil, return: EquivalentLength? = nil) {
|
|
||||||
self.supply = supply
|
|
||||||
self.return = `return`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import Dependencies
|
|||||||
import Fluent
|
import Fluent
|
||||||
import FluentSQLiteDriver
|
import FluentSQLiteDriver
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import ManualDCore
|
||||||
import NIO
|
import NIO
|
||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
// Helper to create an in-memory database for testing.
|
// Helper to create an in-memory database used for testing.
|
||||||
func withDatabase(
|
func withDatabase(
|
||||||
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||||
operation: () async throws -> Void
|
operation: () async throws -> Void
|
||||||
@@ -37,3 +38,18 @@ func withDatabase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set's up the database and a test user for running tests that require a
|
||||||
|
/// a user.
|
||||||
|
func withTestUser(
|
||||||
|
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: (User) async throws -> Void
|
||||||
|
) async throws {
|
||||||
|
try await withDatabase(setupDependencies: setupDependencies) {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
let user = try await users.create(
|
||||||
|
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||||
|
)
|
||||||
|
try await operation(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,18 +12,155 @@ import Vapor
|
|||||||
struct ProjectTests {
|
struct ProjectTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func sanity() {
|
func projectHappyPaths() async throws {
|
||||||
#expect(Bool(true))
|
try await withTestUser { user in
|
||||||
|
@Dependency(\.database.projects) var projects
|
||||||
|
|
||||||
|
let project = try await projects.create(user.id, .mock)
|
||||||
|
|
||||||
|
let got = try await projects.get(project.id)
|
||||||
|
#expect(got == project)
|
||||||
|
|
||||||
|
let page = try await projects.fetch(user.id, .init(page: 1, per: 25))
|
||||||
|
#expect(page.items.first! == project)
|
||||||
|
|
||||||
|
let updated = try await projects.update(project.id, .init(sensibleHeatRatio: 0.83))
|
||||||
|
#expect(updated.sensibleHeatRatio == 0.83)
|
||||||
|
#expect(updated.id == project.id)
|
||||||
|
|
||||||
|
let shr = try await projects.getSensibleHeatRatio(project.id)
|
||||||
|
#expect(shr == 0.83)
|
||||||
|
|
||||||
|
try await projects.delete(project.id)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
}
|
||||||
// func createProject() {
|
|
||||||
// try await withDatabase(migrations: Project.Migrate()) {
|
@Test
|
||||||
// $0.database.projects = .live(database: $1)
|
func notFound() async throws {
|
||||||
// } operation: {
|
try await withDatabase {
|
||||||
// @Dependency(\.database.projects) var projects
|
@Dependency(\.database.projects) var projects
|
||||||
//
|
|
||||||
// let project = try await projects.c
|
await #expect(throws: NotFoundError.self) {
|
||||||
// }
|
try await projects.delete(UUID(0))
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await projects.update(UUID(0), .init(name: "Foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await projects.getSensibleHeatRatio(UUID(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await projects.getCompletedSteps(UUID(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func completedSteps() async throws {
|
||||||
|
try await withTestUser { user in
|
||||||
|
@Dependency(\.database) var database
|
||||||
|
|
||||||
|
let project = try await database.projects.create(user.id, .mock)
|
||||||
|
|
||||||
|
var completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
#expect(completed.equipmentInfo == false)
|
||||||
|
#expect(completed.equivalentLength == false)
|
||||||
|
#expect(completed.frictionRate == false)
|
||||||
|
#expect(completed.rooms == false)
|
||||||
|
|
||||||
|
_ = try await database.equipment.create(
|
||||||
|
.init(projectID: project.id, heatingCFM: 1000, coolingCFM: 1000)
|
||||||
|
)
|
||||||
|
completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
#expect(completed.equipmentInfo == true)
|
||||||
|
|
||||||
|
_ = try await database.componentLosses.create(
|
||||||
|
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||||
|
)
|
||||||
|
completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
#expect(completed.frictionRate == true)
|
||||||
|
|
||||||
|
_ = try await database.rooms.create(
|
||||||
|
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||||
|
)
|
||||||
|
completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
#expect(completed.rooms == true)
|
||||||
|
|
||||||
|
_ = try await database.equivalentLengths.create(
|
||||||
|
.init(
|
||||||
|
projectID: project.id, name: "Supply", type: .supply, straightLengths: [1], groups: [])
|
||||||
|
)
|
||||||
|
completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
// Should not be complete until we have both return and supply for a project.
|
||||||
|
#expect(completed.equivalentLength == false)
|
||||||
|
|
||||||
|
_ = try await database.equivalentLengths.create(
|
||||||
|
.init(
|
||||||
|
projectID: project.id, name: "Return", type: .return, straightLengths: [1], groups: [])
|
||||||
|
)
|
||||||
|
completed = try await database.projects.getCompletedSteps(project.id)
|
||||||
|
#expect(completed.equipmentInfo == true)
|
||||||
|
#expect(completed.equivalentLength == true)
|
||||||
|
#expect(completed.frictionRate == true)
|
||||||
|
#expect(completed.rooms == true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func detail() async throws {
|
||||||
|
try await withTestUser { user in
|
||||||
|
@Dependency(\.database) var database
|
||||||
|
let project = try await database.projects.create(user.id, .mock)
|
||||||
|
|
||||||
|
var detail = try await database.projects.detail(project.id)
|
||||||
|
#expect(detail == nil)
|
||||||
|
|
||||||
|
let equipment = try await database.equipment.create(
|
||||||
|
.init(projectID: project.id, heatingCFM: 1000, coolingCFM: 1000)
|
||||||
|
)
|
||||||
|
detail = try await database.projects.detail(project.id)
|
||||||
|
#expect(detail != nil)
|
||||||
|
|
||||||
|
let componentLoss = try await database.componentLosses.create(
|
||||||
|
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||||
|
)
|
||||||
|
let room = try await database.rooms.create(
|
||||||
|
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||||
|
)
|
||||||
|
let supplyLength = try await database.equivalentLengths.create(
|
||||||
|
.init(
|
||||||
|
projectID: project.id, name: "Supply", type: .supply, straightLengths: [1], groups: [])
|
||||||
|
)
|
||||||
|
let returnLength = try await database.equivalentLengths.create(
|
||||||
|
.init(
|
||||||
|
projectID: project.id, name: "Return", type: .return, straightLengths: [1], groups: [])
|
||||||
|
)
|
||||||
|
detail = try await database.projects.detail(project.id)
|
||||||
|
#expect(detail?.componentLosses == [componentLoss])
|
||||||
|
#expect(detail?.equipmentInfo == equipment)
|
||||||
|
#expect(detail?.rooms == [room])
|
||||||
|
#expect(detail?.equivalentLengths.contains(supplyLength) == true)
|
||||||
|
#expect(detail?.equivalentLengths.contains(returnLength) == true)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Project.Create {
|
||||||
|
|
||||||
|
static let mock = Self(
|
||||||
|
name: "Testy McTestface",
|
||||||
|
streetAddress: "1234 Sesame St",
|
||||||
|
city: "Nowhere",
|
||||||
|
state: "MN",
|
||||||
|
zipCode: "55555",
|
||||||
|
sensibleHeatRatio: 0.83
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Dependencies
|
|||||||
import Foundation
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
import Testing
|
import Testing
|
||||||
|
import Vapor
|
||||||
|
|
||||||
@testable import DatabaseClient
|
@testable import DatabaseClient
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ import Testing
|
|||||||
struct UserDatabaseTests {
|
struct UserDatabaseTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func createUser() async throws {
|
func happyPaths() async throws {
|
||||||
try await withDatabase {
|
try await withDatabase {
|
||||||
@Dependency(\.database.users) var users
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
@@ -22,8 +23,14 @@ struct UserDatabaseTests {
|
|||||||
|
|
||||||
// Test login the user in
|
// Test login the user in
|
||||||
let token = try await users.login(
|
let token = try await users.login(
|
||||||
.init(email: "testy@example.com", password: "super-secret")
|
.init(email: user.email, password: "super-secret")
|
||||||
)
|
)
|
||||||
|
#expect(token.userID == user.id)
|
||||||
|
// Test the same token is returned.
|
||||||
|
let token2 = try await users.login(
|
||||||
|
.init(email: user.email, password: "super-secret")
|
||||||
|
)
|
||||||
|
#expect(token.id == token2.id)
|
||||||
// Test logging out
|
// Test logging out
|
||||||
try await users.logout(token.id)
|
try await users.logout(token.id)
|
||||||
|
|
||||||
@@ -31,7 +38,6 @@ struct UserDatabaseTests {
|
|||||||
|
|
||||||
let shouldBeNilUser = try await users.get(user.id)
|
let shouldBeNilUser = try await users.get(user.id)
|
||||||
#expect(shouldBeNilUser == nil)
|
#expect(shouldBeNilUser == nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,4 +79,73 @@ struct UserDatabaseTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func loginFails() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await users.login(
|
||||||
|
.init(email: "foo@example.com", password: "super-secret")
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = try await users.create(
|
||||||
|
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure can not login with invalid password
|
||||||
|
await #expect(throws: Abort.self) {
|
||||||
|
try await users.login(
|
||||||
|
.init(email: user.email, password: "wrong-password")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func userProfileHappyPath() async throws {
|
||||||
|
try await withTestUser { user in
|
||||||
|
@Dependency(\.database.userProfiles) var profiles
|
||||||
|
let profile = try await profiles.create(
|
||||||
|
.init(
|
||||||
|
userID: user.id,
|
||||||
|
firstName: "Testy",
|
||||||
|
lastName: "McTestface",
|
||||||
|
companyName: "Acme Co.",
|
||||||
|
streetAddress: "12345 Sesame St",
|
||||||
|
city: "Nowhere",
|
||||||
|
state: "FL",
|
||||||
|
zipCode: "55555"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let fetched = try await profiles.fetch(user.id)
|
||||||
|
#expect(fetched == profile)
|
||||||
|
|
||||||
|
let got = try await profiles.get(profile.id)
|
||||||
|
#expect(got == profile)
|
||||||
|
|
||||||
|
let updated = try await profiles.update(profile.id, .init(firstName: "Updated"))
|
||||||
|
#expect(updated.firstName == "Updated")
|
||||||
|
#expect(updated.id == profile.id)
|
||||||
|
|
||||||
|
try await profiles.delete(profile.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func testUserProfileFails() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.userProfiles) var profiles
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await profiles.delete(UUID(0))
|
||||||
|
}
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await profiles.update(UUID(0), .init(firstName: "Foo"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user