Vapor and query caching?

I want to ugrade from vapor 4.53 to 4.76.
I have many tests. But after upgrade, the tests fails.

I realized that the same query is creating and calling only once in a test case.
This is how I call a query:

// in some reqest
return ASZFAccepted.query(on: request.db)
	.filter(\.$author.$id == authorId)
	.sort(\.$created, .descending)
	.first()
	.flatMapThrowing { acceptedASZF -> Response in
		request.logger.info("[aszf] Cached latest ASZF acceptedASZF id: \(String(describing: acceptedASZF.id))")
	}

In a test case I called the above code 3 times. On the first and second time the acceptedASZF is nil, this is ok, becouse there is no object in the database yet. And first time I see the database logs where query creating and calling. But before third time I created an object in database and the result of query is still nil. Why?

Do you definitely create the ASZFAccepted model with the right user ID? Have you tried inspecting the DB to make sure the query matches what you expect and what's in the DB.

There's no query caching in Fluent.

This is how I testing the corresponding codes:

self.app.logger.info("[aszf] Before ASZF accept, latestASZF1: \(latestASZF1)")
let aszfAccept1 = ASZFAcceptRequest(aszfId: latestASZF1, cookiesAccepted: false, dataHandlingAccepted: false, gdprAccepted: false)

let acceptFirstAszfExpectaion = XCTestExpectation(description: "Wait for /aszf/acceptASZF")
try self.app.test(.POST, "/aszf/acceptASZF", headers: token(bearerHTTPHeaderWithContentType: .json), beforeRequest: { (request) in
	try request.content.encode(aszfAccept1)
}, afterResponse: { (response) in
	XCTAssertEqual(response.status.code, 200)
	let ok: String? = response.content["result"]
	XCTAssertNotNil(ok)
	XCTAssertEqual(ok, "ok")
	self.app.logger.info("[aszf] /aszf/acceptASZF done")
	acceptFirstAszfExpectaion.fulfill()
})

if await XCTWaiter.fulfillment(of: [acceptFirstAszfExpectaion], timeout: 1.0, enforceOrder: false) == XCTWaiter.Result.timedOut {
	XCTAssertTrue(false, "[aszf] ASZF accept waiter timeouted!")
}

self.app.logger.info("[aszf] After ASZF accept")

// no new ASZF available
let checkNewAszfExpectaion = XCTestExpectation(description: "Wait for /aszf/checkNew")
try self.app.test(.GET, "/aszf/checkNew", headers: token(bearerHTTPHeaderWithContentType: .json), afterResponse: { (response) in
	XCTAssertEqual(response.status.code, 200)
	let available: Bool? = response.content["result"] // <-- available must be false, because there is no new aszf at this point
	XCTAssertNotNil(available)
	XCTAssertFalse(available!) // <-- assert failed
	self.app.logger.info("[aszf] /aszf/checkNew done")
	checkNewAszfExpectaion.fulfill()
})

Interesting part is when I put a breakpoint after the XCTWaiter, the test paused, and after resume the test will be OK. So the new ASZFAccepted object does not created when afterResponse called?

I've never used the XCTWaiter or any expectation in the previous versions of test.
But if I call the line "acceptFirstAszfExpectaion.fulfill()" half a second later, the next asserts will be ok.
So I replaced the Post request like this:

Task { @MainActor in
	try await Task.sleep(for: .seconds(0.5))
	acceptFirstAszfExpectaion.fulfill()
}

After the next tests passes.

I use postgresql database v11 (yet).
This is how I make the post request:

app.grouped([
	TokenAuthenticator(),
	Author.guardMiddleware()
]).post("aszf", ":module") { (request) throws -> EventLoopFuture<Response> in
	let module = try request.parameters.require("module")
	switch module {

	case "acceptASZF":
		let author = try request.auth.require(Author.self)
		let aszfAcceptRequest = try request.content.decode(ASZFAcceptRequest.self)

		return ASZF.find(aszfAcceptRequest.aszfId, on: app.db)
			.unwrap(or: Abort(.notFound, reason: "No aszf found for id: \(aszfAcceptRequest.aszfId)."))
			.flatMapThrowing { (aszf) -> EventLoopFuture<Void>  in
				let aszfAccepted = ASZFAccepted()
				aszfAccepted.$author.id = try author.requireID()
				aszfAccepted.$aszf.id = try aszf.requireID()
				return aszfAccepted.create(on: app.db)
			}.flatMap { (_) -> EventLoopFuture<Response> in
				if let cookies = aszfAcceptRequest.cookiesAccepted {
					author.cookiesAccepted = cookies
				}

				if let cookies = aszfAcceptRequest.dataHandlingAccepted {
					author.dataHandlingAccepted = cookies
				}

				if let cookies = aszfAcceptRequest.gdprAccepted {
					author.gdprAccepted = cookies
				}

				return author.update(on: app.db).flatMapThrowing { (_) -> Response in
					return try Response(jsonResult: "ok")
				}
			}
	default:
		throw ApiRequestError.invalidModule(module)
	}
}

Any suggestion what I have to change? I doesn't want to use XCTWaiter or any expectaion in tests.

Ok your code is incorrect. flatMapThrowing should return a non-future otherwise you're ending up with a future of a future, so in the final flatMap you're not actually waiting for your save to complete, hence why adding breakpoints or sleeps makes it pass. (I'm surprised you're not getting a warning with your code)

I strongly encourage you to rewrite your code using async/await as it will make it much clearer what's going on and make your code valid.

You shouldn't need to use expectations or XCTWaiter in your tests

1 Like

I thought my code was wrong.
I want to upgrade all my codes to async/await but how can I make sure it's working if the tests fail…
Thanks for your help!

The tests won't fail when the code is correct