import Configuration import Foundation import Hummingbird import HummingbirdTesting import NIOCore import Testing @testable import Website @testable import WebsiteLibrary @Suite("App executable") struct AppTests { // MARK: Constants private let textExtensions: [StaticFile.Extension] = [ .css, .js, .txt ] // Absolute path to the package's "Resources/Static" folder, derived from this // file's location so the static files resolve regardless of the working directory. private let staticFilesPath = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // Tests/App .deletingLastPathComponent() // Tests .deletingLastPathComponent() // package root .appendingPathComponent(.Path.staticResources) .path // MARK: Functional tests @Test func `landing page to be served at root`() 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(response.status == .ok) #expect(response.headers[.contentType] == "text/html; charset=utf-8") #expect(body.contains("Hello world!")) } } } @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( staticFilesPath: staticFilesPath ).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 `readiness check to be served at the readiness path`() async throws { // Live mode runs the application's service group, so the `Fluent` service starts before the // request and shuts its connection pool down after — the router-only mode never would. try await app( staticFilesPath: staticFilesPath ).test(.live) { 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"}"#) } } } @Test(arguments: StaticFile.allCases) func `static files to be served`( staticFile file: StaticFile ) async throws { try await app( staticFilesPath: staticFilesPath ).test(.router) { client in for fileExtension in file.fileExtensions { try await client.execute( uri: "/\(file.relativePath(for: fileExtension))", method: .get ) { response in #expect(response.status == .ok) #expect(response.headers[.contentType] == fileExtension.contentType) let cacheControl = try #require(response.headers[.cacheControl]) #expect(cacheControl.contains("public") == true) #expect(cacheControl.contains("max-age=") == true) if textExtensions.contains(fileExtension) { #expect(cacheControl.contains("must-revalidate") == true) } } } } } @Test func `response to be compressed when the client supports it`() async throws { try await app( staticFilesPath: staticFilesPath ).test(.router) { client in try await client.execute( uri: "/", method: .get, headers: [.acceptEncoding: "gzip"] ) { response in #expect(response.status == .ok) #expect(response.headers[.contentEncoding] == "gzip") } } } @Test func `response to not be compressed when the client does not support it`() async throws { try await app( staticFilesPath: staticFilesPath ).test(.router) { client in try await client.execute( uri: "/", method: .get ) { response in #expect(response.status == .ok) #expect(response.headers[.contentEncoding] == nil) } } } @Test func `error page to be served when not found`() 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(response.status == .notFound) #expect(response.headers[.contentType] == "text/html; charset=utf-8") #expect(body.contains("Page Not Found")) } } } @Test func `security headers to be applied to the landing page`() async throws { try await app( staticFilesPath: staticFilesPath ).test(.router) { client in try await client.execute( uri: "/", method: .get ) { response in #expect(response.status == .ok) #expect(response.headers[.contentSecurityPolicy] == String.Security.contentSecurityPolicy) #expect(response.headers[.xContentTypeOptions] == String.Security.contentTypeOptions) #expect(response.headers[.frameOptions] == String.Security.frameOptions) #expect(response.headers[.referrerPolicy] == String.Security.referrerPolicy) #expect(response.headers[.permissionsPolicy] == String.Security.permissionsPolicy) #expect(response.headers[.strictTransportSecurity] == nil) } } } @Test func `security headers to be applied to the error page`() 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 #expect(response.status == .notFound) #expect(response.headers[.contentSecurityPolicy] == String.Security.contentSecurityPolicy) #expect(response.headers[.xContentTypeOptions] == String.Security.contentTypeOptions) } } } @Test func `strict-transport-security to be applied when configured`() async throws { try await app( staticFilesPath: staticFilesPath, strictTransportSecurity: "max-age=31536000; includeSubDomains" ).test(.router) { client in try await client.execute( uri: "/", method: .get ) { response in #expect(response.status == .ok) #expect(response.headers[.strictTransportSecurity] == "max-age=31536000; includeSubDomains") } } } } // MARK: - Helpers private extension AppTests { // MARK: Methods func app( staticFilesPath: String, strictTransportSecurity: String? = nil ) async -> some ApplicationProtocol { await application( reader: reader( staticFilesPath: staticFilesPath, strictTransportSecurity: strictTransportSecurity ) ) } func reader( staticFilesPath: String, strictTransportSecurity: String? = nil ) -> ConfigReader { ConfigReader(providers: [{ if let strictTransportSecurity { InMemoryProvider(values: [ .HTTP.host: "127.0.0.1", .HTTP.port: "0", .Log.level: "trace", .Path.staticFiles: .init(stringLiteral: staticFilesPath), .Security.strictTransportSecurity: .init(stringLiteral: strictTransportSecurity) ]) } else { InMemoryProvider(values: [ .HTTP.host: "127.0.0.1", .HTTP.port: "0", .Log.level: "trace", .Path.staticFiles: .init(stringLiteral: staticFilesPath), ]) } }()]) } }