diff --git a/Sources/App/Commands/Analyze.swift b/Sources/App/Commands/Analyze.swift index 28dcb7f25..72fbbde29 100644 --- a/Sources/App/Commands/Analyze.swift +++ b/Sources/App/Commands/Analyze.swift @@ -555,7 +555,8 @@ extension Analyze { do { let packageManifest = try await dumpPackage(at: cacheDir) - let spiManifest = Current.loadSPIManifest(cacheDir) + @Dependency(\.environment) var environment + let spiManifest = environment.loadSPIManifest(cacheDir) return PackageInfo(packageManifest: packageManifest, spiManifest: spiManifest) diff --git a/Sources/App/Core/AppEnvironment.swift b/Sources/App/Core/AppEnvironment.swift index 7de880203..74d1f61be 100644 --- a/Sources/App/Core/AppEnvironment.swift +++ b/Sources/App/Core/AppEnvironment.swift @@ -30,9 +30,6 @@ struct AppEnvironment: Sendable { var gitlabApiToken: @Sendable () -> String? var gitlabPipelineToken: @Sendable () -> String? var gitlabPipelineLimit: @Sendable () -> Int - var hideStagingBanner: @Sendable () -> Bool - var maintenanceMessage: @Sendable () -> String? - var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest? var logger: @Sendable () -> Logger var metricsPushGatewayUrl: @Sendable () -> String? var plausibleBackendReportingSiteID: @Sendable () -> String? @@ -46,7 +43,6 @@ struct AppEnvironment: Sendable { _ readme: String) async throws(S3Readme.Error) -> String var storeS3ReadmeImages: @Sendable (_ client: Client, _ imagesToCache: [Github.Readme.ImageToCache]) async throws(S3Readme.Error) -> Void - var timeZone: @Sendable () -> TimeZone var triggerBuild: @Sendable (_ client: Client, _ buildId: Build.Id, _ cloneURL: String, @@ -78,14 +74,6 @@ extension AppEnvironment { Environment.get("GITLAB_PIPELINE_LIMIT").flatMap(Int.init) ?? Constants.defaultGitlabPipelineLimit }, - hideStagingBanner: { - Environment.get("HIDE_STAGING_BANNER").flatMap(\.asBool) - ?? Constants.defaultHideStagingBanner - }, - maintenanceMessage: { - Environment.get("MAINTENANCE_MESSAGE").flatMap(\.trimmed) - }, - loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) }, logger: { logger }, metricsPushGatewayUrl: { Environment.get("METRICS_PUSHGATEWAY_URL") }, plausibleBackendReportingSiteID: { Environment.get("PLAUSIBLE_BACKEND_REPORTING_SITE_ID") }, @@ -107,7 +95,6 @@ extension AppEnvironment { storeS3ReadmeImages: { client, images throws(S3Readme.Error) in try await S3Readme.storeReadmeImages(client: client, imagesToCache: images) }, - timeZone: { .current }, triggerBuild: { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in try await Gitlab.Builder.triggerBuild(client: client, buildId: buildId, diff --git a/Sources/App/Core/Dependencies/EnvironmentClient.swift b/Sources/App/Core/Dependencies/EnvironmentClient.swift index 371b570bf..fa1be0e44 100644 --- a/Sources/App/Core/Dependencies/EnvironmentClient.swift +++ b/Sources/App/Core/Dependencies/EnvironmentClient.swift @@ -14,6 +14,7 @@ import Dependencies import DependenciesMacros +import SPIManifest import Vapor @@ -40,6 +41,9 @@ struct EnvironmentClient { var collectionSigningPrivateKey: @Sendable () -> Data? var current: @Sendable () -> Environment = { XCTFail("current"); return .development } var dbId: @Sendable () -> String? + var hideStagingBanner: @Sendable () -> Bool = { XCTFail("hideStagingBanner"); return Constants.defaultHideStagingBanner } + var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest? + var maintenanceMessage: @Sendable () -> String? var mastodonCredentials: @Sendable () -> Mastodon.Credentials? var random: @Sendable (_ range: ClosedRange) -> Double = { XCTFail("random"); return Double.random(in: $0) } @@ -102,6 +106,14 @@ extension EnvironmentClient: DependencyKey { }, current: { (try? Environment.detect()) ?? .development }, dbId: { Environment.get("DATABASE_ID") }, + hideStagingBanner: { + Environment.get("HIDE_STAGING_BANNER").flatMap(\.asBool) + ?? Constants.defaultHideStagingBanner + }, + loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) }, + maintenanceMessage: { + Environment.get("MAINTENANCE_MESSAGE").flatMap(\.trimmed) + }, mastodonCredentials: { Environment.get("MASTODON_ACCESS_TOKEN") .map(Mastodon.Credentials.init(accessToken:)) @@ -134,7 +146,7 @@ extension EnvironmentClient { extension EnvironmentClient: TestDependencyKey { static var testValue: Self { // sas 2024-11-22: - // For a few attributes we provide a default value overriding the XCTFail, because theis use is too + // For a few attributes we provide a default value overriding the XCTFail, because their use is too // pervasive and would require the vast majority of tests to be wrapped with `withDependencies`. // We can do so at a later time once more tests are transitioned over for other dependencies. This is // the exact same default behaviour we had with the Current dependency injection. It did not have @@ -143,6 +155,7 @@ extension EnvironmentClient: TestDependencyKey { var mock = Self() mock.appVersion = { "test" } mock.current = { .development } + mock.hideStagingBanner = { false } return mock } } diff --git a/Sources/App/Core/Dependencies/HTTPClient.swift b/Sources/App/Core/Dependencies/HTTPClient.swift index 5d04c93a3..f7c5acbe3 100644 --- a/Sources/App/Core/Dependencies/HTTPClient.swift +++ b/Sources/App/Core/Dependencies/HTTPClient.swift @@ -110,5 +110,14 @@ extension HTTPClient.Response { static var notFound: Self { .init(status: .notFound) } static var tooManyRequests: Self { .init(status: .tooManyRequests) } static var ok: Self { .init(status: .ok) } + + static func ok(body: String, headers: HTTPHeaders = .init()) -> Self { + .init(status: .ok, headers: headers, body: .init(string: body)) + } + + static func ok(jsonEncode value: T, headers: HTTPHeaders = .init()) throws -> Self { + let data = try JSONEncoder().encode(value) + return .init(status: .ok, headers: headers, body: .init(data: data)) + } } #endif diff --git a/Sources/App/Core/Extensions/Date+ext.swift b/Sources/App/Core/Extensions/Date+ext.swift index 1b50deb86..a3e8ac795 100644 --- a/Sources/App/Core/Extensions/Date+ext.swift +++ b/Sources/App/Core/Extensions/Date+ext.swift @@ -18,26 +18,29 @@ import Dependencies extension DateFormatter { static var mediumDateFormatter: DateFormatter { + @Dependency(\.timeZone) var timeZone let formatter = DateFormatter() formatter.dateStyle = .medium formatter.locale = .init(identifier: "en_GB") - formatter.timeZone = Current.timeZone() + formatter.timeZone = timeZone return formatter } static var longDateFormatter: DateFormatter { + @Dependency(\.timeZone) var timeZone let formatter = DateFormatter() formatter.dateStyle = .long formatter.locale = .init(identifier: "en_GB") - formatter.timeZone = Current.timeZone() + formatter.timeZone = timeZone return formatter } static var yearMonthDayDateFormatter: DateFormatter { + @Dependency(\.timeZone) var timeZone let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" formatter.locale = .init(identifier: "en_GB") - formatter.timeZone = Current.timeZone() + formatter.timeZone = timeZone return formatter } diff --git a/Sources/App/Views/PublicPage.swift b/Sources/App/Views/PublicPage.swift index 47d5a9a32..689b98838 100644 --- a/Sources/App/Views/PublicPage.swift +++ b/Sources/App/Views/PublicPage.swift @@ -278,8 +278,8 @@ class PublicPage { /// A staging banner, which only appears on the staging/development server. /// - Returns: Either a
element, or nothing. final func stagingBanner() -> Node { - guard !Current.hideStagingBanner() else { return .empty } @Dependency(\.environment) var environment + guard !environment.hideStagingBanner() else { return .empty } if environment.current() == .development { return .div( .class("staging"), diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 62266338a..99586cd2a 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -26,7 +26,7 @@ func routes(_ app: Application) throws { do { // home page app.get { req in - if let maintenanceMessage = Current.maintenanceMessage() { + if let maintenanceMessage = environment.maintenanceMessage() { let model = MaintenanceMessageIndex.Model(markdown: maintenanceMessage) return MaintenanceMessageIndex.View(path: req.url.path, model: model).document() } else { diff --git a/Tests/AppTests/AnalyzeErrorTests.swift b/Tests/AppTests/AnalyzeErrorTests.swift index 5f76656e7..9d1dac693 100644 --- a/Tests/AppTests/AnalyzeErrorTests.swift +++ b/Tests/AppTests/AnalyzeErrorTests.swift @@ -113,102 +113,118 @@ final class AnalyzeErrorTests: AppTestCase { } func test_analyze_refreshCheckout_failed() async throws { - Current.shell.run = { @Sendable cmd, path in - switch cmd { - case _ where cmd.description.contains("git clone https://github.com/foo/1"): - throw SimulatedError() + try await withDependencies { + $0.environment.loadSPIManifest = { _ in nil } + } operation: { + Current.shell.run = { @Sendable cmd, path in + switch cmd { + case _ where cmd.description.contains("git clone https://github.com/foo/1"): + throw SimulatedError() - case .gitFetchAndPruneTags where path.hasSuffix("foo-1"): - throw SimulatedError() + case .gitFetchAndPruneTags where path.hasSuffix("foo-1"): + throw SimulatedError() - default: - return try Self.defaultShellRun(cmd, path) + default: + return try Self.defaultShellRun(cmd, path) + } } - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("refreshCheckout failed"), "was: \(error.message)") + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("refreshCheckout failed"), "was: \(error.message)") + } } } func test_analyze_updateRepository_invalidPackageCachePath() async throws { - // setup - let pkg = try await Package.find(badPackageID, on: app.db).unwrap() - // This may look weird but its currently the only way to actually create an - // invalid package cache path - we need to mess up the package url. - pkg.url = "foo/1" - XCTAssertNil(pkg.cacheDirectoryName) - try await pkg.save(on: app.db) - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains( "AppError.invalidPackageCachePath"), "was: \(error.message)") + try await withDependencies { + $0.environment.loadSPIManifest = { _ in nil } + } operation: { + // setup + let pkg = try await Package.find(badPackageID, on: app.db).unwrap() + // This may look weird but its currently the only way to actually create an + // invalid package cache path - we need to mess up the package url. + pkg.url = "foo/1" + XCTAssertNil(pkg.cacheDirectoryName) + try await pkg.save(on: app.db) + + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains( "AppError.invalidPackageCachePath"), "was: \(error.message)") + } } } func test_analyze_getPackageInfo_gitCheckout_error() async throws { - // setup - Current.shell.run = { @Sendable cmd, path in - switch cmd { - case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"): - throw SimulatedError() - - default: - return try Self.defaultShellRun(cmd, path) + try await withDependencies { + $0.environment.loadSPIManifest = { _ in nil } + } operation: { + // setup + Current.shell.run = { @Sendable cmd, path in + switch cmd { + case .gitCheckout(branch: "main", quiet: true) where path.hasSuffix("foo-1"): + throw SimulatedError() + + default: + return try Self.defaultShellRun(cmd, path) + } } - } - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") + } } } func test_analyze_dumpPackage_missing_manifest() async throws { - // setup - Current.fileManager.fileExists = { @Sendable path in - if path.hasSuffix("github.com-foo-1/Package.swift") { - return false + try await withDependencies { + $0.environment.loadSPIManifest = { _ in nil } + } operation: { + // setup + Current.fileManager.fileExists = { @Sendable path in + if path.hasSuffix("github.com-foo-1/Package.swift") { + return false + } + return true + } + + // MUT + try await Analyze.analyze(client: app.client, + database: app.db, + mode: .limit(10)) + + // validate + try await defaultValidation() + try logger.logs.withValue { logs in + XCTAssertEqual(logs.count, 2) + let error = try logs.last.unwrap() + XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") } - return true - } - - // MUT - try await Analyze.analyze(client: app.client, - database: app.db, - mode: .limit(10)) - - // validate - try await defaultValidation() - try logger.logs.withValue { logs in - XCTAssertEqual(logs.count, 2) - let error = try logs.last.unwrap() - XCTAssertTrue(error.message.contains("AppError.noValidVersions"), "was: \(error.message)") } } diff --git a/Tests/AppTests/AnalyzerTests.swift b/Tests/AppTests/AnalyzerTests.swift index eebb4e44d..e24bb2a82 100644 --- a/Tests/AppTests/AnalyzerTests.swift +++ b/Tests/AppTests/AnalyzerTests.swift @@ -36,6 +36,13 @@ class AnalyzerTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { path in + if path.hasSuffix("foo-1") { + return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) + } else { + return nil + } + } $0.httpClient.mastodonPost = { @Sendable _ in } } operation: { // setup @@ -70,13 +77,6 @@ class AnalyzerTests: AppTestCase { } Current.fileManager.createDirectory = { @Sendable path, _, _ in checkoutDir.setValue(path) } Current.git = .live - Current.loadSPIManifest = { path in - if path.hasSuffix("foo-1") { - return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) - } else { - return nil - } - } Current.shell.run = { @Sendable cmd, path in let trimmedPath = path.replacingOccurrences(of: checkoutDir.value!, with: ".") commands.withValue { @@ -219,6 +219,7 @@ class AnalyzerTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } $0.httpClient.mastodonPost = { @Sendable _ in } } operation: { // setup @@ -359,6 +360,7 @@ class AnalyzerTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } } operation: { // setup let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] @@ -410,6 +412,7 @@ class AnalyzerTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } } operation: { // setup let urls = ["https://github.com/foo/1", "https://github.com/foo/2"] @@ -749,32 +752,36 @@ class AnalyzerTests: AppTestCase { func test_getPackageInfo() async throws { // Tests getPackageInfo(package:version:) - // setup - let commands = QueueIsolated<[String]>([]) - Current.shell.run = { @Sendable cmd, _ in - commands.withValue { - $0.append(cmd.description) - } - if cmd == .swiftDumpPackage { - return #"{ "name": "SPI-Server", "products": [], "targets": [] }"# + try await withDependencies { + $0.environment.loadSPIManifest = { _ in nil } + } operation: { + // setup + let commands = QueueIsolated<[String]>([]) + Current.shell.run = { @Sendable cmd, _ in + commands.withValue { + $0.append(cmd.description) + } + if cmd == .swiftDumpPackage { + return #"{ "name": "SPI-Server", "products": [], "targets": [] }"# + } + return "" } - return "" - } - let pkg = try await savePackage(on: app.db, "https://github.com/foo/1") - try await Repository(package: pkg, name: "1", owner: "foo").save(on: app.db) - let version = try Version(id: UUID(), package: pkg, reference: .tag(.init(0, 4, 2))) - try await version.save(on: app.db) - let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) + let pkg = try await savePackage(on: app.db, "https://github.com/foo/1") + try await Repository(package: pkg, name: "1", owner: "foo").save(on: app.db) + let version = try Version(id: UUID(), package: pkg, reference: .tag(.init(0, 4, 2))) + try await version.save(on: app.db) + let jpr = try await Package.fetchCandidate(app.db, id: pkg.id!) - // MUT - let info = try await Analyze.getPackageInfo(package: jpr, version: version) + // MUT + let info = try await Analyze.getPackageInfo(package: jpr, version: version) - // validation - XCTAssertEqual(commands.value, [ - "git checkout 0.4.2 --quiet", - "swift package dump-package" - ]) - XCTAssertEqual(info.packageManifest.name, "SPI-Server") + // validation + XCTAssertEqual(commands.value, [ + "git checkout 0.4.2 --quiet", + "swift package dump-package" + ]) + XCTAssertEqual(info.packageManifest.name, "SPI-Server") + } } func test_updateVersion() async throws { @@ -885,6 +892,7 @@ class AnalyzerTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } } operation: { // setup Current.git.commitCount = { @Sendable _ in 12 } @@ -1498,6 +1506,7 @@ class AnalyzerTests: AppTestCase { // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2873 try await withDependencies { $0.date.now = .now + $0.environment.loadSPIManifest = { _ in nil } } operation: { // setup let pkg = try await savePackage(on: app.db, id: .id0, "https://github.com/foo/1".url, processingStage: .ingestion) diff --git a/Tests/AppTests/AppEnvironmentTests.swift b/Tests/AppTests/AppEnvironmentTests.swift index 0108b4645..a2ae993d8 100644 --- a/Tests/AppTests/AppEnvironmentTests.swift +++ b/Tests/AppTests/AppEnvironmentTests.swift @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +import XCTest + @testable import App +import Dependencies import Vapor -import XCTest class AppEnvironmentTests: XCTestCase { @@ -31,26 +33,30 @@ class AppEnvironmentTests: XCTestCase { func test_maintenanceMessage() throws { defer { unsetenv("MAINTENANCE_MESSAGE") } - Current.maintenanceMessage = AppEnvironment.live.maintenanceMessage - do { - unsetenv("MAINTENANCE_MESSAGE") - XCTAssertEqual(Current.maintenanceMessage(), nil) - } - do { - setenv("MAINTENANCE_MESSAGE", "foo", 1) - XCTAssertEqual(Current.maintenanceMessage(), "foo") - } - do { - setenv("MAINTENANCE_MESSAGE", "", 1) - XCTAssertEqual(Current.maintenanceMessage(), nil) - } - do { - setenv("MAINTENANCE_MESSAGE", " ", 1) - XCTAssertEqual(Current.maintenanceMessage(), nil) - } - do { - setenv("MAINTENANCE_MESSAGE", " \t\n ", 1) - XCTAssertEqual(Current.maintenanceMessage(), nil) + withDependencies { + $0.environment.maintenanceMessage = EnvironmentClient.liveValue.maintenanceMessage + } operation: { + @Dependency(\.environment) var environment + do { + unsetenv("MAINTENANCE_MESSAGE") + XCTAssertEqual(environment.maintenanceMessage(), nil) + } + do { + setenv("MAINTENANCE_MESSAGE", "foo", 1) + XCTAssertEqual(environment.maintenanceMessage(), "foo") + } + do { + setenv("MAINTENANCE_MESSAGE", "", 1) + XCTAssertEqual(environment.maintenanceMessage(), nil) + } + do { + setenv("MAINTENANCE_MESSAGE", " ", 1) + XCTAssertEqual(environment.maintenanceMessage(), nil) + } + do { + setenv("MAINTENANCE_MESSAGE", " \t\n ", 1) + XCTAssertEqual(environment.maintenanceMessage(), nil) + } } } diff --git a/Tests/AppTests/BlogActionsModelTests.swift b/Tests/AppTests/BlogActionsModelTests.swift index bccc36837..198144087 100644 --- a/Tests/AppTests/BlogActionsModelTests.swift +++ b/Tests/AppTests/BlogActionsModelTests.swift @@ -37,36 +37,40 @@ class BlogActionsModelTests: AppTestCase { """.data(using: .utf8) } - try withDependencies { // Ensure dev shows all summaries - $0.environment.current = { .development } + try withDependencies { + $0.timeZone = .utc } operation: { - // MUT - let summaries = try BlogActions.Model().summaries - - XCTAssertEqual(summaries.count, 2) - XCTAssertEqual(summaries.map(\.slug), ["post-2", "post-1"]) - XCTAssertEqual(summaries.map(\.published), [false, true]) - - let firstSummary = try XCTUnwrap(summaries).first - - // Note that we are testing that the first item in this list is the *last* item in the source YAML - // as the init should reverse the order of posts so that they display in reverse chronological order - XCTAssertEqual(firstSummary, BlogActions.Model.PostSummary(slug: "post-2", - title: "Blog post title two", - summary: "Summary of blog post two", - publishedAt: DateFormatter.yearMonthDayDateFormatter.date(from: "2024-01-02")!, - published: false)) - } - - try withDependencies { // Ensure prod shows only published summaries - $0.environment.current = { .production } - } operation: { - // MUT - let summaries = try BlogActions.Model().summaries - - // validate - XCTAssertEqual(summaries.map(\.slug), ["post-1"]) - XCTAssertEqual(summaries.map(\.published), [true]) + try withDependencies { // Ensure dev shows all summaries + $0.environment.current = { .development } + } operation: { + // MUT + let summaries = try BlogActions.Model().summaries + + XCTAssertEqual(summaries.count, 2) + XCTAssertEqual(summaries.map(\.slug), ["post-2", "post-1"]) + XCTAssertEqual(summaries.map(\.published), [false, true]) + + let firstSummary = try XCTUnwrap(summaries).first + + // Note that we are testing that the first item in this list is the *last* item in the source YAML + // as the init should reverse the order of posts so that they display in reverse chronological order + XCTAssertEqual(firstSummary, BlogActions.Model.PostSummary(slug: "post-2", + title: "Blog post title two", + summary: "Summary of blog post two", + publishedAt: DateFormatter.yearMonthDayDateFormatter.date(from: "2024-01-02")!, + published: false)) + } + + try withDependencies { // Ensure prod shows only published summaries + $0.environment.current = { .production } + } operation: { + // MUT + let summaries = try BlogActions.Model().summaries + + // validate + XCTAssertEqual(summaries.map(\.slug), ["post-1"]) + XCTAssertEqual(summaries.map(\.published), [true]) + } } } @@ -88,11 +92,15 @@ class BlogActionsModelTests: AppTestCase { // setup Current.fileManager = .live - // MUT - let summaries = try BlogActions.Model.allSummaries() - - // validate - XCTAssert(summaries.count > 0) + try withDependencies { + $0.timeZone = .utc + } operation: { + // MUT + let summaries = try BlogActions.Model.allSummaries() + + // validate + XCTAssert(summaries.count > 0) + } } } diff --git a/Tests/AppTests/DocumentationPageProcessorTests.swift b/Tests/AppTests/DocumentationPageProcessorTests.swift index b081a5c76..abed9ada4 100644 --- a/Tests/AppTests/DocumentationPageProcessorTests.swift +++ b/Tests/AppTests/DocumentationPageProcessorTests.swift @@ -16,6 +16,7 @@ import XCTest @testable import App +import Dependencies import InlineSnapshotTesting import SwiftSoup @@ -24,39 +25,43 @@ final class DocumentationPageProcessorTests: AppTestCase { func test_header_linkTitle() throws { // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2249 - // setup - let coreArchive = DocArchive(name: "tecocore", title: "TecoCore") - let signerArchive = DocArchive(name: "tecosigner", title: "Teco Signer") - let archives: [DocumentationPageProcessor.AvailableArchive] = [ - .init(archive: coreArchive, isCurrent: false), - .init(archive: signerArchive, isCurrent: true) - ] - let processor = try XCTUnwrap( - DocumentationPageProcessor( - repositoryOwner: "owner", - repositoryOwnerName: "Owner Name", - repositoryName: "repo", - packageName: "package", - docVersion: .reference("main"), - referenceLatest: .release, - referenceKind: .release, - canonicalUrl: "https://example.com/owner/repo/canonical-ref", - availableArchives: archives, - availableVersions: [ - .init( - kind: .defaultBranch, - reference: "main", - docArchives: [coreArchive, signerArchive], - isLatestStable: false - ) - ], - updatedAt: .t0, - rawHtml: try fixtureString(for: "docc-template.html") + try withDependencies { + $0.timeZone = .utc + } operation: { + // setup + let coreArchive = DocArchive(name: "tecocore", title: "TecoCore") + let signerArchive = DocArchive(name: "tecosigner", title: "Teco Signer") + let archives: [DocumentationPageProcessor.AvailableArchive] = [ + .init(archive: coreArchive, isCurrent: false), + .init(archive: signerArchive, isCurrent: true) + ] + let processor = try XCTUnwrap( + DocumentationPageProcessor( + repositoryOwner: "owner", + repositoryOwnerName: "Owner Name", + repositoryName: "repo", + packageName: "package", + docVersion: .reference("main"), + referenceLatest: .release, + referenceKind: .release, + canonicalUrl: "https://example.com/owner/repo/canonical-ref", + availableArchives: archives, + availableVersions: [ + .init( + kind: .defaultBranch, + reference: "main", + docArchives: [coreArchive, signerArchive], + isLatestStable: false + ) + ], + updatedAt: .t0, + rawHtml: try fixtureString(for: "docc-template.html") + ) ) - ) - // MUT & validate - assertSnapshot(of: processor.header, as: .html) + // MUT & validate + assertSnapshot(of: processor.header, as: .html) + } } func test_rewriteBaseUrls() throws { @@ -187,7 +192,7 @@ final class DocumentationPageProcessorTests: AppTestCase { } } } - + func test_rewriteAttribute_current() throws { do { // test rewriting of un-prefixed src attributes let doc = try SwiftSoup.parse(#""" diff --git a/Tests/AppTests/GithubTests.swift b/Tests/AppTests/GithubTests.swift index b49857def..3d91a21e6 100644 --- a/Tests/AppTests/GithubTests.swift +++ b/Tests/AppTests/GithubTests.swift @@ -469,19 +469,3 @@ class GithubTests: AppTestCase { } } - - -private extension HTTPClient.Response { - static func ok(body: String, headers: HTTPHeaders = .init()) -> Self { - .init(status: .ok, headers: headers, body: .init(string: body)) - } - - static func ok(fixture: String) throws -> Self { - try .init(status: .ok, body: .init(data: fixtureData(for: fixture))) - } - - static func ok(jsonEncode value: T) throws -> Self { - let data = try JSONEncoder().encode(value) - return .init(status: .ok, body: .init(data: data)) - } -} diff --git a/Tests/AppTests/IngestionTests.swift b/Tests/AppTests/IngestionTests.swift index dcb499eed..28ea32859 100644 --- a/Tests/AppTests/IngestionTests.swift +++ b/Tests/AppTests/IngestionTests.swift @@ -431,6 +431,7 @@ class IngestionTests: AppTestCase { try await withDependencies { $0.date.now = .now $0.environment.allowSocialPosts = { false } + $0.environment.loadSPIManifest = { _ in nil } } operation: { [db = app.db] in Current.fileManager.fileExists = { @Sendable _ in true } Current.git.commitCount = { @Sendable _ in 1 } diff --git a/Tests/AppTests/MastodonTests.swift b/Tests/AppTests/MastodonTests.swift index 26a646e6c..056548931 100644 --- a/Tests/AppTests/MastodonTests.swift +++ b/Tests/AppTests/MastodonTests.swift @@ -25,6 +25,7 @@ final class MastodonTests: AppTestCase { let message = QueueIsolated(nil) try await withDependencies { $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } $0.github.fetchLicense = { @Sendable _, _ in nil } $0.github.fetchMetadata = { @Sendable owner, repository in .mock(owner: owner, repository: repository) } $0.github.fetchReadme = { @Sendable _, _ in nil } diff --git a/Tests/AppTests/Mocks/AppEnvironment+mock.swift b/Tests/AppTests/Mocks/AppEnvironment+mock.swift index 3a8e6f1cf..c738f91af 100644 --- a/Tests/AppTests/Mocks/AppEnvironment+mock.swift +++ b/Tests/AppTests/Mocks/AppEnvironment+mock.swift @@ -29,9 +29,6 @@ extension AppEnvironment { gitlabApiToken: { nil }, gitlabPipelineToken: { nil }, gitlabPipelineLimit: { Constants.defaultGitlabPipelineLimit }, - hideStagingBanner: { false }, - maintenanceMessage: { nil }, - loadSPIManifest: { _ in nil }, logger: { logger }, metricsPushGatewayUrl: { "http://pushgateway:9091" }, plausibleBackendReportingSiteID: { nil }, @@ -42,7 +39,6 @@ extension AppEnvironment { siteURL: { Environment.get("SITE_URL") ?? "http://localhost:8080" }, storeS3Readme: { _, _, _ in "s3ObjectUrl" }, storeS3ReadmeImages: { _, _ in }, - timeZone: { .utc }, triggerBuild: { _, _, _, _, _, _, _, _ in .init(status: .ok, webUrl: "http://web_url") } ) } diff --git a/Tests/AppTests/PackageController+routesTests.swift b/Tests/AppTests/PackageController+routesTests.swift index 4d0d8c672..4180dd70f 100644 --- a/Tests/AppTests/PackageController+routesTests.swift +++ b/Tests/AppTests/PackageController+routesTests.swift @@ -568,7 +568,8 @@ class PackageController_routesTests: SnapshotTestCase { try await withDependencies { $0.currentReferenceCache = .disabled $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -651,7 +652,8 @@ class PackageController_routesTests: SnapshotTestCase { try await withDependencies { $0.currentReferenceCache = .disabled $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML(baseURL: "/owner/package/1.0.0")) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML(baseURL: "/owner/package/1.0.0")) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -725,7 +727,8 @@ class PackageController_routesTests: SnapshotTestCase { // /owner/package/documentation/{reference} + various path elements try await withDependencies { $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -804,7 +807,7 @@ class PackageController_routesTests: SnapshotTestCase { // Test documentation routes when no archive is in the path try await withDependencies { $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -1126,6 +1129,7 @@ class PackageController_routesTests: SnapshotTestCase { try await withDependencies { $0.environment.awsDocsBucket = { "docs-bucket" } $0.httpClient.fetchDocumentation = App.HTTPClient.echoURL() + $0.timeZone = .utc } operation: { // The `packageName` property on the `Version` has been set to the lower-cased version so // we can be sure the canonical URL is built from the properties on the `Repository` model. @@ -1158,7 +1162,8 @@ class PackageController_routesTests: SnapshotTestCase { try await withDependencies { $0.currentReferenceCache = .disabled $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -1223,7 +1228,8 @@ class PackageController_routesTests: SnapshotTestCase { $0.currentReferenceCache = .disabled $0.environment.awsDocsBucket = { "docs-bucket" } $0.environment.dbId = { nil } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -1324,8 +1330,9 @@ class PackageController_routesTests: SnapshotTestCase { $0.environment.awsDocsBucket = { "docs-bucket" } $0.httpClient.fetchDocumentation = { @Sendable uri in // embed uri.path in the body as a simple way to test the requested url - .init(status: .ok, body: .init(string: "

\(uri.path)

")) + .ok(body: "

\(uri.path)

") } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "1") @@ -1481,7 +1488,9 @@ class PackageController_routesTests: SnapshotTestCase { try await withDependencies { $0.currentReferenceCache = .disabled $0.environment.awsDocsBucket = { "docs-bucket" } - $0.httpClient.fetchDocumentation = { @Sendable _ in .init(status: .ok, body: .mockIndexHTML()) } + $0.environment.loadSPIManifest = { _ in nil } + $0.httpClient.fetchDocumentation = { @Sendable _ in .ok(body: .mockIndexHTML()) } + $0.timeZone = .utc } operation: { // setup let pkg = try await savePackage(on: app.db, "https://github.com/foo/bar".url, processingStage: .ingestion) @@ -1645,10 +1654,10 @@ private extension String { """# } -private extension ByteBuffer { +private extension String { static func mockIndexHTML(baseURL: String = "/") -> Self { let baseURL = baseURL.hasSuffix("/") ? baseURL : baseURL + "/" - return .init(string: """ + return """ @@ -1672,6 +1681,6 @@ private extension ByteBuffer { - """) + """ } } diff --git a/Tests/AppTests/PipelineTests.swift b/Tests/AppTests/PipelineTests.swift index 8dbbde8d6..24ec8e424 100644 --- a/Tests/AppTests/PipelineTests.swift +++ b/Tests/AppTests/PipelineTests.swift @@ -161,6 +161,7 @@ class PipelineTests: AppTestCase { let urls = ["1", "2", "3"].asGithubUrls try await withDependencies { $0.date.now = .now + $0.environment.loadSPIManifest = { _ in nil } $0.github.fetchLicense = { @Sendable _, _ in nil } $0.github.fetchMetadata = { @Sendable owner, repository in .mock(owner: owner, repository: repository) } $0.github.fetchReadme = { @Sendable _, _ in nil } diff --git a/Tests/AppTests/ReAnalyzeVersionsTests.swift b/Tests/AppTests/ReAnalyzeVersionsTests.swift index 5f6e2dc83..b1354dcd0 100644 --- a/Tests/AppTests/ReAnalyzeVersionsTests.swift +++ b/Tests/AppTests/ReAnalyzeVersionsTests.swift @@ -29,6 +29,7 @@ class ReAnalyzeVersionsTests: AppTestCase { try await withDependencies { $0.date.now = .t0 $0.environment.allowSocialPosts = { true } + $0.environment.loadSPIManifest = { _ in nil } $0.httpClient.mastodonPost = { @Sendable _ in } } operation: { // setup @@ -108,14 +109,6 @@ class ReAnalyzeVersionsTests: AppTestCase { .mock(description: "rel 1.2.3", tagName: "1.2.3") ] try await r.save(on: app.db) - // Package has gained a SPI manifest - Current.loadSPIManifest = { path in - if path.hasSuffix("foo-1") { - return .init(builder: .init(configs: [.init(documentationTargets: ["DocTarget"])])) - } else { - return nil - } - } } do { // assert running analysis again does not update existing versions try await Analyze.analyze(client: app.client, @@ -188,6 +181,7 @@ class ReAnalyzeVersionsTests: AppTestCase { let cutoff = Date.t1 try await withDependencies { $0.date.now = .t2 + $0.environment.loadSPIManifest = { _ in nil } } operation: { let pkg = try await savePackage(on: app.db, "https://github.com/foo/1".url, diff --git a/Tests/AppTests/RoutesTests.swift b/Tests/AppTests/RoutesTests.swift index 44623d6ed..b5d5e32e9 100644 --- a/Tests/AppTests/RoutesTests.swift +++ b/Tests/AppTests/RoutesTests.swift @@ -83,8 +83,8 @@ final class RoutesTests: AppTestCase { func test_maintenanceMessage() throws { try withDependencies { $0.environment.dbId = { nil } + $0.environment.maintenanceMessage = { "MAINTENANCE_MESSAGE" } } operation: { - Current.maintenanceMessage = { "MAINTENANCE_MESSAGE" } try app.test(.GET, "/") { res in XCTAssertContains(res.body.string, "MAINTENANCE_MESSAGE") diff --git a/Tests/AppTests/SitemapTests.swift b/Tests/AppTests/SitemapTests.swift index b3182447b..4c6d77e6f 100644 --- a/Tests/AppTests/SitemapTests.swift +++ b/Tests/AppTests/SitemapTests.swift @@ -142,14 +142,12 @@ class SitemapTests: SnapshotTestCase { $0.environment.awsDocsBucket = { "docs-bucket" } $0.httpClient.fetchDocumentation = { @Sendable url in guard url.path.hasSuffix("/owner/repo0/default/linkable-paths.json") else { throw Abort(.notFound) } - return .init(status: .ok, - body: .init(string: """ + return .ok(body: """ [ "/documentation/foo/bar/1", "/documentation/foo/bar/2", ] """) - ) } } operation: { // setup @@ -187,14 +185,12 @@ class SitemapTests: SnapshotTestCase { $0.environment.awsDocsBucket = { "docs-bucket" } $0.httpClient.fetchDocumentation = { @Sendable url in guard url.path.hasSuffix("/owner/repo0/a-b/linkable-paths.json") else { throw Abort(.notFound) } - return .init(status: .ok, - body: .init(string: """ + return .ok(body: """ [ "/documentation/foo/bar/1", "/documentation/foo/bar/2", ] """) - ) } } operation: { // setup diff --git a/Tests/AppTests/Util.swift b/Tests/AppTests/Util.swift index c360578c7..a68381606 100644 --- a/Tests/AppTests/Util.swift +++ b/Tests/AppTests/Util.swift @@ -108,3 +108,10 @@ func makeBody(_ data: Data) -> ByteBuffer { buffer.writeBytes(data) return buffer } + + +extension HTTPClient.Response { + static func ok(fixture: String) throws -> Self { + try .init(status: .ok, body: .init(data: fixtureData(for: fixture))) + } +} diff --git a/Tests/AppTests/WebpageSnapshotTests.swift b/Tests/AppTests/WebpageSnapshotTests.swift index 372fde567..f933619e8 100644 --- a/Tests/AppTests/WebpageSnapshotTests.swift +++ b/Tests/AppTests/WebpageSnapshotTests.swift @@ -34,6 +34,7 @@ class WebpageSnapshotTests: SnapshotTestCase { withDependencies { $0.environment.current = { .production } $0.environment.dbId = { "db-id" } + $0.timeZone = .utc } operation: { super.invokeTest() }