Project updates from Template (#1)
This PR contains the latest updates from the generic Website template, which have been added while working on #loud-amsterdam. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -14,10 +14,14 @@ struct AppTests {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
// Stylesheets and scripts are referenced through fingerprinted URLs, so they are served immutable.
|
||||
// Referenced through fingerprinted URLs, or an immutable subset file in the case of a font, so all of these are served immutable.
|
||||
private let immutableExtensions: [AssetExtension] = [
|
||||
.css,
|
||||
.js
|
||||
.jpg,
|
||||
.js,
|
||||
.mp4,
|
||||
.webp,
|
||||
.woff2
|
||||
]
|
||||
|
||||
// Absolute path to the copy of the package's "Resources/Static" folder made into the test bundle
|
||||
@@ -107,7 +111,7 @@ struct AppTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: StaticFile.allCases)
|
||||
@Test(arguments: StaticFile.all)
|
||||
func `static files to be served`(
|
||||
staticFile file: StaticFile
|
||||
) async throws {
|
||||
|
||||
@@ -61,6 +61,96 @@ struct ConfigReaderPropertiesTests {
|
||||
#expect(reader(values: [.Analytics.websiteID: ""]).analytics == nil)
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
"shared.css",
|
||||
"shared.js",
|
||||
"milker-400.woff2",
|
||||
"booth.mp4",
|
||||
"portrait.jpg",
|
||||
"portrait.webp"
|
||||
])
|
||||
func `cache control to mark the fingerprinted media immutable`(
|
||||
file: String
|
||||
) throws {
|
||||
let header = try #require(reader().cacheControl.getCacheControlHeader(for: file))
|
||||
|
||||
#expect(header.contains("immutable"))
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
// The icons are exempt: a browser fetches `/favicon.ico` unversioned whatever the markup says, and the manifest names the PNGs.
|
||||
"favicon.ico",
|
||||
"icon-192.png",
|
||||
"icon.svg"
|
||||
])
|
||||
func `cache control to leave the unversioned images mutable`(
|
||||
file: String
|
||||
) throws {
|
||||
let header = try #require(reader().cacheControl.getCacheControlHeader(for: file))
|
||||
|
||||
#expect(!header.contains("immutable"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `cache control to have the unversioned text revalidate`() throws {
|
||||
let header = try #require(reader().cacheControl.getCacheControlHeader(for: "robots.txt"))
|
||||
|
||||
#expect(header.contains("must-revalidate"))
|
||||
#expect(!header.contains("immutable"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `driver to default to the in-memory database`() throws {
|
||||
guard case .inMemory = try reader().driver else {
|
||||
Issue.record("Expected the in-memory driver when 'database.driver' is unset.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `driver to select postgres when configured`() throws {
|
||||
guard case .postgres = try reader(values: [.Database.driver: "postgres"]).driver else {
|
||||
Issue.record("Expected the postgres driver when 'database.driver' selects it.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `driver to throw on an unknown token`() {
|
||||
// A silent fallback would run the deployment on the ephemeral database and discard every write on restart.
|
||||
#expect(throws: ConfigError.unknownDatabaseDriver("mariadb")) {
|
||||
_ = try reader(values: [.Database.driver: "mariadb"]).driver
|
||||
}
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
String.Database.tlsOff,
|
||||
String.Database.tlsPrefer,
|
||||
String.Database.tlsRequire
|
||||
])
|
||||
func `driver to accept a recognized tls token`(token: String) throws {
|
||||
let reader = reader(values: [
|
||||
.Database.driver: .init(stringLiteral: .Database.driverPostgres),
|
||||
.Database.tls: .init(stringLiteral: token)
|
||||
])
|
||||
|
||||
guard case .postgres = try reader.driver else {
|
||||
Issue.record("Expected the postgres driver for the '\(token)' TLS token.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `driver to throw on an unknown tls token`() {
|
||||
// A silent fallback to `prefer` hands the password over in plaintext when the upgrade is stripped.
|
||||
#expect(throws: ConfigError.unknownDatabaseTLS("required")) {
|
||||
_ = try reader(values: [
|
||||
.Database.driver: .init(stringLiteral: .Database.driverPostgres),
|
||||
.Database.tls: "required"
|
||||
]).driver
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"ImageWidth enumeration",
|
||||
.tags(.enumeration)
|
||||
)
|
||||
struct ImageWidthTests {
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
ImageWidth.allCases,
|
||||
[480, 800, 1200]
|
||||
))
|
||||
func `width`(
|
||||
for imageWidth: ImageWidth,
|
||||
expects width: Int
|
||||
) {
|
||||
#expect(imageWidth.width == width)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import Infrastructure
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"StaticFile enumeration",
|
||||
.tags(.enumeration)
|
||||
)
|
||||
struct StaticFileTests {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias File = StaticFile
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.fileExtensions
|
||||
))
|
||||
func `file extensions`(
|
||||
for file: File,
|
||||
expects extensions: [AssetExtension]
|
||||
) {
|
||||
#expect(file.fileExtensions == extensions)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
File.allCases,
|
||||
Self.fileNames
|
||||
))
|
||||
func `file name`(
|
||||
for file: File,
|
||||
expects fileName: String
|
||||
) {
|
||||
#expect(file.fileName == fileName)
|
||||
}
|
||||
|
||||
// MARK: CaseIterable tests
|
||||
|
||||
@Test
|
||||
func `all cases`() {
|
||||
#expect(File.allCases.count == 11)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension StaticFileTests {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
static let fileExtensions: [[AssetExtension]] = [
|
||||
[.png],
|
||||
[.ico],
|
||||
[.svg],
|
||||
[.png],
|
||||
[.png],
|
||||
[.css, .js],
|
||||
[.css, .js],
|
||||
[.txt],
|
||||
[.css, .js],
|
||||
[.webmanifest],
|
||||
[.xml]
|
||||
]
|
||||
static let fileNames: [String] = [
|
||||
"apple-touch-icon",
|
||||
"favicon",
|
||||
"icon",
|
||||
"icon-192",
|
||||
"icon-512",
|
||||
"index",
|
||||
"not-found",
|
||||
"robots",
|
||||
"shared",
|
||||
"site",
|
||||
"sitemap"
|
||||
]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import Elementary
|
||||
import Foundation
|
||||
import Infrastructure
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Page+Defaults extension",
|
||||
.tags(.extensionTests)
|
||||
)
|
||||
struct PageDefaultsTests {
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test
|
||||
func `pairs english with a territory for its social card locale`() {
|
||||
#expect(StubPage().ogLocale == "en_US")
|
||||
}
|
||||
|
||||
/// Open Graph prefers `language_TERRITORY`, but a scraper accepts the bare code — better than pinning a territory the site never named.
|
||||
@Test
|
||||
func `leaves an unpaired language as a bare code`() {
|
||||
#expect(StubPage(locale: .init(identifier: "nl")).ogLocale == "nl")
|
||||
}
|
||||
|
||||
// MARK: Method tests
|
||||
|
||||
@Test
|
||||
func `renders no language alternates without an origin`() {
|
||||
let html = StubPage().languageAlternates(
|
||||
origin: nil,
|
||||
path: "/",
|
||||
languages: Self.languages
|
||||
).render()
|
||||
|
||||
#expect(html.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders no language alternates for a single language`() {
|
||||
let html = StubPage().languageAlternates(
|
||||
origin: "https://example.com",
|
||||
path: "/",
|
||||
languages: [.default]
|
||||
).render()
|
||||
|
||||
#expect(html.isEmpty)
|
||||
}
|
||||
|
||||
/// The default language keeps the bare root; the prefixed edition collapses onto its prefix rather than carrying a trailing slash.
|
||||
@Test
|
||||
func `renders an alternate per language and an x-default at the root`() {
|
||||
let html = StubPage().languageAlternates(
|
||||
origin: "https://example.com",
|
||||
path: "/",
|
||||
languages: Self.languages
|
||||
).render()
|
||||
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="en" href="https://example.com">"#))
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="nl" href="https://example.com/nl">"#))
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="x-default" href="https://example.com">"#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `prefixes the alternates of a nested page`() {
|
||||
let html = StubPage().languageAlternates(
|
||||
origin: "https://example.com",
|
||||
path: "/privacy",
|
||||
languages: Self.languages
|
||||
).render()
|
||||
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="en" href="https://example.com/privacy">"#))
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="nl" href="https://example.com/nl/privacy">"#))
|
||||
#expect(html.contains(#"<link rel="alternate" hreflang="x-default" href="https://example.com/privacy">"#))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension PageDefaultsTests {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// A two-language set, standing in for the multi-language catalog the template itself does not ship.
|
||||
static let languages: [Language] = [
|
||||
.default,
|
||||
.init(identifier: "nl")
|
||||
]
|
||||
|
||||
// MARK: Types
|
||||
|
||||
/// The barest ``Page`` conformance, so the shared defaults can be exercised without a real page's content.
|
||||
struct StubPage: Page {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let assetVersion: String? = nil
|
||||
let locale: Locale
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
init(locale: Locale = .init(identifier: "en")) {
|
||||
self.locale = locale
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var content: some HTML {
|
||||
HTMLRaw("")
|
||||
}
|
||||
|
||||
var scripts: [any Asset] {
|
||||
[]
|
||||
}
|
||||
|
||||
var stylesheets: [any Asset] {
|
||||
[]
|
||||
}
|
||||
|
||||
var title: String {
|
||||
"Stub"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,39 @@ struct IndexPageTests {
|
||||
#expect(html.contains("/js/index.js"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders no font preloads until fonts are declared`() {
|
||||
// The template ships no fonts, so a preload link here would point at a file the service does not serve.
|
||||
#expect(IndexPage.preloadedFonts.isEmpty)
|
||||
#expect(!IndexPage(locale: .init(identifier: "en")).render().contains(#"rel="preload""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders no canonical URL until an origin is given`() {
|
||||
#expect(!IndexPage(locale: .init(identifier: "en")).render().contains(#"rel="canonical""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders the canonical URL as the bare origin for the default language`() {
|
||||
// The root is the one path with no trailing slash for `TrailingSlashRedirectMiddleware` to strip.
|
||||
let html = IndexPage(
|
||||
locale: .init(identifier: "en"),
|
||||
siteOrigin: "https://example.com"
|
||||
).render()
|
||||
|
||||
#expect(html.contains(#"<link rel="canonical" href="https://example.com">"#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders no language alternates for a single-language catalog`() {
|
||||
// The template serves one language, so a set naming one edition would tell a crawler nothing.
|
||||
#expect(Language.all.count == 1)
|
||||
#expect(!IndexPage(
|
||||
locale: .init(identifier: "en"),
|
||||
siteOrigin: "https://example.com"
|
||||
).render().contains("hreflang"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders versioned asset URLs when given a version`() {
|
||||
let html = IndexPage(
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Language type",
|
||||
.tags(.type)
|
||||
)
|
||||
struct LanguageTests {
|
||||
|
||||
// MARK: Catalog tests
|
||||
|
||||
/// The template ships an English-only catalog, so a site adding a language sees this pin fail and reviews the URL rules below.
|
||||
@Test
|
||||
func `derives its languages from the String Catalog`() {
|
||||
#expect(Language.all == [Language(identifier: "en")])
|
||||
#expect(Language.default == Language(identifier: "en"))
|
||||
}
|
||||
|
||||
// MARK: Initializer tests
|
||||
|
||||
@Test
|
||||
func `reads the language a locale names`() {
|
||||
#expect(Language(of: .init(identifier: "en")).identifier == "en")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `reads a regional locale as its primary language`() {
|
||||
#expect(Language(of: .init(identifier: "en_GB")).identifier == "en")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `falls back to the default for a language the catalog does not serve`() {
|
||||
#expect(Language(of: .init(identifier: "fr")) == .default)
|
||||
}
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
@Test
|
||||
func `owns the bare paths as the default language`() {
|
||||
let language = Language.default
|
||||
|
||||
#expect(language.isDefault)
|
||||
#expect(language.pathPrefix.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `prefixes its paths as a non-default language`() {
|
||||
let language = Language(identifier: "nl")
|
||||
|
||||
#expect(!language.isDefault)
|
||||
#expect(language.pathPrefix == "/nl")
|
||||
}
|
||||
|
||||
// MARK: Method tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
["/", "/privacy"],
|
||||
["/", "/privacy"]
|
||||
))
|
||||
func `path`(
|
||||
forDefaultLanguageAt barePath: String,
|
||||
expects path: String
|
||||
) {
|
||||
#expect(Language.default.path(barePath) == path)
|
||||
}
|
||||
|
||||
/// The root collapses onto the prefix alone: a trailing slash is the very spelling `TrailingSlashRedirectMiddleware` redirects away from.
|
||||
@Test(arguments: zip(
|
||||
["/", "/privacy"],
|
||||
["/nl", "/nl/privacy"]
|
||||
))
|
||||
func `path`(
|
||||
forPrefixedLanguageAt barePath: String,
|
||||
expects path: String
|
||||
) {
|
||||
#expect(Language(identifier: "nl").path(barePath) == path)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
["/", "/privacy"],
|
||||
["https://example.com", "https://example.com/privacy"]
|
||||
))
|
||||
func `url`(
|
||||
forDefaultLanguageAt barePath: String,
|
||||
expects url: String
|
||||
) {
|
||||
#expect(Language.default.url(
|
||||
origin: "https://example.com",
|
||||
path: barePath
|
||||
) == url)
|
||||
}
|
||||
|
||||
@Test(arguments: zip(
|
||||
["/", "/privacy"],
|
||||
["https://example.com/nl", "https://example.com/nl/privacy"]
|
||||
))
|
||||
func `url`(
|
||||
forPrefixedLanguageAt barePath: String,
|
||||
expects url: String
|
||||
) {
|
||||
#expect(Language(identifier: "nl").url(
|
||||
origin: "https://example.com",
|
||||
path: barePath
|
||||
) == url)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import Infrastructure
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"StaticFile type",
|
||||
.tags(.type)
|
||||
)
|
||||
struct StaticFileTests {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
typealias File = StaticFile
|
||||
|
||||
// MARK: Computed tests
|
||||
|
||||
/// The path is asserted rather than the three facts behind it: it is the only form the rest of the service sees.
|
||||
@Test(arguments: Self.paths)
|
||||
func `relative paths`(
|
||||
for file: File,
|
||||
expects paths: [String]
|
||||
) {
|
||||
#expect(file.fileExtensions.map(file.relativePath) == paths)
|
||||
}
|
||||
|
||||
/// A preload URL must match the stylesheet's `@font-face` source exactly, so it carries the extension's folder and no version query.
|
||||
@Test
|
||||
func `unversioned font paths`() {
|
||||
let font = File("a-font-400", as: .woff2)
|
||||
|
||||
#expect(font.relativePath(for: .woff2) == "font/a-font-400.woff2")
|
||||
#expect(font.urlPath(for: .woff2) == "/font/a-font-400.woff2")
|
||||
}
|
||||
|
||||
/// Imagery sits in a folder per page, so a declared folder replaces the extension's own for every extension the file has.
|
||||
@Test
|
||||
func `declared folder paths`() {
|
||||
let portrait = File("portrait", in: "img/about", as: .jpg, .webp)
|
||||
|
||||
#expect(portrait.relativePath(for: .jpg) == "img/about/portrait.jpg")
|
||||
#expect(portrait.relativePath(for: .webp) == "img/about/portrait.webp")
|
||||
}
|
||||
|
||||
// MARK: Methods tests
|
||||
|
||||
@Test(arguments: zip(
|
||||
[AssetExtension.jpg, .webp],
|
||||
["jpg", "webp"]
|
||||
))
|
||||
func `srcset offers every rendition narrowest first`(
|
||||
for fileExtension: AssetExtension,
|
||||
expects suffix: String
|
||||
) {
|
||||
let srcSet = File.srcSet(for: fileExtension) { width in
|
||||
File(
|
||||
width == .large ? "portrait" : "portrait-\(width.width)",
|
||||
in: "img/about",
|
||||
as: .jpg, .webp
|
||||
)
|
||||
}
|
||||
|
||||
#expect(srcSet == [
|
||||
"/img/about/portrait-480.\(suffix) 480w",
|
||||
"/img/about/portrait-800.\(suffix) 800w",
|
||||
"/img/about/portrait.\(suffix) 1200w"
|
||||
].joined(separator: ", "))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `srcset versions every rendition when given a version`() {
|
||||
let srcSet = File.srcSet(for: .jpg, version: "0123456789abcdef") { _ in
|
||||
File("portrait", in: "img/about", as: .jpg)
|
||||
}
|
||||
|
||||
#expect(srcSet.components(separatedBy: "?v=0123456789abcdef").count - 1 == ImageWidth.allCases.count)
|
||||
}
|
||||
|
||||
// MARK: Constants tests
|
||||
|
||||
@Test
|
||||
func `all files`() {
|
||||
#expect(File.all.count == Self.paths.count)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension StaticFileTests {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// Every file paired with the path it is served at, one per extension, in ``StaticFile/all`` order.
|
||||
static let paths: [(File, [String])] = [
|
||||
(.appleTouchIcon, ["apple-touch-icon.png"]),
|
||||
(.favicon, ["favicon.ico"]),
|
||||
(.icon, ["icon.svg"]),
|
||||
(.icon192, ["icon-192.png"]),
|
||||
(.icon512, ["icon-512.png"]),
|
||||
(.index, ["css/index.css", "js/index.js"]),
|
||||
(.notFound, ["css/not-found.css", "js/not-found.js"]),
|
||||
(.robots, ["robots.txt"]),
|
||||
(.shared, ["css/shared.css", "js/shared.js"]),
|
||||
(.site, ["site.webmanifest"]),
|
||||
(.sitemap, ["sitemap.xml"]),
|
||||
]
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Hummingbird
|
||||
import HummingbirdTesting
|
||||
import Infrastructure
|
||||
import Logging
|
||||
import NIOCore
|
||||
import Persistence
|
||||
@@ -29,6 +30,7 @@ struct HealthControllerTests {
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentType] == "application/json")
|
||||
#expect(body == #"{"status":"ok"}"#)
|
||||
#expect(response.headers[.robotsTag] == "noindex")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +56,7 @@ struct HealthControllerTests {
|
||||
#expect(response.status == .ok)
|
||||
#expect(response.headers[.contentType] == "application/json")
|
||||
#expect(body == #"{"status":"ready"}"#)
|
||||
#expect(response.headers[.robotsTag] == "noindex")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -99,6 +102,7 @@ struct HealthControllerTests {
|
||||
#expect(response.status == .serviceUnavailable)
|
||||
#expect(response.headers[.contentType] == "application/json")
|
||||
#expect(body == #"{"status":"unavailable"}"#)
|
||||
#expect(response.headers[.robotsTag] == "noindex")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -131,6 +131,37 @@ struct RootControllerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `serves the landing page with a canonical URL when an origin is configured`() async throws {
|
||||
try await app(
|
||||
siteOrigin: "https://example.com"
|
||||
).test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(String(buffer: response.body).contains(#"<link rel="canonical" href="https://example.com">"#))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The template's catalog serves one language, so the default owns every path and no prefixed route is registered.
|
||||
@Test
|
||||
func `registers no prefixed route for a single-language catalog`() async throws {
|
||||
let prefixed = Language.all.filter { !$0.isDefault }
|
||||
|
||||
#expect(prefixed.isEmpty)
|
||||
|
||||
try await app.test(.router) { client in
|
||||
try await client.execute(
|
||||
uri: "/en",
|
||||
method: .get
|
||||
) { response in
|
||||
#expect(response.status == .notFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `embeds no analytics tracker by default`() async throws {
|
||||
try await app.test(.router) { client in
|
||||
@@ -176,13 +207,15 @@ private extension RootControllerTests {
|
||||
// MARK: Methods
|
||||
|
||||
/// Builds an application whose root controller appends the given version token to the landing
|
||||
/// page's asset URLs and embeds the given analytics tracker.
|
||||
/// page's asset URLs, derives its absolute links from the given origin, and embeds the given analytics tracker.
|
||||
/// - Parameters:
|
||||
/// - assetVersion: the version token appended to the page's asset URLs.
|
||||
/// - siteOrigin: the public origin the page derives its canonical URL and language alternates from, or `nil` (the default) to omit them.
|
||||
/// - analytics: the analytics tracker the landing page embeds, or `nil` (the default) to omit it.
|
||||
/// - Returns: the configured application.
|
||||
func app(
|
||||
assetVersion: String? = nil,
|
||||
siteOrigin: String? = nil,
|
||||
analytics: Analytics? = nil
|
||||
) -> some ApplicationProtocol {
|
||||
let router = Router(context: WebsiteRequestContext.self)
|
||||
@@ -193,6 +226,7 @@ private extension RootControllerTests {
|
||||
|
||||
router.addRoutes(RootController<WebsiteRequestContext>(
|
||||
assetVersion: assetVersion,
|
||||
siteOrigin: siteOrigin,
|
||||
analytics: analytics
|
||||
).routes)
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ extension Tag {
|
||||
@Tag static var controller: Tag
|
||||
/// Tests exercising an enumeration of the Website library.
|
||||
@Tag static var enumeration: Tag
|
||||
/// Tests exercising an extension of the Website library.
|
||||
@Tag static var extensionTests: Tag
|
||||
/// Tests exercising a page of the Website library.
|
||||
@Tag static var page: Tag
|
||||
/// Tests exercising a type of the Website library.
|
||||
@Tag static var type: Tag
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user