Renamed the Web package as Infrastructure (#25)

This PR contains the work done to rename the _Web_ package as _Infrastructure_, to provide a clear naming and purpose to this particular package within the project.

To provide further details about the work:

* Infrastructure
  * Asset fingerprinting: an FNV-1a token derived from the static files directory, appended as ?v= to asset URLs so deploys bust caches; pre-rendered pages also revalidate via weak ETags.
  * New middlewares: fixed-window RateLimitMiddleware (per-client budgets keyed by trusted X-Forwarded-For or remote address) and VaryMiddleware (Accept-Encoding on every response); SecurityHeadersMiddleware now also stamps error responses.
  * Auto-generated HEAD endpoints, cache max-age configuration, and Docker build/Compose refinements.
  * Protocols and scaffolding: Asset/AssetExtension, the Page protocol (viewport, stylesheets, scripts, versioned URLs), and LocalizedRequestContext.
  * Rate limiter's counter store swapped from an actor to a Mutex (no executor hop per request) with amortized batch eviction instead of O(n²) scans under client floods.
  * FingerprintAssets reports unreadable files to a logger instead of silently producing a token that never busts their cache.

Reviewed-on: rock-n-code/loud-amsterdam#25
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
2026-07-23 01:04:37 +00:00
committed by javier
parent a868275347
commit cdded06ba3
58 changed files with 2844 additions and 519 deletions
+174 -5
View File
@@ -2,6 +2,7 @@ import Configuration
import Foundation
import Hummingbird
import HummingbirdTesting
import Infrastructure
import NIOCore
import Testing
@@ -12,11 +13,11 @@ import Testing
struct AppTests {
// MARK: Constants
private let textExtensions: [StaticFile.Extension] = [
// Stylesheets and scripts are referenced through fingerprinted URLs, so they are served immutable.
private let immutableExtensions: [AssetExtension] = [
.css,
.js,
.txt
.js
]
// Absolute path to the package's "Resources/Static" folder, derived from this
@@ -48,6 +49,22 @@ struct AppTests {
}
}
@Test
func `landing page to answer a head request`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/",
method: .head
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "text/html; charset=utf-8")
#expect(response.body.readableBytes == 0)
}
}
}
@Test
func `health check to be served at the health path`() async throws {
try await app(
@@ -106,7 +123,9 @@ struct AppTests {
#expect(cacheControl.contains("public") == true)
#expect(cacheControl.contains("max-age=") == true)
if textExtensions.contains(fileExtension) {
if immutableExtensions.contains(fileExtension) {
#expect(cacheControl.contains("immutable") == true)
} else if fileExtension == .txt {
#expect(cacheControl.contains("must-revalidate") == true)
}
}
@@ -114,6 +133,81 @@ struct AppTests {
}
}
@Test
func `versioned asset URL to be served`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/css/shared.css?v=0123456789abcdef",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "text/css")
}
}
}
@Test
func `landing page to reference fingerprinted assets`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(body.contains("/css/shared.css?v="))
#expect(body.contains("/js/shared.js?v="))
}
}
}
@Test
func `landing page to revalidate with an entity tag`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
let eTag = try await client.execute(
uri: "/",
method: .get
) { response in
#expect(response.headers[.cacheControl] == "public, no-cache")
return try #require(response.headers[.eTag])
}
try await client.execute(
uri: "/",
method: .get,
headers: [.ifNoneMatch: eTag]
) { response in
#expect(response.status == .notModified)
#expect(response.body.readableBytes == 0)
#expect(response.headers[.eTag] == eTag)
}
}
}
@Test
func `responses to vary on language and encoding`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/",
method: .get
) { response in
let vary = try #require(response.headers[.vary])
#expect(vary.contains("Accept-Language"))
#expect(vary.contains("Accept-Encoding"))
}
}
}
@Test
func `response to be compressed when the client supports it`() async throws {
try await app(
@@ -163,6 +257,81 @@ struct AppTests {
}
}
@Test
func `error page to reference fingerprinted assets`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(body.contains("/css/error.css?v="))
#expect(body.contains("/js/shared.js?v="))
}
}
}
@Test
func `error page to be served without revalidation headers`() async throws {
// A `304 Not Modified` only ever stands in for a success, so the error page must not
// invite revalidation with an entity tag or a cache policy.
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
#expect(response.status == .notFound)
#expect(response.headers[.eTag] == nil)
#expect(response.headers[.cacheControl] == nil)
}
}
}
@Test
func `error page to vary on language and encoding`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get
) { response in
let vary = try #require(response.headers[.vary])
#expect(vary.contains("Accept-Language"))
#expect(vary.contains("Accept-Encoding"))
}
}
}
@Test
func `landing page to revalidate a conditional head request`() async throws {
try await app(
staticFilesPath: staticFilesPath
).test(.router) { client in
let eTag = try await client.execute(
uri: "/",
method: .get
) { response in
try #require(response.headers[.eTag])
}
try await client.execute(
uri: "/",
method: .head,
headers: [.ifNoneMatch: eTag]
) { response in
#expect(response.status == .notModified)
#expect(response.body.readableBytes == 0)
}
}
}
@Test
func `security headers to be applied to the landing page`() async throws {
try await app(