HTTPS redirect middleware for the Infrastructure package (#52)

This commit is contained in:
2026-08-24 17:32:02 +02:00
parent fcfb3f21d5
commit 1520c44b16
8 changed files with 400 additions and 5 deletions
@@ -0,0 +1,179 @@
import HTTPTypes
import Hummingbird
import HummingbirdTesting
import Testing
@testable import Infrastructure
@Suite(
"HTTPSRedirectMiddleware middleware",
.tags(.middleware)
)
struct HTTPSRedirectMiddlewareTests {
// MARK: Functional tests
@Test
func `redirects a request forwarded over plain http`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "http"]
) { response in
#expect(response.status == .movedPermanently)
#expect(response.headers[.location] == "https://example.com/hello")
}
}
}
@Test
func `preserves the path and the query of the redirected request`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello?utm_source=test&utm_medium=email",
method: .get,
headers: [.xForwardedProto: "http"]
) { response in
#expect(response.headers[.location] == "https://example.com/hello?utm_source=test&utm_medium=email")
}
}
}
@Test
func `matches the forwarded scheme regardless of its casing`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "HTTP"]
) { response in
#expect(response.status == .movedPermanently)
}
}
}
@Test
func `reads the leftmost entry of a proxy chain`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "http, https"]
) { response in
#expect(response.status == .movedPermanently)
}
}
}
@Test
func `passes a request forwarded over https through`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "https"]
) { response in
#expect(response.status == .ok)
#expect(response.headers[.location] == nil)
}
}
}
@Test
func `passes a request without the forwarded header through`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
}
}
}
@Test
func `passes every request through when the header is not trusted`() async throws {
try await app(
configuration: .init(
origin: "https://example.com",
trustForwardedProto: false
)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "http"]
) { response in
#expect(response.status == .ok)
}
}
}
@Test
func `leaves the well-known space on plain http`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/.well-known/acme-challenge/token",
method: .get,
headers: [.xForwardedProto: "http"]
) { response in
#expect(response.status == .ok)
#expect(response.headers[.location] == nil)
}
}
}
@Test
func `passes every request through when the origin is not itself https`() async throws {
try await app(
configuration: .init(
origin: "http://example.com",
trustForwardedProto: true
)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedProto: "http"]
) { response in
#expect(response.status == .ok)
}
}
}
}
// MARK: - Helpers
private extension HTTPSRedirectMiddlewareTests {
// MARK: Methods
/// Builds an application whose router applies the HTTPS-redirect middleware ahead of a `/hello`
/// route returning a plain body and a `/.well-known/acme-challenge/token` route standing in for
/// a certificate authority's challenge file.
func app(
configuration: HTTPSRedirectMiddleware<BasicRequestContext>.Configuration = .init(
origin: "https://example.com",
trustForwardedProto: true
)
) -> some ApplicationProtocol {
let router = Router()
router.addMiddleware {
HTTPSRedirectMiddleware(configuration: configuration)
}
router.get("hello") { _, _ in
"Hello!"
}
router.get(".well-known/acme-challenge/token") { _, _ in
"token"
}
return Application(router: router)
}
}