diff --git a/Sources/App/Core/AppEnvironment.swift b/Sources/App/Core/AppEnvironment.swift index 58941cd11..4aac8d49f 100644 --- a/Sources/App/Core/AppEnvironment.swift +++ b/Sources/App/Core/AppEnvironment.swift @@ -29,8 +29,6 @@ struct AppEnvironment: Sendable { var awsDocsBucket: @Sendable () -> String? var awsReadmeBucket: @Sendable () -> String? var awsSecretAccessKey: @Sendable () -> String? - var buildTimeout: @Sendable () -> Int - var builderToken: @Sendable () -> String? var buildTriggerAllowList: @Sendable () -> [Package.Id] var buildTriggerDownscaling: @Sendable () -> Double var buildTriggerLatestSwiftVersionDownscaling: @Sendable () -> Double @@ -112,8 +110,6 @@ extension AppEnvironment { awsDocsBucket: { Environment.get("AWS_DOCS_BUCKET") }, awsReadmeBucket: { Environment.get("AWS_README_BUCKET") }, awsSecretAccessKey: { Environment.get("AWS_SECRET_ACCESS_KEY") }, - buildTimeout: { Environment.get("BUILD_TIMEOUT").flatMap(Int.init) ?? 10 }, - builderToken: { Environment.get("BUILDER_TOKEN") }, buildTriggerAllowList: { Environment.get("BUILD_TRIGGER_ALLOW_LIST") .map { Data($0.utf8) } diff --git a/Sources/App/Core/Authentication/User.swift b/Sources/App/Core/Authentication/User.swift index 249f3a473..d72c58859 100644 --- a/Sources/App/Core/Authentication/User.swift +++ b/Sources/App/Core/Authentication/User.swift @@ -13,6 +13,7 @@ // limitations under the License. import Authentication +import Dependencies import JWTKit import Vapor import VaporToOpenAPI @@ -54,9 +55,10 @@ extension User { static var builder: Self { .init(name: "builder", identifier: "builder") } struct BuilderAuthenticator: AsyncBearerAuthenticator { + @Dependency(\.environment) var environment + func authenticate(bearer: BearerAuthorization, for request: Request) async throws { - if let builderToken = Current.builderToken(), - bearer.token == builderToken { + if let token = environment.builderToken(), bearer.token == token { request.auth.login(User.builder) } } diff --git a/Sources/App/Core/Dependencies/EnvironmentClient.swift b/Sources/App/Core/Dependencies/EnvironmentClient.swift index 24ac005b0..80561a928 100644 --- a/Sources/App/Core/Dependencies/EnvironmentClient.swift +++ b/Sources/App/Core/Dependencies/EnvironmentClient.swift @@ -23,6 +23,8 @@ struct EnvironmentClient { // regarding the use of XCTFail here. var allowBuildTriggers: @Sendable () -> Bool = { XCTFail(#function); return true } var allowSocialPosts: @Sendable () -> Bool = { XCTFail(#function); return true } + var builderToken: @Sendable () -> String? + var buildTimeout: @Sendable () -> Int = { XCTFail(#function); return 10 } // 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 @@ -45,6 +47,8 @@ extension EnvironmentClient: DependencyKey { .flatMap(\.asBool) ?? Constants.defaultAllowSocialPosts }, + builderToken: { Environment.get("BUILDER_TOKEN") }, + buildTimeout: { Environment.get("BUILD_TIMEOUT").flatMap(Int.init) ?? 10 }, current: { (try? Environment.detect()) ?? .development }, mastodonCredentials: { Environment.get("MASTODON_ACCESS_TOKEN") diff --git a/Sources/App/Core/Gitlab.swift b/Sources/App/Core/Gitlab.swift index a4a6ef4e8..6544a0224 100644 --- a/Sources/App/Core/Gitlab.swift +++ b/Sources/App/Core/Gitlab.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Dependencies import Vapor @@ -76,13 +77,15 @@ extension Gitlab.Builder { reference: Reference, swiftVersion: SwiftVersion, versionID: Version.Id) async throws -> Build.TriggerResponse { + @Dependency(\.environment) var environment + guard let pipelineToken = Current.gitlabPipelineToken(), - let builderToken = Current.builderToken() + let builderToken = environment.builderToken() else { throw Gitlab.Error.missingToken } guard let awsDocsBucket = Current.awsDocsBucket() else { throw Gitlab.Error.missingConfiguration("AWS_DOCS_BUCKET") } - let timeout = Current.buildTimeout() + (isDocBuild ? 5 : 0) + let timeout = environment.buildTimeout() + (isDocBuild ? 5 : 0) let uri: URI = .init(string: "\(projectURL)/trigger/pipeline") let response = try await client diff --git a/Tests/AppTests/ApiTests.swift b/Tests/AppTests/ApiTests.swift index 1afd75e0a..ead2bd307 100644 --- a/Tests/AppTests/ApiTests.swift +++ b/Tests/AppTests/ApiTests.swift @@ -130,80 +130,195 @@ class ApiTests: AppTestCase { } func test_post_buildReport() async throws { - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let versionId = try v.requireID() + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let versionId = try v.requireID() + + do { // MUT - initial insert + let dto: API.PostBuildReportDTO = .init( + buildCommand: "xcodebuild -scheme Foo", + buildDate: .t0, + buildDuration: 123.4, + buildErrors: .init(numSwift6Errors: 42), + builderVersion: "1.2.3", + buildId: .id0, + commitHash: "sha", + jobUrl: "https://example.com/jobs/1", + logUrl: "log url", + platform: .macosXcodebuild, + productDependencies: [.init(identity: "identity", name: "name", url: "url", dependencies: [])], + resolvedDependencies: [.init(packageName: "packageName", repositoryURL: "repositoryURL")], + runnerId: "some-runner", + status: .failed, + swiftVersion: .init(5, 2, 0) + ) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 + let body: ByteBuffer = .init(data: try encoder.encode(dto)) + try await app.test( + .POST, + "api/versions/\(versionId)/build-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res async throws in + // validation + XCTAssertEqual(res.status, .noContent) + let builds = try await Build.query(on: app.db).all() + XCTAssertEqual(builds.count, 1) + let b = try builds.first.unwrap() + XCTAssertEqual(b.id, .id0) + XCTAssertEqual(b.buildCommand, "xcodebuild -scheme Foo") + XCTAssertEqual(b.buildDate, .t0) + XCTAssertEqual(b.buildDuration, 123.4) + XCTAssertEqual(b.buildErrors, .init(numSwift6Errors: 42)) + XCTAssertEqual(b.builderVersion, "1.2.3") + XCTAssertEqual(b.commitHash, "sha") + XCTAssertEqual(b.jobUrl, "https://example.com/jobs/1") + XCTAssertEqual(b.logUrl, "log url") + XCTAssertEqual(b.platform, .macosXcodebuild) + XCTAssertEqual(b.runnerId, "some-runner") + XCTAssertEqual(b.status, .failed) + XCTAssertEqual(b.swiftVersion, .init(5, 2, 0)) + let v = try await Version.find(versionId, on: app.db).unwrap(or: Abort(.notFound)) + XCTAssertEqual(v.productDependencies, [.init(identity: "identity", + name: "name", + url: "url", + dependencies: [])]) + XCTAssertEqual(v.resolvedDependencies, [.init(packageName: "packageName", + repositoryURL: "repositoryURL")]) + // build failed, hence no package platform compatibility yet + let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) + XCTAssertEqual(p.platformCompatibility, []) + }) + } + + do { // MUT - update of the same record + let dto: API.PostBuildReportDTO = .init( + buildId: .id0, + platform: .macosXcodebuild, + resolvedDependencies: [.init(packageName: "foo", + repositoryURL: "http://foo/bar")], + status: .ok, + swiftVersion: .init(5, 2, 0) + ) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/versions/\(versionId)/build-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res async throws in + // validation + XCTAssertEqual(res.status, .noContent) + let builds = try await Build.query(on: app.db).all() + XCTAssertEqual(builds.count, 1) + let b = try builds.first.unwrap() + XCTAssertEqual(b.id, .id0) + XCTAssertEqual(b.platform, .macosXcodebuild) + XCTAssertEqual(b.status, .ok) + XCTAssertEqual(b.swiftVersion, .init(5, 2, 0)) + let v = try await Version.find(versionId, on: app.db).unwrap(or: Abort(.notFound)) + XCTAssertEqual(v.resolvedDependencies, + [.init(packageName: "foo", + repositoryURL: "http://foo/bar")]) + // build ok now -> package is macos compatible + let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) + XCTAssertEqual(p.platformCompatibility, [.macOS]) + }) + } + + do { // MUT - add another build to test Package.platformCompatibility + let dto: API.PostBuildReportDTO = .init( + buildId: .id1, + platform: .iOS, + resolvedDependencies: [.init(packageName: "foo", + repositoryURL: "http://foo/bar")], + status: .ok, + swiftVersion: .init(5, 2, 0) + ) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/versions/\(versionId)/build-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res async throws in + // validation + let builds = try await Build.query(on: app.db).all() + XCTAssertEqual(Set(builds.map(\.id)), Set([.id0, .id1])) + // additional ios build ok -> package is also ios compatible + let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) + XCTAssertEqual(p.platformCompatibility, [.iOS, .macOS]) + }) + } + } + } + + func test_post_buildReport_conflict() async throws { + // Test behaviour when reporting back with a different build id for the same build pair. This would not + // happen in normal behaviour but it _is_ something we rely on when running the builder tests. They + // trigger builds not via trigger commands that prepares a build record before triggering, resolving + // potential conflicts ahead of time. Instead the build is simply triggered and reported back with a + // configured build id. + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let versionId = try v.requireID() + try await Build(id: .id0, version: v, platform: .iOS, status: .failed, swiftVersion: .latest).save(on: app.db) - do { // MUT - initial insert let dto: API.PostBuildReportDTO = .init( - buildCommand: "xcodebuild -scheme Foo", - buildDate: .t0, - buildDuration: 123.4, - buildErrors: .init(numSwift6Errors: 42), - builderVersion: "1.2.3", - buildId: .id0, - commitHash: "sha", - jobUrl: "https://example.com/jobs/1", - logUrl: "log url", - platform: .macosXcodebuild, - productDependencies: [.init(identity: "identity", name: "name", url: "url", dependencies: [])], - resolvedDependencies: [.init(packageName: "packageName", repositoryURL: "repositoryURL")], - runnerId: "some-runner", - status: .failed, - swiftVersion: .init(5, 2, 0) + buildId: .id1, + platform: .iOS, + status: .ok, + swiftVersion: .latest ) - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .secondsSince1970 - let body: ByteBuffer = .init(data: try encoder.encode(dto)) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) try await app.test( .POST, "api/versions/\(versionId)/build-report", headers: .bearerApplicationJSON("secr3t"), body: body, - afterResponse: { res async throws in + afterResponse: { res in // validation XCTAssertEqual(res.status, .noContent) let builds = try await Build.query(on: app.db).all() XCTAssertEqual(builds.count, 1) let b = try builds.first.unwrap() - XCTAssertEqual(b.id, .id0) - XCTAssertEqual(b.buildCommand, "xcodebuild -scheme Foo") - XCTAssertEqual(b.buildDate, .t0) - XCTAssertEqual(b.buildDuration, 123.4) - XCTAssertEqual(b.buildErrors, .init(numSwift6Errors: 42)) - XCTAssertEqual(b.builderVersion, "1.2.3") - XCTAssertEqual(b.commitHash, "sha") - XCTAssertEqual(b.jobUrl, "https://example.com/jobs/1") - XCTAssertEqual(b.logUrl, "log url") - XCTAssertEqual(b.platform, .macosXcodebuild) - XCTAssertEqual(b.runnerId, "some-runner") - XCTAssertEqual(b.status, .failed) - XCTAssertEqual(b.swiftVersion, .init(5, 2, 0)) - let v = try await Version.find(versionId, on: app.db).unwrap(or: Abort(.notFound)) - XCTAssertEqual(v.productDependencies, [.init(identity: "identity", - name: "name", - url: "url", - dependencies: [])]) - XCTAssertEqual(v.resolvedDependencies, [.init(packageName: "packageName", - repositoryURL: "repositoryURL")]) - // build failed, hence no package platform compatibility yet - let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) - XCTAssertEqual(p.platformCompatibility, []) + XCTAssertEqual(b.id, .id1) + XCTAssertEqual(b.platform, .iOS) + XCTAssertEqual(b.status, .ok) + XCTAssertEqual(b.swiftVersion, .latest) }) } + } + + func test_post_buildReport_infrastructureError() async throws { + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p) + try await v.save(on: app.db) + let versionId = try XCTUnwrap(v.id) - do { // MUT - update of the same record let dto: API.PostBuildReportDTO = .init( + buildCommand: "xcodebuild -scheme Foo", buildId: .id0, + jobUrl: "https://example.com/jobs/1", + logUrl: "log url", platform: .macosXcodebuild, - resolvedDependencies: [.init(packageName: "foo", - repositoryURL: "http://foo/bar")], - status: .ok, - swiftVersion: .init(5, 2, 0) - ) + status: .infrastructureError, + swiftVersion: .init(5, 2, 0)) let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) try await app.test( .POST, @@ -216,471 +331,391 @@ class ApiTests: AppTestCase { let builds = try await Build.query(on: app.db).all() XCTAssertEqual(builds.count, 1) let b = try builds.first.unwrap() - XCTAssertEqual(b.id, .id0) - XCTAssertEqual(b.platform, .macosXcodebuild) - XCTAssertEqual(b.status, .ok) - XCTAssertEqual(b.swiftVersion, .init(5, 2, 0)) - let v = try await Version.find(versionId, on: app.db).unwrap(or: Abort(.notFound)) - XCTAssertEqual(v.resolvedDependencies, - [.init(packageName: "foo", - repositoryURL: "http://foo/bar")]) - // build ok now -> package is macos compatible - let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) - XCTAssertEqual(p.platformCompatibility, [.macOS]) + XCTAssertEqual(b.status, .infrastructureError) }) } + } - do { // MUT - add another build to test Package.platformCompatibility - let dto: API.PostBuildReportDTO = .init( - buildId: .id1, - platform: .iOS, - resolvedDependencies: [.init(packageName: "foo", - repositoryURL: "http://foo/bar")], - status: .ok, - swiftVersion: .init(5, 2, 0) - ) + func test_post_buildReport_unauthenticated() async throws { + // Ensure unauthenticated access raises a 401 + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p) + try await v.save(on: app.db) + let versionId = try XCTUnwrap(v.id) + let dto: API.PostBuildReportDTO = .init(buildId: .id0, + platform: .macosXcodebuild, + status: .ok, + swiftVersion: .init(5, 2, 0)) let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + let db = app.db + + // MUT - no auth header try await app.test( .POST, "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), + headers: .applicationJSON, body: body, - afterResponse: { res async throws in + afterResponse: { res in // validation - let builds = try await Build.query(on: app.db).all() - XCTAssertEqual(Set(builds.map(\.id)), Set([.id0, .id1])) - // additional ios build ok -> package is also ios compatible - let p = try await XCTUnwrapAsync(try await Package.find(p.id, on: app.db)) - XCTAssertEqual(p.platformCompatibility, [.iOS, .macOS]) - }) - } - - } - - func test_post_buildReport_conflict() async throws { - // Test behaviour when reporting back with a different build id for the same build pair. This would not - // happen in normal behaviour but it _is_ something we rely on when running the builder tests. They - // trigger builds not via trigger commands that prepares a build record before triggering, resolving - // potential conflicts ahead of time. Instead the build is simply triggered and reported back with a - // configured build id. - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let versionId = try v.requireID() - try await Build(id: .id0, version: v, platform: .iOS, status: .failed, swiftVersion: .latest).save(on: app.db) - - let dto: API.PostBuildReportDTO = .init( - buildId: .id1, - platform: .iOS, - status: .ok, - swiftVersion: .latest - ) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .noContent) - let builds = try await Build.query(on: app.db).all() - XCTAssertEqual(builds.count, 1) - let b = try builds.first.unwrap() - XCTAssertEqual(b.id, .id1) - XCTAssertEqual(b.platform, .iOS) - XCTAssertEqual(b.status, .ok) - XCTAssertEqual(b.swiftVersion, .latest) - }) - } - - func test_post_buildReport_infrastructureError() async throws { - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p) - try await v.save(on: app.db) - let versionId = try XCTUnwrap(v.id) - - let dto: API.PostBuildReportDTO = .init( - buildCommand: "xcodebuild -scheme Foo", - buildId: .id0, - jobUrl: "https://example.com/jobs/1", - logUrl: "log url", - platform: .macosXcodebuild, - status: .infrastructureError, - swiftVersion: .init(5, 2, 0)) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res async throws in - // validation - XCTAssertEqual(res.status, .noContent) - let builds = try await Build.query(on: app.db).all() - XCTAssertEqual(builds.count, 1) - let b = try builds.first.unwrap() - XCTAssertEqual(b.status, .infrastructureError) - }) - } - - func test_post_buildReport_unauthenticated() async throws { - // Ensure unauthenticated access raises a 401 - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p) - try await v.save(on: app.db) - let versionId = try XCTUnwrap(v.id) - let dto: API.PostBuildReportDTO = .init(buildId: .id0, - platform: .macosXcodebuild, - status: .ok, - swiftVersion: .init(5, 2, 0)) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - let db = app.db - - // MUT - no auth header - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .applicationJSON, - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) - } - ) + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) + } + ) - // MUT - wrong token - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("wrong"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) - } - ) + // MUT - wrong token + try await app.test( + .POST, + "api/versions/\(versionId)/build-report", + headers: .bearerApplicationJSON("wrong"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) + } + ) - // MUT - without server token - Current.builderToken = { nil } - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) + // MUT - without server token + try await withDependencies { + $0.environment.builderToken = { nil } + } operation: { + try await app.test( + .POST, + "api/versions/\(versionId)/build-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await Build.query(on: db).count(), 0) + } + ) } - ) + } } func test_post_buildReport_large() async throws { // Ensure we can handle large build reports // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2825 - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let versionId = try v.requireID() - - // MUT - let data = try fixtureData(for: "large-build-report.json") - XCTAssert(data.count > 16_000, "was: \(data.count) bytes") - let body: ByteBuffer = .init(data: data) - let outOfTheWayPort = 12_345 - try await app.testable(method: .running(port: outOfTheWayPort)).test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res async in - // validation - XCTAssertEqual(res.status, .noContent) - }) - } - - func test_post_buildReport_package_updatedAt() async throws { - // Ensure we don't change packages.updatedAt when receiving a build report. - // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/3290#issuecomment-2293101104 - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let originalPackageUpdate = try XCTUnwrap(p.updatedAt) - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let versionId = try v.requireID() - // Sleep for 1ms to ensure we can detect a difference between update times. - try await Task.sleep(nanoseconds: UInt64(1e6)) - - // MUT - let dto: API.PostBuildReportDTO = .init( - buildCommand: "xcodebuild -scheme Foo", - buildDate: .t0, - buildDuration: 123.4, - buildErrors: .init(numSwift6Errors: 42), - builderVersion: "1.2.3", - buildId: .id0, - commitHash: "sha", - jobUrl: "https://example.com/jobs/1", - logUrl: "log url", - platform: .macosXcodebuild, - productDependencies: [.init(identity: "identity", name: "name", url: "url", dependencies: [])], - resolvedDependencies: [.init(packageName: "packageName", repositoryURL: "repositoryURL")], - runnerId: "some-runner", - status: .failed, - swiftVersion: .init(5, 2, 0) - ) - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .secondsSince1970 - let body: ByteBuffer = .init(data: try encoder.encode(dto)) - try await app.test( - .POST, - "api/versions/\(versionId)/build-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res async throws in - // validation - let p = try await XCTUnwrapAsync(await Package.find(p.id, on: app.db)) -#if os(Linux) - if p.updatedAt == originalPackageUpdate { - logWarning() - // When this triggers, remove Task.sleep above and the validtion below until // TEMPORARY - END - // and replace with original assert: - // XCTAssertEqual(p.updatedAt, originalPackageUpdate) - } -#endif - let updatedAt = try XCTUnwrap(p.updatedAt) - // Comaring the dates directly fails due to tiny rounding differences with the new swift-foundation types on Linux - // E.g. - // 1724071056.5824609 - // 1724071056.5824614 - // By testing only to accuracy 10e-5 and delaying by 10e-3 we ensure we properly detect if the value was changed. - XCTAssertEqual(updatedAt.timeIntervalSince1970, originalPackageUpdate.timeIntervalSince1970, accuracy: 10e-5) - // TEMPORARY - END - }) - } - - func test_post_docReport() async throws { - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let b = try Build(version: v, platform: .iOS, status: .ok, swiftVersion: .v3) - try await b.save(on: app.db) - let buildId = try b.requireID() - - do { // MUT - initial insert - let dto: API.PostDocReportDTO = .init(error: "too large", - fileCount: 70_000, - linkablePathsCount: 137, - logUrl: "log url", - mbSize: 900, - status: .skipped) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - try await app.test( + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let versionId = try v.requireID() + + // MUT + let data = try fixtureData(for: "large-build-report.json") + XCTAssert(data.count > 16_000, "was: \(data.count) bytes") + let body: ByteBuffer = .init(data: data) + let outOfTheWayPort = 12_345 + try await app.testable(method: .running(port: outOfTheWayPort)).test( .POST, - "api/builds/\(buildId)/doc-report", + "api/versions/\(versionId)/build-report", headers: .bearerApplicationJSON("secr3t"), body: body, - afterResponse: { res in + afterResponse: { res async in // validation XCTAssertEqual(res.status, .noContent) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 1) - let d = try docUploads.first.unwrap() - XCTAssertEqual(d.error, "too large") - XCTAssertEqual(d.fileCount, 70_000) - XCTAssertEqual(d.linkablePathsCount, 137) - XCTAssertEqual(d.logUrl, "log url") - XCTAssertEqual(d.mbSize, 900) - XCTAssertEqual(d.status, .skipped) }) } + } - do { // send report again to same buildId - let dto: API.PostDocReportDTO = .init( - docArchives: [.init(name: "foo", title: "Foo")], - status: .ok + func test_post_buildReport_package_updatedAt() async throws { + // Ensure we don't change packages.updatedAt when receiving a build report. + // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/3290#issuecomment-2293101104 + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let originalPackageUpdate = try XCTUnwrap(p.updatedAt) + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let versionId = try v.requireID() + // Sleep for 1ms to ensure we can detect a difference between update times. + try await Task.sleep(nanoseconds: UInt64(1e6)) + + // MUT + let dto: API.PostBuildReportDTO = .init( + buildCommand: "xcodebuild -scheme Foo", + buildDate: .t0, + buildDuration: 123.4, + buildErrors: .init(numSwift6Errors: 42), + builderVersion: "1.2.3", + buildId: .id0, + commitHash: "sha", + jobUrl: "https://example.com/jobs/1", + logUrl: "log url", + platform: .macosXcodebuild, + productDependencies: [.init(identity: "identity", name: "name", url: "url", dependencies: [])], + resolvedDependencies: [.init(packageName: "packageName", repositoryURL: "repositoryURL")], + runnerId: "some-runner", + status: .failed, + swiftVersion: .init(5, 2, 0) ) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 + let body: ByteBuffer = .init(data: try encoder.encode(dto)) try await app.test( .POST, - "api/builds/\(buildId)/doc-report", + "api/versions/\(versionId)/build-report", headers: .bearerApplicationJSON("secr3t"), body: body, - afterResponse: { res in + afterResponse: { res async throws in // validation - XCTAssertEqual(res.status, .noContent) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 1) - let d = try docUploads.first.unwrap() - XCTAssertEqual(d.status, .ok) - try await d.$build.load(on: app.db) - try await d.build.$version.load(on: app.db) - XCTAssertEqual(d.build.version.docArchives, - [.init(name: "foo", title: "Foo")]) + let p = try await XCTUnwrapAsync(await Package.find(p.id, on: app.db)) +#if os(Linux) + if p.updatedAt == originalPackageUpdate { + logWarning() + // When this triggers, remove Task.sleep above and the validtion below until // TEMPORARY - END + // and replace with original assert: + // XCTAssertEqual(p.updatedAt, originalPackageUpdate) + } +#endif + let updatedAt = try XCTUnwrap(p.updatedAt) + // Comaring the dates directly fails due to tiny rounding differences with the new swift-foundation types on Linux + // E.g. + // 1724071056.5824609 + // 1724071056.5824614 + // By testing only to accuracy 10e-5 and delaying by 10e-3 we ensure we properly detect if the value was changed. + XCTAssertEqual(updatedAt.timeIntervalSince1970, originalPackageUpdate.timeIntervalSince1970, accuracy: 10e-5) + // TEMPORARY - END }) } + } - do { // make sure a .pending report without docArchives does not reset them - let dto: API.PostDocReportDTO = .init(docArchives: nil, status: .pending) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - try await app.test( - .POST, - "api/builds/\(buildId)/doc-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .noContent) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 1) - let d = try docUploads.first.unwrap() - XCTAssertEqual(d.status, .pending) - try await d.$build.load(on: app.db) - try await d.build.$version.load(on: app.db) - XCTAssertEqual(d.build.version.docArchives, - [.init(name: "foo", title: "Foo")]) - }) + func test_post_docReport() async throws { + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let b = try Build(version: v, platform: .iOS, status: .ok, swiftVersion: .v3) + try await b.save(on: app.db) + let buildId = try b.requireID() + + do { // MUT - initial insert + let dto: API.PostDocReportDTO = .init(error: "too large", + fileCount: 70_000, + linkablePathsCount: 137, + logUrl: "log url", + mbSize: 900, + status: .skipped) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/builds/\(buildId)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .noContent) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 1) + let d = try docUploads.first.unwrap() + XCTAssertEqual(d.error, "too large") + XCTAssertEqual(d.fileCount, 70_000) + XCTAssertEqual(d.linkablePathsCount, 137) + XCTAssertEqual(d.logUrl, "log url") + XCTAssertEqual(d.mbSize, 900) + XCTAssertEqual(d.status, .skipped) + }) + } + + do { // send report again to same buildId + let dto: API.PostDocReportDTO = .init( + docArchives: [.init(name: "foo", title: "Foo")], + status: .ok + ) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/builds/\(buildId)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .noContent) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 1) + let d = try docUploads.first.unwrap() + XCTAssertEqual(d.status, .ok) + try await d.$build.load(on: app.db) + try await d.build.$version.load(on: app.db) + XCTAssertEqual(d.build.version.docArchives, + [.init(name: "foo", title: "Foo")]) + }) + } + + do { // make sure a .pending report without docArchives does not reset them + let dto: API.PostDocReportDTO = .init(docArchives: nil, status: .pending) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/builds/\(buildId)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .noContent) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 1) + let d = try docUploads.first.unwrap() + XCTAssertEqual(d.status, .pending) + try await d.$build.load(on: app.db) + try await d.build.$version.load(on: app.db) + XCTAssertEqual(d.build.version.docArchives, + [.init(name: "foo", title: "Foo")]) + }) + } } } func test_post_docReport_override() async throws { // https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2280 // Ensure a subsequent doc report on a different build does not trip over a UK violation - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let b1 = try Build(id: .id0, version: v, platform: .linux, status: .ok, swiftVersion: .v3) - try await b1.save(on: app.db) - let b2 = try Build(id: .id1, version: v, platform: .macosSpm, status: .ok, swiftVersion: .v3) - try await b2.save(on: app.db) - - do { // initial insert - let dto = API.PostDocReportDTO(status: .pending) - try await app.test( - .POST, - "api/builds/\(b1.id!)/doc-report", - headers: .bearerApplicationJSON("secr3t"), - body: .init(data: try JSONEncoder().encode(dto)), - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .noContent) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 1) - let d = try await DocUpload.query(on: app.db).first() - XCTAssertEqual(d?.$build.id, b1.id) - XCTAssertEqual(d?.status, .pending) - }) - } + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let b1 = try Build(id: .id0, version: v, platform: .linux, status: .ok, swiftVersion: .v3) + try await b1.save(on: app.db) + let b2 = try Build(id: .id1, version: v, platform: .macosSpm, status: .ok, swiftVersion: .v3) + try await b2.save(on: app.db) + + do { // initial insert + let dto = API.PostDocReportDTO(status: .pending) + try await app.test( + .POST, + "api/builds/\(b1.id!)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: .init(data: try JSONEncoder().encode(dto)), + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .noContent) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 1) + let d = try await DocUpload.query(on: app.db).first() + XCTAssertEqual(d?.$build.id, b1.id) + XCTAssertEqual(d?.status, .pending) + }) + } - do { // MUT - override - let dto = API.PostDocReportDTO(status: .ok) - try await app.test( - .POST, - "api/builds/\(b2.id!)/doc-report", - headers: .bearerApplicationJSON("secr3t"), - body: .init(data: try JSONEncoder().encode(dto)), - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .noContent) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 1) - let d = try await DocUpload.query(on: app.db).first() - XCTAssertEqual(d?.$build.id, b2.id) - XCTAssertEqual(d?.status, .ok) - }) + do { // MUT - override + let dto = API.PostDocReportDTO(status: .ok) + try await app.test( + .POST, + "api/builds/\(b2.id!)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: .init(data: try JSONEncoder().encode(dto)), + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .noContent) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 1) + let d = try await DocUpload.query(on: app.db).first() + XCTAssertEqual(d?.$build.id, b2.id) + XCTAssertEqual(d?.status, .ok) + }) + } } } func test_post_docReport_non_existing_build() async throws { - // setup - Current.builderToken = { "secr3t" } - let nonExistingBuildId = UUID() + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let nonExistingBuildId = UUID() + + do { // send report to non-existing buildId + let dto: API.PostDocReportDTO = .init(status: .ok) + let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + try await app.test( + .POST, + "api/builds/\(nonExistingBuildId)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .notFound) + let docUploads = try await DocUpload.query(on: app.db).all() + XCTAssertEqual(docUploads.count, 0) + }) + } + } + } - do { // send report to non-existing buildId + func test_post_docReport_unauthenticated() async throws { + try await withDependencies { + $0.environment.builderToken = { "secr3t" } + } operation: { + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, latest: .defaultBranch) + try await v.save(on: app.db) + let b = try Build(version: v, platform: .iOS, status: .ok, swiftVersion: .v3) + try await b.save(on: app.db) + let buildId = try b.requireID() let dto: API.PostDocReportDTO = .init(status: .ok) let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) + let app = self.app! + + // MUT - no auth header try await app.test( .POST, - "api/builds/\(nonExistingBuildId)/doc-report", - headers: .bearerApplicationJSON("secr3t"), + "api/builds/\(buildId)/doc-report", + headers: .applicationJSON, body: body, afterResponse: { res in // validation - XCTAssertEqual(res.status, .notFound) - let docUploads = try await DocUpload.query(on: app.db).all() - XCTAssertEqual(docUploads.count, 0) - }) - } - } - - func test_post_docReport_unauthenticated() async throws { - // setup - Current.builderToken = { "secr3t" } - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, latest: .defaultBranch) - try await v.save(on: app.db) - let b = try Build(version: v, platform: .iOS, status: .ok, swiftVersion: .v3) - try await b.save(on: app.db) - let buildId = try b.requireID() - let dto: API.PostDocReportDTO = .init(status: .ok) - let body: ByteBuffer = .init(data: try JSONEncoder().encode(dto)) - let app = self.app! - - // MUT - no auth header - try await app.test( - .POST, - "api/builds/\(buildId)/doc-report", - headers: .applicationJSON, - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) - } - ) + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) + } + ) - // MUT - wrong token - try await app.test( - .POST, - "api/builds/\(buildId)/doc-report", - headers: .bearerApplicationJSON("wrong"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) - } - ) + // MUT - wrong token + try await app.test( + .POST, + "api/builds/\(buildId)/doc-report", + headers: .bearerApplicationJSON("wrong"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) + } + ) - // MUT - without server token - Current.builderToken = { nil } - try await app.test( - .POST, - "api/builds/\(buildId)/doc-report", - headers: .bearerApplicationJSON("secr3t"), - body: body, - afterResponse: { res in - // validation - XCTAssertEqual(res.status, .unauthorized) - try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) + // MUT - without server token + try await withDependencies { + $0.environment.builderToken = { nil } + } operation: { + try await app.test( + .POST, + "api/builds/\(buildId)/doc-report", + headers: .bearerApplicationJSON("secr3t"), + body: body, + afterResponse: { res in + // validation + XCTAssertEqual(res.status, .unauthorized) + try await XCTAssertEqualAsync(try await DocUpload.query(on: app.db).count(), 0) + } + ) } - ) + } } func test_BadgeRoute_query() async throws { diff --git a/Tests/AppTests/AppTestCase.swift b/Tests/AppTests/AppTestCase.swift index bb8cd650a..487ea143b 100644 --- a/Tests/AppTests/AppTestCase.swift +++ b/Tests/AppTests/AppTestCase.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import NIOConcurrencyHelpers import PostgresNIO import SQLKit @@ -35,8 +36,16 @@ class AppTestCase: XCTestCase { } func setup(_ environment: Environment) async throws -> Application { - try await Self.setupDb(environment) - return try await Self.setupApp(environment) + try await withDependencies { + // Setting builderToken here when it's also set in all tests may seem redundant but it's + // what allows test_post_buildReport_large to work. + // See https://github.com/pointfreeco/swift-dependencies/discussions/300#discussioncomment-11252906 + // for details. + $0.environment.builderToken = { "secr3t" } + } operation: { + try await Self.setupDb(environment) + return try await Self.setupApp(environment) + } } override func tearDown() async throws { diff --git a/Tests/AppTests/BuildTests.swift b/Tests/AppTests/BuildTests.swift index 12c8954c9..5d7dceb2c 100644 --- a/Tests/AppTests/BuildTests.swift +++ b/Tests/AppTests/BuildTests.swift @@ -14,6 +14,7 @@ @testable import App +import Dependencies import Fluent import PostgresNIO import SQLKit @@ -128,118 +129,126 @@ class BuildTests: AppTestCase { } func test_trigger() async throws { - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - // setup - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, reference: .branch("main")) - try await v.save(on: app.db) - let buildId = UUID() - let versionID = try XCTUnwrap(v.id) - - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - var called = false - let client = MockClient { req, res in - called = true - res.status = .created - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - // validate request data - XCTAssertEqual(try? req.query.decode(Gitlab.Builder.PostDTO.self), - Gitlab.Builder.PostDTO( - token: "pipeline token", - ref: "main", - variables: [ - "API_BASEURL": "http://example.com/api", - "AWS_DOCS_BUCKET": "awsDocsBucket", - "BUILD_ID": buildId.uuidString, - "BUILD_PLATFORM": "macos-xcodebuild", - "BUILDER_TOKEN": "builder token", - "CLONE_URL": "1", - "REFERENCE": "main", - "SWIFT_VERSION": "5.2", - "TIMEOUT": "10m", - "VERSION_ID": versionID.uuidString, - ])) - } - - // MUT - let res = try await Build.trigger(database: app.db, - client: client, - buildId: buildId, - isDocBuild: false, - platform: .macosXcodebuild, - swiftVersion: .init(5, 2, 4), - versionId: versionID) + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, reference: .branch("main")) + try await v.save(on: app.db) + let buildId = UUID() + let versionID = try XCTUnwrap(v.id) + + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + var called = false + let client = MockClient { req, res in + called = true + res.status = .created + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + // validate request data + XCTAssertEqual(try? req.query.decode(Gitlab.Builder.PostDTO.self), + Gitlab.Builder.PostDTO( + token: "pipeline token", + ref: "main", + variables: [ + "API_BASEURL": "http://example.com/api", + "AWS_DOCS_BUCKET": "awsDocsBucket", + "BUILD_ID": buildId.uuidString, + "BUILD_PLATFORM": "macos-xcodebuild", + "BUILDER_TOKEN": "builder token", + "CLONE_URL": "1", + "REFERENCE": "main", + "SWIFT_VERSION": "5.2", + "TIMEOUT": "10m", + "VERSION_ID": versionID.uuidString, + ])) + } - // validate - XCTAssertTrue(called) - XCTAssertEqual(res.status, .created) + // MUT + let res = try await Build.trigger(database: app.db, + client: client, + buildId: buildId, + isDocBuild: false, + platform: .macosXcodebuild, + swiftVersion: .init(5, 2, 4), + versionId: versionID) + + // validate + XCTAssertTrue(called) + XCTAssertEqual(res.status, .created) + } } func test_trigger_isDocBuild() async throws { - // Same test as test_trigger above, except we trigger with isDocBuild: true - // and expect a 15m TIMEOUT instead of 10m - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - // setup - let p = try await savePackage(on: app.db, "1") - let v = try Version(package: p, reference: .branch("main")) - try await v.save(on: app.db) - let buildId = UUID() - let versionID = try XCTUnwrap(v.id) - - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - var called = false - let client = MockClient { req, res in - called = true - res.status = .created - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - // only test the TIMEOUT value, the rest is already tested in `test_trigger` above - let response = try? req.query.decode(Gitlab.Builder.PostDTO.self) - XCTAssertNotNil(response) - XCTAssertEqual(response?.variables["TIMEOUT"], "15m") - } - - // MUT - let res = try await Build.trigger(database: app.db, - client: client, - buildId: buildId, - isDocBuild: true, - platform: .macosXcodebuild, - swiftVersion: .init(5, 2, 4), - versionId: versionID) + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + // Same test as test_trigger above, except we trigger with isDocBuild: true + // and expect a 15m TIMEOUT instead of 10m + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } + // setup + let p = try await savePackage(on: app.db, "1") + let v = try Version(package: p, reference: .branch("main")) + try await v.save(on: app.db) + let buildId = UUID() + let versionID = try XCTUnwrap(v.id) + + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + var called = false + let client = MockClient { req, res in + called = true + res.status = .created + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + // only test the TIMEOUT value, the rest is already tested in `test_trigger` above + let response = try? req.query.decode(Gitlab.Builder.PostDTO.self) + XCTAssertNotNil(response) + XCTAssertEqual(response?.variables["TIMEOUT"], "15m") + } - // validate - XCTAssertTrue(called) - XCTAssertEqual(res.status, .created) + // MUT + let res = try await Build.trigger(database: app.db, + client: client, + buildId: buildId, + isDocBuild: true, + platform: .macosXcodebuild, + swiftVersion: .init(5, 2, 4), + versionId: versionID) + + // validate + XCTAssertTrue(called) + XCTAssertEqual(res.status, .created) + } } func test_query() async throws { diff --git a/Tests/AppTests/BuildTriggerTests.swift b/Tests/AppTests/BuildTriggerTests.swift index 9d64609df..38e0a33cc 100644 --- a/Tests/AppTests/BuildTriggerTests.swift +++ b/Tests/AppTests/BuildTriggerTests.swift @@ -328,220 +328,233 @@ class BuildTriggerTests: AppTestCase { } func test_triggerBuildsUnchecked() async throws { - // setup - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) - let client = MockClient { req, res in - guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } - queries.withValue { $0.append(query) } - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - } + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + // setup + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } - let versionId = UUID() - do { // save package with partially completed builds - let p = Package(id: UUID(), url: "2") - try await p.save(on: app.db) - let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) - try await v.save(on: app.db) - } - let triggers = [BuildTriggerInfo(versionId: versionId, - buildPairs: [BuildPair(.iOS, .v1)])!] + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) + let client = MockClient { req, res in + guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } + queries.withValue { $0.append(query) } + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + } - // MUT - try await triggerBuildsUnchecked(on: app.db, - client: client, - triggers: triggers) + let versionId = UUID() + do { // save package with partially completed builds + let p = Package(id: UUID(), url: "2") + try await p.save(on: app.db) + let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) + try await v.save(on: app.db) + } + let triggers = [BuildTriggerInfo(versionId: versionId, + buildPairs: [BuildPair(.iOS, .v1)])!] - // validate - // ensure Gitlab requests go out - XCTAssertEqual(queries.count, 1) - XCTAssertEqual(queries.value.map { $0.variables["VERSION_ID"] }, [versionId.uuidString]) - XCTAssertEqual(queries.value.map { $0.variables["BUILD_PLATFORM"] }, ["ios"]) - XCTAssertEqual(queries.value.map { $0.variables["SWIFT_VERSION"] }, ["5.8"]) - - // ensure the Build stubs is created to prevent re-selection - let v = try await Version.find(versionId, on: app.db) - try await v?.$builds.load(on: app.db) - XCTAssertEqual(v?.builds.count, 1) - XCTAssertEqual(v?.builds.map(\.status), [.triggered]) - XCTAssertEqual(v?.builds.map(\.jobUrl), ["http://web_url"]) + // MUT + try await triggerBuildsUnchecked(on: app.db, + client: client, + triggers: triggers) + + // validate + // ensure Gitlab requests go out + XCTAssertEqual(queries.count, 1) + XCTAssertEqual(queries.value.map { $0.variables["VERSION_ID"] }, [versionId.uuidString]) + XCTAssertEqual(queries.value.map { $0.variables["BUILD_PLATFORM"] }, ["ios"]) + XCTAssertEqual(queries.value.map { $0.variables["SWIFT_VERSION"] }, ["5.8"]) + + // ensure the Build stubs is created to prevent re-selection + let v = try await Version.find(versionId, on: app.db) + try await v?.$builds.load(on: app.db) + XCTAssertEqual(v?.builds.count, 1) + XCTAssertEqual(v?.builds.map(\.status), [.triggered]) + XCTAssertEqual(v?.builds.map(\.jobUrl), ["http://web_url"]) + } } func test_triggerBuildsUnchecked_supported() async throws { - // Explicitly test the full range of all currently triggered platforms and swift versions - // setup - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) - let client = MockClient { req, res in - guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } - queries.withValue { $0.append(query) } - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - } + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + // Explicitly test the full range of all currently triggered platforms and swift versions + // setup + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } - let pkgId = UUID() - let versionId = UUID() - do { // save package with partially completed builds - let p = Package(id: pkgId, url: "2") - try await p.save(on: app.db) - let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) - try await v.save(on: app.db) - } - let triggers = try await findMissingBuilds(app.db, packageId: pkgId) + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) + let client = MockClient { req, res in + guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } + queries.withValue { $0.append(query) } + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + } - // MUT - try await triggerBuildsUnchecked(on: app.db, - client: client, - triggers: triggers) + let pkgId = UUID() + let versionId = UUID() + do { // save package with partially completed builds + let p = Package(id: pkgId, url: "2") + try await p.save(on: app.db) + let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) + try await v.save(on: app.db) + } + let triggers = try await findMissingBuilds(app.db, packageId: pkgId) - // validate - // ensure Gitlab requests go out - XCTAssertEqual(queries.count, 27) - XCTAssertEqual(queries.value.map { $0.variables["VERSION_ID"] }, - Array(repeating: versionId.uuidString, count: 27)) - let buildPlatforms = queries.value.compactMap { $0.variables["BUILD_PLATFORM"] } - XCTAssertEqual(Dictionary(grouping: buildPlatforms, by: { $0 }) - .mapValues(\.count), - ["ios": 4, - "macos-spm": 4, - "macos-xcodebuild": 4, - "linux": 4, - "watchos": 4, - "visionos": 3, - "tvos": 4]) - let swiftVersions = queries.value.compactMap { $0.variables["SWIFT_VERSION"] } - XCTAssertEqual(Dictionary(grouping: swiftVersions, by: { $0 }) - .mapValues(\.count), - [SwiftVersion.v1.description(droppingZeroes: .patch): 6, - SwiftVersion.v2.description(droppingZeroes: .patch): 7, - SwiftVersion.v3.description(droppingZeroes: .patch): 7, - SwiftVersion.v4.description(droppingZeroes: .patch): 7]) - - // ensure the Build stubs are created to prevent re-selection - let v = try await Version.find(versionId, on: app.db) - try await v?.$builds.load(on: app.db) - XCTAssertEqual(v?.builds.count, 27) - - // ensure re-selection is empty - let candidates = try await fetchBuildCandidates(app.db) - XCTAssertEqual(candidates, []) + // MUT + try await triggerBuildsUnchecked(on: app.db, + client: client, + triggers: triggers) + + // validate + // ensure Gitlab requests go out + XCTAssertEqual(queries.count, 27) + XCTAssertEqual(queries.value.map { $0.variables["VERSION_ID"] }, + Array(repeating: versionId.uuidString, count: 27)) + let buildPlatforms = queries.value.compactMap { $0.variables["BUILD_PLATFORM"] } + XCTAssertEqual(Dictionary(grouping: buildPlatforms, by: { $0 }) + .mapValues(\.count), + ["ios": 4, + "macos-spm": 4, + "macos-xcodebuild": 4, + "linux": 4, + "watchos": 4, + "visionos": 3, + "tvos": 4]) + let swiftVersions = queries.value.compactMap { $0.variables["SWIFT_VERSION"] } + XCTAssertEqual(Dictionary(grouping: swiftVersions, by: { $0 }) + .mapValues(\.count), + [SwiftVersion.v1.description(droppingZeroes: .patch): 6, + SwiftVersion.v2.description(droppingZeroes: .patch): 7, + SwiftVersion.v3.description(droppingZeroes: .patch): 7, + SwiftVersion.v4.description(droppingZeroes: .patch): 7]) + + // ensure the Build stubs are created to prevent re-selection + let v = try await Version.find(versionId, on: app.db) + try await v?.$builds.load(on: app.db) + XCTAssertEqual(v?.builds.count, 27) + + // ensure re-selection is empty + let candidates = try await fetchBuildCandidates(app.db) + XCTAssertEqual(candidates, []) + } } func test_triggerBuildsUnchecked_build_exists() async throws { - // Tests error handling when a build record already exists and `create` raises a - // uq:builds.version_id+builds.platform+builds.swift_version+v2 - // unique key violation. - // The only way this can currently happen is by running a manual trigger command - // from a container in the dev or prod envs (docker exec ...), like so: - // ./Run trigger-builds -v {version-id} -p macos-spm -s 5.7 - // This is how we routinely manually trigger doc-related builds. - // This test ensures that the build record is updated in this case rather than - // being completely ignored because the command errors out. - // See https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2237 - // setup - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) - let client = MockClient { req, res in - guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } - queries.withValue { $0.append(query) } - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - } + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + // Tests error handling when a build record already exists and `create` raises a + // uq:builds.version_id+builds.platform+builds.swift_version+v2 + // unique key violation. + // The only way this can currently happen is by running a manual trigger command + // from a container in the dev or prod envs (docker exec ...), like so: + // ./Run trigger-builds -v {version-id} -p macos-spm -s 5.7 + // This is how we routinely manually trigger doc-related builds. + // This test ensures that the build record is updated in this case rather than + // being completely ignored because the command errors out. + // See https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/2237 + // setup + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } - let buildId = UUID() - let versionId = UUID() - do { // save package with a build that we re-trigger - let p = Package(id: UUID(), url: "2") - try await p.save(on: app.db) - let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) - try await v.save(on: app.db) - try await Build(id: buildId, - version: v, - platform: .macosSpm, - status: .failed, - swiftVersion: .v3).save(on: app.db) + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + let queries = QueueIsolated<[Gitlab.Builder.PostDTO]>([]) + let client = MockClient { req, res in + guard let query = try? req.query.decode(Gitlab.Builder.PostDTO.self) else { return } + queries.withValue { $0.append(query) } + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + } - } - let triggers = [BuildTriggerInfo(versionId: versionId, - buildPairs: [BuildPair(.macosSpm, .v3)])!] + let buildId = UUID() + let versionId = UUID() + do { // save package with a build that we re-trigger + let p = Package(id: UUID(), url: "2") + try await p.save(on: app.db) + let v = try Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) + try await v.save(on: app.db) + try await Build(id: buildId, + version: v, + platform: .macosSpm, + status: .failed, + swiftVersion: .v3).save(on: app.db) - // MUT - try await triggerBuildsUnchecked(on: app.db, - client: client, - triggers: triggers) + } + let triggers = [BuildTriggerInfo(versionId: versionId, + buildPairs: [BuildPair(.macosSpm, .v3)])!] - // validate - // triggerBuildsUnchecked always creates a new buildId, - // so the triggered id must be different from the existing one - let newBuildId = try XCTUnwrap(queries.value.first?.variables["BUILD_ID"] - .flatMap(UUID.init(uuidString:))) - XCTAssertNotEqual(newBuildId, buildId) - - // ensure existing build record is updated - let v = try await Version.find(versionId, on: app.db) - try await v?.$builds.load(on: app.db) - XCTAssertEqual(v?.builds.count, 1) - XCTAssertEqual(v?.builds.map(\.id), [newBuildId]) - XCTAssertEqual(v?.builds.map(\.status), [.triggered]) - XCTAssertEqual(v?.builds.map(\.jobUrl), ["http://web_url"]) + // MUT + try await triggerBuildsUnchecked(on: app.db, + client: client, + triggers: triggers) + + // validate + // triggerBuildsUnchecked always creates a new buildId, + // so the triggered id must be different from the existing one + let newBuildId = try XCTUnwrap(queries.value.first?.variables["BUILD_ID"] + .flatMap(UUID.init(uuidString:))) + XCTAssertNotEqual(newBuildId, buildId) + + // ensure existing build record is updated + let v = try await Version.find(versionId, on: app.db) + try await v?.$builds.load(on: app.db) + XCTAssertEqual(v?.builds.count, 1) + XCTAssertEqual(v?.builds.map(\.id), [newBuildId]) + XCTAssertEqual(v?.builds.map(\.status), [.triggered]) + XCTAssertEqual(v?.builds.map(\.jobUrl), ["http://web_url"]) + } } func test_triggerBuilds_checked() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { // Ensure we respect the pipeline limit when triggering builds // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.gitlabPipelineLimit = { 300 } @@ -649,10 +662,11 @@ class BuildTriggerTests: AppTestCase { func test_triggerBuilds_multiplePackages() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { // Ensure we respect the pipeline limit when triggering builds for multiple package ids // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.gitlabPipelineLimit = { 300 } @@ -698,10 +712,10 @@ class BuildTriggerTests: AppTestCase { func test_triggerBuilds_trimming() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } } operation: { // Ensure we trim builds as part of triggering // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.gitlabPipelineLimit = { 300 } @@ -733,10 +747,11 @@ class BuildTriggerTests: AppTestCase { func test_triggerBuilds_error() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { // Ensure we trim builds as part of triggering // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.gitlabPipelineLimit = { 300 } @@ -827,81 +842,86 @@ class BuildTriggerTests: AppTestCase { } func test_override_switch() async throws { - // Ensure don't trigger if the override is off - // setup - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - // Use live dependency but replace actual client with a mock so we can - // assert on the details being sent without actually making a request - Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in - try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: cloneURL, - isDocBuild: isDocBuild, - platform: platform, - reference: ref, - swiftVersion: swiftVersion, - versionID: versionID) - } - var triggerCount = 0 - let client = MockClient { _, res in - triggerCount += 1 - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - } - try await withDependencies { - // confirm that the off switch prevents triggers - $0.environment.allowBuildTriggers = { false } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { - let pkgId = UUID() - let versionId = UUID() - let p = Package(id: pkgId, url: "1") - try await p.save(on: app.db) - try await Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) - .save(on: app.db) + // Ensure don't trigger if the override is off + // setup + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } + // Use live dependency but replace actual client with a mock so we can + // assert on the details being sent without actually making a request + Current.triggerBuild = { client, buildId, cloneURL, isDocBuild, platform, ref, swiftVersion, versionID in + try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: cloneURL, + isDocBuild: isDocBuild, + platform: platform, + reference: ref, + swiftVersion: swiftVersion, + versionID: versionID) + } + var triggerCount = 0 + let client = MockClient { _, res in + triggerCount += 1 + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + } - // MUT - try await triggerBuilds(on: app.db, - client: client, - mode: .packageId(pkgId, force: false)) + try await withDependencies { + // confirm that the off switch prevents triggers + $0.environment.allowBuildTriggers = { false } + } operation: { + let pkgId = UUID() + let versionId = UUID() + let p = Package(id: pkgId, url: "1") + try await p.save(on: app.db) + try await Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) + .save(on: app.db) - // validate - XCTAssertEqual(triggerCount, 0) - } + // MUT + try await triggerBuilds(on: app.db, + client: client, + mode: .packageId(pkgId, force: false)) - triggerCount = 0 + // validate + XCTAssertEqual(triggerCount, 0) + } - try await withDependencies { - // flipping the switch to on should allow triggers to proceed - $0.environment.allowBuildTriggers = { true } - } operation: { - let pkgId = UUID() - let versionId = UUID() - let p = Package(id: pkgId, url: "2") - try await p.save(on: app.db) - try await Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) - .save(on: app.db) + triggerCount = 0 - // MUT - try await triggerBuilds(on: app.db, - client: client, - mode: .packageId(pkgId, force: false)) + try await withDependencies { + // flipping the switch to on should allow triggers to proceed + $0.environment.allowBuildTriggers = { true } + } operation: { + let pkgId = UUID() + let versionId = UUID() + let p = Package(id: pkgId, url: "2") + try await p.save(on: app.db) + try await Version(id: versionId, package: p, latest: .defaultBranch, reference: .branch("main")) + .save(on: app.db) - // validate - XCTAssertEqual(triggerCount, 27) + // MUT + try await triggerBuilds(on: app.db, + client: client, + mode: .packageId(pkgId, force: false)) + + // validate + XCTAssertEqual(triggerCount, 27) + } } } func test_downscaling() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { // Test build trigger downscaling behaviour // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.buildTriggerDownscaling = { 0.05 } // 5% downscaling rate @@ -970,10 +990,11 @@ class BuildTriggerTests: AppTestCase { func test_downscaling_allow_list_override() async throws { try await withDependencies { $0.environment.allowBuildTriggers = { true } + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } } operation: { // Test build trigger downscaling behaviour for allow-listed packages // setup - Current.builderToken = { "builder token" } Current.gitlabPipelineToken = { "pipeline token" } Current.siteURL = { "http://example.com" } Current.buildTriggerDownscaling = { 0.05 } // 5% downscaling rate diff --git a/Tests/AppTests/GitlabBuilderTests.swift b/Tests/AppTests/GitlabBuilderTests.swift index 8136ed603..4611107b2 100644 --- a/Tests/AppTests/GitlabBuilderTests.swift +++ b/Tests/AppTests/GitlabBuilderTests.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 GitlabBuilderTests: AppTestCase { @@ -31,7 +33,7 @@ class GitlabBuilderTests: AppTestCase { // Ensure the POST variables are encoded correctly // setup let app = try await setup(.testing) - + try await App.run { let req = Request(application: app, on: app.eventLoopGroup.next()) let dto = Gitlab.Builder.PostDTO(token: "token", @@ -52,78 +54,86 @@ class GitlabBuilderTests: AppTestCase { } func test_triggerBuild() async throws { - Current.awsDocsBucket = { "docs-bucket" } - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } - let buildId = UUID() - let versionID = UUID() - - var called = false - let client = MockClient { req, res in - called = true - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - // validate - XCTAssertEqual(try? req.query.decode(Gitlab.Builder.PostDTO.self), - Gitlab.Builder.PostDTO( - token: "pipeline token", - ref: "main", - variables: [ - "API_BASEURL": "http://example.com/api", - "AWS_DOCS_BUCKET": "docs-bucket", - "BUILD_ID": buildId.uuidString, - "BUILD_PLATFORM": "macos-spm", - "BUILDER_TOKEN": "builder token", - "CLONE_URL": "https://github.com/daveverwer/LeftPad.git", - "REFERENCE": "1.2.3", - "SWIFT_VERSION": "5.2", - "TIMEOUT": "10m", - "VERSION_ID": versionID.uuidString, - ])) - } + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + Current.awsDocsBucket = { "docs-bucket" } + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } + let buildId = UUID() + let versionID = UUID() + + var called = false + let client = MockClient { req, res in + called = true + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + // validate + XCTAssertEqual(try? req.query.decode(Gitlab.Builder.PostDTO.self), + Gitlab.Builder.PostDTO( + token: "pipeline token", + ref: "main", + variables: [ + "API_BASEURL": "http://example.com/api", + "AWS_DOCS_BUCKET": "docs-bucket", + "BUILD_ID": buildId.uuidString, + "BUILD_PLATFORM": "macos-spm", + "BUILDER_TOKEN": "builder token", + "CLONE_URL": "https://github.com/daveverwer/LeftPad.git", + "REFERENCE": "1.2.3", + "SWIFT_VERSION": "5.2", + "TIMEOUT": "10m", + "VERSION_ID": versionID.uuidString, + ])) + } - // MUT - _ = try await Gitlab.Builder.triggerBuild(client: client, - buildId: buildId, - cloneURL: "https://github.com/daveverwer/LeftPad.git", - isDocBuild: false, - platform: .macosSpm, - reference: .tag(.init(1, 2, 3)), - swiftVersion: .init(5, 2, 4), - versionID: versionID) - XCTAssertTrue(called) + // MUT + _ = try await Gitlab.Builder.triggerBuild(client: client, + buildId: buildId, + cloneURL: "https://github.com/daveverwer/LeftPad.git", + isDocBuild: false, + platform: .macosSpm, + reference: .tag(.init(1, 2, 3)), + swiftVersion: .init(5, 2, 4), + versionID: versionID) + XCTAssertTrue(called) + } } func test_issue_588() async throws { - Current.awsDocsBucket = { "docs-bucket" } - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - Current.siteURL = { "http://example.com" } + try await withDependencies { + $0.environment.builderToken = { "builder token" } + $0.environment.buildTimeout = { 10 } + } operation: { + Current.awsDocsBucket = { "docs-bucket" } + Current.gitlabPipelineToken = { "pipeline token" } + Current.siteURL = { "http://example.com" } + + var called = false + let client = MockClient { req, res in + called = true + try? res.content.encode( + Gitlab.Builder.Response.init(webUrl: "http://web_url") + ) + // validate + let swiftVersion = (try? req.query.decode(Gitlab.Builder.PostDTO.self)) + .flatMap { $0.variables["SWIFT_VERSION"] } + XCTAssertEqual(swiftVersion, "6.0") + } - var called = false - let client = MockClient { req, res in - called = true - try? res.content.encode( - Gitlab.Builder.Response.init(webUrl: "http://web_url") - ) - // validate - let swiftVersion = (try? req.query.decode(Gitlab.Builder.PostDTO.self)) - .flatMap { $0.variables["SWIFT_VERSION"] } - XCTAssertEqual(swiftVersion, "6.0") + // MUT + _ = try await Gitlab.Builder.triggerBuild(client: client, + buildId: .id0, + cloneURL: "https://github.com/daveverwer/LeftPad.git", + isDocBuild: false, + platform: .macosSpm, + reference: .tag(.init(1, 2, 3)), + swiftVersion: .v6_0, + versionID: .id1) + XCTAssertTrue(called) } - - // MUT - _ = try await Gitlab.Builder.triggerBuild(client: client, - buildId: .id0, - cloneURL: "https://github.com/daveverwer/LeftPad.git", - isDocBuild: false, - platform: .macosSpm, - reference: .tag(.init(1, 2, 3)), - swiftVersion: .v6_0, - versionID: .id1) - XCTAssertTrue(called) } func test_getStatusCount() async throws { @@ -166,41 +176,44 @@ class LiveGitlabBuilderTests: AppTestCase { "This is a live trigger test for end-to-end testing of pre-release builder versions" ) - // set build branch to trigger on - Gitlab.Builder.branch = "main" + try await withDependencies { + $0.environment.builderToken = { + // Set this to a valid value if you want to report build results back to the server + ProcessInfo.processInfo.environment["LIVE_BUILDER_TOKEN"] + } + } operation: { + // set build branch to trigger on + Gitlab.Builder.branch = "main" + + // make sure environment variables are configured for live access + Current.awsDocsBucket = { "spi-dev-docs" } + Current.gitlabPipelineToken = { + // This Gitlab token is required in order to trigger the pipeline + ProcessInfo.processInfo.environment["LIVE_GITLAB_PIPELINE_TOKEN"] + } + Current.siteURL = { "https://staging.swiftpackageindex.com" } + + let buildId = UUID() - // make sure environment variables are configured for live access - Current.awsDocsBucket = { "spi-dev-docs" } - Current.builderToken = { - // Set this to a valid value if you want to report build results back to the server - ProcessInfo.processInfo.environment["LIVE_BUILDER_TOKEN"] - } - Current.gitlabPipelineToken = { - // This Gitlab token is required in order to trigger the pipeline - ProcessInfo.processInfo.environment["LIVE_GITLAB_PIPELINE_TOKEN"] + // use a valid uuid from a live db if reporting back should succeed + // SemanticVersion 0.3.2 on staging + let versionID = UUID(uuidString: "93d8c545-15c4-43c2-946f-1b625e2596f9")! + + // MUT + let res = try await Gitlab.Builder.triggerBuild( + client: app.client, + buildId: buildId, + cloneURL: "https://github.com/SwiftPackageIndex/SemanticVersion.git", + isDocBuild: false, + platform: .macosSpm, + reference: .tag(.init(0, 3, 2)), + swiftVersion: .v4, + versionID: versionID) + + print("status: \(res.status)") + print("buildId: \(buildId)") + print("webUrl: \(res.webUrl)") } - Current.siteURL = { "https://staging.swiftpackageindex.com" } - - let buildId = UUID() - - // use a valid uuid from a live db if reporting back should succeed - // SemanticVersion 0.3.2 on staging - let versionID = UUID(uuidString: "93d8c545-15c4-43c2-946f-1b625e2596f9")! - - // MUT - let res = try await Gitlab.Builder.triggerBuild( - client: app.client, - buildId: buildId, - cloneURL: "https://github.com/SwiftPackageIndex/SemanticVersion.git", - isDocBuild: false, - platform: .macosSpm, - reference: .tag(.init(0, 3, 2)), - swiftVersion: .v4, - versionID: versionID) - - print("status: \(res.status)") - print("buildId: \(buildId)") - print("webUrl: \(res.webUrl)") } } diff --git a/Tests/AppTests/MetricsTests.swift b/Tests/AppTests/MetricsTests.swift index d72c5df71..dddd889ab 100644 --- a/Tests/AppTests/MetricsTests.swift +++ b/Tests/AppTests/MetricsTests.swift @@ -22,31 +22,34 @@ import XCTest class MetricsTests: AppTestCase { func test_basic() async throws { - // setup - trigger build to increment counter - Current.builderToken = { "builder token" } - Current.gitlabPipelineToken = { "pipeline token" } - let versionId = UUID() - do { // save minimal package + version - let p = Package(id: UUID(), url: "1") - try await p.save(on: app.db) - try await Version(id: versionId, package: p, reference: .branch("main")).save(on: app.db) + try await withDependencies { + $0.environment.builderToken = { "builder token" } + } operation: { + // setup - trigger build to increment counter + Current.gitlabPipelineToken = { "pipeline token" } + let versionId = UUID() + do { // save minimal package + version + let p = Package(id: UUID(), url: "1") + try await p.save(on: app.db) + try await Version(id: versionId, package: p, reference: .branch("main")).save(on: app.db) + } + try await triggerBuildsUnchecked(on: app.db, + client: app.client, + triggers: [ + .init(versionId: versionId, + buildPairs: [.init(.macosSpm, .v3)])! + ]) + + // MUT + try await app.test(.GET, "metrics", afterResponse: { res async in + // validation + XCTAssertEqual(res.status, .ok) + let content = res.body.asString() + XCTAssertTrue(content.contains( + #"spi_build_trigger_count{swiftVersion="\#(SwiftVersion.v3)", platform="macos-spm"}"# + ), "was:\n\(content)") + }) } - try await triggerBuildsUnchecked(on: app.db, - client: app.client, - triggers: [ - .init(versionId: versionId, - buildPairs: [.init(.macosSpm, .v3)])! - ]) - - // MUT - try await app.test(.GET, "metrics", afterResponse: { res async in - // validation - XCTAssertEqual(res.status, .ok) - let content = res.body.asString() - XCTAssertTrue(content.contains( - #"spi_build_trigger_count{swiftVersion="\#(SwiftVersion.v3)", platform="macos-spm"}"# - ), "was:\n\(content)") - }) } func test_versions_added() async throws { diff --git a/Tests/AppTests/Mocks/AppEnvironment+mock.swift b/Tests/AppTests/Mocks/AppEnvironment+mock.swift index fdc88cfc6..c6b0f2787 100644 --- a/Tests/AppTests/Mocks/AppEnvironment+mock.swift +++ b/Tests/AppTests/Mocks/AppEnvironment+mock.swift @@ -28,8 +28,6 @@ extension AppEnvironment { awsDocsBucket: { "awsDocsBucket" }, awsReadmeBucket: { "awsReadmeBucket" }, awsSecretAccessKey: { nil }, - buildTimeout: { 10 }, - builderToken: { nil }, buildTriggerAllowList: { [] }, buildTriggerDownscaling: { 1.0 }, buildTriggerLatestSwiftVersionDownscaling: { 1.0 },