Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import FoundationNetworking


struct AppEnvironment: Sendable {
var allowTwitterPosts: @Sendable () -> Bool
var apiSigningKey: @Sendable () -> String?
var appVersion: @Sendable () -> String?
var awsAccessKeyId: @Sendable () -> String?
Expand Down Expand Up @@ -57,8 +56,6 @@ struct AppEnvironment: Sendable {
var httpClient: @Sendable () -> Client
var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest?
var logger: @Sendable () -> Logger
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
var mastodonPost: @Sendable (_ client: Client, _ post: String) async throws -> Void
var metricsPushGatewayUrl: @Sendable () -> String?
var plausibleBackendReportingSiteID: @Sendable () -> String?
var postPlausibleEvent: @Sendable (Client, Plausible.Event.Kind, Plausible.Path, User?) async throws -> Void
Expand Down Expand Up @@ -109,11 +106,6 @@ extension AppEnvironment {
nonisolated(unsafe) static var logger: Logger!

static let live = AppEnvironment(
allowTwitterPosts: {
Environment.get("ALLOW_TWITTER_POSTS")
.flatMap(\.asBool)
?? Constants.defaultAllowTwitterPosts
},
apiSigningKey: { Environment.get("API_SIGNING_KEY") },
appVersion: { App.appVersion },
awsAccessKeyId: { Environment.get("AWS_ACCESS_KEY_ID") },
Expand Down Expand Up @@ -186,11 +178,6 @@ extension AppEnvironment {
httpClient: { httpClient },
loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) },
logger: { logger },
mastodonCredentials: {
Environment.get("MASTODON_ACCESS_TOKEN")
.map(Mastodon.Credentials.init(accessToken:))
},
mastodonPost: { client, message in try await Mastodon.post(client: client, message: message) },
metricsPushGatewayUrl: { Environment.get("METRICS_PUSHGATEWAY_URL") },
plausibleBackendReportingSiteID: { Environment.get("PLAUSIBLE_BACKEND_REPORTING_SITE_ID") },
postPlausibleEvent: { client, kind, path, user in try await Plausible.postEvent(client: client, kind: kind, path: path, user: user) },
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Vapor

enum Constants {
static let defaultAllowBuildTriggering = true
static let defaultAllowTwitterPosts = true
static let defaultAllowSocialPosts = true
static let defaultGitlabPipelineLimit = 200
static let defaultHideStagingBanner = false

Expand Down
15 changes: 14 additions & 1 deletion Sources/App/Core/Dependencies/EnvironmentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ struct EnvironmentClient {
// See https://swiftpackageindex.com/pointfreeco/swift-dependencies/main/documentation/dependenciesmacros/dependencyclient()#Restrictions
// regarding the use of XCTFail here.
var allowBuildTriggers: @Sendable () -> Bool = { XCTFail(#function); return true }
var allowSocialPosts: @Sendable () -> Bool = { XCTFail(#function); return true }
// We're not defaulting current to XCTFail, because its 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 have with the Current dependency injection: it defaults to
// .development and does not raise an error when not injected.
var current: @Sendable () -> Environment = { .development }
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
var mastodonPost: @Sendable (_ client: Client, _ post: String) async throws -> Void
}


Expand All @@ -37,7 +40,17 @@ extension EnvironmentClient: DependencyKey {
allowBuildTriggers: {
Environment.get("ALLOW_BUILD_TRIGGERS").flatMap(\.asBool) ?? Constants.defaultAllowBuildTriggering
},
current: { (try? Environment.detect()) ?? .development }
allowSocialPosts: {
Environment.get("ALLOW_SOCIAL_POSTS")
.flatMap(\.asBool)
?? Constants.defaultAllowSocialPosts
},
current: { (try? Environment.detect()) ?? .development },
mastodonCredentials: {
Environment.get("MASTODON_ACCESS_TOKEN")
.map(Mastodon.Credentials.init(accessToken:))
},
mastodonPost: { client, message in try await Mastodon.post(client: client, message: message) }
)
}
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/App/Core/Mastodon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Dependencies
import Vapor


Expand All @@ -27,7 +28,8 @@ enum Mastodon {

// NB: _testEncodedURL is a callback that exists purely to be able to regression test the encoded value
static func post(client: Client, message: String, _testEncodedURL: (String) -> Void = { _ in }) async throws {
guard let credentials = Current.mastodonCredentials() else {
@Dependency(\.environment) var environment
guard let credentials = environment.mastodonCredentials() else {
throw Social.Error.missingCredentials
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/App/Core/Social.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Dependencies
import SemanticVersion
import Vapor

Expand Down Expand Up @@ -96,16 +97,15 @@ enum Social {
static func postToFirehose(client: Client,
package: Joined<Package, Repository>,
version: Version) async throws {
guard Current.allowTwitterPosts() else {
throw Error.postingDisabled
}
@Dependency(\.environment) var environment
guard environment.allowSocialPosts() else { throw Error.postingDisabled }
guard let message = firehoseMessage(package: package,
version: version,
maxLength: postMaxLength) else {
throw Error.invalidMessage
}
// Ignore errors from here for now to keep concurrency simpler
async let _ = try? await Current.mastodonPost(client, message)
async let _ = try? await environment.mastodonPost(client, message)
}

static func postToFirehose(client: Client,
Expand Down
188 changes: 90 additions & 98 deletions Tests/AppTests/AnalyzeErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,125 +98,117 @@ final class AnalyzeErrorTests: AppTestCase {
}

Current.shell.run = Self.defaultShellRun
}

Current.mastodonPost = { [socialPosts = self.socialPosts] _, message in
socialPosts.withValue { $0.append(message) }
override func invokeTest() {
withDependencies {
$0.date.now = .t0
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable [socialPosts = self.socialPosts] _, message in
socialPosts.withValue { $0.append(message) }
}
} operation: {
super.invokeTest()
}
}

func test_analyze_refreshCheckout_failed() async throws {
try await withDependencies {
$0.date.now = .t0
} 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()

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)")
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()

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)")
}
}

func test_analyze_updateRepository_invalidPackageCachePath() async throws {
try await withDependencies {
$0.date.now = .t0
} 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)")
}
// 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 {
try await withDependencies {
$0.date.now = .t0
} 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)
}
// 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 {
try await withDependencies {
$0.date.now = .t0
} 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)")
// 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)")
}
}

Expand Down
7 changes: 7 additions & 0 deletions Tests/AppTests/AnalyzerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class AnalyzerTests: AppTestCase {
// expected shell commands for the happy path.)
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in }
} operation: {
// setup
let urls = ["https://github.com/foo/1", "https://github.com/foo/2"]
Expand Down Expand Up @@ -216,6 +218,8 @@ class AnalyzerTests: AppTestCase {
// changing as well as a tag being moved to a different commit.
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in }
} operation: {
// setup
let pkgId = UUID()
Expand Down Expand Up @@ -354,6 +358,7 @@ class AnalyzerTests: AppTestCase {
// Ensure packages record success/error status
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
} operation: {
// setup
let urls = ["https://github.com/foo/1", "https://github.com/foo/2"]
Expand Down Expand Up @@ -404,6 +409,7 @@ class AnalyzerTests: AppTestCase {
// Test to ensure exceptions don't interrupt processing
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
} operation: {
// setup
let urls = ["https://github.com/foo/1", "https://github.com/foo/2"]
Expand Down Expand Up @@ -881,6 +887,7 @@ class AnalyzerTests: AppTestCase {
// https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/29
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
} operation: {
// setup
Current.git.commitCount = { @Sendable _ in 12 }
Expand Down
Loading
Loading