From bf7b47a5ad91becd461a4a20eea222bf05157d81 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:45:29 -0500 Subject: [PATCH 1/6] Bump dependency versions and clean up language flags --- Package.swift | 10 ++-------- Package@swift-5.9.swift | 7 ++----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Package.swift b/Package.swift index fb06cf28..d481859c 100644 --- a/Package.swift +++ b/Package.swift @@ -13,8 +13,8 @@ let package = Package( .library(name: "Fluent", targets: ["Fluent"]), ], dependencies: [ - .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.94.1"), + .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.4"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.101.0"), ], targets: [ .target( @@ -40,10 +40,4 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("ImportObjcForwardDeclarations"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("IsolatedDefaultValues"), - .enableUpcomingFeature("GlobalConcurrency"), - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("StrictConcurrency=complete"), ] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 183bfe65..3b4afba6 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -13,8 +13,8 @@ let package = Package( .library(name: "Fluent", targets: ["Fluent"]), ], dependencies: [ - .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.94.1"), + .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.4"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.101.0"), ], targets: [ .target( @@ -43,8 +43,5 @@ var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ForwardTrailingClosures"), .enableUpcomingFeature("ImportObjcForwardDeclarations"), .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("IsolatedDefaultValues"), - .enableUpcomingFeature("GlobalConcurrency"), - .enableUpcomingFeature("StrictConcurrency"), .enableExperimentalFeature("StrictConcurrency=complete"), ] } From f1114211ea62c0b5d4a66e8bec2dac6a333ae945 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:46:08 -0500 Subject: [PATCH 2/6] Make it easier to override the logger on a database when requesting it (since `.logging(to:)` doesn't usually work as intended) --- Sources/Fluent/FluentProvider.swift | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/Sources/Fluent/FluentProvider.swift b/Sources/Fluent/FluentProvider.swift index 17fb18a0..2f83723f 100644 --- a/Sources/Fluent/FluentProvider.swift +++ b/Sources/Fluent/FluentProvider.swift @@ -12,15 +12,20 @@ extension Request { } public func db(_ id: DatabaseID?) -> any Database { - self.application - .databases - .database( - id, - logger: self.logger, - on: self.eventLoop, - history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil, - pageSizeLimit: self.fluent.pagination.pageSizeLimit != nil ? self.fluent.pagination.pageSizeLimit?.value : self.application.fluent.pagination.pageSizeLimit - )! + self.db(id, logger: self.logger) + } + + public func db(_ id: DatabaseID?, logger: Logger) -> any Database { + self.application.databases.database( + id, + logger: logger, + on: self.eventLoop, + history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil, + // Use map() (not flatMap()) so if pageSizeLimit is non-nil but the value is nil + // the request's "no limit" setting overrides the app's setting. + pageSizeLimit: self.fluent.pagination.pageSizeLimit.map(\.value) ?? + self.application.fluent.pagination.pageSizeLimit + )! } public var fluent: Fluent { @@ -34,14 +39,17 @@ extension Application { } public func db(_ id: DatabaseID?) -> any Database { - self.databases - .database( - id, - logger: self.logger, - on: self.eventLoopGroup.any(), - history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil, - pageSizeLimit: self.fluent.pagination.pageSizeLimit - )! + self.db(id, logger: self.logger) + } + + public func db(_ id: DatabaseID?, logger: Logger) -> any Database { + self.databases.database( + id, + logger: logger, + on: self.eventLoopGroup.any(), + history: self.fluent.history.historyEnabled ? self.fluent.history.history : nil, + pageSizeLimit: self.fluent.pagination.pageSizeLimit + )! } public var databases: Databases { From 44e72515be64c767cd3bfb647566882635e78364 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:47:01 -0500 Subject: [PATCH 3/6] Use the `willBootAsync` lifecycle callback to avoid calling `.wait()` when auto-migration is requested on the commandline. --- Sources/Fluent/FluentProvider.swift | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Sources/Fluent/FluentProvider.swift b/Sources/Fluent/FluentProvider.swift index 2f83723f..742f91b3 100644 --- a/Sources/Fluent/FluentProvider.swift +++ b/Sources/Fluent/FluentProvider.swift @@ -107,18 +107,17 @@ extension Application { } struct Lifecycle: LifecycleHandler { - func willBoot(_ application: Application) throws { - struct Signature: CommandSignature { - @Flag(name: "auto-migrate", help: "If true, Fluent will automatically migrate your database on boot") - var autoMigrate: Bool - - @Flag(name: "auto-revert", help: "If true, Fluent will automatically revert your database on boot") - var autoRevert: Bool + struct Signature: CommandSignature { + @Flag(name: "auto-migrate", help: "If true, Fluent will automatically migrate your database on boot") + var autoMigrate: Bool - init() {} - } + @Flag(name: "auto-revert", help: "If true, Fluent will automatically revert your database on boot") + var autoRevert: Bool + } + func willBoot(_ application: Application) throws { let signature = try Signature(from: &application.environment.commandInput) + if signature.autoRevert { try application.autoRevert().wait() } @@ -126,6 +125,17 @@ extension Application { try application.autoMigrate().wait() } } + + func willBootAsync(_ application: Application) async throws { + let signature = try Signature(from: &application.environment.commandInput) + + if signature.autoRevert { + try await application.autoRevert() + } + if signature.autoMigrate { + try await application.autoMigrate() + } + } func shutdown(_ application: Application) { application.databases.shutdown() From 228c557fa45948c91d5c13b399836382a86c946b Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:47:38 -0500 Subject: [PATCH 4/6] Code style, improve the console output in the migration command --- Sources/Fluent/FluentProvider.swift | 25 ++++++------------------- Sources/Fluent/MigrateCommand.swift | 20 ++++++-------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/Sources/Fluent/FluentProvider.swift b/Sources/Fluent/FluentProvider.swift index 742f91b3..6a02d882 100644 --- a/Sources/Fluent/FluentProvider.swift +++ b/Sources/Fluent/FluentProvider.swift @@ -61,7 +61,7 @@ extension Application { } public var migrator: Migrator { - Migrator( + .init( databases: self.databases, migrations: self.migrations, logger: self.logger, @@ -93,10 +93,7 @@ extension Application { let migrationLogLevel: NIOLockedValueBox init(threadPool: NIOThreadPool, on eventLoopGroup: any EventLoopGroup, migrationLogLevel: Logger.Level) { - self.databases = Databases( - threadPool: threadPool, - on: eventLoopGroup - ) + self.databases = Databases(threadPool: threadPool, on: eventLoopGroup) self.migrations = .init() self.migrationLogLevel = .init(migrationLogLevel) } @@ -166,21 +163,11 @@ extension Application { nonmutating set { self.storage.migrationLogLevel.withLockedValue { $0 = newValue } } } - public var history: History { - .init(fluent: self) - } - - public struct History { - let fluent: Fluent - } + public struct History { let fluent: Fluent } + public var history: History { .init(fluent: self) } - public var pagination: Pagination { - .init(fluent: self) - } - - public struct Pagination { - let fluent: Fluent - } + public struct Pagination { let fluent: Fluent } + public var pagination: Pagination { .init(fluent: self) } } public var fluent: Fluent { diff --git a/Sources/Fluent/MigrateCommand.swift b/Sources/Fluent/MigrateCommand.swift index 398b27aa..7d80d9f0 100644 --- a/Sources/Fluent/MigrateCommand.swift +++ b/Sources/Fluent/MigrateCommand.swift @@ -30,16 +30,12 @@ public final class MigrateCommand: AsyncCommand { private func revert(using context: CommandContext) async throws { let migrations = try await context.application.migrator.previewRevertLastBatch().get() - guard migrations.count > 0 else { - context.console.print("No migrations to revert.") - return + guard !migrations.isEmpty else { + return context.console.print("No migrations to revert.") } context.console.print("The following migration(s) will be reverted:") for (migration, dbid) in migrations { - context.console.print("- ", newLine: false) - context.console.error(migration.name, newLine: false) - context.console.print(" on ", newLine: false) - context.console.print(dbid?.string ?? "default") + context.console.output("- \(migration.name, color: .red) on \(dbid?.string ?? "", style: .info)") } if context.console.confirm("Would you like to continue?".consoleText(.warning)) { try await context.application.migrator.revertLastBatch().get() @@ -51,16 +47,12 @@ public final class MigrateCommand: AsyncCommand { private func prepare(using context: CommandContext) async throws { let migrations = try await context.application.migrator.previewPrepareBatch().get() - guard migrations.count > 0 else { - context.console.print("No new migrations.") - return + guard !migrations.isEmpty else { + return context.console.print("No new migrations.") } context.console.print("The following migration(s) will be prepared:") for (migration, dbid) in migrations { - context.console.print("+ ", newLine: false) - context.console.success(migration.name, newLine: false) - context.console.print(" on ", newLine: false) - context.console.print(dbid?.string ?? "default") + context.console.output("+ \(migration.name, color: .green) on \(dbid?.string ?? "", style: .info)") } if context.console.confirm("Would you like to continue?".consoleText(.warning)) { try await context.application.migrator.prepareBatch().get() From e23d43711db2d21e888534ac92c9905b66c5067a Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:48:39 -0500 Subject: [PATCH 5/6] Make all the tests fully async and update them to use Application.make() etc. --- Tests/FluentTests/CacheTests.swift | 40 +++++----- Tests/FluentTests/CredentialTests.swift | 89 +++++++++------------- Tests/FluentTests/OperatorTests.swift | 1 + Tests/FluentTests/PaginationTests.swift | 75 +++++++++++-------- Tests/FluentTests/QueryHistoryTests.swift | 91 +++++++++++------------ Tests/FluentTests/RepositoryTests.swift | 82 ++++++++++---------- Tests/FluentTests/SessionTests.swift | 36 +++++---- 7 files changed, 210 insertions(+), 204 deletions(-) diff --git a/Tests/FluentTests/CacheTests.swift b/Tests/FluentTests/CacheTests.swift index a29f7d59..a7cfcb4d 100644 --- a/Tests/FluentTests/CacheTests.swift +++ b/Tests/FluentTests/CacheTests.swift @@ -4,26 +4,34 @@ import Fluent import Vapor final class CacheTests: XCTestCase { + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + func testCacheMigrationName() { XCTAssertEqual(CacheEntry.migration.name, "Fluent.CacheEntry.Create") } func testCacheGet() async throws { - let app = Application(.testing) - defer { app.shutdown() } - // Setup test db. let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) - app.migrations.add(CacheEntry.migration) + self.app.databases.use(test.configuration, as: .test) + self.app.migrations.add(CacheEntry.migration) // Configure cache. - app.caches.use(.fluent) + self.app.caches.use(.fluent) // simulate cache miss test.append([]) do { - let foo = try await app.cache.get("foo", as: String.self) + let foo = try await self.app.cache.get("foo", as: String.self) XCTAssertNil(foo) } @@ -33,15 +41,12 @@ final class CacheTests: XCTestCase { "value": "\"bar\"" ])]) do { - let foo = try await app.cache.get("foo", as: String.self) + let foo = try await self.app.cache.get("foo", as: String.self) XCTAssertEqual(foo, "bar") } } func testCacheSet() async throws { - let app = Application(.testing) - defer { app.shutdown() } - // Setup test db. let test = CallbackTestDatabase { query in switch query.input[0] { @@ -51,19 +56,16 @@ final class CacheTests: XCTestCase { XCTAssertEqual(value, "\"bar\"") default: XCTFail("unexpected value") } - default: XCTFail("unexpected input") } - return [ - TestOutput(["id": UUID()]) - ] + return [TestOutput(["id": UUID()])] } - app.databases.use(test.configuration, as: .test) - app.migrations.add(CacheEntry.migration) + self.app.databases.use(test.configuration, as: .test) + self.app.migrations.add(CacheEntry.migration) // Configure cache. - app.caches.use(.fluent) + self.app.caches.use(.fluent) - try await app.cache.set("foo", to: "bar") + try await self.app.cache.set("foo", to: "bar") } } diff --git a/Tests/FluentTests/CredentialTests.swift b/Tests/FluentTests/CredentialTests.swift index 317085eb..c7e489a2 100644 --- a/Tests/FluentTests/CredentialTests.swift +++ b/Tests/FluentTests/CredentialTests.swift @@ -5,38 +5,35 @@ import Vapor import FluentKit final class CredentialTests: XCTestCase { - - func testCredentialsAuthentication() throws { - let app = Application(.testing) - defer { app.shutdown() } - - // Setup test db. + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + + func testCredentialsAuthentication() async throws { let testDB = ArrayTestDatabase() - app.databases.use(testDB.configuration, as: .test) - - // Configure sessions. - app.middleware.use(app.sessions.middleware) - - // Setup routes. - let sessionRoutes = app.grouped(CredentialsUser.sessionAuthenticator()) - - let credentialRoutes = sessionRoutes.grouped(CredentialsUser.credentialsAuthenticator()) - credentialRoutes.post("login") { req -> Response in + self.app.databases.use(testDB.configuration, as: .test) + self.app.middleware.use(self.app.sessions.middleware) + let sessionRoutes = self.app.grouped(CredentialsUser.sessionAuthenticator()) + sessionRoutes.grouped(CredentialsUser.credentialsAuthenticator()).post("login") { req in guard req.auth.has(CredentialsUser.self) else { throw Abort(.unauthorized) } return req.redirect(to: "/protected") } - - let protectedRoutes = sessionRoutes.grouped(CredentialsUser.redirectMiddleware(path: "/login")) - protectedRoutes.get("protected") { req -> HTTPStatus in + sessionRoutes.grouped(CredentialsUser.redirectMiddleware(path: "/login")).get("protected") { req in _ = try req.auth.require(CredentialsUser.self) - return .ok + return HTTPStatus.ok } // Create user - let password = "password-\(Int.random())" - let passwordHash = try Bcrypt.hash(password) + let password = "password-\(Int.random())", passwordHash = try Bcrypt.hash(password) let testUser = CredentialsUser(id: UUID(), username: "user-\(Int.random())", password: passwordHash) testDB.append([TestOutput(testUser)]) testDB.append([TestOutput(testUser)]) @@ -45,7 +42,7 @@ final class CredentialTests: XCTestCase { // Test login let loginData = ModelCredentials(username: testUser.username, password: password) - try app.test(.POST, "/login", beforeRequest: { req in + try await self.app.test(.POST, "/login", beforeRequest: { req in try req.content.encode(loginData, as: .urlEncodedForm) }) { res in XCTAssertEqual(res.status, .seeOther) @@ -53,49 +50,35 @@ final class CredentialTests: XCTestCase { let sessionID = try XCTUnwrap(res.headers.setCookie?["vapor-session"]?.string) // Test accessing protected route - try app.test(.GET, "/protected", beforeRequest: { req in + try await self.app.test(.GET, "/protected", beforeRequest: { req in var cookies = HTTPCookies() cookies["vapor-session"] = .init(string: sessionID) req.headers.cookie = cookies - }) { res in + }) { res async in XCTAssertEqual(res.status, .ok) } } } func testAsyncCredentialsAuthentication() async throws { - let app = Application(.testing) - defer { app.shutdown() } - - - // Setup test db. let testDB = ArrayTestDatabase() - - app.databases.use(testDB.configuration, as: .test) - - // Configure sessions. - app.middleware.use(app.sessions.middleware) + self.app.databases.use(testDB.configuration, as: .test) + self.app.middleware.use(self.app.sessions.middleware) + let sessionRoutes = self.app.grouped(CredentialsUser.sessionAuthenticator()) - // Setup routes. - let sessionRoutes = app.grouped(CredentialsUser.sessionAuthenticator()) - - let credentialRoutes = sessionRoutes.grouped(CredentialsUser.asyncCredentialsAuthenticator()) - credentialRoutes.post("login") { req -> Response in + sessionRoutes.grouped(CredentialsUser.asyncCredentialsAuthenticator()).post("login") { req async throws in guard req.auth.has(CredentialsUser.self) else { throw Abort(.unauthorized) } return req.redirect(to: "/protected") } - - let protectedRoutes = sessionRoutes.grouped(CredentialsUser.redirectMiddleware(path: "/login")) - protectedRoutes.get("protected") { req -> HTTPStatus in + sessionRoutes.grouped(CredentialsUser.redirectMiddleware(path: "/login")).get("protected") { req async throws in _ = try req.auth.require(CredentialsUser.self) - return .ok + return HTTPStatus.ok } // Create user - let password = "password-\(Int.random())" - let passwordHash = try Bcrypt.hash(password) + let password = "password-\(Int.random())", passwordHash = try Bcrypt.hash(password) let testUser = CredentialsUser(id: UUID(), username: "user-\(Int.random())", password: passwordHash) testDB.append([TestOutput(testUser)]) testDB.append([TestOutput(testUser)]) @@ -104,19 +87,17 @@ final class CredentialTests: XCTestCase { // Test login let loginData = ModelCredentials(username: testUser.username, password: password) - try app.test(.POST, "/login", beforeRequest: { req in - try req.content.encode(loginData, as: .urlEncodedForm) - }) { res in - XCTAssertEqual(res.status, .seeOther) - XCTAssertEqual(res.headers[.location].first, "/protected") - let sessionID = try XCTUnwrap(res.headers.setCookie?["vapor-session"]?.string) + try await self.app.test(.POST, "/login", beforeRequest: { try $0.content.encode(loginData, as: .urlEncodedForm) }) { + XCTAssertEqual($0.status, .seeOther) + XCTAssertEqual($0.headers[.location].first, "/protected") + let sessionID = try XCTUnwrap($0.headers.setCookie?["vapor-session"]?.string) // Test accessing protected route - try app.test(.GET, "/protected", beforeRequest: { req in + try await app.test(.GET, "/protected", beforeRequest: { req in var cookies = HTTPCookies() cookies["vapor-session"] = .init(string: sessionID) req.headers.cookie = cookies - }) { res in + }) { res async in XCTAssertEqual(res.status, .ok) } } diff --git a/Tests/FluentTests/OperatorTests.swift b/Tests/FluentTests/OperatorTests.swift index c1b35401..fead161a 100644 --- a/Tests/FluentTests/OperatorTests.swift +++ b/Tests/FluentTests/OperatorTests.swift @@ -30,6 +30,7 @@ final class OperatorTests: XCTestCase { .filter(\.$name !~ ["Earth", "Mars"]) } } + private final class Planet: Model, @unchecked Sendable { static let schema = "planets" diff --git a/Tests/FluentTests/PaginationTests.swift b/Tests/FluentTests/PaginationTests.swift index cc4ac94f..9106a2b8 100644 --- a/Tests/FluentTests/PaginationTests.swift +++ b/Tests/FluentTests/PaginationTests.swift @@ -6,10 +6,18 @@ import FluentKit import NIOConcurrencyHelpers final class PaginationTests: XCTestCase { - func testPagination() throws { - let app = Application(.testing) - defer { app.shutdown() } - + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + + func testPagination() async throws { let rows: NIOLockedValueBox<[TestOutput]> = .init([]) for i in 1...1_000 { rows.withLockedValue { $0.append(TestOutput([ @@ -35,33 +43,43 @@ final class PaginationTests: XCTestCase { return result } } - app.databases.use(test.configuration, as: .test) - app.get("todos") { req -> EventLoopFuture> in - Todo.query(on: req.db).paginate(for: req) + self.app.databases.use(test.configuration, as: .test) + self.app.get("todos") { req -> Page in + try await Todo.query(on: req.db).paginate(for: req) + } + self.app.get("todos-elf") { req -> Page in + try await Todo.query(on: req.db).paginate(for: req).get() } - try app.test(.GET, "todos") { res in + try await self.app.test(.GET, "todos") { res async throws in XCTAssertEqual(res.status, .ok) let todos = try res.content.decode(Page.self) XCTAssertEqual(todos.items[0].id, 1) XCTAssertEqual(todos.items.count, 10) - }.test(.GET, "todos?page=2") { res in + }.test(.GET, "todos-elf") { res async throws in + XCTAssertEqual(res.status, .ok) + let todos = try res.content.decode(Page.self) + XCTAssertEqual(todos.items[0].id, 1) + XCTAssertEqual(todos.items.count, 10) + }.test(.GET, "todos-elf?page=invalid") { res async throws in + XCTAssertEqual(res.status, .badRequest) + }.test(.GET, "todos?page=2") { res async throws in XCTAssertEqual(res.status, .ok) let todos = try res.content.decode(Page.self) XCTAssertEqual(todos.items[0].id, 11) XCTAssertEqual(todos.items.count, 10) - }.test(.GET, "todos?page=2&per=15") { res in + }.test(.GET, "todos?page=2&per=15") { res async throws in XCTAssertEqual(res.status, .ok) let todos = try res.content.decode(Page.self) XCTAssertEqual(todos.items[0].id, 16) XCTAssertEqual(todos.items.count, 15) - }.test(.GET, "todos?page=1000&per=1") { res in + }.test(.GET, "todos?page=1000&per=1") { res async throws in XCTAssertEqual(res.status, .ok) let todos = try res.content.decode(Page.self) XCTAssertEqual(todos.items[0].id, 1000) XCTAssertEqual(todos.items.count, 1) - }.test(.GET, "todos?page=1&per=1") { res in + }.test(.GET, "todos?page=1&per=1") { res async throws in XCTAssertEqual(res.status, .ok) let todos = try res.content.decode(Page.self) XCTAssertEqual(todos.items[0].id, 1) @@ -69,10 +87,7 @@ final class PaginationTests: XCTestCase { } } - func testPaginationLimits() throws { - let app = Application(.testing) - defer { app.shutdown() } - + func testPaginationLimits() async throws { let rows = [ TestOutput(["id": 1, "title": "a"]), TestOutput(["id": 2, "title": "b"]), @@ -97,44 +112,44 @@ final class PaginationTests: XCTestCase { } } - app.databases.use(test.configuration, as: .test) - app.fluent.pagination.pageSizeLimit = 4 + self.app.databases.use(test.configuration, as: .test) + self.app.fluent.pagination.pageSizeLimit = 4 - app.get("todos-request-limit") { req -> EventLoopFuture> in + self.app.get("todos-request-limit") { req -> Page in req.fluent.pagination.pageSizeLimit = 2 - return Todo.query(on: req.db).paginate(for: req) + return try await Todo.query(on: req.db).paginate(for: req) } - app.get("todos-request-no-limit") { req -> EventLoopFuture> in + self.app.get("todos-request-no-limit") { req -> Page in req.fluent.pagination.pageSizeLimit = .noLimit - return Todo.query(on: req.db).paginate(for: req) + return try await Todo.query(on: req.db).paginate(for: req) } - app.get("todos-request-app-limit") { req -> EventLoopFuture> in + self.app.get("todos-request-app-limit") { req -> Page in req.fluent.pagination.pageSizeLimit = nil - return Todo.query(on: req.db).paginate(for: req) + return try await Todo.query(on: req.db).paginate(for: req) } - app.get("todos-app-limit") { req -> EventLoopFuture> in - Todo.query(on: req.db).paginate(for: req) + self.app.get("todos-app-limit") { req -> Page in + try await Todo.query(on: req.db).paginate(for: req) } - try app.test(.GET, "todos-request-limit?page=1&per=5") { response in + try await self.app.test(.GET, "todos-request-limit?page=1&per=5") { response async throws in XCTAssertEqual(response.status, .ok) let todos = try response.content.decode(Page.self) XCTAssertEqual(todos.items.count, 2, "Should be capped by request-level limit.") } - .test(.GET, "todos-request-no-limit?page=1&per=5") { response in + .test(.GET, "todos-request-no-limit?page=1&per=5") { response async throws in XCTAssertEqual(response.status, .ok) let todos = try response.content.decode(Page.self) XCTAssertEqual(todos.items.count, 5, "Request-level override should suspend app-level limit.") } - .test(.GET, "todos-request-app-limit?page=1&per=5") { response in + .test(.GET, "todos-request-app-limit?page=1&per=5") { response async throws in XCTAssertEqual(response.status, .ok) let todos = try response.content.decode(Page.self) XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.") } - .test(.GET, "todos-app-limit?page=1&per=5") { response in + .test(.GET, "todos-app-limit?page=1&per=5") { response async throws in XCTAssertEqual(response.status, .ok) let todos = try response.content.decode(Page.self) XCTAssertEqual(todos.items.count, 4, "Should be capped by app-level limit.") diff --git a/Tests/FluentTests/QueryHistoryTests.swift b/Tests/FluentTests/QueryHistoryTests.swift index cdcb91c8..15b6cca2 100644 --- a/Tests/FluentTests/QueryHistoryTests.swift +++ b/Tests/FluentTests/QueryHistoryTests.swift @@ -5,61 +5,61 @@ import XCTVapor import FluentKit final class QueryHistoryTests: XCTestCase { - func testQueryHistoryDisabled() throws { - let app = Application(.testing) - defer { app.shutdown() } - + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + + func testQueryHistoryDisabled() async throws { let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) + self.app.databases.use(test.configuration, as: .test) test.append([ TestOutput(["id": 1, "content": "a"]), TestOutput(["id": 2, "content": "b"]), ]) - app.get("foo") { req -> EventLoopFuture<[Post]> in - return Post.query(on: req.db).all().map { posts in - XCTAssertEqual(req.fluent.history.queries.count, 0) - return posts - } + self.app.get("foo") { req -> [Post] in + let posts = try await Post.query(on: req.db).all() + XCTAssertEqual(req.fluent.history.queries.count, 0) + return posts } - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) } } - func testQueryHistoryEnabled() throws { - let app = Application(.testing) - defer { app.shutdown() } - + func testQueryHistoryEnabled() async throws { let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) + self.app.databases.use(test.configuration, as: .test) test.append([ TestOutput(["id": 1, "content": "a"]), TestOutput(["id": 2, "content": "b"]), ]) - app.get("foo") { req -> EventLoopFuture<[Post]> in + self.app.get("foo") { req -> [Post] in req.fluent.history.start() - return Post.query(on: req.db).all().map { posts in - XCTAssertEqual(req.fluent.history.queries.count, 1) - return posts - } + let posts = try await Post.query(on: req.db).all() + XCTAssertEqual(req.fluent.history.queries.count, 1) + return posts } - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) } } - func testQueryHistoryEnableAndDisable() throws { - let app = Application(.testing) - defer { app.shutdown() } - + func testQueryHistoryEnableAndDisable() async throws { let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) + self.app.databases.use(test.configuration, as: .test) test.append([ TestOutput(["id": 1, "content": "a"]), @@ -70,54 +70,49 @@ final class QueryHistoryTests: XCTestCase { TestOutput(["id": 2, "content": "b"]), ]) - app.get("foo") { req -> EventLoopFuture<[Post]> in + self.app.get("foo") { req -> [Post] in req.fluent.history.start() - return Post.query(on: req.db).all().flatMap { posts -> EventLoopFuture<[Post]> in - XCTAssertEqual(req.fluent.history.queries.count, 1) - req.fluent.history.stop() - - return Post.query(on: req.db).all() - }.map { posts in - XCTAssertEqual(req.fluent.history.queries.count, 1) - return posts - } + _ = try await Post.query(on: req.db).all() + XCTAssertEqual(req.fluent.history.queries.count, 1) + req.fluent.history.stop() + + let posts = try await Post.query(on: req.db).all() + XCTAssertEqual(req.fluent.history.queries.count, 1) + return posts } - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) } } func testQueryHistoryForApp() async throws { - let app = Application(.testing) - defer { app.shutdown() } - - app.fluent.history.start() + self.app.fluent.history.start() let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) + self.app.databases.use(test.configuration, as: .test) test.append([ TestOutput(["id": 1, "content": "a"]), TestOutput(["id": 2, "content": "b"]), ]) - _ = try await Post.query(on: app.db).all() + _ = try await Post.query(on: self.app.db).all() test.append([ TestOutput(["id": 1, "content": "a"]), TestOutput(["id": 2, "content": "b"]), ]) - _ = try await Post.query(on: app.db).all() + _ = try await Post.query(on: self.app.db).all() test.append([ TestOutput(["id": 1, "content": "a"]), TestOutput(["id": 2, "content": "b"]), ]) - app.fluent.history.stop() - _ = try await Post.query(on: app.db).all() - XCTAssertEqual(app.fluent.history.queries.count, 2) + self.app.fluent.history.stop() + _ = try await Post.query(on: self.app.db).all() + XCTAssertEqual(self.app.fluent.history.queries.count, 2) } } diff --git a/Tests/FluentTests/RepositoryTests.swift b/Tests/FluentTests/RepositoryTests.swift index d643cf70..a0c052a6 100644 --- a/Tests/FluentTests/RepositoryTests.swift +++ b/Tests/FluentTests/RepositoryTests.swift @@ -3,51 +3,57 @@ import Vapor import XCTFluent import XCTVapor import FluentKit +import NIOConcurrencyHelpers final class RepositoryTests: XCTestCase { - func testRepositoryPatternStatic() throws { - let app = Application(.testing) - defer { app.shutdown() } - - var posts: [Post] = [ + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + + func testRepositoryPatternStatic() async throws { + let posts: NIOLockedValueBox<[Post]> = .init([ .init(content: "a"), .init(content: "b") - ] + ]) - app.posts.use { - TestPostRepository(posts: posts, eventLoop: $0.eventLoop) + self.app.posts.use { + TestPostRepository(posts: posts.withLockedValue { $0 }, eventLoop: $0.eventLoop) } - app.get("foo") { req -> EventLoopFuture<[Post]> in - req.posts.all() + self.app.get("foo") { req -> [Post] in + try await req.posts.all() } - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) - XCTAssertEqualJSON(res.body.string, posts) + XCTAssertEqualJSON(res.body.string, posts.withLockedValue { $0 }) } - posts.append(.init(content: "c")) + posts.withLockedValue { $0.append(.init(content: "c")) } - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) - XCTAssertEqualJSON(res.body.string, posts) + XCTAssertEqualJSON(res.body.string, posts.withLockedValue { $0 }) } } - func testRepositoryPatternDatabase() throws { - let app = Application(.testing) - defer { app.shutdown() } - + func testRepositoryPatternDatabase() async throws { let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) + self.app.databases.use(test.configuration, as: .test) - app.posts.use { req in + self.app.posts.use { req in DatabasePostRepository(database: req.db(.test)) } - app.get("foo") { req -> EventLoopFuture<[Post]> in - req.posts.all() + self.app.get("foo") { req -> [Post] in + try await req.posts.all() } let posts: [Post] = [ @@ -60,7 +66,7 @@ final class RepositoryTests: XCTestCase { TestOutput(["id": 2, "content": "b"]), ]) - try app.testable().test(.GET, "foo") { res in + try await self.app.testable().test(.GET, "foo") { res async in XCTAssertEqual(res.status, .ok) XCTAssertEqualJSON(res.body.string, posts) } @@ -69,7 +75,7 @@ final class RepositoryTests: XCTestCase { extension ByteBuffer { var string: String { - .init(decoding: self.readableBytesView, as: UTF8.self) + self.getString(at: self.readerIndex, length: self.readableBytes)! } } @@ -83,19 +89,17 @@ private extension Application { private struct PostRepositoryKey: StorageKey { typealias Value = PostRepositoryFactory } + var posts: PostRepositoryFactory { - get { - self.storage[PostRepositoryKey.self] ?? .init() - } - set { - self.storage[PostRepositoryKey.self] = newValue - } + get { self.storage[PostRepositoryKey.self] ?? .init() } + set { self.storage[PostRepositoryKey.self] = newValue } } } private struct PostRepositoryFactory: @unchecked Sendable { // not actually Sendable but the compiler doesn't need to know that - var makePosts: ((Request) -> any PostRepository)? - mutating func use(_ makePosts: @escaping (Request) -> any PostRepository) { + var makePosts: (@Sendable (Request) -> any PostRepository)? + + mutating func use(_ makePosts: @escaping @Sendable (Request) -> any PostRepository) { self.makePosts = makePosts } } @@ -125,18 +129,18 @@ private struct TestPostRepository: PostRepository { let posts: [Post] let eventLoop: any EventLoop - func all() -> EventLoopFuture<[Post]> { - self.eventLoop.makeSucceededFuture(self.posts) - } + func all() -> EventLoopFuture<[Post]> { self.eventLoop.makeSucceededFuture(self.posts) } + func all() async throws -> [Post] { self.posts } } private struct DatabasePostRepository: PostRepository { let database: any Database - func all() -> EventLoopFuture<[Post]> { - database.query(Post.self).all() - } + + func all() -> EventLoopFuture<[Post]> { self.database.query(Post.self).all() } + func all() async throws -> [Post] { try await self.database.query(Post.self).all() } } private protocol PostRepository { func all() -> EventLoopFuture<[Post]> + func all() async throws -> [Post] } diff --git a/Tests/FluentTests/SessionTests.swift b/Tests/FluentTests/SessionTests.swift index 0f59edde..efe4fd2c 100644 --- a/Tests/FluentTests/SessionTests.swift +++ b/Tests/FluentTests/SessionTests.swift @@ -5,32 +5,40 @@ import Vapor import FluentKit final class SessionTests: XCTestCase { + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + func testSessionMigrationName() { XCTAssertEqual(SessionRecord.migration.name, "Fluent.SessionRecord.Create") } - func testSessions() throws { - let app = Application(.testing) - defer { app.shutdown() } - + func testSessions() async throws { // Setup test db. let test = ArrayTestDatabase() - app.databases.use(test.configuration, as: .test) - app.migrations.add(SessionRecord.migration) + self.app.databases.use(test.configuration, as: .test) + self.app.migrations.add(SessionRecord.migration) // Configure sessions. - app.sessions.use(.fluent) - app.middleware.use(app.sessions.middleware) + self.app.sessions.use(.fluent) + self.app.middleware.use(self.app.sessions.middleware) // Setup routes. - app.get("set", ":value") { req -> HTTPStatus in + self.app.get("set", ":value") { req -> HTTPStatus in req.session.data["name"] = req.parameters.get("value") return .ok } - app.get("get") { req -> String in + self.app.get("get") { req -> String in req.session.data["name"] ?? "n/a" } - app.get("del") { req -> HTTPStatus in + self.app.get("del") { req -> HTTPStatus in req.session.destroy() return .ok } @@ -39,7 +47,7 @@ final class SessionTests: XCTestCase { test.append([TestOutput()]) // Store session id. var sessionID: String? - try app.test(.GET, "/set/vapor") { res in + try await self.app.test(.GET, "/set/vapor") { res async in sessionID = res.headers.setCookie?["vapor-session"]?.string XCTAssertEqual(res.status, .ok) } @@ -54,11 +62,11 @@ final class SessionTests: XCTestCase { ]) // Add empty query output for session update. test.append([]) - try app.test(.GET, "/get", beforeRequest: { req in + try await self.app.test(.GET, "/get", beforeRequest: { req async in var cookies = HTTPCookies() cookies["vapor-session"] = .init(string: sessionID!) req.headers.cookie = cookies - }) { res in + }) { res async in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "vapor") } From d3579e23f675577f9a7327e925ccdede43aa8669 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 30 May 2024 00:54:36 -0500 Subject: [PATCH 6/6] Remove pointless Dependabot config --- .github/dependabot.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7810eff1..14c39b4b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,4 @@ version: 2 -enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" @@ -11,14 +10,3 @@ updates: dependencies: patterns: - "*" - - package-ecosystem: "swift" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 6 - allow: - - dependency-type: all - groups: - all-dependencies: - patterns: - - "*"