Skip to content

Commit c770da2

Browse files
committed
added Netrc Downloader tests w/mock
1 parent de63346 commit c770da2

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

Sources/TSCUtility/Downloader.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,23 @@ public final class FoundationDownloader: NSObject, Downloader {
113113
completion: @escaping Downloader.Completion
114114
) {
115115
queue.addOperation {
116-
let task = self.session.downloadTask(with: url)
116+
var request = URLRequest(url: url)
117+
118+
if #available(OSX 10.13, *) {
119+
switch Netrc.load() {
120+
case let .success(netrc):
121+
if let authorization = netrc.authorization(for: url) {
122+
request.addValue(authorization, forHTTPHeaderField: "Authorization")
123+
}
124+
case .failure(_):
125+
break // Failure cases unhandled
126+
}
127+
} else {
128+
// Netrc loading is not supported for OSX < 10.13; continue with task-
129+
// initialization without attempting to append netrc-based credentials.
130+
}
131+
132+
let task = self.session.downloadTask(with: request)
117133
let download = Download(
118134
task: task,
119135
destination: destination,

Sources/TSCUtility/Netrc.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ public struct Netrc {
77
/// Representation of `machine` connection settings & `default` connection settings. If `default` connection settings present, they will be last element.
88
public let machines: [Machine]
99

10-
init(machines: [Machine]) {
10+
private init(machines: [Machine]) {
1111
self.machines = machines
1212
}
1313

14+
/// Testing API. Not for productive use.
15+
/// See: [Remove @testable from codebase](https://github.com/apple/swift-package-manager/commit/b6349d516d2f9b2f26ddae9de2c594ede24af7d6)
16+
public static var _mock: Netrc? = nil
17+
1418
/// Basic authorization header string
1519
/// - Parameter url: URI of network resource to be accessed
1620
/// - Returns: (optional) Basic Authorization header string to be added to the request
@@ -26,6 +30,9 @@ public struct Netrc {
2630
/// - Parameter fileURL: Location of netrc file, defaults to `~/.netrc`
2731
/// - Returns: `Netrc` container with parsed connection settings, or error
2832
public static func load(from fileURL: Foundation.URL = Foundation.URL(fileURLWithPath: "\(NSHomeDirectory())/.netrc")) -> Result<Netrc, Netrc.Error> {
33+
34+
guard _mock == nil else { return .success(_mock!) }
35+
2936
guard FileManager.default.fileExists(atPath: fileURL.path) else { return .failure(.fileNotFound(fileURL)) }
3037
guard FileManager.default.isReadableFile(atPath: fileURL.path),
3138
let fileContents = try? String(contentsOf: fileURL, encoding: .utf8) else { return .failure(.unreadableFile(fileURL)) }

Tests/TSCUtilityTests/DownloaderTests.swift

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import FoundationNetworking
1818
#endif
1919

2020
class DownloaderTests: XCTestCase {
21+
22+
override func tearDown() {
23+
if #available(OSX 10.13, *) {
24+
Netrc._mock = nil
25+
}
26+
}
2127

2228
func testSuccess() {
2329
// FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain.
@@ -73,6 +79,140 @@ class DownloaderTests: XCTestCase {
7379
}
7480
#endif
7581
}
82+
83+
@available(OSX 10.13, *)
84+
func testAuthenticatedSuccess() {
85+
86+
let netrcContent = "machine protected.downloader-tests.com login anonymous password qwerty"
87+
guard case .success(let netrc) = Netrc.from(netrcContent) else {
88+
return XCTFail("Cannot load netrc content")
89+
}
90+
let authData = "anonymous:qwerty".data(using: .utf8)!
91+
let testAuthHeader = "Basic \(authData.base64EncodedString())"
92+
Netrc._mock = netrc
93+
94+
#if os(macOS)
95+
let configuration = URLSessionConfiguration.default
96+
configuration.protocolClasses = [MockAuthenticatingURLProtocol.self]
97+
let downloader = FoundationDownloader(configuration: configuration)
98+
99+
mktmpdir { tmpdir in
100+
let url = URL(string: "https://protected.downloader-tests.com/testBasics.zip")!
101+
let destination = tmpdir.appending(component: "download")
102+
103+
let didStartLoadingExpectation = XCTestExpectation(description: "didStartLoading")
104+
let progress50Expectation = XCTestExpectation(description: "progress50")
105+
let progress100Expectation = XCTestExpectation(description: "progress100")
106+
let successExpectation = XCTestExpectation(description: "success")
107+
MockAuthenticatingURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
108+
109+
downloader.downloadFile(at: url, to: destination, progress: { bytesDownloaded, totalBytesToDownload in
110+
111+
XCTAssertEqual(MockAuthenticatingURLProtocol.authenticationHeader(for: url), testAuthHeader)
112+
113+
switch (bytesDownloaded, totalBytesToDownload) {
114+
case (512, 1024):
115+
progress50Expectation.fulfill()
116+
case (1024, 1024):
117+
progress100Expectation.fulfill()
118+
default:
119+
XCTFail("unexpected progress")
120+
}
121+
}, completion: { result in
122+
switch result {
123+
case .success:
124+
XCTAssert(localFileSystem.exists(destination))
125+
let bytes = ByteString(Array(repeating: 0xbe, count: 512) + Array(repeating: 0xef, count: 512))
126+
XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)
127+
successExpectation.fulfill()
128+
case .failure(let error):
129+
XCTFail("\(error)")
130+
}
131+
})
132+
133+
wait(for: [didStartLoadingExpectation], timeout: 1.0)
134+
135+
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: [
136+
"Content-Length": "1024"
137+
])!
138+
139+
MockAuthenticatingURLProtocol.sendResponse(response, for: url)
140+
MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xbe, count: 512), for: url)
141+
wait(for: [progress50Expectation], timeout: 1.0)
142+
MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xef, count: 512), for: url)
143+
wait(for: [progress100Expectation], timeout: 1.0)
144+
MockAuthenticatingURLProtocol.sendCompletion(for: url)
145+
wait(for: [successExpectation], timeout: 1.0)
146+
}
147+
#endif
148+
}
149+
150+
@available(OSX 10.13, *)
151+
func testDefaultAuthenticationSuccess() {
152+
153+
let netrcContent = "default login default password default"
154+
guard case .success(let netrc) = Netrc.from(netrcContent) else {
155+
return XCTFail("Cannot load netrc content")
156+
}
157+
let authData = "default:default".data(using: .utf8)!
158+
let testAuthHeader = "Basic \(authData.base64EncodedString())"
159+
Netrc._mock = netrc
160+
161+
#if os(macOS)
162+
let configuration = URLSessionConfiguration.default
163+
configuration.protocolClasses = [MockAuthenticatingURLProtocol.self]
164+
let downloader = FoundationDownloader(configuration: configuration)
165+
166+
mktmpdir { tmpdir in
167+
let url = URL(string: "https://restricted.downloader-tests.com/testBasics.zip")!
168+
let destination = tmpdir.appending(component: "download")
169+
170+
let didStartLoadingExpectation = XCTestExpectation(description: "didStartLoading")
171+
let progress50Expectation = XCTestExpectation(description: "progress50")
172+
let progress100Expectation = XCTestExpectation(description: "progress100")
173+
let successExpectation = XCTestExpectation(description: "success")
174+
MockAuthenticatingURLProtocol.notifyDidStartLoading(for: url, completion: { didStartLoadingExpectation.fulfill() })
175+
176+
downloader.downloadFile(at: url, to: destination, progress: { bytesDownloaded, totalBytesToDownload in
177+
178+
XCTAssertEqual(MockAuthenticatingURLProtocol.authenticationHeader(for: url), testAuthHeader)
179+
180+
switch (bytesDownloaded, totalBytesToDownload) {
181+
case (512, 1024):
182+
progress50Expectation.fulfill()
183+
case (1024, 1024):
184+
progress100Expectation.fulfill()
185+
default:
186+
XCTFail("unexpected progress")
187+
}
188+
}, completion: { result in
189+
switch result {
190+
case .success:
191+
XCTAssert(localFileSystem.exists(destination))
192+
let bytes = ByteString(Array(repeating: 0xbe, count: 512) + Array(repeating: 0xef, count: 512))
193+
XCTAssertEqual(try! localFileSystem.readFileContents(destination), bytes)
194+
successExpectation.fulfill()
195+
case .failure(let error):
196+
XCTFail("\(error)")
197+
}
198+
})
199+
200+
wait(for: [didStartLoadingExpectation], timeout: 1.0)
201+
202+
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: [
203+
"Content-Length": "1024"
204+
])!
205+
206+
MockAuthenticatingURLProtocol.sendResponse(response, for: url)
207+
MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xbe, count: 512), for: url)
208+
wait(for: [progress50Expectation], timeout: 1.0)
209+
MockAuthenticatingURLProtocol.sendData(Data(repeating: 0xef, count: 512), for: url)
210+
wait(for: [progress100Expectation], timeout: 1.0)
211+
MockAuthenticatingURLProtocol.sendCompletion(for: url)
212+
wait(for: [successExpectation], timeout: 1.0)
213+
}
214+
#endif
215+
}
76216

77217
func testClientError() {
78218
// FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain.
@@ -209,6 +349,16 @@ private struct DummyError: Error {
209349

210350
private typealias Action = () -> Void
211351

352+
private class MockAuthenticatingURLProtocol: MockURLProtocol {
353+
354+
fileprivate static func authenticationHeader(for url: Foundation.URL) -> String? {
355+
guard let instance = instance(for: url) else {
356+
fatalError("url did not start loading")
357+
}
358+
return instance.request.allHTTPHeaderFields?["Authorization"]
359+
}
360+
}
361+
212362
private class MockURLProtocol: URLProtocol {
213363
private static var queue = DispatchQueue(label: "org.swift.swiftpm.basic-tests.mock-url-protocol")
214364
private static var observers: [Foundation.URL: Action] = [:]
@@ -310,6 +460,10 @@ private class MockURLProtocol: URLProtocol {
310460
Self.instances[url] = nil
311461
}
312462
}
463+
464+
fileprivate static func instance(for url: Foundation.URL) -> URLProtocol? {
465+
return Self.instances[url]
466+
}
313467
}
314468

315469
class FailingFileSystem: FileSystem {

0 commit comments

Comments
 (0)