feat: Begins live database client tests.
All checks were successful
CI / Linux Tests (push) Successful in 5m35s
All checks were successful
CI / Linux Tests (push) Successful in 5m35s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,3 +13,4 @@ tailwindcss
|
|||||||
*.pdf
|
*.pdf
|
||||||
.env
|
.env
|
||||||
.env*
|
.env*
|
||||||
|
default.profraw
|
||||||
|
|||||||
@@ -87,7 +87,15 @@ let package = Package(
|
|||||||
.product(name: "Vapor", package: "vapor"),
|
.product(name: "Vapor", package: "vapor"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "DatabaseClientTests",
|
||||||
|
dependencies: [
|
||||||
|
.target(name: "App"),
|
||||||
|
.target(name: "DatabaseClient"),
|
||||||
|
.product(name: "DependenciesTestSupport", package: "swift-dependencies"),
|
||||||
|
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
|
||||||
|
]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "EnvClient",
|
name: "EnvClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ private func setupDatabase(
|
|||||||
|
|
||||||
let databaseClient = makeDatabaseClient(app.db)
|
let databaseClient = makeDatabaseClient(app.db)
|
||||||
|
|
||||||
if app.environment != .testing {
|
|
||||||
try await app.migrations.add(databaseClient.migrations())
|
try await app.migrations.add(databaseClient.migrations())
|
||||||
}
|
|
||||||
|
|
||||||
return databaseClient
|
return databaseClient
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import DependenciesMacros
|
|||||||
import Fluent
|
import Fluent
|
||||||
import Foundation
|
import Foundation
|
||||||
import ManualDCore
|
import ManualDCore
|
||||||
|
import SQLKit
|
||||||
|
|
||||||
extension DatabaseClient.ComponentLosses: TestDependencyKey {
|
extension DatabaseClient.ComponentLosses: TestDependencyKey {
|
||||||
public static let testValue = Self()
|
public static let testValue = Self()
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ extension Snapshotting where Value == (any HTML), Format == String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Snapshotting where Value == String, Format == String {
|
// extension Snapshotting where Value == String, Format == String {
|
||||||
public static var html: Snapshotting {
|
// public static var html: Snapshotting {
|
||||||
var snapshotting = SimplySnapshotting.lines
|
// var snapshotting = SimplySnapshotting.lines
|
||||||
.pullback { $0 }
|
// .pullback { $0 }
|
||||||
|
//
|
||||||
snapshotting.pathExtension = "html"
|
// snapshotting.pathExtension = "html"
|
||||||
return snapshotting
|
// return snapshotting
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ public struct PdfClient: Sendable {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - request: The project data used to generate the pdf.
|
/// - request: The project data used to generate the pdf.
|
||||||
public func generatePdf(request: Request) async throws -> Response {
|
public func generatePdf(request: Request) async throws -> Response {
|
||||||
let html = try await self.html(request)
|
try await self.generatePdf(request.project.id, html(request))
|
||||||
return try await self.generatePdf(request.project.id, html)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -76,17 +75,26 @@ extension PdfClient: DependencyKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension PdfClient {
|
extension PdfClient {
|
||||||
/// Container for the data required to generate a pdf for a given project.
|
/// Represents the data required to generate a pdf for a given project.
|
||||||
public struct Request: Codable, Equatable, Sendable {
|
public struct Request: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The project we're generating a pdf for.
|
||||||
public let project: Project
|
public let project: Project
|
||||||
|
/// The rooms in the project.
|
||||||
public let rooms: [Room]
|
public let rooms: [Room]
|
||||||
|
/// The component pressure losses for the project.
|
||||||
public let componentLosses: [ComponentPressureLoss]
|
public let componentLosses: [ComponentPressureLoss]
|
||||||
|
/// The calculated duct sizes for the project.
|
||||||
public let ductSizes: DuctSizes
|
public let ductSizes: DuctSizes
|
||||||
|
/// The equipment information for the project.
|
||||||
public let equipmentInfo: EquipmentInfo
|
public let equipmentInfo: EquipmentInfo
|
||||||
|
/// The max supply equivalent length for the project.
|
||||||
public let maxSupplyTEL: EquivalentLength
|
public let maxSupplyTEL: EquivalentLength
|
||||||
|
/// The max return equivalent length for the project.
|
||||||
public let maxReturnTEL: EquivalentLength
|
public let maxReturnTEL: EquivalentLength
|
||||||
|
/// The calculated design friction rate for the project.
|
||||||
public let frictionRate: FrictionRate
|
public let frictionRate: FrictionRate
|
||||||
|
/// The project wide sensible heat ratio.
|
||||||
public let projectSHR: Double
|
public let projectSHR: Double
|
||||||
|
|
||||||
var totalEquivalentLength: Double {
|
var totalEquivalentLength: Double {
|
||||||
@@ -116,9 +124,12 @@ extension PdfClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the response after generating a pdf.
|
||||||
public struct Response: Equatable, Sendable {
|
public struct Response: Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The path to the html file used to generate the pdf from.
|
||||||
public let htmlPath: String
|
public let htmlPath: String
|
||||||
|
/// The path to the pdf file.
|
||||||
public let pdfPath: String
|
public let pdfPath: String
|
||||||
|
|
||||||
public init(htmlPath: String, pdfPath: String) {
|
public init(htmlPath: String, pdfPath: String) {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
public struct ProjectClientError: Error {
|
|
||||||
public let reason: String
|
|
||||||
|
|
||||||
public init(_ reason: String) {
|
|
||||||
self.reason = reason
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,9 +18,12 @@ extension DependencyValues {
|
|||||||
/// for the view controller to render views.
|
/// for the view controller to render views.
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ProjectClient: Sendable {
|
public struct ProjectClient: Sendable {
|
||||||
public var calculateDuctSizes: @Sendable (Project.ID) async throws -> DuctSizes
|
|
||||||
|
/// Calculates the room duct sizes for the given project.
|
||||||
public var calculateRoomDuctSizes:
|
public var calculateRoomDuctSizes:
|
||||||
@Sendable (Project.ID) async throws -> [DuctSizes.RoomContainer]
|
@Sendable (Project.ID) async throws -> [DuctSizes.RoomContainer]
|
||||||
|
|
||||||
|
/// Calculates the trunk duct sizes for the given project.
|
||||||
public var calculateTrunkDuctSizes:
|
public var calculateTrunkDuctSizes:
|
||||||
@Sendable (Project.ID) async throws -> [DuctSizes.TrunkContainer]
|
@Sendable (Project.ID) async throws -> [DuctSizes.TrunkContainer]
|
||||||
|
|
||||||
@@ -28,6 +31,14 @@ public struct ProjectClient: Sendable {
|
|||||||
@Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse
|
@Sendable (User.ID, Project.Create) async throws -> CreateProjectResponse
|
||||||
|
|
||||||
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
public var frictionRate: @Sendable (Project.ID) async throws -> FrictionRateResponse
|
||||||
|
public var generatePdf: @Sendable (Project.ID) async throws -> Response
|
||||||
|
|
||||||
|
public func calculateDuctSizes(_ projectID: Project.ID) async throws -> DuctSizes {
|
||||||
|
.init(
|
||||||
|
rooms: try await calculateRoomDuctSizes(projectID),
|
||||||
|
trunks: try await calculateTrunkDuctSizes(projectID)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProjectClient: TestDependencyKey {
|
extension ProjectClient: TestDependencyKey {
|
||||||
|
|||||||
@@ -9,32 +9,18 @@ extension DatabaseClient {
|
|||||||
details: Project.Detail
|
details: Project.Detail
|
||||||
) async throws -> (DuctSizes, DuctSizeSharedRequest) {
|
) async throws -> (DuctSizes, DuctSizeSharedRequest) {
|
||||||
let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
|
let (rooms, shared) = try await calculateRoomDuctSizes(details: details)
|
||||||
let (trunks, _) = try await calculateTrunkDuctSizes(details: details)
|
|
||||||
return (.init(rooms: rooms, trunks: trunks), shared)
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateDuctSizes(
|
|
||||||
projectID: Project.ID
|
|
||||||
) async throws -> (DuctSizes, DuctSizeSharedRequest, [Room]) {
|
|
||||||
@Dependency(\.manualD) var manualD
|
|
||||||
|
|
||||||
let shared = try await sharedDuctRequest(projectID)
|
|
||||||
let rooms = try await rooms.fetch(projectID)
|
|
||||||
|
|
||||||
return try await (
|
return try await (
|
||||||
manualD.calculateDuctSizes(
|
.init(
|
||||||
rooms: rooms,
|
rooms: rooms,
|
||||||
trunks: trunkSizes.fetch(projectID),
|
trunks: calculateTrunkDuctSizes(details: details, shared: shared)
|
||||||
sharedRequest: shared
|
|
||||||
),
|
),
|
||||||
shared,
|
shared
|
||||||
rooms
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateRoomDuctSizes(
|
func calculateRoomDuctSizes(
|
||||||
details: Project.Detail
|
details: Project.Detail
|
||||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
) async throws -> (rooms: [DuctSizes.RoomContainer], shared: DuctSizeSharedRequest) {
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
let shared = try sharedDuctRequest(details: details)
|
let shared = try sharedDuctRequest(details: details)
|
||||||
@@ -42,54 +28,29 @@ extension DatabaseClient {
|
|||||||
return (rooms, shared)
|
return (rooms, shared)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateRoomDuctSizes(
|
func calculateTrunkDuctSizes(
|
||||||
projectID: Project.ID
|
details: Project.Detail,
|
||||||
) async throws -> ([DuctSizes.RoomContainer], DuctSizeSharedRequest) {
|
shared: DuctSizeSharedRequest? = nil
|
||||||
|
) async throws -> [DuctSizes.TrunkContainer] {
|
||||||
@Dependency(\.manualD) var manualD
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
let shared = try await sharedDuctRequest(projectID)
|
let sharedRequest: DuctSizeSharedRequest
|
||||||
|
if let shared {
|
||||||
return try await (
|
sharedRequest = shared
|
||||||
manualD.calculateRoomSizes(
|
} else {
|
||||||
rooms: rooms.fetch(projectID),
|
sharedRequest = try sharedDuctRequest(details: details)
|
||||||
sharedRequest: shared
|
|
||||||
),
|
|
||||||
shared
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateTrunkDuctSizes(
|
return try await manualD.calculateTrunkSizes(
|
||||||
details: Project.Detail
|
|
||||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
|
||||||
@Dependency(\.manualD) var manualD
|
|
||||||
|
|
||||||
let shared = try sharedDuctRequest(details: details)
|
|
||||||
let trunks = try await manualD.calculateTrunkSizes(
|
|
||||||
rooms: details.rooms,
|
rooms: details.rooms,
|
||||||
trunks: details.trunks,
|
trunks: details.trunks,
|
||||||
sharedRequest: shared
|
sharedRequest: sharedRequest
|
||||||
)
|
|
||||||
return (trunks, shared)
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateTrunkDuctSizes(
|
|
||||||
projectID: Project.ID
|
|
||||||
) async throws -> ([DuctSizes.TrunkContainer], DuctSizeSharedRequest) {
|
|
||||||
@Dependency(\.manualD) var manualD
|
|
||||||
|
|
||||||
let shared = try await sharedDuctRequest(projectID)
|
|
||||||
|
|
||||||
return try await (
|
|
||||||
manualD.calculateTrunkSizes(
|
|
||||||
rooms: rooms.fetch(projectID),
|
|
||||||
trunks: trunkSizes.fetch(projectID),
|
|
||||||
sharedRequest: shared
|
|
||||||
),
|
|
||||||
shared
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
|
func sharedDuctRequest(details: Project.Detail) throws -> DuctSizeSharedRequest {
|
||||||
|
let projectSHR = try details.project.ensuredSHR()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let dfrResponse = designFrictionRate(
|
let dfrResponse = designFrictionRate(
|
||||||
componentLosses: details.componentLosses,
|
componentLosses: details.componentLosses,
|
||||||
@@ -100,10 +61,6 @@ extension DatabaseClient {
|
|||||||
throw ProjectClientError("Project not complete.")
|
throw ProjectClientError("Project not complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let projectSHR = details.project.sensibleHeatRatio else {
|
|
||||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
@@ -115,32 +72,6 @@ extension DatabaseClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sharedDuctRequest(_ projectID: Project.ID) async throws -> DuctSizeSharedRequest {
|
|
||||||
|
|
||||||
guard let dfrResponse = try await designFrictionRate(projectID: projectID) else {
|
|
||||||
throw ProjectClientError("Project not complete.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let ensuredTEL = try dfrResponse.ensureMaxContainer()
|
|
||||||
|
|
||||||
return try await .init(
|
|
||||||
equipmentInfo: dfrResponse.equipmentInfo,
|
|
||||||
maxSupplyLength: ensuredTEL.supply,
|
|
||||||
maxReturnLenght: ensuredTEL.return,
|
|
||||||
designFrictionRate: dfrResponse.designFrictionRate,
|
|
||||||
projectSHR: ensuredSHR(projectID)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the project sensible heat ratio or throws an error if it's nil.
|
|
||||||
func ensuredSHR(_ projectID: Project.ID) async throws -> Double {
|
|
||||||
guard let projectSHR = try await projects.getSensibleHeatRatio(projectID) else {
|
|
||||||
throw ProjectClientError("Project sensible heat ratio not set.")
|
|
||||||
}
|
|
||||||
return projectSHR
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal container.
|
// Internal container.
|
||||||
struct DesignFrictionRateResponse: Equatable, Sendable {
|
struct DesignFrictionRateResponse: Equatable, Sendable {
|
||||||
|
|
||||||
@@ -183,18 +114,13 @@ extension DatabaseClient {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func designFrictionRate(
|
}
|
||||||
projectID: Project.ID
|
|
||||||
) async throws -> DesignFrictionRateResponse? {
|
|
||||||
|
|
||||||
guard let equipmentInfo = try await equipment.fetch(projectID) else {
|
extension Project {
|
||||||
return nil
|
func ensuredSHR() throws -> Double {
|
||||||
|
guard let shr = sensibleHeatRatio else {
|
||||||
|
throw ProjectClientError("Sensible heat ratio not set on project id: \(id)")
|
||||||
}
|
}
|
||||||
|
return shr
|
||||||
return try await designFrictionRate(
|
|
||||||
componentLosses: componentLosses.fetch(projectID),
|
|
||||||
equipmentInfo: equipmentInfo,
|
|
||||||
equivalentLengths: equivalentLengths.fetchMax(projectID)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import ManualDClient
|
||||||
|
import ManualDCore
|
||||||
|
import PdfClient
|
||||||
|
|
||||||
|
extension DatabaseClient {
|
||||||
|
|
||||||
|
/// Generate a pdf request for the given project.
|
||||||
|
func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
||||||
|
@Dependency(\.manualD) var manualD
|
||||||
|
|
||||||
|
guard let projectDetails = try await projects.detail(projectID) else {
|
||||||
|
throw ProjectClientError.notFound(.project(projectID))
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
|
||||||
|
|
||||||
|
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
|
||||||
|
guard let frictionRate = frictionRateResponse.frictionRate else {
|
||||||
|
throw ProjectClientError.notFound(.frictionRate(projectID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
details: projectDetails,
|
||||||
|
ductSizes: ductSizes,
|
||||||
|
shared: shared,
|
||||||
|
frictionRate: frictionRate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PdfClient.Request {
|
||||||
|
fileprivate init(
|
||||||
|
details: Project.Detail,
|
||||||
|
ductSizes: DuctSizes,
|
||||||
|
shared: DuctSizeSharedRequest,
|
||||||
|
frictionRate: FrictionRate
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
project: details.project,
|
||||||
|
rooms: details.rooms,
|
||||||
|
componentLosses: details.componentLosses,
|
||||||
|
ductSizes: ductSizes,
|
||||||
|
equipmentInfo: details.equipmentInfo,
|
||||||
|
maxSupplyTEL: shared.maxSupplyLength,
|
||||||
|
maxReturnTEL: shared.maxReturnLenght,
|
||||||
|
frictionRate: frictionRate,
|
||||||
|
projectSHR: shared.projectSHR
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,14 +15,17 @@ extension ProjectClient: DependencyKey {
|
|||||||
@Dependency(\.fileClient) var fileClient
|
@Dependency(\.fileClient) var fileClient
|
||||||
|
|
||||||
return .init(
|
return .init(
|
||||||
calculateDuctSizes: { projectID in
|
|
||||||
try await database.calculateDuctSizes(projectID: projectID).0
|
|
||||||
},
|
|
||||||
calculateRoomDuctSizes: { projectID in
|
calculateRoomDuctSizes: { projectID in
|
||||||
try await database.calculateRoomDuctSizes(projectID: projectID).0
|
guard let details = try await database.projects.detail(projectID) else {
|
||||||
|
throw ProjectClientError.notFound(.project(projectID))
|
||||||
|
}
|
||||||
|
return try await database.calculateRoomDuctSizes(details: details).rooms
|
||||||
},
|
},
|
||||||
calculateTrunkDuctSizes: { projectID in
|
calculateTrunkDuctSizes: { projectID in
|
||||||
try await database.calculateTrunkDuctSizes(projectID: projectID).0
|
guard let details = try await database.projects.detail(projectID) else {
|
||||||
|
throw ProjectClientError.notFound(.project(projectID))
|
||||||
|
}
|
||||||
|
return try await database.calculateTrunkDuctSizes(details: details)
|
||||||
},
|
},
|
||||||
createProject: { userID, request in
|
createProject: { userID, request in
|
||||||
let project = try await database.projects.create(userID, request)
|
let project = try await database.projects.create(userID, request)
|
||||||
@@ -58,50 +61,3 @@ extension ProjectClient: DependencyKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DatabaseClient {
|
|
||||||
|
|
||||||
fileprivate func makePdfRequest(_ projectID: Project.ID) async throws -> PdfClient.Request {
|
|
||||||
@Dependency(\.manualD) var manualD
|
|
||||||
|
|
||||||
guard let projectDetails = try await projects.detail(projectID) else {
|
|
||||||
throw ProjectClientError("Project not found. id: \(projectID)")
|
|
||||||
}
|
|
||||||
|
|
||||||
let (ductSizes, shared) = try await calculateDuctSizes(details: projectDetails)
|
|
||||||
|
|
||||||
let frictionRateResponse = try await manualD.frictionRate(details: projectDetails)
|
|
||||||
guard let frictionRate = frictionRateResponse.frictionRate else {
|
|
||||||
throw ProjectClientError("Friction rate not found. id: \(projectID)")
|
|
||||||
}
|
|
||||||
|
|
||||||
return .init(
|
|
||||||
details: projectDetails,
|
|
||||||
ductSizes: ductSizes,
|
|
||||||
shared: shared,
|
|
||||||
frictionRate: frictionRate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PdfClient.Request {
|
|
||||||
init(
|
|
||||||
details: Project.Detail,
|
|
||||||
ductSizes: DuctSizes,
|
|
||||||
shared: DuctSizeSharedRequest,
|
|
||||||
frictionRate: FrictionRate
|
|
||||||
) {
|
|
||||||
self.init(
|
|
||||||
project: details.project,
|
|
||||||
rooms: details.rooms,
|
|
||||||
componentLosses: details.componentLosses,
|
|
||||||
ductSizes: ductSizes,
|
|
||||||
equipmentInfo: details.equipmentInfo,
|
|
||||||
maxSupplyTEL: shared.maxSupplyLength,
|
|
||||||
maxReturnTEL: shared.maxReturnLenght,
|
|
||||||
frictionRate: frictionRate,
|
|
||||||
projectSHR: shared.projectSHR
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
Sources/ProjectClient/ProjectClientError.swift
Normal file
35
Sources/ProjectClient/ProjectClientError.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Foundation
|
||||||
|
import ManualDCore
|
||||||
|
|
||||||
|
public struct ProjectClientError: Error {
|
||||||
|
public let reason: String
|
||||||
|
|
||||||
|
public init(_ reason: String) {
|
||||||
|
self.reason = reason
|
||||||
|
}
|
||||||
|
|
||||||
|
static func notFound(_ notFound: NotFound) -> Self {
|
||||||
|
.init(notFound.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotFound {
|
||||||
|
case project(Project.ID)
|
||||||
|
case frictionRate(Project.ID)
|
||||||
|
|
||||||
|
var reason: String {
|
||||||
|
switch self {
|
||||||
|
case .project(let id):
|
||||||
|
return "Project not found. id: \(id)"
|
||||||
|
case .frictionRate(let id):
|
||||||
|
return """
|
||||||
|
Friction unable to be calculated. id: \(id)
|
||||||
|
|
||||||
|
This usually means that not all the required steps have been completed.
|
||||||
|
|
||||||
|
Calculating the friction rate requires the component pressure losses to be set and
|
||||||
|
have a max equivalent length for both the supply and return.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Tests/DatabaseClientTests/Helpers.swift
Normal file
39
Tests/DatabaseClientTests/Helpers.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import App
|
||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import Fluent
|
||||||
|
import FluentSQLiteDriver
|
||||||
|
import Foundation
|
||||||
|
import NIO
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
// Helper to create an in-memory database for testing.
|
||||||
|
func withDatabase(
|
||||||
|
setupDependencies: (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async throws {
|
||||||
|
let app = try await Application.make(.testing)
|
||||||
|
do {
|
||||||
|
try await configure(app)
|
||||||
|
let database = app.db
|
||||||
|
try await app.autoMigrate()
|
||||||
|
|
||||||
|
try await withDependencies {
|
||||||
|
$0.uuid = .incrementing
|
||||||
|
$0.date = .init { Date() }
|
||||||
|
$0.database = .live(database: database)
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
|
||||||
|
try await app.autoRevert()
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
try? await app.autoRevert()
|
||||||
|
try await app.asyncShutdown()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
Tests/DatabaseClientTests/ProjectTests.swift
Normal file
29
Tests/DatabaseClientTests/ProjectTests.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Dependencies
|
||||||
|
import DependenciesTestSupport
|
||||||
|
import Fluent
|
||||||
|
import FluentSQLiteDriver
|
||||||
|
import ManualDCore
|
||||||
|
import Testing
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
@testable import DatabaseClient
|
||||||
|
|
||||||
|
@Suite
|
||||||
|
struct ProjectTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func sanity() {
|
||||||
|
#expect(Bool(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// func createProject() {
|
||||||
|
// try await withDatabase(migrations: Project.Migrate()) {
|
||||||
|
// $0.database.projects = .live(database: $1)
|
||||||
|
// } operation: {
|
||||||
|
// @Dependency(\.database.projects) var projects
|
||||||
|
//
|
||||||
|
// let project = try await projects.c
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
76
Tests/DatabaseClientTests/UserTests.swift
Normal file
76
Tests/DatabaseClientTests/UserTests.swift
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import DatabaseClient
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import ManualDCore
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import DatabaseClient
|
||||||
|
|
||||||
|
@Suite
|
||||||
|
struct UserDatabaseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func createUser() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
|
let user = try await users.create(
|
||||||
|
.init(email: "testy@example.com", password: "super-secret", confirmPassword: "super-secret")
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(user.email == "testy@example.com")
|
||||||
|
|
||||||
|
// Test login the user in
|
||||||
|
let token = try await users.login(
|
||||||
|
.init(email: "testy@example.com", password: "super-secret")
|
||||||
|
)
|
||||||
|
// Test logging out
|
||||||
|
try await users.logout(token.id)
|
||||||
|
|
||||||
|
try await users.delete(user.id)
|
||||||
|
|
||||||
|
let shouldBeNilUser = try await users.get(user.id)
|
||||||
|
#expect(shouldBeNilUser == nil)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func createUserFails() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
|
||||||
|
await #expect(throws: ValidationError.self) {
|
||||||
|
try await users.create(.init(email: "", password: "", confirmPassword: ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
await #expect(throws: ValidationError.self) {
|
||||||
|
try await users.create(.init(email: "testy@example.com", password: "", confirmPassword: ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
await #expect(throws: ValidationError.self) {
|
||||||
|
try await users.create(
|
||||||
|
.init(email: "testy@example.com", password: "super-secret", confirmPassword: ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func deleteFailsWithInvalidUserID() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
await #expect(throws: NotFoundError.self) {
|
||||||
|
try await users.delete(UUID(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func logoutIgnoresUnfoundTokenID() async throws {
|
||||||
|
try await withDatabase {
|
||||||
|
@Dependency(\.database.users) var users
|
||||||
|
try await users.logout(UUID(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -100,6 +100,12 @@ struct ViewControllerTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mockDuctSizes = DuctSizes.mock(
|
||||||
|
equipmentInfo: equipment,
|
||||||
|
rooms: rooms,
|
||||||
|
trunks: trunks
|
||||||
|
)
|
||||||
|
|
||||||
try await withDefaultDependencies {
|
try await withDefaultDependencies {
|
||||||
$0.database.projects.get = { _ in project }
|
$0.database.projects.get = { _ in project }
|
||||||
$0.database.projects.getCompletedSteps = { _ in
|
$0.database.projects.getCompletedSteps = { _ in
|
||||||
@@ -113,8 +119,11 @@ struct ViewControllerTests {
|
|||||||
.init(supply: tels.first, return: tels.last)
|
.init(supply: tels.first, return: tels.last)
|
||||||
}
|
}
|
||||||
$0.database.componentLosses.fetch = { _ in componentLosses }
|
$0.database.componentLosses.fetch = { _ in componentLosses }
|
||||||
$0.projectClient.calculateDuctSizes = { _ in
|
$0.projectClient.calculateRoomDuctSizes = { _ in
|
||||||
.mock(equipmentInfo: equipment, rooms: rooms, trunks: trunks)
|
mockDuctSizes.rooms
|
||||||
|
}
|
||||||
|
$0.projectClient.calculateTrunkDuctSizes = { _ in
|
||||||
|
mockDuctSizes.trunks
|
||||||
}
|
}
|
||||||
} operation: {
|
} operation: {
|
||||||
@Dependency(\.viewController) var viewController
|
@Dependency(\.viewController) var viewController
|
||||||
|
|||||||
10
justfile
10
justfile
@@ -21,3 +21,13 @@ run-docker:
|
|||||||
|
|
||||||
test-docker: (build-docker "docker/Dockerfile.test")
|
test-docker: (build-docker "docker/Dockerfile.test")
|
||||||
@docker run --rm {{docker_image}}:{{docker_tag}} swift test
|
@docker run --rm {{docker_image}}:{{docker_tag}} swift test
|
||||||
|
|
||||||
|
code-coverage:
|
||||||
|
@llvm-cov report \
|
||||||
|
"$(find $(swift build --show-bin-path) -name '*.xctest')" \
|
||||||
|
-instr-profile=.build/debug/codecov/default.profdata \
|
||||||
|
-ignore-filename-regex=".build|Tests" \
|
||||||
|
-use-color
|
||||||
|
|
||||||
|
test:
|
||||||
|
@swift test --enable-code-coverage
|
||||||
|
|||||||
Reference in New Issue
Block a user