diff --git a/Package.swift b/Package.swift index b8b5cb4..4029233 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), // 🍃 An expressive, performant, and extensible templating language built for Swift. .package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"), - // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors + // 🔵 Non-blocking, event-driven networking for Swift. Used for, custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), .package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"), diff --git a/Sources/DatabaseClient/Employees.swift b/Sources/DatabaseClient/Employees.swift index e295eb1..dc83d7f 100644 --- a/Sources/DatabaseClient/Employees.swift +++ b/Sources/DatabaseClient/Employees.swift @@ -32,3 +32,70 @@ extension Employee.Update: Content {} extension DatabaseClient.Employees: TestDependencyKey { public static let testValue = Self() } + +#if DEBUG + + typealias EmployeeMockStorage = MockStorage< + Employee, + Employee.Create, + DatabaseClient.Employees.FetchRequest, + Void, + Employee.Update + > + + private extension EmployeeMockStorage { + + init(_ mocks: [Employee]) { + @Dependency(\.date.now) var now + @Dependency(\.uuid) var uuid + self.init( + mocks, + create: { employee in + Employee( + id: uuid(), + active: employee.active ?? true, + createdAt: now, + firstName: employee.firstName, + lastName: employee.lastName, + updatedAt: now + ) + }, + fetch: { request in + switch request { + case .all: + return { _ in true } + case .active: + return { $0.active == true } + case .inactive: + return { $0.active == false } + } + }, + update: { employee, updates in + let model = Employee( + id: employee.id, + active: updates.active ?? employee.active, + createdAt: employee.createdAt, + firstName: updates.firstName ?? employee.firstName, + lastName: updates.lastName ?? employee.lastName, + updatedAt: now + ) + + employee = model + } + ) + } + } + + public extension DatabaseClient.Employees { + static func mock(_ mocks: [Employee] = []) -> Self { + let storage = EmployeeMockStorage(mocks) + return .init( + create: { try await storage.create($0) }, + delete: { try await storage.delete($0) }, + fetchAll: { try await storage.fetchAll($0) }, + get: { try await storage.get($0) }, + update: { try await storage.update($0, $1) } + ) + } + } +#endif diff --git a/Sources/DatabaseClient/MockStorage.swift b/Sources/DatabaseClient/MockStorage.swift new file mode 100644 index 0000000..32d1622 --- /dev/null +++ b/Sources/DatabaseClient/MockStorage.swift @@ -0,0 +1,143 @@ +#if DEBUG + import Dependencies + import Vapor + + actor MockStorage< + Model: Identifiable, + Create: Sendable, + Fetch: Sendable, + Get: Sendable, + Update: Sendable + > where Model: Sendable { + @Dependency(\.date.now) var now + @Dependency(\.uuid) var uuid + + private var storage: [Model.ID: Model] + + private let modelFromCreate: @Sendable (Create) -> Model + private let fetchToPredicate: @Sendable (Fetch) -> ((Model) -> Bool) + private let fetchExtras: @Sendable (Fetch, [Model]) async throws -> [Model] + private let applyUpdates: @Sendable (inout Model, Update, Get?) async throws -> Void + private let getExtras: @Sendable (Get?, Model) async throws -> Model + + init( + _ mocks: [Model] = [], + create modelFromCreate: @Sendable @escaping (Create) -> Model, + fetch fetchToPredicate: @Sendable @escaping (Fetch) -> ((Model) -> Bool), + fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 }, + get getExtras: @Sendable @escaping (Get?, Model) async throws -> Model, + update applyUpdates: @Sendable @escaping (inout Model, Update, Get?) async throws -> Void + ) { + self.storage = mocks.reduce(into: [Model.ID: Model]()) { $0[$1.id] = $1 } + self.modelFromCreate = modelFromCreate + self.fetchToPredicate = fetchToPredicate + self.fetchExtras = fetchExtras + self.applyUpdates = applyUpdates + self.getExtras = getExtras + } + + func count() async throws -> Int { + storage.count + } + + func create(_ create: Create) async throws -> Model { + let model = modelFromCreate(create) + storage[model.id] = model + return model + } + + func delete(_ id: Model.ID) async throws { + storage[id] = nil + } + + func fetchAll(_ request: Fetch) async throws -> [Model] { + let predicate = fetchToPredicate(request) + return try await fetchExtras(request, Array(storage.values.filter { predicate($0) })) + } + + func get(_ id: Model.ID, _ request: Get?) async throws -> Model? { + if let model = storage[id] { + return try await getExtras(request, model) + } + return nil + } + + func update(_ id: Model.ID, _ updates: Update, _ get: Get?) async throws -> Model { + guard var model = storage[id] else { + throw Abort(.badRequest, reason: "Model not found.") + } + try await applyUpdates(&model, updates, get) + storage[id] = model + return model + } + } + + extension MockStorage where Get == Void { + init( + _ mocks: [Model] = [], + create modelFromCreate: @Sendable @escaping (Create) -> Model, + fetch fetchToPredicate: @Sendable @escaping (Fetch) -> ((Model) -> Bool), + fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 }, + update applyUpdates: @Sendable @escaping (inout Model, Update) async throws -> Void + ) { + self.init( + mocks, + create: modelFromCreate, + fetch: fetchToPredicate, + fetchExtras: fetchExtras, + get: { _, model in model }, + update: { model, updates, _ in try await applyUpdates(&model, updates) } + ) + } + + func get(_ id: Model.ID) async throws -> Model? { + storage[id] + } + + func update(_ id: Model.ID, _ updates: Update) async throws -> Model { + try await update(id, updates, ()) + } + + } + + extension MockStorage where Fetch == Void { + init( + _ mocks: [Model] = [], + create modelFromCreate: @Sendable @escaping (Create) -> Model, + fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 }, + get getExtras: @Sendable @escaping (Get?, Model) async throws -> Model, + update applyUpdates: @Sendable @escaping (inout Model, Update, Get?) async throws -> Void + ) { + self.init( + mocks, + create: modelFromCreate, + fetch: { _ in { _ in true } }, + fetchExtras: fetchExtras, + get: getExtras, + update: applyUpdates + ) + } + + func fetchAll() async throws -> [Model] { + try await fetchAll(()) + } + } + + extension MockStorage where Fetch == Void, Get == Void { + init( + _ mocks: [Model] = [], + create modelFromCreate: @Sendable @escaping (Create) -> Model, + fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 }, + update applyUpdates: @Sendable @escaping (inout Model, Update) async throws -> Void + ) { + self.init( + mocks, + create: modelFromCreate, + fetchExtras: fetchExtras, + get: { _, model in model }, + update: { model, updates, _ in try await applyUpdates(&model, updates) } + ) + } + + } +#endif diff --git a/Sources/DatabaseClient/VendorBranches.swift b/Sources/DatabaseClient/VendorBranches.swift index f39be7a..e82362b 100644 --- a/Sources/DatabaseClient/VendorBranches.swift +++ b/Sources/DatabaseClient/VendorBranches.swift @@ -15,7 +15,6 @@ public extension DatabaseClient { public enum FetchRequest: Equatable { case all case `for`(vendorID: Vendor.ID) - case withVendor } public func fetchAll() async throws -> [VendorBranch] { @@ -32,3 +31,61 @@ extension DatabaseClient.VendorBranches.FetchRequest: Content {} extension DatabaseClient.VendorBranches: TestDependencyKey { public static let testValue: DatabaseClient.VendorBranches = Self() } + +#if DEBUG + typealias VendorBranchMockStorage = MockStorage< + VendorBranch, + VendorBranch.Create, + DatabaseClient.VendorBranches.FetchRequest, + Void, + VendorBranch.Update + > + + private extension VendorBranchMockStorage { + + init(_ mocks: [VendorBranch]) { + @Dependency(\.date.now) var now + @Dependency(\.uuid) var uuid + + self.init( + mocks, + create: { + VendorBranch(id: uuid(), name: $0.name, vendorID: $0.vendorID, createdAt: now, updatedAt: now) + }, + fetch: { request in + switch request { + case .all: + return { _ in true } + case let .for(vendorID): + return { $0.vendorID == vendorID } + } + }, + update: { branch, updates in + let model = VendorBranch( + id: branch.id, + name: updates.name ?? branch.name, + vendorID: branch.vendorID, + createdAt: branch.createdAt, + updatedAt: now + ) + + branch = model + } + ) + } + } + + public extension DatabaseClient.VendorBranches { + static func mock(_ mocks: [VendorBranch] = []) -> Self { + let storage = VendorBranchMockStorage(mocks) + return .init( + create: { try await storage.create($0) }, + delete: { try await storage.delete($0) }, + fetchAll: { try await storage.fetchAll($0) }, + get: { try await storage.get($0) }, + update: { try await storage.update($0, $1) } + ) + } + } + +#endif diff --git a/Sources/DatabaseClient/Vendors.swift b/Sources/DatabaseClient/Vendors.swift index a40804c..12c2751 100644 --- a/Sources/DatabaseClient/Vendors.swift +++ b/Sources/DatabaseClient/Vendors.swift @@ -51,3 +51,94 @@ extension DatabaseClient.Vendors.GetRequest: Content {} extension DatabaseClient.Vendors: TestDependencyKey { public static let testValue: DatabaseClient.Vendors = Self() } + +#if DEBUG + + typealias VendorMockStorage = MockStorage< + Vendor, + Vendor.Create, + DatabaseClient.Vendors.FetchRequest, + DatabaseClient.Vendors.GetRequest, + Vendor.Update + > + + private extension VendorMockStorage { + + // swiftlint:disable function_body_length + static func vendors(_ mocks: [Vendor]) -> Self { + @Dependency(\.date.now) var now + @Dependency(\.uuid) var uuid + @Dependency(\.database.vendorBranches) var vendorBranches + + return .init( + mocks, + create: { + Vendor( + id: uuid(), + name: $0.name, + createdAt: now, + updatedAt: now + ) + }, + fetch: { _ in + { _ in true } + }, + fetchExtras: { request, models in + guard request == .withBranches else { return models } + let branches = try await vendorBranches.fetchAll() + return models.map { model in + Vendor( + id: model.id, + name: model.name, + branches: Array(branches.filter { $0.vendorID == model.id }), + createdAt: model.createdAt, + updatedAt: model.updatedAt + ) + } + }, + get: { req, model in + guard req == .withBranches else { return model } + let branches = try await vendorBranches.fetchAll(.for(vendorID: model.id)) + return Vendor( + id: model.id, + name: model.name, + branches: branches, + createdAt: model.createdAt, + updatedAt: model.updatedAt + ) + }, + update: { model, updates, get in + var branches: [VendorBranch]? + + if get == .withBranches { + branches = try await vendorBranches.fetchAll(.for(vendorID: model.id)) + } + + let vendor = Vendor( + id: model.id, + name: updates.name ?? model.name, + branches: branches ?? model.branches, + createdAt: model.createdAt, + updatedAt: now + ) + model = vendor + } + ) + } + // swiftlint:enable function_body_length + } + + public extension DatabaseClient.Vendors { + static func mock(_ mocks: [Vendor]) -> Self { + let storage = VendorMockStorage.vendors(mocks) + return .init( + create: { try await storage.create($0) }, + delete: { try await storage.delete($0) }, + fetchAll: { try await storage.fetchAll($0) }, + get: { try await storage.get($0, $1) }, + update: { req, updates, get in try await storage.update(req, updates, get) } + ) + } + } + +#endif diff --git a/Sources/DatabaseClientLive/VendorBranches.swift b/Sources/DatabaseClientLive/VendorBranches.swift index 60dab83..770797b 100644 --- a/Sources/DatabaseClientLive/VendorBranches.swift +++ b/Sources/DatabaseClientLive/VendorBranches.swift @@ -16,13 +16,11 @@ public extension DatabaseClient.VendorBranches { } try await model.delete(on: db) } fetchAll: { request in - var query = VendorBranchModel.query(on: db) + let query = VendorBranchModel.query(on: db) switch request { case .all: break - case .withVendor: - query = query.with(\.$vendor) case let .for(vendorID: vendorID): let branches = try await VendorModel.query(on: db) .filter(\.$id == vendorID) diff --git a/Tests/DatabaseClientTests/DatabaseClientTests.swift b/Tests/DatabaseClientTests/DatabaseClientTests.swift index 15e65f0..dd52856 100644 --- a/Tests/DatabaseClientTests/DatabaseClientTests.swift +++ b/Tests/DatabaseClientTests/DatabaseClientTests.swift @@ -1,6 +1,7 @@ @testable import DatabaseClientLive import Dependencies import FluentSQLiteDriver +import Foundation import Logging import NIO import SharedModels @@ -48,10 +49,12 @@ struct DatabaseClientTests { } } - @Test - func employees() async throws { + @Test(arguments: EmployeeTestFactory.testCases) + func employees(factory: EmployeeTestFactory) async throws { try await withDatabase(migrations: Employee.Migrate()) { - $0.database.employees = .live(database: $1) + $0.uuid = .incrementing + $0.date = .init { Date() } + $0.database.employees = factory.handler($1) } operation: { @Dependency(\.database.employees) var employees @@ -90,11 +93,11 @@ struct DatabaseClientTests { } } - @Test - func vendors() async throws { + @Test(arguments: VendorTestFactory.testCases) + func vendors(factory: VendorTestFactory) async throws { try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) { - $0.database.vendors = .live(database: $1) - $0.database.vendorBranches = .live(database: $1) + $0.database.vendorBranches = factory.handler($1).0 + $0.database.vendors = factory.handler($1).1 } operation: { @Dependency(\.database.vendorBranches) var branches @Dependency(\.database.vendors) var vendors @@ -119,7 +122,7 @@ struct DatabaseClientTests { 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")) + let updated = try await vendors.update(vendor.id, with: .init(name: "Johnstone")) #expect(updated.name == "Johnstone") try await vendors.delete(vendor.id) @@ -129,11 +132,11 @@ struct DatabaseClientTests { } } - @Test - func vendorBranches() async throws { + @Test(arguments: VendorTestFactory.testCases) + func vendorBranches(factory: VendorTestFactory) async throws { try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) { - $0.database.vendors = .live(database: $1) - $0.database.vendorBranches = .live(database: $1) + $0.database.vendorBranches = factory.handler($1).0 + $0.database.vendors = factory.handler($1).1 } operation: { @Dependency(\.database.vendorBranches) var branches @Dependency(\.database.vendors) var vendors @@ -180,6 +183,8 @@ struct DatabaseClientTests { } try await withDependencies { + $0.uuid = .incrementing + $0.date = .init { Date() } setupDependencies(&$0, database) } operation: { try await operation() @@ -192,3 +197,22 @@ struct DatabaseClientTests { await dbs.shutdownAsync() } } + +struct EmployeeTestFactory { + let handler: (any Database) -> DatabaseClient.Employees + + static var testCases: [Self] { [ + .init(handler: { .live(database: $0) }), + .init(handler: { _ in .mock([]) }) + ] } +} + +struct VendorTestFactory { + let handler: (any Database) -> (DatabaseClient.VendorBranches, DatabaseClient.Vendors) + + static var testCases: [Self] { [ + .init(handler: { (.live(database: $0), .live(database: $0)) }), + .init(handler: { _ in (.mock([]), .mock([])) }) + ] } + +}