View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.test.util.http;
20  
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.UncheckedIOException;
26  import java.net.ServerSocket;
27  import java.net.URI;
28  import java.net.URL;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.nio.file.StandardCopyOption;
34  import java.util.HashMap;
35  import java.util.Map;
36  import java.util.concurrent.atomic.AtomicReference;
37  import java.util.function.Supplier;
38  
39  import org.eclipse.aether.ConfigurationProperties;
40  import org.eclipse.aether.DefaultRepositoryCache;
41  import org.eclipse.aether.DefaultRepositorySystemSession;
42  import org.eclipse.aether.DefaultSessionData;
43  import org.eclipse.aether.internal.impl.transport.http.DefaultChecksumExtractor;
44  import org.eclipse.aether.internal.impl.transport.http.Nx2ChecksumExtractor;
45  import org.eclipse.aether.internal.impl.transport.http.XChecksumExtractor;
46  import org.eclipse.aether.internal.test.util.TestFileUtils;
47  import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
48  import org.eclipse.aether.repository.Authentication;
49  import org.eclipse.aether.repository.Proxy;
50  import org.eclipse.aether.repository.RemoteRepository;
51  import org.eclipse.aether.spi.connector.transport.GetTask;
52  import org.eclipse.aether.spi.connector.transport.PeekTask;
53  import org.eclipse.aether.spi.connector.transport.PutTask;
54  import org.eclipse.aether.spi.connector.transport.Transporter;
55  import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
56  import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy;
57  import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
58  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
59  import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
60  import org.eclipse.aether.spi.connector.transport.http.RFC9457.HttpRFC9457Exception;
61  import org.eclipse.aether.transfer.NoTransporterException;
62  import org.eclipse.aether.transfer.TransferCancelledException;
63  import org.eclipse.aether.util.repository.AuthenticationBuilder;
64  import org.junit.jupiter.api.AfterEach;
65  import org.junit.jupiter.api.BeforeEach;
66  import org.junit.jupiter.api.Test;
67  import org.junit.jupiter.api.TestInfo;
68  import org.junit.jupiter.api.Timeout;
69  
70  import static java.util.Objects.requireNonNull;
71  import static org.junit.jupiter.api.Assertions.assertEquals;
72  import static org.junit.jupiter.api.Assertions.assertNotNull;
73  import static org.junit.jupiter.api.Assertions.assertNull;
74  import static org.junit.jupiter.api.Assertions.assertThrows;
75  import static org.junit.jupiter.api.Assertions.assertTrue;
76  import static org.junit.jupiter.api.Assertions.fail;
77  
78  /**
79   * Common set of tests against Http transporter.
80   */
81  @SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:MethodName"})
82  public class HttpTransporterTest {
83  
84      protected static final Path KEY_STORE_PATH = Paths.get("target/keystore");
85  
86      protected static final Path KEY_STORE_SELF_SIGNED_PATH = Paths.get("target/keystore-self-signed");
87  
88      protected static final Path TRUST_STORE_PATH = Paths.get("target/trustStore");
89  
90      static {
91          // Warning: "cross connected" with HttpServer!
92          System.setProperty(
93                  "javax.net.ssl.trustStore", KEY_STORE_PATH.toAbsolutePath().toString());
94          System.setProperty("javax.net.ssl.trustStorePassword", "server-pwd");
95          System.setProperty(
96                  "javax.net.ssl.keyStore", TRUST_STORE_PATH.toAbsolutePath().toString());
97          System.setProperty("javax.net.ssl.keyStorePassword", "client-pwd");
98  
99          System.setProperty("javax.net.ssl.trustStoreType", "jks");
100         System.setProperty("javax.net.ssl.keyStoreType", "jks");
101         // System.setProperty("javax.net.debug", "all");
102     }
103 
104     private final Supplier<HttpTransporterFactory> transporterFactorySupplier;
105 
106     protected DefaultRepositorySystemSession session;
107 
108     protected HttpTransporterFactory factory;
109 
110     protected HttpTransporter transporter;
111 
112     protected Runnable closer;
113 
114     protected File repoDir;
115 
116     protected HttpServer httpServer;
117 
118     protected Authentication auth;
119 
120     protected Proxy proxy;
121 
122     protected HttpTransporterTest(Supplier<HttpTransporterFactory> transporterFactorySupplier) {
123         this.transporterFactorySupplier = requireNonNull(transporterFactorySupplier);
124 
125         if (!Files.isRegularFile(KEY_STORE_PATH)) {
126             URL keyStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store");
127             URL keyStoreSelfSignedUrl =
128                     HttpTransporterTest.class.getClassLoader().getResource("ssl/server-store-selfsigned");
129             URL trustStoreUrl = HttpTransporterTest.class.getClassLoader().getResource("ssl/client-store");
130 
131             try {
132                 try (InputStream keyStoreStream = keyStoreUrl.openStream();
133                         InputStream keyStoreSelfSignedStream = keyStoreSelfSignedUrl.openStream();
134                         InputStream trustStoreStream = trustStoreUrl.openStream()) {
135                     Files.copy(keyStoreStream, KEY_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
136                     Files.copy(
137                             keyStoreSelfSignedStream, KEY_STORE_SELF_SIGNED_PATH, StandardCopyOption.REPLACE_EXISTING);
138                     Files.copy(trustStoreStream, TRUST_STORE_PATH, StandardCopyOption.REPLACE_EXISTING);
139                 }
140             } catch (IOException e) {
141                 throw new UncheckedIOException(e);
142             }
143         }
144     }
145 
146     protected static ChecksumExtractor standardChecksumExtractor() {
147         HashMap<String, ChecksumExtractorStrategy> strategies = new HashMap<>();
148         strategies.put("1", new Nx2ChecksumExtractor());
149         strategies.put("2", new XChecksumExtractor());
150         return new DefaultChecksumExtractor(strategies);
151     }
152 
153     protected RemoteRepository newRepo(String url) {
154         return new RemoteRepository.Builder("test", "default", url)
155                 .setAuthentication(auth)
156                 .setProxy(proxy)
157                 .build();
158     }
159 
160     protected void newTransporter(String url) throws Exception {
161         if (transporter != null) {
162             transporter.close();
163             transporter = null;
164         }
165         if (closer != null) {
166             closer.run();
167             closer = null;
168         }
169         session = new DefaultRepositorySystemSession(session);
170         session.setData(new DefaultSessionData());
171         transporter = factory.newInstance(session, newRepo(url));
172     }
173 
174     protected static final long OLD_FILE_TIMESTAMP = 160660800000L;
175 
176     @BeforeEach
177     protected void setUp(TestInfo testInfo) throws Exception {
178         System.out.println("=== " + testInfo.getDisplayName() + " ===");
179         session = new DefaultRepositorySystemSession(h -> {
180             this.closer = h;
181             return true;
182         });
183         session.setLocalRepositoryManager(new TestLocalRepositoryManager());
184         factory = transporterFactorySupplier.get();
185         repoDir = TestFileUtils.createTempDir();
186         TestFileUtils.writeString(new File(repoDir, "file.txt"), "test");
187         TestFileUtils.writeString(new File(repoDir, "dir/file.txt"), "test");
188         TestFileUtils.writeString(new File(repoDir, "dir/oldFile.txt"), "oldTest", OLD_FILE_TIMESTAMP);
189         TestFileUtils.writeString(new File(repoDir, "empty.txt"), "");
190         TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space");
191         File resumable = new File(repoDir, "resume.txt");
192         TestFileUtils.writeString(resumable, "resumable");
193         resumable.setLastModified(System.currentTimeMillis() - 90 * 1000);
194         httpServer = new HttpServer().setRepoDir(repoDir).start();
195         newTransporter(httpServer.getHttpUrl());
196     }
197 
198     @AfterEach
199     protected void tearDown() throws Exception {
200         if (transporter != null) {
201             transporter.close();
202             transporter = null;
203         }
204         if (closer != null) {
205             closer.run();
206             closer = null;
207         }
208         if (httpServer != null) {
209             httpServer.stop();
210             httpServer = null;
211         }
212         factory = null;
213         session = null;
214     }
215 
216     @Test
217     protected void testClassify() {
218         assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException()));
219         assertEquals(Transporter.ERROR_OTHER, transporter.classify(new HttpTransporterException(403)));
220         assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new HttpTransporterException(404)));
221     }
222 
223     @Test
224     protected void testPeek() throws Exception {
225         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
226     }
227 
228     @Test
229     protected void testRetryHandler_defaultCount_positive() throws Exception {
230         httpServer.setConnectionsToClose(3);
231         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
232     }
233 
234     @Test
235     protected void testRetryHandler_defaultCount_negative() throws Exception {
236         httpServer.setConnectionsToClose(4);
237         try {
238             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
239             fail("Expected error");
240         } catch (Exception expected) {
241         }
242     }
243 
244     @Test
245     protected void testRetryHandler_explicitCount_positive() throws Exception {
246         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 10);
247         newTransporter(httpServer.getHttpUrl());
248         httpServer.setConnectionsToClose(10);
249         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
250     }
251 
252     @Test
253     protected void testRetryHandler_disabled() throws Exception {
254         session.setConfigProperty(ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT, 0);
255         newTransporter(httpServer.getHttpUrl());
256         httpServer.setConnectionsToClose(1);
257         try {
258             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
259         } catch (Exception expected) {
260         }
261     }
262 
263     @Test
264     protected void testPeek_NotFound() throws Exception {
265         try {
266             transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
267             fail("Expected error");
268         } catch (HttpTransporterException e) {
269             assertEquals(404, e.getStatusCode());
270             assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
271         }
272     }
273 
274     @Test
275     protected void testPeek_Closed() throws Exception {
276         transporter.close();
277         try {
278             transporter.peek(new PeekTask(URI.create("repo/missing.txt")));
279             fail("Expected error");
280         } catch (IllegalStateException e) {
281             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
282         }
283     }
284 
285     @Test
286     protected void testPeek_Authenticated() throws Exception {
287         httpServer.setAuthentication("testuser", "testpass");
288         auth = new AuthenticationBuilder()
289                 .addUsername("testuser")
290                 .addPassword("testpass")
291                 .build();
292         newTransporter(httpServer.getHttpUrl());
293         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
294     }
295 
296     @Test
297     protected void testPeek_Unauthenticated() throws Exception {
298         httpServer.setAuthentication("testuser", "testpass");
299         try {
300             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
301             fail("Expected error");
302         } catch (HttpTransporterException e) {
303             assertEquals(401, e.getStatusCode());
304             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
305         }
306     }
307 
308     @Test
309     protected void testPeek_ProxyAuthenticated() throws Exception {
310         httpServer.setProxyAuthentication("testuser", "testpass");
311         auth = new AuthenticationBuilder()
312                 .addUsername("testuser")
313                 .addPassword("testpass")
314                 .build();
315         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
316         newTransporter("http://bad.localhost:1/");
317         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
318     }
319 
320     @Test
321     protected void testPeek_ProxyUnauthenticated() throws Exception {
322         httpServer.setProxyAuthentication("testuser", "testpass");
323         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
324         newTransporter("http://bad.localhost:1/");
325         try {
326             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
327             fail("Expected error");
328         } catch (HttpTransporterException e) {
329             assertEquals(407, e.getStatusCode());
330             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
331         }
332     }
333 
334     @Test
335     protected void testPeek_SSL() throws Exception {
336         httpServer.addSslConnector();
337         newTransporter(httpServer.getHttpsUrl());
338         transporter.peek(new PeekTask(URI.create("repo/file.txt")));
339     }
340 
341     @Test
342     protected void testPeek_Redirect() throws Exception {
343         httpServer.addSslConnector();
344         transporter.peek(new PeekTask(URI.create("redirect/file.txt")));
345         transporter.peek(new PeekTask(URI.create("redirect/file.txt?scheme=https")));
346     }
347 
348     @Test
349     protected void testGet_ToMemory() throws Exception {
350         RecordingTransportListener listener = new RecordingTransportListener();
351         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
352         transporter.get(task);
353         assertEquals("test", task.getDataString());
354         assertEquals(0L, listener.getDataOffset());
355         assertEquals(4L, listener.getDataLength());
356         assertEquals(1, listener.getStartedCount());
357         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
358         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
359     }
360 
361     @Test
362     protected void testGet_ToFile() throws Exception {
363         File file = TestFileUtils.createTempFile("failure");
364         RecordingTransportListener listener = new RecordingTransportListener();
365         GetTask task = new GetTask(URI.create("repo/file.txt"))
366                 .setDataPath(file.toPath())
367                 .setListener(listener);
368         transporter.get(task);
369         assertEquals("test", TestFileUtils.readString(file));
370         assertEquals(0L, listener.getDataOffset());
371         assertEquals(4L, listener.getDataLength());
372         assertEquals(1, listener.getStartedCount());
373         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
374         assertEquals("test", listener.getBaos().toString(StandardCharsets.UTF_8));
375     }
376 
377     @Test
378     protected void testGet_ToFileTimestamp() throws Exception {
379         File file = TestFileUtils.createTempFile("failure");
380         RecordingTransportListener listener = new RecordingTransportListener();
381         GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt"))
382                 .setDataPath(file.toPath())
383                 .setListener(listener);
384         transporter.get(task);
385         assertEquals("oldTest", TestFileUtils.readString(file));
386         assertEquals(0L, listener.getDataOffset());
387         assertEquals(7L, listener.getDataLength());
388         assertEquals(1, listener.getStartedCount());
389         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
390         assertEquals("oldTest", listener.getBaos().toString(StandardCharsets.UTF_8));
391         assertEquals(file.lastModified(), OLD_FILE_TIMESTAMP);
392     }
393 
394     @Test
395     protected void testGet_EmptyResource() throws Exception {
396         File file = TestFileUtils.createTempFile("failure");
397         RecordingTransportListener listener = new RecordingTransportListener();
398         GetTask task = new GetTask(URI.create("repo/empty.txt"))
399                 .setDataPath(file.toPath())
400                 .setListener(listener);
401         transporter.get(task);
402         assertEquals("", TestFileUtils.readString(file));
403         assertEquals(0L, listener.getDataOffset());
404         assertEquals(0L, listener.getDataLength());
405         assertEquals(1, listener.getStartedCount());
406         assertEquals(0, listener.getProgressedCount());
407         assertEquals("", listener.getBaos().toString(StandardCharsets.UTF_8));
408     }
409 
410     @Test
411     protected void testGet_EncodedResourcePath() throws Exception {
412         GetTask task = new GetTask(URI.create("repo/some%20space.txt"));
413         transporter.get(task);
414         assertEquals("space", task.getDataString());
415     }
416 
417     @Test
418     protected void testGet_Authenticated() throws Exception {
419         httpServer.setAuthentication("testuser", "testpass");
420         auth = new AuthenticationBuilder()
421                 .addUsername("testuser")
422                 .addPassword("testpass")
423                 .build();
424         newTransporter(httpServer.getHttpUrl());
425         RecordingTransportListener listener = new RecordingTransportListener();
426         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
427         transporter.get(task);
428         assertEquals("test", task.getDataString());
429         assertEquals(0L, listener.getDataOffset());
430         assertEquals(4L, listener.getDataLength());
431         assertEquals(1, listener.getStartedCount());
432         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
433         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
434     }
435 
436     @Test
437     protected void testGet_Unauthenticated() throws Exception {
438         httpServer.setAuthentication("testuser", "testpass");
439         try {
440             transporter.get(new GetTask(URI.create("repo/file.txt")));
441             fail("Expected error");
442         } catch (HttpTransporterException e) {
443             assertEquals(401, e.getStatusCode());
444             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
445         }
446     }
447 
448     @Test
449     protected void testGet_ProxyAuthenticated() throws Exception {
450         httpServer.setProxyAuthentication("testuser", "testpass");
451         Authentication auth = new AuthenticationBuilder()
452                 .addUsername("testuser")
453                 .addPassword("testpass")
454                 .build();
455         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
456         newTransporter("http://bad.localhost:1/");
457         RecordingTransportListener listener = new RecordingTransportListener();
458         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
459         transporter.get(task);
460         assertEquals("test", task.getDataString());
461         assertEquals(0L, listener.getDataOffset());
462         assertEquals(4L, listener.getDataLength());
463         assertEquals(1, listener.getStartedCount());
464         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
465         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
466     }
467 
468     @Test
469     protected void testGet_ProxyUnauthenticated() throws Exception {
470         httpServer.setProxyAuthentication("testuser", "testpass");
471         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
472         newTransporter("http://bad.localhost:1/");
473         try {
474             transporter.get(new GetTask(URI.create("repo/file.txt")));
475             fail("Expected error");
476         } catch (HttpTransporterException e) {
477             assertEquals(407, e.getStatusCode());
478             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
479         }
480     }
481 
482     @Test
483     protected void testGet_RFC9457Response() throws Exception {
484         try {
485             transporter.get(new GetTask(URI.create("rfc9457/file.txt")));
486             fail("Expected error");
487         } catch (HttpRFC9457Exception e) {
488             assertEquals(403, e.getStatusCode());
489             assertEquals(e.getPayload().getType(), URI.create("https://example.com/probs/out-of-credit"));
490             assertEquals(e.getPayload().getStatus(), 403);
491             assertEquals(e.getPayload().getTitle(), "You do not have enough credit.");
492             assertEquals(e.getPayload().getDetail(), "Your current balance is 30, but that costs 50.");
493             assertEquals(e.getPayload().getInstance(), URI.create("/account/12345/msgs/abc"));
494         }
495     }
496 
497     @Test
498     protected void testGet_RFC9457Response_with_missing_fields() throws Exception {
499         try {
500             transporter.get(new GetTask(URI.create("rfc9457/missing_fields.txt")));
501             fail("Expected error");
502         } catch (HttpRFC9457Exception e) {
503             assertEquals(403, e.getStatusCode());
504             assertEquals(e.getPayload().getType(), URI.create("about:blank"));
505             assertNull(e.getPayload().getStatus());
506             assertNull(e.getPayload().getTitle());
507             assertNull(e.getPayload().getDetail());
508             assertNull(e.getPayload().getInstance());
509         }
510     }
511 
512     @Test
513     protected void testGet_SSL() throws Exception {
514         httpServer.addSslConnector();
515         newTransporter(httpServer.getHttpsUrl());
516         RecordingTransportListener listener = new RecordingTransportListener();
517         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
518         transporter.get(task);
519         assertEquals("test", task.getDataString());
520         assertEquals(0L, listener.getDataOffset());
521         assertEquals(4L, listener.getDataLength());
522         assertEquals(1, listener.getStartedCount());
523         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
524         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
525     }
526 
527     @Test
528     protected void testGet_SSL_WithServerErrors() throws Exception {
529         httpServer.setServerErrorsBeforeWorks(1);
530         httpServer.addSslConnector();
531         newTransporter(httpServer.getHttpsUrl());
532         for (int i = 1; i < 3; i++) {
533             try {
534                 RecordingTransportListener listener = new RecordingTransportListener();
535                 GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
536                 transporter.get(task);
537                 assertEquals("test", task.getDataString());
538                 assertEquals(0L, listener.getDataOffset());
539                 assertEquals(4L, listener.getDataLength());
540                 assertEquals(1, listener.getStartedCount());
541                 assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
542                 assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
543             } catch (HttpTransporterException e) {
544                 assertEquals(500, e.getStatusCode());
545             }
546         }
547     }
548 
549     @Test
550     protected void testGet_HTTPS_Unknown_SecurityMode() throws Exception {
551         session.setConfigProperty(ConfigurationProperties.HTTPS_SECURITY_MODE, "unknown");
552         httpServer.addSelfSignedSslConnector();
553         try {
554             newTransporter(httpServer.getHttpsUrl());
555             fail("Unsupported security mode");
556         } catch (IllegalArgumentException a) {
557             // good
558         }
559     }
560 
561     @Test
562     protected void testGet_HTTPS_Insecure_SecurityMode() throws Exception {
563         // here we use alternate server-store-selfigned key (as the key set it static initializer is probably already
564         // used to init SSLContext/SSLSocketFactory/etc
565         session.setConfigProperty(
566                 ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
567         httpServer.addSelfSignedSslConnector();
568         newTransporter(httpServer.getHttpsUrl());
569         RecordingTransportListener listener = new RecordingTransportListener();
570         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
571         transporter.get(task);
572         assertEquals("test", task.getDataString());
573         assertEquals(0L, listener.getDataOffset());
574         assertEquals(4L, listener.getDataLength());
575         assertEquals(1, listener.getStartedCount());
576         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
577         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
578     }
579 
580     @Test
581     protected void testGet_HTTPS_HTTP2Only_Insecure_SecurityMode() throws Exception {
582         // here we use alternate server-store-selfigned key (as the key set it static initializer is probably already
583         // used to init SSLContext/SSLSocketFactory/etc
584         enableHttp2Protocol();
585         session.setConfigProperty(
586                 ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
587         httpServer.addSelfSignedSslConnectorHttp2Only();
588         newTransporter(httpServer.getHttpsUrl());
589         RecordingTransportListener listener = new RecordingTransportListener();
590         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
591         transporter.get(task);
592         assertEquals("test", task.getDataString());
593         assertEquals(0L, listener.getDataOffset());
594         assertEquals(4L, listener.getDataLength());
595         assertEquals(1, listener.getStartedCount());
596         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
597         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
598     }
599 
600     protected void enableHttp2Protocol() {}
601 
602     @Test
603     protected void testGet_Redirect() throws Exception {
604         httpServer.addSslConnector();
605         RecordingTransportListener listener = new RecordingTransportListener();
606         GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
607         transporter.get(task);
608         assertEquals("test", task.getDataString());
609         assertEquals(0L, listener.getDataOffset());
610         assertEquals(4L, listener.getDataLength());
611         assertEquals(1, listener.getStartedCount());
612         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
613         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
614     }
615 
616     @Test
617     protected void testGet_Resume() throws Exception {
618         File file = TestFileUtils.createTempFile("re");
619         RecordingTransportListener listener = new RecordingTransportListener();
620         GetTask task = new GetTask(URI.create("repo/resume.txt"))
621                 .setDataPath(file.toPath(), true)
622                 .setListener(listener);
623         transporter.get(task);
624         assertEquals("resumable", TestFileUtils.readString(file));
625         assertEquals(1L, listener.getStartedCount());
626         assertEquals(2L, listener.getDataOffset());
627         assertEquals(9, listener.getDataLength());
628         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
629         assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8));
630     }
631 
632     @Test
633     protected void testGet_ResumeLocalContentsOutdated() throws Exception {
634         File file = TestFileUtils.createTempFile("re");
635         file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
636         RecordingTransportListener listener = new RecordingTransportListener();
637         GetTask task = new GetTask(URI.create("repo/resume.txt"))
638                 .setDataPath(file.toPath(), true)
639                 .setListener(listener);
640         transporter.get(task);
641         assertEquals("resumable", TestFileUtils.readString(file));
642         assertEquals(1L, listener.getStartedCount());
643         assertEquals(0L, listener.getDataOffset());
644         assertEquals(9, listener.getDataLength());
645         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
646         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
647     }
648 
649     @Test
650     protected void testGet_ResumeRangesNotSupportedByServer() throws Exception {
651         httpServer.setRangeSupport(false);
652         File file = TestFileUtils.createTempFile("re");
653         RecordingTransportListener listener = new RecordingTransportListener();
654         GetTask task = new GetTask(URI.create("repo/resume.txt"))
655                 .setDataPath(file.toPath(), true)
656                 .setListener(listener);
657         transporter.get(task);
658         assertEquals("resumable", TestFileUtils.readString(file));
659         assertEquals(1L, listener.getStartedCount());
660         assertEquals(0L, listener.getDataOffset());
661         assertEquals(9, listener.getDataLength());
662         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
663         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
664     }
665 
666     @Test
667     protected void testGet_Checksums_Nexus() throws Exception {
668         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS);
669         GetTask task = new GetTask(URI.create("repo/file.txt"));
670         transporter.get(task);
671         assertEquals("test", task.getDataString());
672         assertEquals(
673                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
674     }
675 
676     @Test
677     protected void testGet_Checksums_XChecksum() throws Exception {
678         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM);
679         GetTask task = new GetTask(URI.create("repo/file.txt"));
680         transporter.get(task);
681         assertEquals("test", task.getDataString());
682         assertEquals(
683                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
684     }
685 
686     @Test
687     protected void testGet_FileHandleLeak() throws Exception {
688         for (int i = 0; i < 100; i++) {
689             File file = TestFileUtils.createTempFile("failure");
690             transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
691             assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
692         }
693     }
694 
695     @Test
696     protected void testGet_NotFound() throws Exception {
697         try {
698             transporter.get(new GetTask(URI.create("repo/missing.txt")));
699             fail("Expected error");
700         } catch (HttpTransporterException e) {
701             assertEquals(404, e.getStatusCode());
702             assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
703         }
704     }
705 
706     @Test
707     protected void testGet_Closed() throws Exception {
708         transporter.close();
709         try {
710             transporter.get(new GetTask(URI.create("repo/file.txt")));
711             fail("Expected error");
712         } catch (IllegalStateException e) {
713             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
714         }
715     }
716 
717     @Test
718     protected void testGet_StartCancelled() throws Exception {
719         RecordingTransportListener listener = new RecordingTransportListener();
720         listener.cancelStart();
721         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
722         try {
723             transporter.get(task);
724             fail("Expected error");
725         } catch (TransferCancelledException e) {
726             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
727         }
728         assertEquals(0L, listener.getDataOffset());
729         assertEquals(4L, listener.getDataLength());
730         assertEquals(1, listener.getStartedCount());
731         assertEquals(0, listener.getProgressedCount());
732     }
733 
734     @Test
735     protected void testGet_ProgressCancelled() throws Exception {
736         RecordingTransportListener listener = new RecordingTransportListener();
737         listener.cancelProgress();
738         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
739         try {
740             transporter.get(task);
741             fail("Expected error");
742         } catch (TransferCancelledException e) {
743             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
744         }
745         assertEquals(0L, listener.getDataOffset());
746         assertEquals(4L, listener.getDataLength());
747         assertEquals(1, listener.getStartedCount());
748         assertEquals(1, listener.getProgressedCount());
749     }
750 
751     @Test
752     protected void testPut_FromMemory() throws Exception {
753         RecordingTransportListener listener = new RecordingTransportListener();
754         PutTask task =
755                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
756         transporter.put(task);
757         assertEquals(0L, listener.getDataOffset());
758         assertEquals(6L, listener.getDataLength());
759         assertEquals(1, listener.getStartedCount());
760         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
761         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
762     }
763 
764     @Test
765     protected void testPut_FromFile() throws Exception {
766         File file = TestFileUtils.createTempFile("upload");
767         RecordingTransportListener listener = new RecordingTransportListener();
768         PutTask task =
769                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
770         transporter.put(task);
771         assertEquals(0L, listener.getDataOffset());
772         assertEquals(6L, listener.getDataLength());
773         assertEquals(1, listener.getStartedCount());
774         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
775         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
776     }
777 
778     @Test
779     protected void testPut_EmptyResource() throws Exception {
780         RecordingTransportListener listener = new RecordingTransportListener();
781         PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
782         transporter.put(task);
783         assertEquals(0L, listener.getDataOffset());
784         assertEquals(0L, listener.getDataLength());
785         assertEquals(1, listener.getStartedCount());
786         assertEquals(0, listener.getProgressedCount());
787         assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
788     }
789 
790     @Test
791     protected void testPut_EncodedResourcePath() throws Exception {
792         RecordingTransportListener listener = new RecordingTransportListener();
793         PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
794                 .setListener(listener)
795                 .setDataString("OK");
796         transporter.put(task);
797         assertEquals(0L, listener.getDataOffset());
798         assertEquals(2L, listener.getDataLength());
799         assertEquals(1, listener.getStartedCount());
800         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
801         assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
802     }
803 
804     @Test
805     protected void testPut_Authenticated_ExpectContinue() throws Exception {
806         httpServer.setAuthentication("testuser", "testpass");
807         auth = new AuthenticationBuilder()
808                 .addUsername("testuser")
809                 .addPassword("testpass")
810                 .build();
811         newTransporter(httpServer.getHttpUrl());
812         RecordingTransportListener listener = new RecordingTransportListener();
813         PutTask task =
814                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
815         transporter.put(task);
816         assertEquals(0L, listener.getDataOffset());
817         assertEquals(6L, listener.getDataLength());
818         assertEquals(1, listener.getStartedCount());
819         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
820         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
821     }
822 
823     @Test
824     protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
825         // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
826         session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
827         httpServer.setAuthentication("testuser", "testpass");
828         httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
829         auth = new AuthenticationBuilder()
830                 .addUsername("testuser")
831                 .addPassword("testpass")
832                 .build();
833         newTransporter(httpServer.getHttpUrl());
834         RecordingTransportListener listener = new RecordingTransportListener();
835         PutTask task =
836                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
837         transporter.put(task);
838         assertEquals(0L, listener.getDataOffset());
839         assertEquals(6L, listener.getDataLength());
840         assertEquals(1, listener.getStartedCount());
841         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
842         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
843     }
844 
845     @Test
846     protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
847         httpServer.setAuthentication("testuser", "testpass");
848         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
849         auth = new AuthenticationBuilder()
850                 .addUsername("testuser")
851                 .addPassword("testpass")
852                 .build();
853         newTransporter(httpServer.getHttpUrl());
854         RecordingTransportListener listener = new RecordingTransportListener();
855         PutTask task =
856                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
857         transporter.put(task);
858         assertEquals(0L, listener.getDataOffset());
859         assertEquals(6L, listener.getDataLength());
860         assertEquals(1, listener.getStartedCount());
861         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
862         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
863     }
864 
865     @Test
866     protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
867         session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
868         httpServer.setAuthentication("testuser", "testpass");
869         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
870         auth = new AuthenticationBuilder()
871                 .addUsername("testuser")
872                 .addPassword("testpass")
873                 .build();
874         newTransporter(httpServer.getHttpUrl());
875         RecordingTransportListener listener = new RecordingTransportListener();
876         PutTask task =
877                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
878         transporter.put(task);
879         assertEquals(0L, listener.getDataOffset());
880         assertEquals(6L, listener.getDataLength());
881         assertEquals(1, listener.getStartedCount()); // w/ expectContinue enabled would have here 2
882         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
883         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
884     }
885 
886     @Test
887     protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
888         Map<String, String> headers = new HashMap<>();
889         headers.put("Expect", "100-continue");
890         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
891         httpServer.setAuthentication("testuser", "testpass");
892         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
893         auth = new AuthenticationBuilder()
894                 .addUsername("testuser")
895                 .addPassword("testpass")
896                 .build();
897         newTransporter(httpServer.getHttpUrl());
898         RecordingTransportListener listener = new RecordingTransportListener();
899         PutTask task =
900                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
901         transporter.put(task);
902         assertEquals(0L, listener.getDataOffset());
903         assertEquals(6L, listener.getDataLength());
904         assertEquals(1, listener.getStartedCount());
905         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
906         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
907     }
908 
909     @Test
910     protected void testPut_Unauthenticated() throws Exception {
911         httpServer.setAuthentication("testuser", "testpass");
912         RecordingTransportListener listener = new RecordingTransportListener();
913         PutTask task =
914                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
915         try {
916             transporter.put(task);
917             fail("Expected error");
918         } catch (HttpTransporterException e) {
919             assertEquals(401, e.getStatusCode());
920             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
921         }
922         assertEquals(0, listener.getStartedCount());
923         assertEquals(0, listener.getProgressedCount());
924     }
925 
926     @Test
927     protected void testPut_ProxyAuthenticated() throws Exception {
928         httpServer.setProxyAuthentication("testuser", "testpass");
929         Authentication auth = new AuthenticationBuilder()
930                 .addUsername("testuser")
931                 .addPassword("testpass")
932                 .build();
933         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
934         newTransporter("http://bad.localhost:1/");
935         RecordingTransportListener listener = new RecordingTransportListener();
936         PutTask task =
937                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
938         transporter.put(task);
939         assertEquals(0L, listener.getDataOffset());
940         assertEquals(6L, listener.getDataLength());
941         assertEquals(1, listener.getStartedCount());
942         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
943         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
944     }
945 
946     @Test
947     protected void testPut_ProxyUnauthenticated() throws Exception {
948         httpServer.setProxyAuthentication("testuser", "testpass");
949         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
950         newTransporter("http://bad.localhost:1/");
951         RecordingTransportListener listener = new RecordingTransportListener();
952         PutTask task =
953                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
954         try {
955             transporter.put(task);
956             fail("Expected error");
957         } catch (HttpTransporterException e) {
958             assertEquals(407, e.getStatusCode());
959             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
960         }
961         assertEquals(0, listener.getStartedCount());
962         assertEquals(0, listener.getProgressedCount());
963     }
964 
965     @Test
966     protected void testPut_SSL() throws Exception {
967         httpServer.addSslConnector();
968         httpServer.setAuthentication("testuser", "testpass");
969         auth = new AuthenticationBuilder()
970                 .addUsername("testuser")
971                 .addPassword("testpass")
972                 .build();
973         newTransporter(httpServer.getHttpsUrl());
974         RecordingTransportListener listener = new RecordingTransportListener();
975         PutTask task =
976                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
977         transporter.put(task);
978         assertEquals(0L, listener.getDataOffset());
979         assertEquals(6L, listener.getDataLength());
980         assertEquals(1, listener.getStartedCount());
981         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
982         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
983     }
984 
985     @Test
986     protected void testPut_FileHandleLeak() throws Exception {
987         for (int i = 0; i < 100; i++) {
988             File src = TestFileUtils.createTempFile("upload");
989             File dst = new File(repoDir, "file.txt");
990             transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
991             assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
992             assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
993         }
994     }
995 
996     @Test
997     protected void testPut_Closed() throws Exception {
998         transporter.close();
999         try {
1000             transporter.put(new PutTask(URI.create("repo/missing.txt")));
1001             fail("Expected error");
1002         } catch (IllegalStateException e) {
1003             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1004         }
1005     }
1006 
1007     @Test
1008     protected void testPut_StartCancelled() throws Exception {
1009         RecordingTransportListener listener = new RecordingTransportListener();
1010         listener.cancelStart();
1011         PutTask task =
1012                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1013         try {
1014             transporter.put(task);
1015             fail("Expected error");
1016         } catch (TransferCancelledException e) {
1017             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1018         }
1019         assertEquals(0L, listener.getDataOffset());
1020         assertEquals(6L, listener.getDataLength());
1021         assertEquals(1, listener.getStartedCount());
1022         assertEquals(0, listener.getProgressedCount());
1023     }
1024 
1025     @Test
1026     protected void testPut_ProgressCancelled() throws Exception {
1027         RecordingTransportListener listener = new RecordingTransportListener();
1028         listener.cancelProgress();
1029         PutTask task =
1030                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1031         try {
1032             transporter.put(task);
1033             fail("Expected error");
1034         } catch (TransferCancelledException e) {
1035             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1036         }
1037         assertEquals(0L, listener.getDataOffset());
1038         assertEquals(6L, listener.getDataLength());
1039         assertEquals(1, listener.getStartedCount());
1040         assertEquals(1, listener.getProgressedCount());
1041     }
1042 
1043     @Test
1044     protected void testGetPut_AuthCache() throws Exception {
1045         httpServer.setAuthentication("testuser", "testpass");
1046         auth = new AuthenticationBuilder()
1047                 .addUsername("testuser")
1048                 .addPassword("testpass")
1049                 .build();
1050         newTransporter(httpServer.getHttpUrl());
1051         GetTask get = new GetTask(URI.create("repo/file.txt"));
1052         transporter.get(get);
1053         RecordingTransportListener listener = new RecordingTransportListener();
1054         PutTask task =
1055                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1056         transporter.put(task);
1057         assertEquals(1, listener.getStartedCount());
1058     }
1059 
1060     @Test
1061     protected void testPut_PreemptiveIsDefault() throws Exception {
1062         httpServer.setAuthentication("testuser", "testpass");
1063         auth = new AuthenticationBuilder()
1064                 .addUsername("testuser")
1065                 .addPassword("testpass")
1066                 .build();
1067         newTransporter(httpServer.getHttpUrl());
1068         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1069         transporter.put(task);
1070         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1071     }
1072 
1073     @Test
1074     protected void testPut_AuthCache() throws Exception {
1075         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1076         httpServer.setAuthentication("testuser", "testpass");
1077         auth = new AuthenticationBuilder()
1078                 .addUsername("testuser")
1079                 .addPassword("testpass")
1080                 .build();
1081         newTransporter(httpServer.getHttpUrl());
1082         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1083         transporter.put(task);
1084         assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1085         httpServer.getLogEntries().clear();
1086         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1087         transporter.put(task);
1088         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1089     }
1090 
1091     @Test
1092     protected void testPut_AuthCache_Preemptive() throws Exception {
1093         httpServer.setAuthentication("testuser", "testpass");
1094         auth = new AuthenticationBuilder()
1095                 .addUsername("testuser")
1096                 .addPassword("testpass")
1097                 .build();
1098         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1099         newTransporter(httpServer.getHttpUrl());
1100         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1101         transporter.put(task);
1102         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1103         httpServer.getLogEntries().clear();
1104         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1105         transporter.put(task);
1106         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1107     }
1108 
1109     @Test
1110     @Timeout(20)
1111     protected void testConcurrency() throws Exception {
1112         httpServer.setAuthentication("testuser", "testpass");
1113         auth = new AuthenticationBuilder()
1114                 .addUsername("testuser")
1115                 .addPassword("testpass")
1116                 .build();
1117         newTransporter(httpServer.getHttpUrl());
1118         final AtomicReference<Throwable> error = new AtomicReference<>();
1119         Thread[] threads = new Thread[20];
1120         for (int i = 0; i < threads.length; i++) {
1121             final String path = "repo/file.txt?i=" + i;
1122             threads[i] = new Thread(() -> {
1123                 try {
1124                     for (int j = 0; j < 100; j++) {
1125                         GetTask task = new GetTask(URI.create(path));
1126                         transporter.get(task);
1127                         assertEquals("test", task.getDataString());
1128                     }
1129                 } catch (Throwable t) {
1130                     error.compareAndSet(null, t);
1131                     System.err.println(path);
1132                     t.printStackTrace();
1133                 }
1134             });
1135             threads[i].setName("Task-" + i);
1136         }
1137         for (Thread thread : threads) {
1138             thread.start();
1139         }
1140         for (Thread thread : threads) {
1141             thread.join();
1142         }
1143         assertNull(error.get(), String.valueOf(error.get()));
1144     }
1145 
1146     @Test
1147     @Timeout(10)
1148     protected void testConnectTimeout() throws Exception {
1149         session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1150         int port = 1;
1151         newTransporter("http://localhost:" + port);
1152         try {
1153             transporter.get(new GetTask(URI.create("repo/file.txt")));
1154             fail("Expected error");
1155         } catch (Exception e) {
1156             // impl specific "timeout" exception
1157             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1158         }
1159     }
1160 
1161     @Test
1162     @Timeout(10)
1163     protected void testRequestTimeout() throws Exception {
1164         session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1165         ServerSocket server = new ServerSocket(0);
1166         try (server) {
1167             newTransporter("http://localhost:" + server.getLocalPort());
1168             try {
1169                 transporter.get(new GetTask(URI.create("repo/file.txt")));
1170                 fail("Expected error");
1171             } catch (Exception e) {
1172                 assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1173                 assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1174             }
1175         }
1176     }
1177 
1178     @Test
1179     protected void testUserAgent() throws Exception {
1180         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1181         newTransporter(httpServer.getHttpUrl());
1182         transporter.get(new GetTask(URI.create("repo/file.txt")));
1183         assertEquals(1, httpServer.getLogEntries().size());
1184         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1185             assertEquals("SomeTest/1.0", log.getHeaders().get("User-Agent"));
1186         }
1187     }
1188 
1189     @Test
1190     protected void testCustomHeaders() throws Exception {
1191         Map<String, String> headers = new HashMap<>();
1192         headers.put("User-Agent", "Custom/1.0");
1193         headers.put("X-CustomHeader", "Custom-Value");
1194         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1195         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1196         newTransporter(httpServer.getHttpUrl());
1197         transporter.get(new GetTask(URI.create("repo/file.txt")));
1198         assertEquals(1, httpServer.getLogEntries().size());
1199         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1200             for (Map.Entry<String, String> entry : headers.entrySet()) {
1201                 assertEquals(entry.getValue(), log.getHeaders().get(entry.getKey()), entry.getKey());
1202             }
1203         }
1204     }
1205 
1206     @Test
1207     protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1208         String username = "testuser", password = "testpass";
1209         httpServer.setProxyAuthentication(username, password);
1210         auth = new AuthenticationBuilder()
1211                 .addUsername(username)
1212                 .addPassword(password)
1213                 .build();
1214         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1215         newTransporter("http://" + httpServer.getHost() + ":12/");
1216         try {
1217             transporter.get(new GetTask(URI.create("repo/file.txt")));
1218             fail("Server auth must not be used as proxy auth");
1219         } catch (HttpTransporterException e) {
1220             assertEquals(407, e.getStatusCode());
1221         } catch (IOException e) {
1222             // accepted as well: point is to fail
1223         }
1224     }
1225 
1226     @Test
1227     protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1228         String username = "testuser", password = "testpass";
1229         httpServer.setAuthentication(username, password);
1230         Authentication auth = new AuthenticationBuilder()
1231                 .addUsername(username)
1232                 .addPassword(password)
1233                 .build();
1234         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1235         newTransporter("http://" + httpServer.getHost() + ":12/");
1236         try {
1237             transporter.get(new GetTask(URI.create("repo/file.txt")));
1238             fail("Proxy auth must not be used as server auth");
1239         } catch (HttpTransporterException e) {
1240             assertEquals(401, e.getStatusCode());
1241         } catch (IOException e) {
1242             // accepted as well: point is to fail
1243         }
1244     }
1245 
1246     @Test
1247     protected void testAuthSchemeReuse() throws Exception {
1248         httpServer.setAuthentication("testuser", "testpass");
1249         httpServer.setProxyAuthentication("proxyuser", "proxypass");
1250         session.setCache(new DefaultRepositoryCache());
1251         auth = new AuthenticationBuilder()
1252                 .addUsername("testuser")
1253                 .addPassword("testpass")
1254                 .build();
1255         Authentication auth = new AuthenticationBuilder()
1256                 .addUsername("proxyuser")
1257                 .addPassword("proxypass")
1258                 .build();
1259         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1260         newTransporter("http://bad.localhost:1/");
1261         GetTask task = new GetTask(URI.create("repo/file.txt"));
1262         transporter.get(task);
1263         assertEquals("test", task.getDataString());
1264         assertEquals(3, httpServer.getLogEntries().size());
1265         httpServer.getLogEntries().clear();
1266         newTransporter("http://bad.localhost:1/");
1267         task = new GetTask(URI.create("repo/file.txt"));
1268         transporter.get(task);
1269         assertEquals("test", task.getDataString());
1270         assertEquals(1, httpServer.getLogEntries().size());
1271         assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Authorization"));
1272         assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Proxy-Authorization"));
1273     }
1274 
1275     @Test
1276     protected void testAuthSchemePreemptive() throws Exception {
1277         httpServer.setAuthentication("testuser", "testpass");
1278         session.setCache(new DefaultRepositoryCache());
1279         auth = new AuthenticationBuilder()
1280                 .addUsername("testuser")
1281                 .addPassword("testpass")
1282                 .build();
1283 
1284         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1285         newTransporter(httpServer.getHttpUrl());
1286         GetTask task = new GetTask(URI.create("repo/file.txt"));
1287         transporter.get(task);
1288         assertEquals("test", task.getDataString());
1289         // there ARE challenge round-trips
1290         assertEquals(2, httpServer.getLogEntries().size());
1291 
1292         httpServer.getLogEntries().clear();
1293 
1294         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1295         newTransporter(httpServer.getHttpUrl());
1296         task = new GetTask(URI.create("repo/file.txt"));
1297         transporter.get(task);
1298         assertEquals("test", task.getDataString());
1299         // there are NO challenge round-trips, all goes through at first
1300         assertEquals(1, httpServer.getLogEntries().size());
1301     }
1302 
1303     @Test
1304     void testInit_BadProtocol() {
1305         assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1306     }
1307 
1308     @Test
1309     void testInit_BadUrl() {
1310         assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1311     }
1312 
1313     @Test
1314     void testInit_CaseInsensitiveProtocol() throws Exception {
1315         newTransporter("http://localhost");
1316         newTransporter("HTTP://localhost");
1317         newTransporter("Http://localhost");
1318         newTransporter("https://localhost");
1319         newTransporter("HTTPS://localhost");
1320         newTransporter("HttpS://localhost");
1321     }
1322 }