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
5 changes: 0 additions & 5 deletions Sources/App/Core/AppEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@ struct AppEnvironment: Sendable {
var gitlabPipelineLimit: @Sendable () -> Int
var hideStagingBanner: @Sendable () -> Bool
var maintenanceMessage: @Sendable () -> String?
var httpClient: @Sendable () -> Client
var loadSPIManifest: @Sendable (String) -> SPIManifest.Manifest?
var logger: @Sendable () -> Logger
var metricsPushGatewayUrl: @Sendable () -> String?
var plausibleBackendReportingSiteID: @Sendable () -> String?
var processingBuildBacklog: @Sendable () -> Bool
var runnerIds: @Sendable () -> [String]
var setHTTPClient: @Sendable (Client) -> Void
var setLogger: @Sendable (Logger) -> Void
var shell: Shell
var siteURL: @Sendable () -> String
Expand All @@ -65,7 +63,6 @@ struct AppEnvironment: Sendable {


extension AppEnvironment {
nonisolated(unsafe) static var httpClient: Client!
nonisolated(unsafe) static var logger: Logger!

static let live = AppEnvironment(
Expand Down Expand Up @@ -96,7 +93,6 @@ extension AppEnvironment {
maintenanceMessage: {
Environment.get("MAINTENANCE_MESSAGE").flatMap(\.trimmed)
},
httpClient: { httpClient },
loadSPIManifest: { path in SPIManifest.Manifest.load(in: path) },
logger: { logger },
metricsPushGatewayUrl: { Environment.get("METRICS_PUSHGATEWAY_URL") },
Expand All @@ -110,7 +106,6 @@ extension AppEnvironment {
.flatMap { try? JSONDecoder().decode([String].self, from: $0) }
?? []
},
setHTTPClient: { client in Self.httpClient = client },
setLogger: { logger in Self.logger = logger },
shell: .live,
siteURL: { Environment.get("SITE_URL") ?? "http://localhost:8080" },
Expand Down
3 changes: 0 additions & 3 deletions Sources/App/Core/Dependencies/EnvironmentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ struct EnvironmentClient {
var currentReferenceCache: @Sendable () -> CurrentReferenceCache?
var dbId: @Sendable () -> String?
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
#warning("drop client parameter and move this to httpClient")
var mastodonPost: @Sendable (_ client: Client, _ message: String) async throws -> Void
var random: @Sendable (_ range: ClosedRange<Double>) -> Double = { XCTFail("random"); return Double.random(in: $0) }

enum FailureMode: String {
Expand Down Expand Up @@ -110,7 +108,6 @@ extension EnvironmentClient: DependencyKey {
Environment.get("MASTODON_ACCESS_TOKEN")
.map(Mastodon.Credentials.init(accessToken:))
},
mastodonPost: { client, message in try await Mastodon.post(client: client, message: message) },
random: { range in Double.random(in: range) },
shouldFail: { failureMode in
let shouldFail = Environment.get("FAILURE_MODE")
Expand Down
6 changes: 4 additions & 2 deletions Sources/App/Core/Dependencies/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ struct HTTPClient {
typealias Request = Vapor.HTTPClient.Request
typealias Response = Vapor.HTTPClient.Response

var post: @Sendable (_ url: String, _ headers: HTTPHeaders, _ body: Data) async throws -> Response
var post: @Sendable (_ url: String, _ headers: HTTPHeaders, _ body: Data?) async throws -> Response
var fetchDocumentation: @Sendable (_ url: URI) async throws -> Response
var fetchHTTPStatusCode: @Sendable (_ url: String) async throws -> HTTPStatus
var mastodonPost: @Sendable (_ message: String) async throws -> Void
var postPlausibleEvent: @Sendable (_ kind: Plausible.Event.Kind, _ path: Plausible.Path, _ user: User?) async throws -> Void
}

extension HTTPClient: DependencyKey {
static var liveValue: HTTPClient {
.init(
post: { url, headers, body in
let req = try Request(url: url, method: .POST, headers: headers, body: .data(body))
let req = try Request(url: url, method: .POST, headers: headers, body: body.map({.data($0)}))
return try await Vapor.HTTPClient.shared.execute(request: req).get()
},
fetchDocumentation: { url in
Expand All @@ -53,6 +54,7 @@ extension HTTPClient: DependencyKey {
try await client.shutdown()
}
},
mastodonPost: { message in try await Mastodon.post(message: message) },
postPlausibleEvent: { kind, path, user in
try await Plausible.postEvent(kind: kind, path: path, user: user)
}
Expand Down
29 changes: 20 additions & 9 deletions Sources/App/Core/Mastodon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,45 @@ import Vapor

enum Mastodon {

private static let instance = "mas.to"
private static let apiURL = "https://\(instance)/api/v1/statuses"
private static let apiHost = "mas.to"
private static let apiPath = "/api/v1/statuses"
static let postMaxLength = 490 // 500, leaving some buffer for unicode accounting oddities

struct Credentials {
var accessToken: String
}

// 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 {
static func apiURL(with message: String) throws -> String {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change allowed us to get rid of this ugly hack with the trailing closure that was purely used for testing.

var components = URLComponents()
components.scheme = "https"
components.host = apiHost
components.path = apiPath
components.queryItems = [URLQueryItem(name: "status", value: message)]
guard let url = components.string else {
throw Social.Error.invalidURL
}
return url
}

static func post(message: String) async throws {
@Dependency(\.environment) var environment
@Dependency(\.httpClient) var httpClient
@Dependency(\.uuid) var uuid
guard let credentials = environment.mastodonCredentials() else {
throw Social.Error.missingCredentials
}

let headers = HTTPHeaders([
("Authorization", "Bearer \(credentials.accessToken)"),
("Idempotency-Key", UUID().uuidString),
("Idempotency-Key", uuid().uuidString),
])

struct Query: Encodable {
var status: String
}

let res = try await client.post(URI(string: apiURL), headers: headers) { req in
try req.query.encode(Query(status: message))
_testEncodedURL(req.url.string)
}
let res = try await httpClient.post(url: apiURL(with: message), headers: headers, body: nil)

guard res.status == .ok else {
throw Social.Error.requestFailed(res.status, res.body?.asString() ?? "")
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/App/Core/Social.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum Social {

enum Error: LocalizedError {
case invalidMessage
case invalidURL
case missingCredentials
case postingDisabled
case requestFailed(HTTPStatus, String)
Expand Down Expand Up @@ -98,14 +99,15 @@ enum Social {
package: Joined<Package, Repository>,
version: Version) async throws {
@Dependency(\.environment) var environment
@Dependency(\.httpClient) var httpClient
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 environment.mastodonPost(client: client, message: message)
async let _ = try? await httpClient.mastodonPost(message: message)
}

static func postToFirehose(client: Client,
Expand Down
1 change: 0 additions & 1 deletion Sources/App/configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public func configure(_ app: Application) async throws -> String {

app.logger.component = "server"
Current.setLogger(app.logger)
Current.setHTTPClient(app.client)

// It will be tempting to uncomment/re-add these lines in the future. We should not enable
// server-side compression as long as we pass requests through Cloudflare, which compresses
Expand Down
2 changes: 1 addition & 1 deletion Tests/AppTests/AnalyzeErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ final class AnalyzeErrorTests: AppTestCase {
withDependencies {
$0.date.now = .t0
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable [socialPosts = self.socialPosts] _, message in
$0.httpClient.mastodonPost = { @Sendable [socialPosts = self.socialPosts] message in
socialPosts.withValue { $0.append(message) }
}
} operation: {
Expand Down
4 changes: 2 additions & 2 deletions Tests/AppTests/AnalyzerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class AnalyzerTests: AppTestCase {
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in }
$0.httpClient.mastodonPost = { @Sendable _ in }
} operation: {
// setup
let urls = ["https://github.com/foo/1", "https://github.com/foo/2"]
Expand Down Expand Up @@ -219,7 +219,7 @@ class AnalyzerTests: AppTestCase {
try await withDependencies {
$0.date.now = .now
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in }
$0.httpClient.mastodonPost = { @Sendable _ in }
} operation: {
// setup
let pkgId = UUID()
Expand Down
54 changes: 54 additions & 0 deletions Tests/AppTests/LiveTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

@testable import App

import Dependencies
import XCTVapor


class LiveTests: XCTestCase {

func test_Mastodon_post() async throws {
// Only run this test manually to confirm posting works
try XCTSkipIf(true)

try await withDependencies {
$0.environment.mastodonCredentials = { .dev }
$0.httpClient = .liveValue
$0.uuid = .init(UUID.init)
} operation: {
let message = Social.versionUpdateMessage(
packageName: "packageName",
repositoryOwnerName: "owner",
url: "http://localhost:8080/owner/SomePackage",
version: .init(2, 6, 4),
summary: "Testing, testing αβγ ÄÖÜß 🔤 ✅",
maxLength: Social.postMaxLength
)

try await Mastodon.post(message: message)
}
}

}


extension Mastodon.Credentials {
// https://mas.to/@spi_test
static var dev: Self? {
guard let accessToken = Environment.get("DEV_MASTODON_ACCESS_TOKEN") else { return nil }
return .init(accessToken: accessToken)
}
}
2 changes: 1 addition & 1 deletion Tests/AppTests/MastodonTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class MastodonTests: AppTestCase {
let message = QueueIsolated<String?>(nil)
try await withDependencies {
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, msg in
$0.httpClient.mastodonPost = { @Sendable msg in
if message.value == nil {
message.setValue(msg)
} else {
Expand Down
2 changes: 0 additions & 2 deletions Tests/AppTests/Mocks/AppEnvironment+mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,12 @@ extension AppEnvironment {
gitlabPipelineLimit: { Constants.defaultGitlabPipelineLimit },
hideStagingBanner: { false },
maintenanceMessage: { nil },
httpClient: { httpClient },
loadSPIManifest: { _ in nil },
logger: { logger },
metricsPushGatewayUrl: { "http://pushgateway:9091" },
plausibleBackendReportingSiteID: { nil },
processingBuildBacklog: { false },
runnerIds: { [] },
setHTTPClient: { client in Self.httpClient = client },
setLogger: { logger in Self.logger = logger },
shell: .mock,
siteURL: { Environment.get("SITE_URL") ?? "http://localhost:8080" },
Expand Down
2 changes: 2 additions & 0 deletions Tests/AppTests/PlausibleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class PlausibleTests: XCTestCase {
$0.httpClient.post = { @Sendable _, _, body in
await called.withValue { $0 = true }
// validate
let body = try XCTUnwrap(body)
XCTAssertEqual(try? JSONDecoder().decode(Plausible.Event.self, from: body),
.init(name: .pageview,
url: "https://foo.bar/api/search",
Expand All @@ -59,6 +60,7 @@ final class PlausibleTests: XCTestCase {
$0.httpClient.post = { @Sendable _, _, body in
await called.withValue { $0 = true }
// validate
let body = try XCTUnwrap(body)
XCTAssertEqual(try? JSONDecoder().decode(Plausible.Event.self, from: body),
.init(name: .pageview,
url: "https://foo.bar/api/packages/{owner}/{repository}",
Expand Down
2 changes: 1 addition & 1 deletion Tests/AppTests/ReAnalyzeVersionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ReAnalyzeVersionsTests: AppTestCase {
try await withDependencies {
$0.date.now = .t0
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in }
$0.httpClient.mastodonPost = { @Sendable _ in }
} operation: {
// setup
// - package dump does not include toolsVersion, targets to simulate an "old version"
Expand Down
33 changes: 23 additions & 10 deletions Tests/AppTests/SocialTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class SocialTests: AppTestCase {

try await withDependencies {
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, _ in posted.withLockedValue { $0 += 1 } }
$0.httpClient.mastodonPost = { @Sendable _ in posted.withLockedValue { $0 += 1 } }
} operation: {
// MUT
try await Social.postToFirehose(client: app.client,
Expand Down Expand Up @@ -228,7 +228,7 @@ class SocialTests: AppTestCase {

try await withDependencies {
$0.environment.allowSocialPosts = { true }
$0.environment.mastodonPost = { @Sendable _, msg in
$0.httpClient.mastodonPost = { @Sendable msg in
XCTAssertTrue(msg.contains("v2.0.0"))
posted.withLockedValue { $0 += 1 }
}
Expand All @@ -244,8 +244,24 @@ class SocialTests: AppTestCase {
}

func test_urlEncoding() async throws {
await withDependencies {
let called = ActorIsolated(false)
try await withDependencies {
$0.environment.mastodonCredentials = { .init(accessToken: "fakeToken") }
$0.httpClient.post = { @Sendable url, headers, _ in
// validate
assertInlineSnapshot(of: url, as: .lines) {
"""
https://mas.to/api/v1/statuses?status=%E2%AC%86%EF%B8%8F%20owner%20just%20released%20packageName%20v2.6.4%0A%0Ahttp://localhost:8080/owner/SuperAwesomePackage%23releases
"""
}
XCTAssertEqual(headers, HTTPHeaders([
("Authorization", "Bearer fakeToken"),
("Idempotency-Key", UUID.id0.uuidString),
]))
await called.withValue{ $0 = true }
return .ok
}
$0.uuid = .constant(.id0)
} operation: {
// setup
let message = Social.versionUpdateMessage(
Expand All @@ -258,13 +274,10 @@ class SocialTests: AppTestCase {
)

// MUT
try? await Mastodon.post(client: app.client, message: message) { encoded in
assertInlineSnapshot(of: encoded, as: .lines) {
"""
https://mas.to/api/v1/statuses?status=%E2%AC%86%EF%B8%8F%20owner%20just%20released%20packageName%20v2.6.4%0A%0Ahttp%3A%2F%2Flocalhost%3A8080%2Fowner%2FSuperAwesomePackage%23releases
"""
}
}
try await Mastodon.post(message: message)

// validate
try await XCTAssertEqualAsync(await called.value, true)
}
}

Expand Down
Loading