@@ -18,6 +18,7 @@ import FoundationNetworking
1818#endif
1919
2020class DownloaderTests : XCTestCase {
21+
2122 func testSuccess( ) {
2223 // FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain.
2324 #if os(macOS)
@@ -72,6 +73,140 @@ class DownloaderTests: XCTestCase {
7273 }
7374 #endif
7475 }
76+
77+ #if os(macOS)
78+ @available ( OSX 10 . 13 , * )
79+ /// Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`,
80+ /// which is only available in macOS 10.13+ at this time.
81+ func testAuthenticatedSuccess( ) {
82+ let netrcContent = " machine protected.downloader-tests.com login anonymous password qwerty "
83+ guard case . success( let netrc) = Netrc . from ( netrcContent) else {
84+ return XCTFail ( " Cannot load netrc content " )
85+ }
86+ let authData = " anonymous:qwerty " . data ( using: . utf8) !
87+ let testAuthHeader = " Basic \( authData. base64EncodedString ( ) ) "
88+
89+ let configuration = URLSessionConfiguration . default
90+ configuration. protocolClasses = [ MockAuthenticatingURLProtocol . self]
91+ let downloader = FoundationDownloader ( configuration: configuration)
92+
93+ mktmpdir { tmpdir in
94+ let url = URL ( string: " https://protected.downloader-tests.com/testBasics.zip " ) !
95+ let destination = tmpdir. appending ( component: " download " )
96+
97+ let didStartLoadingExpectation = XCTestExpectation ( description: " didStartLoading " )
98+ let progress50Expectation = XCTestExpectation ( description: " progress50 " )
99+ let progress100Expectation = XCTestExpectation ( description: " progress100 " )
100+ let successExpectation = XCTestExpectation ( description: " success " )
101+ MockAuthenticatingURLProtocol . notifyDidStartLoading ( for: url, completion: { didStartLoadingExpectation. fulfill ( ) } )
102+
103+ downloader. downloadFile ( at: url, to: destination, withAuthorizationProvider: netrc, progress: { bytesDownloaded, totalBytesToDownload in
104+
105+ XCTAssertEqual ( MockAuthenticatingURLProtocol . authenticationHeader ( for: url) , testAuthHeader)
106+
107+ switch ( bytesDownloaded, totalBytesToDownload) {
108+ case ( 512 , 1024 ) :
109+ progress50Expectation. fulfill ( )
110+ case ( 1024 , 1024 ) :
111+ progress100Expectation. fulfill ( )
112+ default :
113+ XCTFail ( " unexpected progress " )
114+ }
115+ } , completion: { result in
116+ switch result {
117+ case . success:
118+ XCTAssert ( localFileSystem. exists ( destination) )
119+ let bytes = ByteString ( Array ( repeating: 0xbe , count: 512 ) + Array( repeating: 0xef , count: 512 ) )
120+ XCTAssertEqual ( try ! localFileSystem. readFileContents ( destination) , bytes)
121+ successExpectation. fulfill ( )
122+ case . failure( let error) :
123+ XCTFail ( " \( error) " )
124+ }
125+ } )
126+
127+ wait ( for: [ didStartLoadingExpectation] , timeout: 1.0 )
128+
129+ let response = HTTPURLResponse ( url: url, statusCode: 200 , httpVersion: " 1.1 " , headerFields: [
130+ " Content-Length " : " 1024 "
131+ ] ) !
132+
133+ MockAuthenticatingURLProtocol . sendResponse ( response, for: url)
134+ MockAuthenticatingURLProtocol . sendData ( Data ( repeating: 0xbe , count: 512 ) , for: url)
135+ wait ( for: [ progress50Expectation] , timeout: 1.0 )
136+ MockAuthenticatingURLProtocol . sendData ( Data ( repeating: 0xef , count: 512 ) , for: url)
137+ wait ( for: [ progress100Expectation] , timeout: 1.0 )
138+ MockAuthenticatingURLProtocol . sendCompletion ( for: url)
139+ wait ( for: [ successExpectation] , timeout: 1.0 )
140+ }
141+ }
142+ #endif
143+
144+ #if os(macOS)
145+ @available ( OSX 10 . 13 , * )
146+ /// Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`,
147+ /// which is only available in macOS 10.13+ at this time.
148+ func testDefaultAuthenticationSuccess( ) {
149+ let netrcContent = " default login default password default "
150+ guard case . success( let netrc) = Netrc . from ( netrcContent) else {
151+ return XCTFail ( " Cannot load netrc content " )
152+ }
153+ let authData = " default:default " . data ( using: . utf8) !
154+ let testAuthHeader = " Basic \( authData. base64EncodedString ( ) ) "
155+
156+ let configuration = URLSessionConfiguration . default
157+ configuration. protocolClasses = [ MockAuthenticatingURLProtocol . self]
158+ let downloader = FoundationDownloader ( configuration: configuration)
159+
160+ mktmpdir { tmpdir in
161+ let url = URL ( string: " https://restricted.downloader-tests.com/testBasics.zip " ) !
162+ let destination = tmpdir. appending ( component: " download " )
163+
164+ let didStartLoadingExpectation = XCTestExpectation ( description: " didStartLoading " )
165+ let progress50Expectation = XCTestExpectation ( description: " progress50 " )
166+ let progress100Expectation = XCTestExpectation ( description: " progress100 " )
167+ let successExpectation = XCTestExpectation ( description: " success " )
168+ MockAuthenticatingURLProtocol . notifyDidStartLoading ( for: url, completion: { didStartLoadingExpectation. fulfill ( ) } )
169+
170+ downloader. downloadFile ( at: url, to: destination, withAuthorizationProvider: netrc, progress: { bytesDownloaded, totalBytesToDownload in
171+
172+ XCTAssertEqual ( MockAuthenticatingURLProtocol . authenticationHeader ( for: url) , testAuthHeader)
173+
174+ switch ( bytesDownloaded, totalBytesToDownload) {
175+ case ( 512 , 1024 ) :
176+ progress50Expectation. fulfill ( )
177+ case ( 1024 , 1024 ) :
178+ progress100Expectation. fulfill ( )
179+ default :
180+ XCTFail ( " unexpected progress " )
181+ }
182+ } , completion: { result in
183+ switch result {
184+ case . success:
185+ XCTAssert ( localFileSystem. exists ( destination) )
186+ let bytes = ByteString ( Array ( repeating: 0xbe , count: 512 ) + Array( repeating: 0xef , count: 512 ) )
187+ XCTAssertEqual ( try ! localFileSystem. readFileContents ( destination) , bytes)
188+ successExpectation. fulfill ( )
189+ case . failure( let error) :
190+ XCTFail ( " \( error) " )
191+ }
192+ } )
193+
194+ wait ( for: [ didStartLoadingExpectation] , timeout: 1.0 )
195+
196+ let response = HTTPURLResponse ( url: url, statusCode: 200 , httpVersion: " 1.1 " , headerFields: [
197+ " Content-Length " : " 1024 "
198+ ] ) !
199+
200+ MockAuthenticatingURLProtocol . sendResponse ( response, for: url)
201+ MockAuthenticatingURLProtocol . sendData ( Data ( repeating: 0xbe , count: 512 ) , for: url)
202+ wait ( for: [ progress50Expectation] , timeout: 1.0 )
203+ MockAuthenticatingURLProtocol . sendData ( Data ( repeating: 0xef , count: 512 ) , for: url)
204+ wait ( for: [ progress100Expectation] , timeout: 1.0 )
205+ MockAuthenticatingURLProtocol . sendCompletion ( for: url)
206+ wait ( for: [ successExpectation] , timeout: 1.0 )
207+ }
208+ }
209+ #endif
75210
76211 func testClientError( ) {
77212 // FIXME: Remove once https://github.com/apple/swift-corelibs-foundation/pull/2593 gets inside a toolchain.
@@ -208,6 +343,16 @@ private struct DummyError: Error {
208343
209344private typealias Action = ( ) -> Void
210345
346+ private class MockAuthenticatingURLProtocol : MockURLProtocol {
347+
348+ fileprivate static func authenticationHeader( for url: Foundation . URL ) -> String ? {
349+ guard let instance = instance ( for: url) else {
350+ fatalError ( " url did not start loading " )
351+ }
352+ return instance. request. allHTTPHeaderFields ? [ " Authorization " ]
353+ }
354+ }
355+
211356private class MockURLProtocol : URLProtocol {
212357 private static var queue = DispatchQueue ( label: " org.swift.swiftpm.basic-tests.mock-url-protocol " )
213358 private static var observers : [ Foundation . URL : Action ] = [ : ]
@@ -309,6 +454,10 @@ private class MockURLProtocol: URLProtocol {
309454 Self . instances [ url] = nil
310455 }
311456 }
457+
458+ fileprivate static func instance( for url: Foundation . URL ) -> URLProtocol ? {
459+ return Self . instances [ url]
460+ }
312461}
313462
314463class FailingFileSystem : FileSystem {
0 commit comments