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         session.setConfigProperty(
585                 ConfigurationProperties.HTTPS_SECURITY_MODE, ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE);
586         httpServer.addSelfSignedSslConnectorHttp2Only();
587         newTransporter(httpServer.getHttpsUrl());
588         RecordingTransportListener listener = new RecordingTransportListener();
589         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
590         transporter.get(task);
591         assertEquals("test", task.getDataString());
592         assertEquals(0L, listener.getDataOffset());
593         assertEquals(4L, listener.getDataLength());
594         assertEquals(1, listener.getStartedCount());
595         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
596         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
597     }
598 
599     @Test
600     protected void testGet_Redirect() throws Exception {
601         httpServer.addSslConnector();
602         RecordingTransportListener listener = new RecordingTransportListener();
603         GetTask task = new GetTask(URI.create("redirect/file.txt?scheme=https")).setListener(listener);
604         transporter.get(task);
605         assertEquals("test", task.getDataString());
606         assertEquals(0L, listener.getDataOffset());
607         assertEquals(4L, listener.getDataLength());
608         assertEquals(1, listener.getStartedCount());
609         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
610         assertEquals(task.getDataString(), listener.getBaos().toString(StandardCharsets.UTF_8));
611     }
612 
613     @Test
614     protected void testGet_Resume() throws Exception {
615         File file = TestFileUtils.createTempFile("re");
616         RecordingTransportListener listener = new RecordingTransportListener();
617         GetTask task = new GetTask(URI.create("repo/resume.txt"))
618                 .setDataPath(file.toPath(), true)
619                 .setListener(listener);
620         transporter.get(task);
621         assertEquals("resumable", TestFileUtils.readString(file));
622         assertEquals(1L, listener.getStartedCount());
623         assertEquals(2L, listener.getDataOffset());
624         assertEquals(9, listener.getDataLength());
625         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
626         assertEquals("sumable", listener.getBaos().toString(StandardCharsets.UTF_8));
627     }
628 
629     @Test
630     protected void testGet_ResumeLocalContentsOutdated() throws Exception {
631         File file = TestFileUtils.createTempFile("re");
632         file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
633         RecordingTransportListener listener = new RecordingTransportListener();
634         GetTask task = new GetTask(URI.create("repo/resume.txt"))
635                 .setDataPath(file.toPath(), true)
636                 .setListener(listener);
637         transporter.get(task);
638         assertEquals("resumable", TestFileUtils.readString(file));
639         assertEquals(1L, listener.getStartedCount());
640         assertEquals(0L, listener.getDataOffset());
641         assertEquals(9, listener.getDataLength());
642         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
643         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
644     }
645 
646     @Test
647     protected void testGet_ResumeRangesNotSupportedByServer() throws Exception {
648         httpServer.setRangeSupport(false);
649         File file = TestFileUtils.createTempFile("re");
650         RecordingTransportListener listener = new RecordingTransportListener();
651         GetTask task = new GetTask(URI.create("repo/resume.txt"))
652                 .setDataPath(file.toPath(), true)
653                 .setListener(listener);
654         transporter.get(task);
655         assertEquals("resumable", TestFileUtils.readString(file));
656         assertEquals(1L, listener.getStartedCount());
657         assertEquals(0L, listener.getDataOffset());
658         assertEquals(9, listener.getDataLength());
659         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
660         assertEquals("resumable", listener.getBaos().toString(StandardCharsets.UTF_8));
661     }
662 
663     @Test
664     protected void testGet_Checksums_Nexus() throws Exception {
665         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.NEXUS);
666         GetTask task = new GetTask(URI.create("repo/file.txt"));
667         transporter.get(task);
668         assertEquals("test", task.getDataString());
669         assertEquals(
670                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
671     }
672 
673     @Test
674     protected void testGet_Checksums_XChecksum() throws Exception {
675         httpServer.setChecksumHeader(HttpServer.ChecksumHeader.XCHECKSUM);
676         GetTask task = new GetTask(URI.create("repo/file.txt"));
677         transporter.get(task);
678         assertEquals("test", task.getDataString());
679         assertEquals(
680                 "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", task.getChecksums().get("SHA-1"));
681     }
682 
683     @Test
684     protected void testGet_FileHandleLeak() throws Exception {
685         for (int i = 0; i < 100; i++) {
686             File file = TestFileUtils.createTempFile("failure");
687             transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
688             assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
689         }
690     }
691 
692     @Test
693     protected void testGet_NotFound() throws Exception {
694         try {
695             transporter.get(new GetTask(URI.create("repo/missing.txt")));
696             fail("Expected error");
697         } catch (HttpTransporterException e) {
698             assertEquals(404, e.getStatusCode());
699             assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(e));
700         }
701     }
702 
703     @Test
704     protected void testGet_Closed() throws Exception {
705         transporter.close();
706         try {
707             transporter.get(new GetTask(URI.create("repo/file.txt")));
708             fail("Expected error");
709         } catch (IllegalStateException e) {
710             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
711         }
712     }
713 
714     @Test
715     protected void testGet_StartCancelled() throws Exception {
716         RecordingTransportListener listener = new RecordingTransportListener();
717         listener.cancelStart();
718         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
719         try {
720             transporter.get(task);
721             fail("Expected error");
722         } catch (TransferCancelledException e) {
723             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
724         }
725         assertEquals(0L, listener.getDataOffset());
726         assertEquals(4L, listener.getDataLength());
727         assertEquals(1, listener.getStartedCount());
728         assertEquals(0, listener.getProgressedCount());
729     }
730 
731     @Test
732     protected void testGet_ProgressCancelled() throws Exception {
733         RecordingTransportListener listener = new RecordingTransportListener();
734         listener.cancelProgress();
735         GetTask task = new GetTask(URI.create("repo/file.txt")).setListener(listener);
736         try {
737             transporter.get(task);
738             fail("Expected error");
739         } catch (TransferCancelledException e) {
740             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
741         }
742         assertEquals(0L, listener.getDataOffset());
743         assertEquals(4L, listener.getDataLength());
744         assertEquals(1, listener.getStartedCount());
745         assertEquals(1, listener.getProgressedCount());
746     }
747 
748     @Test
749     protected void testPut_FromMemory() throws Exception {
750         RecordingTransportListener listener = new RecordingTransportListener();
751         PutTask task =
752                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
753         transporter.put(task);
754         assertEquals(0L, listener.getDataOffset());
755         assertEquals(6L, listener.getDataLength());
756         assertEquals(1, listener.getStartedCount());
757         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
758         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
759     }
760 
761     @Test
762     protected void testPut_FromFile() throws Exception {
763         File file = TestFileUtils.createTempFile("upload");
764         RecordingTransportListener listener = new RecordingTransportListener();
765         PutTask task =
766                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
767         transporter.put(task);
768         assertEquals(0L, listener.getDataOffset());
769         assertEquals(6L, listener.getDataLength());
770         assertEquals(1, listener.getStartedCount());
771         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
772         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
773     }
774 
775     @Test
776     protected void testPut_EmptyResource() throws Exception {
777         RecordingTransportListener listener = new RecordingTransportListener();
778         PutTask task = new PutTask(URI.create("repo/file.txt")).setListener(listener);
779         transporter.put(task);
780         assertEquals(0L, listener.getDataOffset());
781         assertEquals(0L, listener.getDataLength());
782         assertEquals(1, listener.getStartedCount());
783         assertEquals(0, listener.getProgressedCount());
784         assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
785     }
786 
787     @Test
788     protected void testPut_EncodedResourcePath() throws Exception {
789         RecordingTransportListener listener = new RecordingTransportListener();
790         PutTask task = new PutTask(URI.create("repo/some%20space.txt"))
791                 .setListener(listener)
792                 .setDataString("OK");
793         transporter.put(task);
794         assertEquals(0L, listener.getDataOffset());
795         assertEquals(2L, listener.getDataLength());
796         assertEquals(1, listener.getStartedCount());
797         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
798         assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
799     }
800 
801     @Test
802     protected void testPut_Authenticated_ExpectContinue() throws Exception {
803         httpServer.setAuthentication("testuser", "testpass");
804         auth = new AuthenticationBuilder()
805                 .addUsername("testuser")
806                 .addPassword("testpass")
807                 .build();
808         newTransporter(httpServer.getHttpUrl());
809         RecordingTransportListener listener = new RecordingTransportListener();
810         PutTask task =
811                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
812         transporter.put(task);
813         assertEquals(0L, listener.getDataOffset());
814         assertEquals(6L, listener.getDataLength());
815         assertEquals(1, listener.getStartedCount());
816         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
817         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
818     }
819 
820     @Test
821     protected void testPut_Authenticated_ExpectContinueBroken() throws Exception {
822         // this makes OPTIONS recover, and have only 1 PUT (startedCount=1 as OPTIONS is not counted)
823         session.setConfigProperty(ConfigurationProperties.HTTP_SUPPORT_WEBDAV, true);
824         httpServer.setAuthentication("testuser", "testpass");
825         httpServer.setExpectSupport(HttpServer.ExpectContinue.BROKEN);
826         auth = new AuthenticationBuilder()
827                 .addUsername("testuser")
828                 .addPassword("testpass")
829                 .build();
830         newTransporter(httpServer.getHttpUrl());
831         RecordingTransportListener listener = new RecordingTransportListener();
832         PutTask task =
833                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
834         transporter.put(task);
835         assertEquals(0L, listener.getDataOffset());
836         assertEquals(6L, listener.getDataLength());
837         assertEquals(1, listener.getStartedCount());
838         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
839         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
840     }
841 
842     @Test
843     protected void testPut_Authenticated_ExpectContinueRejected() throws Exception {
844         httpServer.setAuthentication("testuser", "testpass");
845         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
846         auth = new AuthenticationBuilder()
847                 .addUsername("testuser")
848                 .addPassword("testpass")
849                 .build();
850         newTransporter(httpServer.getHttpUrl());
851         RecordingTransportListener listener = new RecordingTransportListener();
852         PutTask task =
853                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
854         transporter.put(task);
855         assertEquals(0L, listener.getDataOffset());
856         assertEquals(6L, listener.getDataLength());
857         assertEquals(1, listener.getStartedCount());
858         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
859         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
860     }
861 
862     @Test
863     protected void testPut_Authenticated_ExpectContinueDisabled() throws Exception {
864         session.setConfigProperty(ConfigurationProperties.HTTP_EXPECT_CONTINUE, false);
865         httpServer.setAuthentication("testuser", "testpass");
866         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL); // if transport tries Expect/Continue explode
867         auth = new AuthenticationBuilder()
868                 .addUsername("testuser")
869                 .addPassword("testpass")
870                 .build();
871         newTransporter(httpServer.getHttpUrl());
872         RecordingTransportListener listener = new RecordingTransportListener();
873         PutTask task =
874                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
875         transporter.put(task);
876         assertEquals(0L, listener.getDataOffset());
877         assertEquals(6L, listener.getDataLength());
878         assertEquals(1, listener.getStartedCount()); // w/ expectContinue enabled would have here 2
879         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
880         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
881     }
882 
883     @Test
884     protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() throws Exception {
885         Map<String, String> headers = new HashMap<>();
886         headers.put("Expect", "100-continue");
887         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
888         httpServer.setAuthentication("testuser", "testpass");
889         httpServer.setExpectSupport(HttpServer.ExpectContinue.FAIL);
890         auth = new AuthenticationBuilder()
891                 .addUsername("testuser")
892                 .addPassword("testpass")
893                 .build();
894         newTransporter(httpServer.getHttpUrl());
895         RecordingTransportListener listener = new RecordingTransportListener();
896         PutTask task =
897                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
898         transporter.put(task);
899         assertEquals(0L, listener.getDataOffset());
900         assertEquals(6L, listener.getDataLength());
901         assertEquals(1, listener.getStartedCount());
902         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
903         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
904     }
905 
906     @Test
907     protected void testPut_Unauthenticated() throws Exception {
908         httpServer.setAuthentication("testuser", "testpass");
909         RecordingTransportListener listener = new RecordingTransportListener();
910         PutTask task =
911                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
912         try {
913             transporter.put(task);
914             fail("Expected error");
915         } catch (HttpTransporterException e) {
916             assertEquals(401, e.getStatusCode());
917             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
918         }
919         assertEquals(0, listener.getStartedCount());
920         assertEquals(0, listener.getProgressedCount());
921     }
922 
923     @Test
924     protected void testPut_ProxyAuthenticated() throws Exception {
925         httpServer.setProxyAuthentication("testuser", "testpass");
926         Authentication auth = new AuthenticationBuilder()
927                 .addUsername("testuser")
928                 .addPassword("testpass")
929                 .build();
930         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
931         newTransporter("http://bad.localhost:1/");
932         RecordingTransportListener listener = new RecordingTransportListener();
933         PutTask task =
934                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
935         transporter.put(task);
936         assertEquals(0L, listener.getDataOffset());
937         assertEquals(6L, listener.getDataLength());
938         assertEquals(1, listener.getStartedCount());
939         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
940         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
941     }
942 
943     @Test
944     protected void testPut_ProxyUnauthenticated() throws Exception {
945         httpServer.setProxyAuthentication("testuser", "testpass");
946         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
947         newTransporter("http://bad.localhost:1/");
948         RecordingTransportListener listener = new RecordingTransportListener();
949         PutTask task =
950                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
951         try {
952             transporter.put(task);
953             fail("Expected error");
954         } catch (HttpTransporterException e) {
955             assertEquals(407, e.getStatusCode());
956             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
957         }
958         assertEquals(0, listener.getStartedCount());
959         assertEquals(0, listener.getProgressedCount());
960     }
961 
962     @Test
963     protected void testPut_SSL() throws Exception {
964         httpServer.addSslConnector();
965         httpServer.setAuthentication("testuser", "testpass");
966         auth = new AuthenticationBuilder()
967                 .addUsername("testuser")
968                 .addPassword("testpass")
969                 .build();
970         newTransporter(httpServer.getHttpsUrl());
971         RecordingTransportListener listener = new RecordingTransportListener();
972         PutTask task =
973                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
974         transporter.put(task);
975         assertEquals(0L, listener.getDataOffset());
976         assertEquals(6L, listener.getDataLength());
977         assertEquals(1, listener.getStartedCount());
978         assertTrue(listener.getProgressedCount() > 0, "Count: " + listener.getProgressedCount());
979         assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
980     }
981 
982     @Test
983     protected void testPut_FileHandleLeak() throws Exception {
984         for (int i = 0; i < 100; i++) {
985             File src = TestFileUtils.createTempFile("upload");
986             File dst = new File(repoDir, "file.txt");
987             transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
988             assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
989             assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
990         }
991     }
992 
993     @Test
994     protected void testPut_Closed() throws Exception {
995         transporter.close();
996         try {
997             transporter.put(new PutTask(URI.create("repo/missing.txt")));
998             fail("Expected error");
999         } catch (IllegalStateException e) {
1000             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1001         }
1002     }
1003 
1004     @Test
1005     protected void testPut_StartCancelled() throws Exception {
1006         RecordingTransportListener listener = new RecordingTransportListener();
1007         listener.cancelStart();
1008         PutTask task =
1009                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1010         try {
1011             transporter.put(task);
1012             fail("Expected error");
1013         } catch (TransferCancelledException e) {
1014             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1015         }
1016         assertEquals(0L, listener.getDataOffset());
1017         assertEquals(6L, listener.getDataLength());
1018         assertEquals(1, listener.getStartedCount());
1019         assertEquals(0, listener.getProgressedCount());
1020     }
1021 
1022     @Test
1023     protected void testPut_ProgressCancelled() throws Exception {
1024         RecordingTransportListener listener = new RecordingTransportListener();
1025         listener.cancelProgress();
1026         PutTask task =
1027                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1028         try {
1029             transporter.put(task);
1030             fail("Expected error");
1031         } catch (TransferCancelledException e) {
1032             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1033         }
1034         assertEquals(0L, listener.getDataOffset());
1035         assertEquals(6L, listener.getDataLength());
1036         assertEquals(1, listener.getStartedCount());
1037         assertEquals(1, listener.getProgressedCount());
1038     }
1039 
1040     @Test
1041     protected void testGetPut_AuthCache() throws Exception {
1042         httpServer.setAuthentication("testuser", "testpass");
1043         auth = new AuthenticationBuilder()
1044                 .addUsername("testuser")
1045                 .addPassword("testpass")
1046                 .build();
1047         newTransporter(httpServer.getHttpUrl());
1048         GetTask get = new GetTask(URI.create("repo/file.txt"));
1049         transporter.get(get);
1050         RecordingTransportListener listener = new RecordingTransportListener();
1051         PutTask task =
1052                 new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataString("upload");
1053         transporter.put(task);
1054         assertEquals(1, listener.getStartedCount());
1055     }
1056 
1057     @Test
1058     protected void testPut_PreemptiveIsDefault() throws Exception {
1059         httpServer.setAuthentication("testuser", "testpass");
1060         auth = new AuthenticationBuilder()
1061                 .addUsername("testuser")
1062                 .addPassword("testpass")
1063                 .build();
1064         newTransporter(httpServer.getHttpUrl());
1065         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1066         transporter.put(task);
1067         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1068     }
1069 
1070     @Test
1071     protected void testPut_AuthCache() throws Exception {
1072         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH, false);
1073         httpServer.setAuthentication("testuser", "testpass");
1074         auth = new AuthenticationBuilder()
1075                 .addUsername("testuser")
1076                 .addPassword("testpass")
1077                 .build();
1078         newTransporter(httpServer.getHttpUrl());
1079         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1080         transporter.put(task);
1081         assertEquals(2, httpServer.getLogEntries().size()); // put (challenged) + put w/ auth
1082         httpServer.getLogEntries().clear();
1083         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1084         transporter.put(task);
1085         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1086     }
1087 
1088     @Test
1089     protected void testPut_AuthCache_Preemptive() throws Exception {
1090         httpServer.setAuthentication("testuser", "testpass");
1091         auth = new AuthenticationBuilder()
1092                 .addUsername("testuser")
1093                 .addPassword("testpass")
1094                 .build();
1095         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1096         newTransporter(httpServer.getHttpUrl());
1097         PutTask task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1098         transporter.put(task);
1099         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1100         httpServer.getLogEntries().clear();
1101         task = new PutTask(URI.create("repo/file.txt")).setDataString("upload");
1102         transporter.put(task);
1103         assertEquals(1, httpServer.getLogEntries().size()); // put w/ auth
1104     }
1105 
1106     @Test
1107     @Timeout(20)
1108     protected void testConcurrency() throws Exception {
1109         httpServer.setAuthentication("testuser", "testpass");
1110         auth = new AuthenticationBuilder()
1111                 .addUsername("testuser")
1112                 .addPassword("testpass")
1113                 .build();
1114         newTransporter(httpServer.getHttpUrl());
1115         final AtomicReference<Throwable> error = new AtomicReference<>();
1116         Thread[] threads = new Thread[20];
1117         for (int i = 0; i < threads.length; i++) {
1118             final String path = "repo/file.txt?i=" + i;
1119             threads[i] = new Thread(() -> {
1120                 try {
1121                     for (int j = 0; j < 100; j++) {
1122                         GetTask task = new GetTask(URI.create(path));
1123                         transporter.get(task);
1124                         assertEquals("test", task.getDataString());
1125                     }
1126                 } catch (Throwable t) {
1127                     error.compareAndSet(null, t);
1128                     System.err.println(path);
1129                     t.printStackTrace();
1130                 }
1131             });
1132             threads[i].setName("Task-" + i);
1133         }
1134         for (Thread thread : threads) {
1135             thread.start();
1136         }
1137         for (Thread thread : threads) {
1138             thread.join();
1139         }
1140         assertNull(error.get(), String.valueOf(error.get()));
1141     }
1142 
1143     @Test
1144     @Timeout(10)
1145     protected void testConnectTimeout() throws Exception {
1146         session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, 100);
1147         int port = 1;
1148         newTransporter("http://localhost:" + port);
1149         try {
1150             transporter.get(new GetTask(URI.create("repo/file.txt")));
1151             fail("Expected error");
1152         } catch (Exception e) {
1153             // impl specific "timeout" exception
1154             assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1155         }
1156     }
1157 
1158     @Test
1159     @Timeout(10)
1160     protected void testRequestTimeout() throws Exception {
1161         session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, 100);
1162         ServerSocket server = new ServerSocket(0);
1163         try (server) {
1164             newTransporter("http://localhost:" + server.getLocalPort());
1165             try {
1166                 transporter.get(new GetTask(URI.create("repo/file.txt")));
1167                 fail("Expected error");
1168             } catch (Exception e) {
1169                 assertTrue(e.getClass().getSimpleName().contains("Timeout"));
1170                 assertEquals(Transporter.ERROR_OTHER, transporter.classify(e));
1171             }
1172         }
1173     }
1174 
1175     @Test
1176     protected void testUserAgent() throws Exception {
1177         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1178         newTransporter(httpServer.getHttpUrl());
1179         transporter.get(new GetTask(URI.create("repo/file.txt")));
1180         assertEquals(1, httpServer.getLogEntries().size());
1181         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1182             assertEquals("SomeTest/1.0", log.getHeaders().get("User-Agent"));
1183         }
1184     }
1185 
1186     @Test
1187     protected void testCustomHeaders() throws Exception {
1188         Map<String, String> headers = new HashMap<>();
1189         headers.put("User-Agent", "Custom/1.0");
1190         headers.put("X-CustomHeader", "Custom-Value");
1191         session.setConfigProperty(ConfigurationProperties.USER_AGENT, "SomeTest/1.0");
1192         session.setConfigProperty(ConfigurationProperties.HTTP_HEADERS + ".test", headers);
1193         newTransporter(httpServer.getHttpUrl());
1194         transporter.get(new GetTask(URI.create("repo/file.txt")));
1195         assertEquals(1, httpServer.getLogEntries().size());
1196         for (HttpServer.LogEntry log : httpServer.getLogEntries()) {
1197             for (Map.Entry<String, String> entry : headers.entrySet()) {
1198                 assertEquals(entry.getValue(), log.getHeaders().get(entry.getKey()), entry.getKey());
1199             }
1200         }
1201     }
1202 
1203     @Test
1204     protected void testServerAuthScope_NotUsedForProxy() throws Exception {
1205         String username = "testuser", password = "testpass";
1206         httpServer.setProxyAuthentication(username, password);
1207         auth = new AuthenticationBuilder()
1208                 .addUsername(username)
1209                 .addPassword(password)
1210                 .build();
1211         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort());
1212         newTransporter("http://" + httpServer.getHost() + ":12/");
1213         try {
1214             transporter.get(new GetTask(URI.create("repo/file.txt")));
1215             fail("Server auth must not be used as proxy auth");
1216         } catch (HttpTransporterException e) {
1217             assertEquals(407, e.getStatusCode());
1218         } catch (IOException e) {
1219             // accepted as well: point is to fail
1220         }
1221     }
1222 
1223     @Test
1224     protected void testProxyAuthScope_NotUsedForServer() throws Exception {
1225         String username = "testuser", password = "testpass";
1226         httpServer.setAuthentication(username, password);
1227         Authentication auth = new AuthenticationBuilder()
1228                 .addUsername(username)
1229                 .addPassword(password)
1230                 .build();
1231         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1232         newTransporter("http://" + httpServer.getHost() + ":12/");
1233         try {
1234             transporter.get(new GetTask(URI.create("repo/file.txt")));
1235             fail("Proxy auth must not be used as server auth");
1236         } catch (HttpTransporterException e) {
1237             assertEquals(401, e.getStatusCode());
1238         } catch (IOException e) {
1239             // accepted as well: point is to fail
1240         }
1241     }
1242 
1243     @Test
1244     protected void testAuthSchemeReuse() throws Exception {
1245         httpServer.setAuthentication("testuser", "testpass");
1246         httpServer.setProxyAuthentication("proxyuser", "proxypass");
1247         session.setCache(new DefaultRepositoryCache());
1248         auth = new AuthenticationBuilder()
1249                 .addUsername("testuser")
1250                 .addPassword("testpass")
1251                 .build();
1252         Authentication auth = new AuthenticationBuilder()
1253                 .addUsername("proxyuser")
1254                 .addPassword("proxypass")
1255                 .build();
1256         proxy = new Proxy(Proxy.TYPE_HTTP, httpServer.getHost(), httpServer.getHttpPort(), auth);
1257         newTransporter("http://bad.localhost:1/");
1258         GetTask task = new GetTask(URI.create("repo/file.txt"));
1259         transporter.get(task);
1260         assertEquals("test", task.getDataString());
1261         assertEquals(3, httpServer.getLogEntries().size());
1262         httpServer.getLogEntries().clear();
1263         newTransporter("http://bad.localhost:1/");
1264         task = new GetTask(URI.create("repo/file.txt"));
1265         transporter.get(task);
1266         assertEquals("test", task.getDataString());
1267         assertEquals(1, httpServer.getLogEntries().size());
1268         assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Authorization"));
1269         assertNotNull(httpServer.getLogEntries().get(0).getHeaders().get("Proxy-Authorization"));
1270     }
1271 
1272     @Test
1273     protected void testAuthSchemePreemptive() throws Exception {
1274         httpServer.setAuthentication("testuser", "testpass");
1275         session.setCache(new DefaultRepositoryCache());
1276         auth = new AuthenticationBuilder()
1277                 .addUsername("testuser")
1278                 .addPassword("testpass")
1279                 .build();
1280 
1281         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, false);
1282         newTransporter(httpServer.getHttpUrl());
1283         GetTask task = new GetTask(URI.create("repo/file.txt"));
1284         transporter.get(task);
1285         assertEquals("test", task.getDataString());
1286         // there ARE challenge round-trips
1287         assertEquals(2, httpServer.getLogEntries().size());
1288 
1289         httpServer.getLogEntries().clear();
1290 
1291         session.setConfigProperty(ConfigurationProperties.HTTP_PREEMPTIVE_AUTH, true);
1292         newTransporter(httpServer.getHttpUrl());
1293         task = new GetTask(URI.create("repo/file.txt"));
1294         transporter.get(task);
1295         assertEquals("test", task.getDataString());
1296         // there are NO challenge round-trips, all goes through at first
1297         assertEquals(1, httpServer.getLogEntries().size());
1298     }
1299 
1300     @Test
1301     void testInit_BadProtocol() {
1302         assertThrows(NoTransporterException.class, () -> newTransporter("bad:/void"));
1303     }
1304 
1305     @Test
1306     void testInit_BadUrl() {
1307         assertThrows(NoTransporterException.class, () -> newTransporter("http://localhost:NaN"));
1308     }
1309 
1310     @Test
1311     void testInit_CaseInsensitiveProtocol() throws Exception {
1312         newTransporter("http://localhost");
1313         newTransporter("HTTP://localhost");
1314         newTransporter("Http://localhost");
1315         newTransporter("https://localhost");
1316         newTransporter("HTTPS://localhost");
1317         newTransporter("HttpS://localhost");
1318     }
1319 }