import Hummingbird import HummingbirdTesting import Logging import NIOCore import Persistence import Testing @testable import WebsiteLibrary @Suite( "HealthController controller", .tags(.controller) ) struct HealthControllerTests { // MARK: Functional tests @Test func `serves the status payload at the health path`() async throws { try await app( probe: nil ).test(.router) { client in try await client.execute( uri: "/health", method: .get ) { response in let body = String(buffer: response.body) #expect(response.status == .ok) #expect(response.headers[.contentType] == "application/json") #expect(body == #"{"status":"ok"}"#) } } } @Test func `serves ready at the readiness path when the database is reachable`() async throws { let service = Service( driver: .inMemory, logger: Logger(label: "test") ) let fluent = service() do { try await app( probe: Probe(fluent: fluent) ).test(.router) { client in try await client.execute( uri: "/health/ready", method: .get ) { response in let body = String(buffer: response.body) #expect(response.status == .ok) #expect(response.headers[.contentType] == "application/json") #expect(body == #"{"status":"ready"}"#) } } } catch { try? await fluent.shutdown() throw error } try await fluent.shutdown() } @Test func `serves unavailable at the readiness path when the database is unreachable`() async throws { // Port 1 on the loopback interface has nothing listening, so the probe's connection is refused // immediately instead of timing out. let service = Service( driver: .mysql( .init( host: "127.0.0.1", port: 1, name: "unreachable", username: "nobody", password: "nothing", tls: .off, maxConnectionsPerEventLoop: 1 ) ), logger: Logger(label: "test") ) let fluent = service() do { try await app( probe: Probe(fluent: fluent) ).test(.router) { client in try await client.execute( uri: "/health/ready", method: .get ) { response in let body = String(buffer: response.body) #expect(response.status == .serviceUnavailable) #expect(response.headers[.contentType] == "application/json") #expect(body == #"{"status":"unavailable"}"#) } } } catch { try? await fluent.shutdown() throw error } try await fluent.shutdown() } @Test func `does not serve the readiness path without a probe`() async throws { try await app(probe: nil).test(.router) { client in try await client.execute( uri: "/health/ready", method: .get ) { response in #expect(response.status == .notFound) } } } } // MARK: - Helpers private extension HealthControllerTests { /// Builds a test application serving the ``HealthController`` routes for the given probe. /// - Parameter probe: the probe supplied to the controller, or `nil` for liveness only. /// - Returns: the configured test application. func app( probe: Probe? ) -> some ApplicationProtocol { Application(router: { let router = Router() router.addRoutes(HealthController(probe: probe).routes) return router }()) } }