Initial commit.

This commit is contained in:
2026-08-19 23:19:08 +02:00
commit 22e737d9c2
153 changed files with 11680 additions and 0 deletions
@@ -0,0 +1,79 @@
import Testing
@testable import Infrastructure
@Suite(
"FNV1aHash type",
.tags(.type)
)
struct FNV1aHashTests {
// MARK: Functional tests
@Test(arguments: [
("", "cbf29ce484222325"),
("a", "af63dc4c8601ec8c"),
("b", "af63df4c8601f1a5"),
("foobar", "85944171f73967e8"),
])
func `matches the published FNV-1a 64-bit test vectors`(
input: String,
digest: String
) {
var hash = FNV1aHash()
hash.combine(input.utf8)
#expect(hash.digest == digest)
}
@Test
func `pads the digest to sixteen characters`() {
var hash = FNV1aHash()
// "aa" hashes to 0x089c4307b54596b7, whose leading zero the digest must keep.
hash.combine("aa".utf8)
#expect(hash.digest == "089c4307b54596b7")
#expect(hash.digest.count == 16)
}
@Test
func `hashes incrementally combined bytes as one stream`() {
var combined = FNV1aHash()
var whole = FNV1aHash()
combined.combine("foo".utf8)
combined.combine("bar".utf8)
whole.combine("foobar".utf8)
#expect(combined.digest == whole.digest)
}
@Test
func `distinguishes the order of the combined bytes`() {
var forward = FNV1aHash()
var backward = FNV1aHash()
forward.combine("ab".utf8)
backward.combine("ba".utf8)
#expect(forward.digest != backward.digest)
}
@Test
func `digests without consuming the running hash`() {
var hash = FNV1aHash()
hash.combine("foo".utf8)
let first = hash.digest
#expect(hash.digest == first)
hash.combine("bar".utf8)
#expect(hash.digest != first)
}
}
@@ -0,0 +1,74 @@
import Testing
@testable import Infrastructure
@Suite(
"AssetExtension enumeration",
.tags(.asset)
)
struct AssetExtensionTests {
// MARK: Computed tests
@Test(arguments: zip(
Self.extensions,
Self.contentTypes
))
func `content type`(
for fileExtension: AssetExtension,
expects contentType: String
) {
#expect(fileExtension.contentType == contentType)
}
@Test(arguments: zip(
Self.extensions,
Self.folders
))
func `folder`(
for fileExtension: AssetExtension,
expects folder: String?
) {
#expect(fileExtension.folder == folder)
}
}
// MARK: - Helpers
private extension AssetExtensionTests {
// MARK: Constants
static let extensions: [AssetExtension] = [
.css,
.js,
.png,
.ico,
.svg,
.txt,
.webmanifest,
.xml
]
static let contentTypes: [String] = [
"text/css",
"text/javascript",
"image/png",
"image/vnd.microsoft.icon",
"image/svg+xml",
"text/plain",
"application/manifest+json",
"application/xml"
]
static let folders: [String?] = [
"css",
"js",
nil,
nil,
nil,
nil,
nil,
nil
]
}
@@ -0,0 +1,159 @@
import Hummingbird
import HummingbirdTesting
import NIOCore
import Testing
@testable import Infrastructure
@Suite(
"addController method",
.tags(.`extension`)
)
struct RouterMethodsTests {
// MARK: Functional tests
@Test
func `adds the routes of every listed controller`() async throws {
let router = Router()
router.addController {
StubController(path: "first")
StubController(path: "second")
}
try await Application(router: router).test(.router) { client in
try await client.execute(
uri: "/first",
method: .get
) { response in
#expect(response.status == .ok)
#expect(String(buffer: response.body) == "first")
}
try await client.execute(
uri: "/second",
method: .get
) { response in
#expect(response.status == .ok)
#expect(String(buffer: response.body) == "second")
}
}
}
@Test(arguments: [true, false])
func `adds a controller behind a condition only when the condition holds`(
condition: Bool
) async throws {
let router = Router()
router.addController {
StubController(path: "always")
if condition {
StubController(path: "conditional")
}
}
try await Application(router: router).test(.router) { client in
try await client.execute(
uri: "/always",
method: .get
) { response in
#expect(response.status == .ok)
}
try await client.execute(
uri: "/conditional",
method: .get
) { response in
#expect(response.status == (condition ? .ok : .notFound))
}
}
}
@Test(arguments: [true, false])
func `adds only the taken branch of a condition`(
takesFirst: Bool
) async throws {
let router = Router()
router.addController {
if takesFirst {
StubController(path: "first")
} else {
StubController(path: "second")
}
}
try await Application(router: router).test(.router) { client in
try await client.execute(
uri: "/first",
method: .get
) { response in
#expect(response.status == (takesFirst ? .ok : .notFound))
}
try await client.execute(
uri: "/second",
method: .get
) { response in
#expect(response.status == (takesFirst ? .notFound : .ok))
}
}
}
@Test
func `adds a controller for every iteration of a loop`() async throws {
let paths = ["one", "two", "three"]
let router = Router()
router.addController {
for path in paths {
StubController(path: path)
}
}
try await Application(router: router).test(.router) { client in
for path in paths {
try await client.execute(
uri: "/\(path)",
method: .get
) { response in
#expect(response.status == .ok)
#expect(String(buffer: response.body) == path)
}
}
}
}
@Test
func `returns the router so calls can be chained`() async throws {
let router = Router()
router
.addController {
StubController(path: "first")
}
.addController {
StubController(path: "second")
}
try await Application(router: router).test(.router) { client in
try await client.execute(
uri: "/first",
method: .get
) { response in
#expect(response.status == .ok)
}
try await client.execute(
uri: "/second",
method: .get
) { response in
#expect(response.status == .ok)
}
}
}
}
@@ -0,0 +1,174 @@
import Foundation
import Testing
@testable import Infrastructure
@Suite(
"FingerprintAssets method",
.tags(.asset)
)
struct FingerprintAssetsTests {
// MARK: Properties
private let fingerprint = FingerprintAssets()
// MARK: Functional tests
@Test
func `fingerprints the files under a directory`() throws {
let directory = try makeDirectory(files: [
"css/site.css": "body { margin: 0; }",
"robots.txt": "User-agent: *"
])
defer {
removeDirectory(directory)
}
let token = try #require(fingerprint(directory.path))
#expect(token.count == 16)
#expect(token.allSatisfy { $0.isHexDigit })
}
@Test
func `agrees across directories with identical contents`() throws {
let files = [
"css/site.css": "body { margin: 0; }",
"js/site.js": "console.log(1);"
]
let first = try makeDirectory(files: files)
let second = try makeDirectory(files: files)
defer {
removeDirectory(first)
removeDirectory(second)
}
#expect(fingerprint(first.path) == fingerprint(second.path))
}
@Test
func `changes the token when a file's contents change`() throws {
let directory = try makeDirectory(files: [
"css/site.css": "body { margin: 0; }"
])
defer {
removeDirectory(directory)
}
let before = fingerprint(directory.path)
try "body { margin: 1px; }".write(
to: directory.appendingPathComponent("css/site.css"),
atomically: true,
encoding: .utf8
)
#expect(fingerprint(directory.path) != before)
}
@Test
func `changes the token when a file is renamed`() throws {
let contents = "body { margin: 0; }"
let first = try makeDirectory(files: ["css/site.css": contents])
let second = try makeDirectory(files: ["css/main.css": contents])
defer {
removeDirectory(first)
removeDirectory(second)
}
#expect(fingerprint(first.path) != fingerprint(second.path))
}
@Test
func `changes the token when a file is added`() throws {
let directory = try makeDirectory(files: [
"css/site.css": "body { margin: 0; }"
])
defer {
removeDirectory(directory)
}
let before = fingerprint(directory.path)
try "console.log(1);".write(
to: directory.appendingPathComponent("site.js"),
atomically: true,
encoding: .utf8
)
#expect(fingerprint(directory.path) != before)
}
@Test
func `returns nil for a directory without files`() throws {
let directory = try makeDirectory(files: [:])
defer {
removeDirectory(directory)
}
#expect(fingerprint(directory.path) == nil)
}
@Test
func `returns nil for a missing directory`() {
let missing = FileManager.default.temporaryDirectory
.appendingPathComponent("FingerprintAssetsTests-missing-\(UUID().uuidString)")
#expect(fingerprint(missing.path) == nil)
}
}
// MARK: - Helpers
private extension FingerprintAssetsTests {
// MARK: Methods
/// Creates a unique temporary directory holding the given files, keyed by relative path.
/// - Parameter files: the files to create, keyed by their path relative to the directory.
/// - Returns: the URL of the created directory.
func makeDirectory(
files: [String: String]
) throws -> URL {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("FingerprintAssetsTests-\(UUID().uuidString)")
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
for (relativePath, contents) in files {
let file = directory.appendingPathComponent(relativePath)
try FileManager.default.createDirectory(
at: file.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try contents.write(
to: file,
atomically: true,
encoding: .utf8
)
}
return directory
}
/// Removes a temporary directory created by ``makeDirectory(files:)``.
/// - Parameter directory: the URL of the directory to remove.
func removeDirectory(
_ directory: URL
) {
try? FileManager.default.removeItem(at: directory)
}
}
@@ -0,0 +1,72 @@
import Foundation
import HTTPTypes
import Hummingbird
import HummingbirdTesting
import NIOCore
import Testing
@testable import Infrastructure
@Suite(
"LocalizationMiddleware middleware",
.tags(.middleware)
)
struct LocalizationMiddlewareTests {
// MARK: Constants
private let app: Application = .init(router: {
let router = Router(context: StubRequestContext.self)
router.addMiddleware {
LocalizationMiddleware(bundle: .module)
}
router.get("language") { _, context in
context.language
}
return router
}())
// MARK: Functional tests
@Test
func `negotiates a supported language from the header`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/language",
method: .get,
headers: [.acceptLanguage: "de-DE,de;q=0.9"]
) { response in
#expect(String(buffer: response.body) == "de")
}
}
}
@Test
func `falls back to the default without a header`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/language",
method: .get
) { response in
#expect(String(buffer: response.body) == "en")
}
}
}
@Test
func `falls back to the default for an unsupported language`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/language",
method: .get,
headers: [.acceptLanguage: "fr-FR,fr;q=0.9"]
) { response in
#expect(String(buffer: response.body) == "en")
}
}
}
}
@@ -0,0 +1,194 @@
import Foundation
import Hummingbird
import HummingbirdTesting
import NIOCore
import Testing
@testable import Infrastructure
@Suite(
"NotFoundMiddleware middleware",
.tags(.middleware)
)
struct NotFoundMiddlewareTests {
// MARK: Constants
private let app: Application = .init(router: {
let router = Router(context: StubRequestContext.self)
router.addMiddleware {
LocalizationMiddleware(bundle: .module)
NotFoundMiddleware(bundle: .module) {
StubPage(locale: $0)
}
}
router.get("hello") { _, _ in
"Hello!"
}
router.get("boom") { _, _ -> String in
throw HTTPError(.badRequest)
}
return router
}())
// MARK: Functional tests
@Test
func `renders the error page for an unmatched request`() async throws {
try await app.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(response.status == .notFound)
#expect(response.headers[.contentType] == "text/html; charset=utf-8")
#expect(response.headers[.contentLanguage] == "en")
#expect(response.headers[.vary] == "Accept-Language")
#expect(body.contains("Stub content"))
}
}
}
@Test
func `renders the error page in the negotiated language`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get,
headers: [.acceptLanguage: "de-DE,de;q=0.9"]
) { response in
#expect(response.status == .notFound)
#expect(response.headers[.contentLanguage] == "de")
}
}
}
@Test
func `passes a matched response through untouched`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.body == ByteBuffer(string: "Hello!"))
}
}
}
@Test
func `rethrows a non-not-found error unchanged`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/boom",
method: .get
) { response in
let body = String(buffer: response.body)
#expect(response.status == .badRequest)
#expect(!body.contains("Stub content"))
}
}
}
@Test
func `renders versioned asset URLs when given a version`() async throws {
try await app(
assetVersion: "0123456789abcdef"
).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/stub.css?v=0123456789abcdef"))
#expect(body.contains("/js/stub.js?v=0123456789abcdef"))
}
}
}
@Test
func `renders unversioned asset URLs by default`() async throws {
try await app.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(#"href="/css/stub.css""#))
#expect(!body.contains("?v="))
}
}
}
@Test
func `serves the error page 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.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 `serves the full error page to a conditional request`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/this-path-does-not-exist",
method: .get,
headers: [.ifNoneMatch: "*"]
) { response in
let body = String(buffer: response.body)
#expect(response.status == .notFound)
#expect(body.contains("Stub content"))
}
}
}
}
// MARK: - Helpers
private extension NotFoundMiddlewareTests {
// MARK: Methods
/// Builds an application whose not-found middleware appends the given version token to the
/// error page's asset URLs.
/// - Parameter assetVersion: the version token appended to the page's asset URLs.
/// - Returns: the configured application.
func app(
assetVersion: String?
) -> some ApplicationProtocol {
let router = Router(context: StubRequestContext.self)
router.addMiddleware {
LocalizationMiddleware(bundle: .module)
NotFoundMiddleware(bundle: .module) {
StubPage(
locale: $0,
assetVersion: assetVersion
)
}
}
return Application(router: router)
}
}
@@ -0,0 +1,173 @@
import Hummingbird
import HummingbirdTesting
import Testing
@testable import Infrastructure
@Suite(
"RateLimitMiddleware middleware",
.tags(.middleware)
)
struct RateLimitMiddlewareTests {
// MARK: Functional tests
@Test
func `admits requests within the limit`() async throws {
try await app(
configuration: .init(limit: 3)
).test(.router) { client in
for _ in 1 ... 3 {
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
}
}
}
}
@Test
func `rejects a request over the limit with a retry-after header`() async throws {
try await app(
configuration: .init(limit: 2)
).test(.router) { client in
for _ in 1 ... 2 {
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
}
}
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .tooManyRequests)
let retryAfter = try #require(response.headers[.retryAfter])
#expect(try #require(Int(retryAfter)) >= 1)
}
}
}
@Test
func `admits requests again once the window resets`() async throws {
try await app(
configuration: .init(
limit: 1,
window: .milliseconds(50)
)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
}
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .tooManyRequests)
}
try await Task.sleep(for: .milliseconds(100))
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
}
}
}
@Test
func `separates clients by their forwarded address when trusted`() async throws {
try await app(
configuration: .init(
limit: 1,
trustForwardedFor: true
)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedFor: "203.0.113.7"]
) { response in
#expect(response.status == .ok)
}
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedFor: "203.0.113.8"]
) { response in
#expect(response.status == .ok)
}
// The first entry names the client; the appended proxy hop must not change its key.
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedFor: "203.0.113.7, 10.0.0.1"]
) { response in
#expect(response.status == .tooManyRequests)
}
}
}
@Test
func `ignores the forwarded address when not trusted`() async throws {
try await app(
configuration: .init(limit: 1)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedFor: "203.0.113.7"]
) { response in
#expect(response.status == .ok)
}
// Without trust (and without a connection address in router-only testing), every client
// shares one bucket, so a rotated header must not mint a fresh budget.
try await client.execute(
uri: "/hello",
method: .get,
headers: [.xForwardedFor: "203.0.113.8"]
) { response in
#expect(response.status == .tooManyRequests)
}
}
}
}
// MARK: - Helpers
private extension RateLimitMiddlewareTests {
// MARK: Methods
/// Builds an application whose router applies the rate-limit middleware ahead of a single
/// `/hello` route returning a plain body.
func app(
configuration: RateLimitMiddleware<BasicRequestContext>.Configuration
) -> some ApplicationProtocol {
let router = Router()
router.addMiddleware {
RateLimitMiddleware(configuration: configuration)
}
router.get("hello") { _, _ in
"Hello!"
}
return Application(router: router)
}
}
@@ -0,0 +1,159 @@
import Hummingbird
import HummingbirdTesting
import Testing
@testable import Infrastructure
@Suite(
"SecurityHeadersMiddleware middleware",
.tags(.middleware)
)
struct SecurityHeadersMiddlewareTests {
// MARK: Functional tests
@Test
func `applies the default security headers to a response`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentSecurityPolicy] == .Security.contentSecurityPolicy)
#expect(response.headers[.xContentTypeOptions] == .Security.contentTypeOptions)
#expect(response.headers[.frameOptions] == .Security.frameOptions)
#expect(response.headers[.referrerPolicy] == .Security.referrerPolicy)
#expect(response.headers[.permissionsPolicy] == .Security.permissionsPolicy)
}
}
}
@Test
func `omits strict-transport-security by default`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.headers[.strictTransportSecurity] == nil)
}
}
}
@Test
func `applies strict-transport-security when configured`() async throws {
let value = "max-age=31536000; includeSubDomains"
try await app(
configuration: .init(strictTransportSecurity: value)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.headers[.strictTransportSecurity] == value)
}
}
}
@Test
func `applies a custom header value`() async throws {
let value = "default-src 'none'"
try await app(
configuration: .init(contentSecurityPolicy: value)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.headers[.contentSecurityPolicy] == value)
}
}
}
@Test
func `omits a header whose configured value is nil`() async throws {
try await app(
configuration: .init(contentTypeOptions: nil)
).test(.router) { client in
try await client.execute(
uri: "/hello",
method: .get
) { response in
#expect(response.headers[.xContentTypeOptions] == nil)
}
}
}
@Test
func `replaces an existing header value set downstream`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/weak",
method: .get
) { response in
#expect(response.headers[.xContentTypeOptions] == .Security.contentTypeOptions)
}
}
}
@Test
func `applies the security headers to an error response`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/throws",
method: .get
) { response in
#expect(response.status == .badRequest)
#expect(response.headers[.contentSecurityPolicy] == .Security.contentSecurityPolicy)
#expect(response.headers[.xContentTypeOptions] == .Security.contentTypeOptions)
#expect(response.headers[.frameOptions] == .Security.frameOptions)
#expect(response.headers[.referrerPolicy] == .Security.referrerPolicy)
#expect(response.headers[.permissionsPolicy] == .Security.permissionsPolicy)
}
}
}
}
// MARK: - Helpers
private extension SecurityHeadersMiddlewareTests {
// MARK: Methods
/// Builds an application whose router applies the security-headers middleware ahead of three
/// routes: `/hello` returns a plain body, `/weak` returns a response that already carries a
/// deliberately weak `X-Content-Type-Options` value for the middleware to override, and
/// `/throws` fails with an `HTTPError` the way the controllers do on invalid input.
func app(
configuration: SecurityHeadersMiddleware<BasicRequestContext>.Configuration = .init()
) -> some ApplicationProtocol {
let router = Router()
router.addMiddleware {
SecurityHeadersMiddleware(configuration: configuration)
}
router.get("hello") { _, _ in
"Hello!"
}
router.get("weak") { _, _ -> Response in
var response = Response(status: .ok)
response.headers[.xContentTypeOptions] = "weak"
return response
}
router.get("throws") { _, _ -> Response in
throw HTTPError(.badRequest)
}
return Application(router: router)
}
}
@@ -0,0 +1,134 @@
import HTTPTypes
import Hummingbird
import HummingbirdTesting
import Testing
@testable import Infrastructure
@Suite(
"VaryMiddleware middleware",
.tags(.middleware)
)
struct VaryMiddlewareTests {
// MARK: Functional tests
@Test
func `adds the default field to a response without a vary header`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/plain",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.headers[.vary] == "Accept-Encoding")
}
}
}
@Test
func `appends to an existing vary header`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/localized",
method: .get
) { response in
#expect(response.headers[.vary] == "Accept-Language, Accept-Encoding")
}
}
}
@Test
func `does not duplicate a name already present`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/encoded",
method: .get
) { response in
#expect(response.headers[.vary] == "Accept-Encoding")
}
}
}
@Test
func `matches an existing name regardless of its casing`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/lowercased",
method: .get
) { response in
#expect(response.headers[.vary] == "accept-encoding")
}
}
}
@Test
func `normalizes the whitespace of an existing list`() async throws {
try await app().test(.router) { client in
try await client.execute(
uri: "/spaced",
method: .get
) { response in
#expect(response.headers[.vary] == "Accept-Language, User-Agent, Accept-Encoding")
}
}
}
@Test
func `appends every configured field`() async throws {
try await app(
fields: [.acceptEncoding, .acceptLanguage]
).test(.router) { client in
try await client.execute(
uri: "/plain",
method: .get
) { response in
#expect(response.headers[.vary] == "Accept-Encoding, Accept-Language")
}
}
}
}
// MARK: - Helpers
private extension VaryMiddlewareTests {
// MARK: Methods
/// Builds an application whose router applies the vary middleware ahead of routes whose
/// responses carry different `Vary` starting points: `/plain` none, `/localized` an
/// `Accept-Language`, `/encoded` an `Accept-Encoding` already, `/lowercased` a lowercase
/// `accept-encoding`, and `/spaced` a list with irregular whitespace.
func app(
fields: [HTTPField.Name] = [.acceptEncoding]
) -> some ApplicationProtocol {
let router = Router()
router.addMiddleware {
VaryMiddleware(fields: fields)
}
router.get("plain") { _, _ in
"Hello!"
}
for (path, vary) in [
("localized", "Accept-Language"),
("encoded", "Accept-Encoding"),
("lowercased", "accept-encoding"),
("spaced", "Accept-Language , User-Agent"),
] {
router.get(RouterPath(path)) { _, _ -> Response in
var response = Response(status: .ok)
response.headers[.vary] = vary
return response
}
}
return Application(router: router)
}
}
@@ -0,0 +1,92 @@
import Testing
@testable import Infrastructure
@Suite(
"Asset protocol",
.tags(.`protocol`)
)
struct AssetTests {
// MARK: Properties
private let image = StubAsset(
fileExtensions: [.png],
fileName: "icon"
)
private let portrait = StubAsset(
folder: "img/organizer",
fileExtensions: [.jpg],
fileName: "portrait"
)
private let shared = StubAsset(
fileExtensions: [.css, .js],
fileName: "shared"
)
// MARK: Method tests
@Test
func `relative path nests the file inside its extension's folder`() {
#expect(shared.relativePath(for: .css) == "css/shared.css")
#expect(shared.relativePath(for: .js) == "js/shared.js")
}
@Test
func `relative path prefers the asset's own folder over the extension's`() {
#expect(portrait.relativePath(for: .jpg) == "img/organizer/portrait.jpg")
}
@Test
func `relative path keeps the file at the root without a folder`() {
#expect(image.relativePath(for: .png) == "icon.png")
}
@Test
func `url path prefixes the relative path with a slash`() {
#expect(shared.urlPath(for: .css) == "/css/shared.css")
#expect(image.urlPath(for: .png) == "/icon.png")
}
@Test
func `url path appends a version token as a query parameter`() {
#expect(shared.urlPath(
for: .css,
version: "0123456789abcdef"
) == "/css/shared.css?v=0123456789abcdef")
}
@Test(arguments: [nil, ""] as [String?])
func `url path without a version`(
version: String?
) {
#expect(shared.urlPath(
for: .css,
version: version
) == "/css/shared.css")
}
@Test(arguments: [
"",
".",
"Resources/Static"
])
func `path relative to`(
_ basePath: String
) {
for fileExtension in shared.fileExtensions {
let pathRelativeToBasePath = shared.path(
relativeTo: basePath,
for: fileExtension
)
let relativePath = shared.relativePath(for: fileExtension)
if basePath.isEmpty {
#expect(pathRelativeToBasePath == relativePath)
} else {
#expect(pathRelativeToBasePath == "\(basePath)/\(relativePath)")
}
}
}
}
@@ -0,0 +1,213 @@
import Elementary
import Foundation
import Testing
@testable import Infrastructure
@Suite(
"Page protocol",
.tags(.`protocol`)
)
struct PageTests {
// MARK: Functional tests
@Test
func `assembles the document around the page's parts`() {
let html = StubPage().render()
#expect(html.contains("<title>Stub Page</title>"))
#expect(html.contains(#"lang="en""#))
#expect(html.contains(#"name="viewport""#))
#expect(html.contains(#"<meta name="stub" content="marker">"#))
#expect(html.contains(#"<link rel="stylesheet" href="/css/stub.css">"#))
#expect(html.contains(#"<script defer src="/js/stub.js"></script>"#))
#expect(html.contains("Stub content"))
}
@Test
func `places the metadata between the viewport and the stylesheets`() throws {
let html = StubPage().render()
let viewport = try #require(html.range(of: #"name="viewport""#))
let metadata = try #require(html.range(of: #"name="stub""#))
let stylesheet = try #require(html.range(of: "/css/stub.css"))
#expect(viewport.lowerBound < metadata.lowerBound)
#expect(metadata.lowerBound < stylesheet.lowerBound)
}
@Test
func `renders the scripts deferred in the head, after the stylesheets`() throws {
let html = StubPage().render()
let stylesheet = try #require(html.range(of: "/css/stub.css"))
let script = try #require(html.range(of: "/js/stub.js"))
let content = try #require(html.range(of: "Stub content"))
// Deferred head scripts start downloading during head parsing but still execute, in order, only after the
// document is parsed the semantics end-of-body tags gave, minus the late download start.
#expect(stylesheet.lowerBound < script.lowerBound)
#expect(script.lowerBound < content.lowerBound)
}
@Test
func `omits the summary and canonical tags by default`() {
let html = StubPage().render()
#expect(!html.contains(#"name="description""#))
#expect(!html.contains(#"rel="canonical""#))
}
@Test
func `renders the summary and canonical tags when provided`() {
let html = StubPage(
canonicalURL: "https://stub.example/",
summary: "A stub page."
).render()
#expect(html.contains(#"<meta name="description" content="A stub page.">"#))
#expect(html.contains(#"<link rel="canonical" href="https://stub.example/">"#))
}
@Test
func `omits the social card tags by default`() {
let html = StubPage().render()
#expect(!html.contains(#"property="og:"#))
#expect(!html.contains(#"name="twitter:card""#))
}
@Test
func `renders the social card tags when provided`() {
let html = StubPage(socialCard: .init(
title: "Stub Page",
summary: "A stub page.",
url: "https://stub.example/",
image: .init(
url: "https://stub.example/img/card.png",
width: 2400,
height: 1260
)
)).render()
#expect(html.contains(#"<meta property="og:type" content="website">"#))
#expect(html.contains(#"<meta property="og:title" content="Stub Page">"#))
#expect(html.contains(#"<meta property="og:description" content="A stub page.">"#))
#expect(html.contains(#"<meta property="og:url" content="https://stub.example/">"#))
#expect(html.contains(#"<meta property="og:image" content="https://stub.example/img/card.png">"#))
#expect(html.contains(#"<meta property="og:image:width" content="2400">"#))
#expect(html.contains(#"<meta property="og:image:height" content="1260">"#))
#expect(html.contains(#"<meta name="twitter:card" content="summary_large_image">"#))
}
@Test
func `omits the structured data script by default`() {
let html = StubPage().render()
#expect(!html.contains("application/ld+json"))
}
@Test
func `renders the structured data script when provided`() {
let html = StubPage(structuredData: .init(
name: "Stub Site",
url: "https://stub.example/",
logo: "https://stub.example/logo.png",
profiles: ["https://social.example/stub"]
)).render()
#expect(html.contains(
#"<script type="application/ld+json">"# +
#"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://stub.example/#organization","name":"Stub Site","url":"https://stub.example/","logo":"https://stub.example/logo.png","sameAs":["https://social.example/stub"]},"# +
#"{"@type":"WebSite","name":"Stub Site","url":"https://stub.example/","publisher":{"@id":"https://stub.example/#organization"}}]}"# +
#"</script>"#
))
}
@Test
func `omits the analytics tracker by default`() {
let html = StubPage().render()
#expect(!html.contains("data-website-id"))
#expect(!html.contains(#"rel="preconnect""#))
}
@Test
func `renders the analytics tracker when provided`() throws {
let html = StubPage(analytics: .init(
scriptURL: "https://analytics.example.com/script",
websiteID: "0000-website-id",
domains: "example.com"
)).render()
#expect(html.contains(#"<script defer src="https://analytics.example.com/script" data-website-id="0000-website-id" data-domains="example.com" data-exclude-hash="true" data-do-not-track="true" data-performance="true"></script>"#))
// The preconnect hint warms the tracker origin's connection before the parser reaches the script tag.
let preconnect = try #require(html.range(of: #"<link rel="preconnect" href="https://analytics.example.com">"#))
let script = try #require(html.range(of: #"<script defer src="https://analytics.example.com/script""#))
#expect(preconnect.lowerBound < script.lowerBound)
}
@Test
func `omits the session recorder script by default`() {
let html = StubPage(analytics: .init(
scriptURL: "https://analytics.example.com/script",
websiteID: "0000-website-id",
domains: "example.com"
)).render()
#expect(!html.contains("recorder.js"))
}
@Test
func `renders the session recorder script when recorder mode is on`() throws {
let html = StubPage(analytics: .init(
scriptURL: "https://analytics.example.com/script",
websiteID: "0000-website-id",
domains: "example.com",
recorder: true
)).render()
#expect(html.contains(#"<script defer src="https://analytics.example.com/recorder.js" data-website-id="0000-website-id"></script>"#))
let tracker = try #require(html.range(of: #"<script defer src="https://analytics.example.com/script""#))
let recorder = try #require(html.range(of: #"<script defer src="https://analytics.example.com/recorder.js""#))
#expect(tracker.lowerBound < recorder.lowerBound)
}
@Test
func `omits the analytics behavior flags that are disabled`() {
let html = StubPage(analytics: .init(
scriptURL: "https://analytics.example.com/script",
websiteID: "0000-website-id",
domains: "example.com",
excludeHash: true,
doNotTrack: false,
performance: false
)).render()
#expect(html.contains(#"<script defer src="https://analytics.example.com/script" data-website-id="0000-website-id" data-domains="example.com" data-exclude-hash="true"></script>"#))
#expect(!html.contains("data-do-not-track"))
#expect(!html.contains("data-performance"))
}
@Test
func `appends the version token to the asset URLs`() {
let html = StubPage(assetVersion: "0123456789abcdef").render()
#expect(html.contains("/css/stub.css?v=0123456789abcdef"))
#expect(html.contains("/js/stub.js?v=0123456789abcdef"))
}
@Test
func `derives the document language from the locale`() {
let html = StubPage(locale: .init(identifier: "de-DE")).render()
#expect(html.contains(#"lang="de""#))
}
}
@@ -0,0 +1,105 @@
import Testing
@testable import Infrastructure
@Suite(
"Analytics.Event type",
.tags(.type)
)
struct AnalyticsEventTests {
// MARK: Computed tests
@Test
func `renders the event name alone when it carries no properties`() {
let event = Analytics.Event(name: "instagram")
#expect(event.attributes.count == 1)
#expect(event.attributes[0] == Analytics.Attribute(
"data-umami-event",
value: "instagram"
))
}
@Test
func `renders each property as an attribute suffixed by its key`() {
let event = Analytics.Event(
name: "playlist",
properties: ["set": "avc-xi"]
)
#expect(event.attributes.count == 2)
#expect(event.attributes[1] == Analytics.Attribute(
"data-umami-event-set",
value: "avc-xi"
))
}
@Test
func `renders the name first, then the properties in the order given`() {
let event = Analytics.Event(
name: "interview",
properties: [
"placement": "credit",
"locale": "en",
]
)
// The order is the rendered attribute order, which a dictionary would leave to churn between builds.
#expect(event.attributes.map(\.name) == [
"data-umami-event",
"data-umami-event-placement",
"data-umami-event-locale",
])
#expect(event.attributes.map(\.value) == ["interview", "credit", "en"])
}
// MARK: Equatable tests
@Test
func `matches an event rendering the same attributes`() {
let event = Analytics.Event(
name: "playlist",
properties: ["set": "avc-xi"]
)
#expect(event == Analytics.Event(
name: "playlist",
properties: ["set": "avc-xi"]
))
}
@Test(arguments: [
Analytics.Event(
name: "playlist",
properties: ["set": "avc-ix"]
),
Analytics.Event(
name: "playlist",
properties: ["show": "avc-xi"]
),
Analytics.Event(
name: "apple_music",
properties: ["set": "avc-xi"]
),
Analytics.Event(name: "playlist"),
Analytics.Event(
name: "playlist",
properties: [
"set": "avc-xi",
"placement": "strip",
]
),
])
func `differs from an event rendering anything else`(
from other: Analytics.Event
) {
let event = Analytics.Event(
name: "playlist",
properties: ["set": "avc-xi"]
)
#expect(event != other)
}
}
@@ -0,0 +1,135 @@
import Testing
@testable import Infrastructure
@Suite(
"Analytics type",
.tags(.type)
)
struct AnalyticsTests {
// MARK: Functional tests
@Test
func `lists the website id and domains with every behavior flag on by default`() {
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: "example.com"
)
#expect(analytics.attributes.map(\.name) == [
"data-website-id",
"data-domains",
"data-exclude-hash",
"data-do-not-track",
"data-performance",
])
#expect(analytics.attributes.map(\.value) == [
"id-123",
"example.com",
"true",
"true",
"true",
])
}
@Test
func `derives its origin from the script URL`() {
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: "example.com"
)
#expect(analytics.origin == "https://analytics.example.com")
}
@Test
func `keeps an explicit port in its origin`() {
let analytics = Analytics(
scriptURL: "http://localhost:3000/script",
websiteID: "id-123",
domains: "localhost"
)
#expect(analytics.origin == "http://localhost:3000")
}
@Test
func `carries no origin for a script URL without a scheme or host`() {
let analytics = Analytics(
scriptURL: "/script",
websiteID: "id-123",
domains: "example.com"
)
#expect(analytics.origin == nil)
}
@Test
func `carries no recorder script URL by default`() {
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: "example.com"
)
#expect(analytics.recorderScriptURL == nil)
}
@Test
func `derives its recorder script URL from the origin when recorder mode is on`() {
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: "example.com",
recorder: true
)
#expect(analytics.recorderScriptURL == "https://analytics.example.com/recorder.js")
}
@Test
func `carries no recorder script URL when no origin can be derived`() {
let analytics = Analytics(
scriptURL: "/script",
websiteID: "id-123",
domains: "example.com",
recorder: true
)
#expect(analytics.recorderScriptURL == nil)
}
@Test
func `omits the disabled behavior flags`() {
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: "example.com",
excludeHash: true,
doNotTrack: false,
performance: false
)
#expect(analytics.attributes.map(\.name) == [
"data-website-id",
"data-domains",
"data-exclude-hash",
])
}
@Test
func `omits the domains filter when it is empty`() {
// The tracker reads the attribute as an allowlist, so rendering it empty would filter out every host rather than none.
let analytics = Analytics(
scriptURL: "https://analytics.example.com/script",
websiteID: "id-123",
domains: ""
)
#expect(!analytics.attributes.map(\.name).contains("data-domains"))
}
}
@@ -0,0 +1,91 @@
import Testing
@testable import Infrastructure
@Suite(
"SocialCard type",
.tags(.type)
)
struct SocialCardTests {
// MARK: Functional tests
@Test
func `derives the full tag list from a complete card`() {
let card = SocialCard(
title: "A Title",
summary: "A summary.",
url: "https://site.example/",
siteName: "A Site",
locale: "en",
image: .init(
url: "https://site.example/img/card.png",
width: 2400,
height: 1260,
alt: "An image."
)
)
#expect(card.tags == [
.init("website", name: .type),
.init("A Site", name: .siteName),
.init("A Title", name: .title),
.init("A summary.", name: .description),
.init("https://site.example/", name: .url),
.init("en", name: .locale),
.init("https://site.example/img/card.png", name: .image),
.init("2400", name: .imageWidth),
.init("1260", name: .imageHeight),
.init("An image.", name: .imageAlt),
.init("summary_large_image", name: .twitter),
])
}
@Test
func `omits the tags of the facts a minimal card does not carry`() {
let card = SocialCard(title: "A Title")
#expect(card.tags == [
.init("website", name: .type),
.init("A Title", name: .title),
.init("summary_large_image", name: .twitter),
])
}
@Test
func `omits the image alt tag when the image carries none`() {
let card = SocialCard(
title: "A Title",
image: .init(
url: "https://site.example/img/card.png",
width: 2400,
height: 1260
)
)
let names = card.tags.map(\.name)
#expect(names.contains(.image))
#expect(!names.contains(.imageAlt))
}
@Test
func `carries the type and style it is given`() {
let card = SocialCard(
title: "A Title",
type: "article",
style: .summary
)
#expect(card.tags.contains(.init("article", name: .type)))
#expect(card.tags.contains(.init("summary", name: .twitter)))
}
@Test
func `keys a tag by the attribute its name dictates`() {
#expect(SocialCard.Tag.Name.twitter.attribute == .name)
#expect(SocialCard.Tag.Name.title.attribute == .property)
#expect(SocialCard.Tag("website", name: .type).attribute == .property)
}
}
@@ -0,0 +1,159 @@
import Foundation
import Testing
@testable import Infrastructure
@Suite(
"StructuredData type",
.tags(.type)
)
struct StructuredDataTests {
// MARK: Functional tests
@Test
func `derives the full payload from complete data`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/",
logo: "https://site.example/logo.png",
profiles: [
"https://social.example/a-site",
"https://videos.example/a-site",
]
)
#expect(data.payload == #"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://site.example/#organization","name":"A Site","url":"https://site.example/","logo":"https://site.example/logo.png","# +
#""sameAs":["https://social.example/a-site","https://videos.example/a-site"]},"# +
#"{"@type":"WebSite","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"}}]}"#
)
}
@Test
func `omits the properties of the facts minimal data does not carry`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/"
)
#expect(data.payload == #"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://site.example/#organization","name":"A Site","url":"https://site.example/"},"# +
#"{"@type":"WebSite","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"}}]}"#
)
}
@Test
func `renders a composed node graph`() {
let data = StructuredData(nodes: [
.init(
type: "MusicEvent",
properties: [
.init(.name, value: .string("A Gig")),
.init("location", value: .node(.init(
type: "Place",
properties: [
.init(.name, value: .string("A Venue")),
]
))),
.init("organizer", value: .reference("https://site.example/#organization")),
]
),
])
#expect(data.payload == #"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"MusicEvent","name":"A Gig","location":{"@type":"Place","name":"A Venue"},"# +
#""organizer":{"@id":"https://site.example/#organization"}}]}"#
)
}
@Test
func `escapes the values it embeds in the payload`() {
let data = StructuredData(
name: #"A "Quoted" \ Site"#,
url: "https://site.example/</script>"
)
#expect(data.payload.contains(#""name":"A \"Quoted\" \\ Site""#))
// The `<` is escaped so a value can never close the script tag embedding the payload.
#expect(!data.payload.contains("</script>"))
#expect(data.payload.contains("\\" + "u003c/script>"))
}
@Test
func `renders a node fragment with its identifier`() {
let node = StructuredData.Node(
type: "Organization",
id: "https://site.example/#organization",
properties: [
.init(.name, value: .string("A Site")),
]
)
#expect(node.fragment == #"{"@type":"Organization","@id":"https://site.example/#organization","name":"A Site"}"#)
}
@Test
func `renders a node fragment without an identifier or properties`() {
let node = StructuredData.Node(
type: "Organization",
properties: []
)
#expect(node.fragment == #"{"@type":"Organization"}"#)
}
@Test
func `renders the common names and kinds by their schema.org spelling`() {
#expect(StructuredData.Property.Name.logo.rawValue == "logo")
#expect(StructuredData.Property.Name.name.rawValue == "name")
#expect(StructuredData.Property.Name.publisher.rawValue == "publisher")
#expect(StructuredData.Property.Name.sameAs.rawValue == "sameAs")
#expect(StructuredData.Property.Name.url.rawValue == "url")
#expect(StructuredData.Node.Kind.organization.rawValue == "Organization")
#expect(StructuredData.Node.Kind.website.rawValue == "WebSite")
}
@Test
func `renders the fragment of every value case`() {
#expect(StructuredData.Value.string("A Value").fragment == #""A Value""#)
#expect(StructuredData.Value.array([.string("A"), .string("B")]).fragment == #"["A","B"]"#)
#expect(StructuredData.Value.reference("https://site.example/#organization").fragment == #"{"@id":"https://site.example/#organization"}"#)
#expect(StructuredData.Value.node(.init(type: "Place", properties: [])).fragment == #"{"@type":"Place"}"#)
}
@Test
func `renders a string as a quoted literal`() {
#expect(StructuredData.Value.literal("A Value") == #""A Value""#)
#expect(StructuredData.Value.literal("") == "\"\"")
}
@Test
func `pads the escape of a control character to four digits`() {
#expect(StructuredData.Value.literal("\u{0}") == "\"" + "\\" + "u0000" + "\"")
#expect(StructuredData.Value.literal("\u{1f}") == "\"" + "\\" + "u001f" + "\"")
#expect(StructuredData.Value.literal("\u{a}") == "\"" + "\\" + "u000a" + "\"")
// The first scalar past the control range passes through untouched.
#expect(StructuredData.Value.literal(" ") == #"" ""#)
}
@Test
func `derives a payload that parses back to the facts it carries`() throws {
let name = "A \"Site\"\nwith \\ every <hazard>"
let data = StructuredData(
name: name,
url: "https://site.example/</script>",
logo: "https://site.example/logo.png",
profiles: ["https://social.example/a-site"]
)
let object = try JSONSerialization.jsonObject(with: Data(data.payload.utf8))
let graph = try #require((object as? [String: Any])?["@graph"] as? [[String: Any]])
#expect(graph.count == 2)
#expect(graph[0]["name"] as? String == name)
#expect(graph[0]["sameAs"] as? [String] == ["https://social.example/a-site"])
#expect(graph[1]["url"] as? String == "https://site.example/</script>")
}
}