diff --git a/Sources/ValidatorCore/AppError.swift b/Sources/ValidatorCore/AppError.swift index 415fe87..16fd2f9 100644 --- a/Sources/ValidatorCore/AppError.swift +++ b/Sources/ValidatorCore/AppError.swift @@ -22,6 +22,7 @@ enum AppError: Error { case invalidPackage(url: PackageURL) case invalidDenyListUrl(string: String) case ioError(String) + case manifestNotFound(owner: String, name: String) case noData(URL) case rateLimited(until: Date) case repositoryNotFound(owner: String, name: String) diff --git a/Sources/ValidatorCore/Commands/CheckDependencies2.swift b/Sources/ValidatorCore/Commands/CheckDependencies2.swift index 518e10f..77078d2 100644 --- a/Sources/ValidatorCore/Commands/CheckDependencies2.swift +++ b/Sources/ValidatorCore/Commands/CheckDependencies2.swift @@ -62,6 +62,7 @@ public struct CheckDependencies2: AsyncParsableCommand { if idx % 10 == 0 { print("Progress:", idx, "/", missing.count) } + // resolve redirects print("Processing:", dep.packageURL, "...") guard let resolved = await Current.resolvePackageRedirectsAsync(dep.packageURL).url else { @@ -69,13 +70,25 @@ public struct CheckDependencies2: AsyncParsableCommand { print(" ... ⛔ redirect resolution returned nil") continue } + if resolved.canonicalPackageURL.canonicalPath != dep.canonicalPath { print(" ... redirected to:", resolved) } + if allPackages.contains(resolved.canonicalPackageURL) { print(" ... ⛔ already indexed") continue } + + do { // run package dump to validate + let repo = try await Current.fetchRepositoryAsync(client, resolved) + let manifest = try await Package.getManifestURL(client: client, repository: repo) + _ = try Current.decodeManifest(manifest) + } catch { + print(" ... ⛔ \(error)") + continue + } + if newPackages.insert(resolved.appendingGitExtension().canonicalPackageURL).inserted { print("✅ ADD (\(newPackages.count)):", resolved.appendingGitExtension()) } diff --git a/Sources/ValidatorCore/Environment.swift b/Sources/ValidatorCore/Environment.swift index 75e615d..e560fbe 100644 --- a/Sources/ValidatorCore/Environment.swift +++ b/Sources/ValidatorCore/Environment.swift @@ -54,9 +54,10 @@ extension Environment { fetch: { client, _ in client.eventLoopGroup.next().makeFailedFuture(AppError.runtimeError("unimplemented")) }, fetchDependencies: { _ in [] }, fetchRepository: { client, _, _ in - client.eventLoopGroup.next().makeSucceededFuture( - Github.Repository(default_branch: "main", fork: false)) }, - fetchRepositoryAsync: { _, _ in .init(default_branch: "main", fork: false) }, + client.eventLoopGroup.next() + .makeSucceededFuture( Github.Repository(defaultBranch: "main", owner: "foo", name: "bar") ) + }, + fetchRepositoryAsync: { _, _ in .init(defaultBranch: "main", owner: "foo", name: "bar") }, githubToken: { nil }, resolvePackageRedirects: { eventLoop, url in eventLoop.makeSucceededFuture(.initial(url)) diff --git a/Sources/ValidatorCore/Github.swift b/Sources/ValidatorCore/Github.swift index 7730164..302d175 100644 --- a/Sources/ValidatorCore/Github.swift +++ b/Sources/ValidatorCore/Github.swift @@ -27,8 +27,30 @@ import CDispatch // for NSEC_PER_SEC https://github.com/apple/swift-corelibs-lib enum Github { struct Repository: Codable, Equatable { - let default_branch: String - let fork: Bool + var defaultBranch: String + var fork: Bool + var name: String + var owner: Owner + + struct Owner: Codable, Equatable { + var login: String + } + + enum CodingKeys: String, CodingKey { + case defaultBranch = "default_branch" + case fork + case name + case owner + } + + init(defaultBranch: String, fork: Bool = false, owner: String, name: String) { + self.defaultBranch = defaultBranch + self.fork = fork + self.name = name + self.owner = .init(login: owner) + } + + var path: String { "\(owner.login)/\(name)" } } static func packageList() throws -> [PackageURL] { @@ -182,6 +204,28 @@ extension Github { } + static func listRepositoryFilePaths(client: HTTPClient, repository: Repository) async throws -> [String] { + let apiURL = URL( string: "https://api.github.com/repos/\(repository.path)/git/trees/\(repository.defaultBranch)" )! + struct Response: Decodable { + var tree: [File] + + struct File: Decodable { + var type: FileType + var path: String + + enum FileType: String, Decodable { + case blob + case tree + } + } + } + return try await fetch(Response.self, client: client, url: apiURL) + .tree + .filter{ $0.type == .blob } + .map(\.path) + } + + static func fetch(_ type: T.Type, client: HTTPClient, url: URL) async throws -> T { let body = try await Current.fetch(client, url).get() do { diff --git a/Sources/ValidatorCore/Package.swift b/Sources/ValidatorCore/Package.swift index d20c956..2e12199 100644 --- a/Sources/ValidatorCore/Package.swift +++ b/Sources/ValidatorCore/Package.swift @@ -118,13 +118,26 @@ extension Package { enum Manifest {} typealias ManifestURL = Tagged + @available(*, deprecated) static func getManifestURL(client: HTTPClient, packageURL: PackageURL) -> EventLoopFuture { Current.fetchRepository(client, packageURL.owner, packageURL.repository) - .map(\.default_branch) + .map(\.defaultBranch) .map { defaultBranch in URL(string: "https://raw.githubusercontent.com/\(packageURL.owner)/\(packageURL.repository)/\(defaultBranch)/Package.swift")! } .map(ManifestURL.init(rawValue:)) } + static func getManifestURL(client: HTTPClient, repository: Github.Repository) async throws -> ManifestURL { + let manifestFiles = try await Github.listRepositoryFilePaths(client: client, repository: repository) + .filter { $0.hasPrefix("Package") } + .filter { $0.hasSuffix(".swift") } + .sorted() + guard let manifestFile = manifestFiles.last else { + throw AppError.manifestNotFound(owner: repository.owner.login, name: repository.name) + } + let url = URL(string: "https://raw.githubusercontent.com/\(repository.path)/\(repository.defaultBranch)/\(manifestFile)")! + return .init(rawValue: url) + } + } diff --git a/Tests/ValidatorTests/CheckDependencies2Tests.swift b/Tests/ValidatorTests/CheckDependencies2Tests.swift index 4753541..8251904 100644 --- a/Tests/ValidatorTests/CheckDependencies2Tests.swift +++ b/Tests/ValidatorTests/CheckDependencies2Tests.swift @@ -17,6 +17,7 @@ import XCTest @testable import ValidatorCore import CanonicalPackageURL +import NIO final class CheckDependencies2Tests: XCTestCase { @@ -42,7 +43,8 @@ final class CheckDependencies2Tests: XCTestCase { .init(.p2, dependencies: [.p3]), ]} var saved: [PackageURL]? = nil - Current.fileManager.createFile = { _, data, _ in + Current.fileManager.createFile = { path, data, _ in + guard path.hasSuffix("package.json") else { return false } guard let data = data else { XCTFail("data must not be nil") return false @@ -54,13 +56,39 @@ final class CheckDependencies2Tests: XCTestCase { saved = list return true } + Current.fetchRepositoryAsync = { _, url in + if url == PackageURL.p3 { + return .init(defaultBranch: "main", owner: "org", name: "3") + } else { + throw Error.unexpectedCall + } + } + Current.fetch = { client, url in + // getManifestURL -> Github.listRepositoryFilePaths -> Github.fetch + guard url.absoluteString == "https://api.github.com/repos/org/3/git/trees/main" else { + return client.eventLoopGroup.next().makeFailedFuture(Error.unexpectedCall) + } + return client.eventLoopGroup.next().makeSucceededFuture( + ByteBuffer(data: .listRepositoryFilePaths(for: "org/3")) + ) + } + var decodeCalled = false + Current.decodeManifest = { url in + guard url.absoluteString == "https://raw.githubusercontent.com/org/3/main/Package.swift" else { + throw Error.unexpectedCall + } + decodeCalled = true + return .init(name: "3", products: [], dependencies: []) + } check.packageUrls = [.p1, .p2] + check.output = "package.json" // MUT try await check.run() // validate XCTAssertEqual(saved, [.p1, .p2, .p3]) + XCTAssertTrue(decodeCalled) } func test_run_list_newer() async throws { @@ -86,7 +114,30 @@ final class CheckDependencies2Tests: XCTestCase { saved = list return true } + Current.fetchRepositoryAsync = { _, url in + if url == PackageURL.p3 { + return .init(defaultBranch: "main", owner: "org", name: "3") + } else { + throw Error.unexpectedCall + } + } + Current.fetch = { client, url in + // getManifestURL -> Github.listRepositoryFilePaths -> Github.fetch + guard url.absoluteString == "https://api.github.com/repos/org/3/git/trees/main" else { + return client.eventLoopGroup.next().makeFailedFuture(Error.unexpectedCall) + } + return client.eventLoopGroup.next().makeSucceededFuture( + ByteBuffer(data: .listRepositoryFilePaths(for: "org/3")) + ) + } + Current.decodeManifest = { url in + guard url.absoluteString == "https://raw.githubusercontent.com/org/3/main/Package.swift" else { + throw Error.unexpectedCall + } + return .init(name: "3", products: [], dependencies: []) + } check.packageUrls = [.p1, .p2, .p4] + check.output = "package.json" // MUT try await check.run() @@ -95,9 +146,61 @@ final class CheckDependencies2Tests: XCTestCase { XCTAssertEqual(saved, [.p1, .p2, .p3, .p4]) } + func test_run_manifest_validation() async throws { + // Ensure validation via package dump is performed on new packages. + // setup + Current = .mock + Current.fetchDependencies = { _ in [ + .init(.p1, dependencies: []), + .init(.p2, dependencies: [.p3]), + ]} + var saved: [PackageURL]? = nil + Current.fileManager.createFile = { path, data, _ in + guard path.hasSuffix("package.json"), + let data = data, + let list = try? JSONDecoder().decode([PackageURL].self, from: data) else { return false } + saved = list + return true + } + Current.fetchRepositoryAsync = { _, url in + if url == PackageURL.p3 { + return .init(defaultBranch: "main", owner: "org", name: "3") + } else { + throw Error.unexpectedCall + } + } + Current.fetch = { client, url in + // getManifestURL -> Github.listRepositoryFilePaths -> Github.fetch + guard url.absoluteString == "https://api.github.com/repos/org/3/git/trees/main" else { + return client.eventLoopGroup.next().makeFailedFuture(Error.unexpectedCall) + } + return client.eventLoopGroup.next().makeSucceededFuture( + ByteBuffer(data: .listRepositoryFilePaths(for: "org/3")) + ) + } + Current.decodeManifest = { url in + guard url.absoluteString == "https://raw.githubusercontent.com/org/3/main/Package.swift" else { + throw Error.unexpectedCall + } + // simulate a bad manifest + throw AppError.dumpPackageError("simulated decoding error") + } + + check.packageUrls = [.p1, .p2] + check.output = "package.json" + + // MUT + try await check.run() + + // validate + XCTAssertEqual(saved, [.p1, .p2]) + } + } +private enum Error: Swift.Error { case unexpectedCall } + private extension PackageURL { static let p1 = PackageURL(argument: "https://github.com/org/1.git")! static let p2 = PackageURL(argument: "https://github.com/org/2.git")! @@ -116,3 +219,26 @@ private extension SwiftPackageIndexAPI.PackageRecord { self.init(id: .init(), url: url, resolvedDependencies: dependencies) } } + +private extension Data { + static func listRepositoryFilePaths(for path: String) -> Self { + .init(""" + { + "url" : "https://api.github.com/repos/\(path)/git/trees/ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "tree" : [ + { + "size" : 1122, + "type" : "blob", + "path" : "Package.swift", + "url" : "https://api.github.com/repos/\(path)/git/blobs/bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2", + "mode" : "100644", + "sha" : "bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2" + } + ], + "sha" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "truncated" : false + } + """.utf8 + ) + } +} diff --git a/Tests/ValidatorTests/Fixtures/github-files-response-SwiftyJSON.json b/Tests/ValidatorTests/Fixtures/github-files-response-SwiftyJSON.json new file mode 100644 index 0000000..90d9070 --- /dev/null +++ b/Tests/ValidatorTests/Fixtures/github-files-response-SwiftyJSON.json @@ -0,0 +1,101 @@ +{ + "tree" : [ + { + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/024f01f8183865bd26e15a7828b0d3a33028d5c5", + "path" : ".gitignore", + "sha" : "024f01f8183865bd26e15a7828b0d3a33028d5c5", + "size" : 290, + "type" : "blob", + "mode" : "100644" + }, + { + "path" : ".swift-version", + "type" : "blob", + "mode" : "100644", + "sha" : "6b244dcd6960b101b0ab4d9e5162d39632dec80c", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/6b244dcd6960b101b0ab4d9e5162d39632dec80c", + "size" : 6 + }, + { + "type" : "blob", + "path" : ".travis.yml", + "mode" : "100644", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/70e2b4cdfc5b7e26d7f55d97833f96aeb7d5c481", + "size" : 1697, + "sha" : "70e2b4cdfc5b7e26d7f55d97833f96aeb7d5c481" + }, + { + "sha" : "a7af196875ec68b64f30963189392f71ab9ecf79", + "type" : "blob", + "mode" : "100644", + "size" : 1075, + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/a7af196875ec68b64f30963189392f71ab9ecf79", + "path" : "LICENSE" + }, + { + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/e0570d6a1e3f5b1b4bd213315d8f0966ca8f675f", + "path" : "Package.pins", + "mode" : "100644", + "type" : "blob", + "size" : 53, + "sha" : "e0570d6a1e3f5b1b4bd213315d8f0966ca8f675f" + }, + { + "path" : "Package.swift", + "mode" : "100644", + "size" : 825, + "type" : "blob", + "sha" : "63080bd3c929a37b0c8d0796b5d08e51794c18c2", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/63080bd3c929a37b0c8d0796b5d08e51794c18c2" + }, + { + "type" : "blob", + "path" : "Package@swift-4.2.swift", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/7bd26320357525801b4607dcf78bfeab4ece8508", + "sha" : "7bd26320357525801b4607dcf78bfeab4ece8508", + "size" : 1517, + "mode" : "100644" + }, + { + "path" : "Package@swift-4.swift", + "type" : "blob", + "sha" : "17d053009476faba4f7832d8fb0f408d6cf94ffa", + "mode" : "100644", + "size" : 1517, + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/17d053009476faba4f7832d8fb0f408d6cf94ffa" + }, + { + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/b263042f912ec32a770d2ba254bb6a8c2a49122a", + "path" : "Package@swift-5.swift", + "size" : 1517, + "type" : "blob", + "mode" : "100644", + "sha" : "b263042f912ec32a770d2ba254bb6a8c2a49122a" + }, + { + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/blobs\/4003b2930a1f8488067c543810d02e6fa2c7c949", + "type" : "blob", + "size" : 8861, + "mode" : "100644", + "path" : "README.md", + "sha" : "4003b2930a1f8488067c543810d02e6fa2c7c949" + }, + { + "mode" : "040000", + "sha" : "4134f985c3bf0d4fc7029a127fa5082f2a7755a3", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/trees\/4134f985c3bf0d4fc7029a127fa5082f2a7755a3", + "type" : "tree", + "path" : "Sources" + }, + { + "type" : "tree", + "sha" : "02fff52637912ccbcbbbbf2aa469de994c8b3441", + "path" : "Tests", + "mode" : "040000", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/trees\/02fff52637912ccbcbbbbf2aa469de994c8b3441" + } + ], + "sha" : "f9b4754017cfad4701f0838b2e507b442eaca70a", + "url" : "https:\/\/api.github.com\/repos\/IBM-Swift\/SwiftyJSON\/git\/trees\/f9b4754017cfad4701f0838b2e507b442eaca70a", + "truncated" : false +} diff --git a/Tests/ValidatorTests/Fixtures/github-files-response.json b/Tests/ValidatorTests/Fixtures/github-files-response.json new file mode 100644 index 0000000..fbeb362 --- /dev/null +++ b/Tests/ValidatorTests/Fixtures/github-files-response.json @@ -0,0 +1,76 @@ +{ + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/trees\/ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "tree" : [ + { + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/trees\/985905a81c0fbbea830070a058a4368b64c983d5", + "path" : ".github", + "sha" : "985905a81c0fbbea830070a058a4368b64c983d5", + "mode" : "040000", + "type" : "tree" + }, + { + "type" : "blob", + "mode" : "100644", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/5812d0fd3ed81435c93f9cecf6ac5d6878949140", + "sha" : "5812d0fd3ed81435c93f9cecf6ac5d6878949140", + "size" : 50, + "path" : ".gitignore" + }, + { + "mode" : "100644", + "size" : 100, + "path" : ".spi.yml", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/03cc90728b8a45dd7adb42f3c214ccd4b8be4eb4", + "type" : "blob", + "sha" : "03cc90728b8a45dd7adb42f3c214ccd4b8be4eb4" + }, + { + "type" : "blob", + "size" : 160, + "sha" : "bb3db5b735bdaed95d21048ddbd6a002697af90c", + "path" : "FUNDING.yml", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/bb3db5b735bdaed95d21048ddbd6a002697af90c", + "mode" : "100644" + }, + { + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/dd5b3a58aa1849f452abc9b5cd1638dc71a5e482", + "path" : "LICENSE", + "size" : 10141, + "type" : "blob", + "mode" : "100644", + "sha" : "dd5b3a58aa1849f452abc9b5cd1638dc71a5e482" + }, + { + "size" : 1122, + "type" : "blob", + "path" : "Package.swift", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2", + "mode" : "100644", + "sha" : "bf4aa0c6a8bd9f749c2f96905c40bf2f70ef97d2" + }, + { + "mode" : "100644", + "path" : "README.md", + "type" : "blob", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/blobs\/d6f2896b11f092297ec18b217c5d1e78d0b0a939", + "sha" : "d6f2896b11f092297ec18b217c5d1e78d0b0a939", + "size" : 2973 + }, + { + "mode" : "040000", + "type" : "tree", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/trees\/296bc6314f88a78e0421e24ecf0b04fc5cc74fdc", + "path" : "Sources", + "sha" : "296bc6314f88a78e0421e24ecf0b04fc5cc74fdc" + }, + { + "path" : "Tests", + "mode" : "040000", + "sha" : "beeb61970de55ecaadd5443a96f9daddce0de9b4", + "type" : "tree", + "url" : "https:\/\/api.github.com\/repos\/SwiftPackageIndex\/SemanticVersion\/git\/trees\/beeb61970de55ecaadd5443a96f9daddce0de9b4" + } + ], + "sha" : "ea8eea9d89842a29af1b8e6c7677f1c86e72fa42", + "truncated" : false +} diff --git a/Tests/ValidatorTests/GithubTests.swift b/Tests/ValidatorTests/GithubTests.swift index 356607f..48e8730 100644 --- a/Tests/ValidatorTests/GithubTests.swift +++ b/Tests/ValidatorTests/GithubTests.swift @@ -17,6 +17,7 @@ import XCTest @testable import ValidatorCore import AsyncHTTPClient +import NIO final class GithubTests: XCTestCase { @@ -25,7 +26,7 @@ final class GithubTests: XCTestCase { // setup let client = HTTPClient(eventLoopGroupProvider: .singleton) defer { try? client.syncShutdown() } - let repo = Github.Repository(default_branch: "main", fork: false) + let repo = Github.Repository(defaultBranch: "main", owner: "foo", name: "bar") var calls = 0 Current.fetch = { client, _ in calls += 1 @@ -68,6 +69,23 @@ final class GithubTests: XCTestCase { XCTAssertEqual(calls, 3) } + func test_listRepositoryFilePaths() async throws { + // setup + let client = HTTPClient(eventLoopGroupProvider: .singleton) + defer { try? client.syncShutdown() } + let data = try fixtureData(for: "github-files-response.json") + Current.fetch = { client, _ in + return client.eventLoopGroup.next().makeSucceededFuture(ByteBuffer(data: data)) + } + let repo = Github.Repository(defaultBranch: "main", owner: "SwiftPackageIndex", name: "SemanticVersion") + + // MUT + let paths = try await Github.listRepositoryFilePaths(client: client, repository: repo) + + // validate + XCTAssertEqual(paths, [".gitignore", ".spi.yml", "FUNDING.yml", "LICENSE", "Package.swift", "README.md"]) + } + } diff --git a/Tests/ValidatorTests/ValidatorTests.swift b/Tests/ValidatorTests/ValidatorTests.swift index 2b36092..d601321 100644 --- a/Tests/ValidatorTests/ValidatorTests.swift +++ b/Tests/ValidatorTests/ValidatorTests.swift @@ -75,7 +75,7 @@ final class ValidatorTests: XCTestCase { } } - func test_getManifestURL() throws { + func test_getManifestURL_deprecated() throws { // setup let pkgURL = PackageURL(argument: "https://github.com/foo/bar")! let client = HTTPClient(eventLoopGroupProvider: .singleton) @@ -90,6 +90,43 @@ final class ValidatorTests: XCTestCase { .init("https://raw.githubusercontent.com/foo/bar/main/Package.swift")) } + func test_getManifestURL() async throws { + // setup + let client = HTTPClient(eventLoopGroupProvider: .singleton) + defer { try? client.syncShutdown() } + let data = try fixtureData(for: "github-files-response.json") + Current.fetch = { client, _ in + return client.eventLoopGroup.next().makeSucceededFuture(ByteBuffer(data: data)) + } + let repo = Github.Repository(defaultBranch: "main", owner: "SwiftPackageIndex", name: "SemanticVersion") + + // MUT + let url = try await Package.getManifestURL(client: client, repository: repo) + + // validate + XCTAssertEqual(url, + .init("https://raw.githubusercontent.com/SwiftPackageIndex/SemanticVersion/main/Package.swift")) + } + + func test_getManifestURL_multiple() async throws { + // Get manifest URL when there are multiple Package*.swift manifest versions + // setup + let client = HTTPClient(eventLoopGroupProvider: .singleton) + defer { try? client.syncShutdown() } + let data = try fixtureData(for: "github-files-response-SwiftyJSON.json") + Current.fetch = { client, _ in + return client.eventLoopGroup.next().makeSucceededFuture(ByteBuffer(data: data)) + } + let repo = Github.Repository(defaultBranch: "master", owner: "IBM-Swift", name: "SwiftyJSON") + + // MUT + let url = try await Package.getManifestURL(client: client, repository: repo) + + // validate + XCTAssertEqual(url, + .init("https://raw.githubusercontent.com/IBM-Swift/SwiftyJSON/master/Package@swift-5.swift")) + } + func test_findDependencies() throws { // Basic findDependencies test // setup