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.connector.basic;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.URI;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.HashMap;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  
33  import org.eclipse.aether.internal.test.util.TestChecksumProcessor;
34  import org.eclipse.aether.internal.test.util.TestFileUtils;
35  import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
36  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
37  import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
38  import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
39  import org.eclipse.aether.spi.io.PathProcessorSupport;
40  import org.eclipse.aether.transfer.ChecksumFailureException;
41  import org.junit.jupiter.api.BeforeEach;
42  import org.junit.jupiter.api.Test;
43  
44  import static org.eclipse.aether.connector.basic.TestChecksumAlgorithmSelector.MD5;
45  import static org.eclipse.aether.connector.basic.TestChecksumAlgorithmSelector.SHA1;
46  import static org.junit.jupiter.api.Assertions.*;
47  
48  public class ChecksumValidatorTest {
49  
50      private static class StubChecksumPolicy implements ChecksumPolicy {
51  
52          boolean inspectAll;
53  
54          boolean tolerateFailure;
55  
56          private final ArrayList<String> callbacks = new ArrayList<>();
57  
58          private Object conclusion;
59  
60          @Override
61          public boolean onChecksumMatch(String algorithm, ChecksumKind kind) {
62              callbacks.add(String.format("match(%s, %s)", algorithm, kind));
63              if (inspectAll) {
64                  if (conclusion == null) {
65                      conclusion = true;
66                  }
67                  return false;
68              }
69              return true;
70          }
71  
72          @Override
73          public void onChecksumMismatch(String algorithm, ChecksumKind kind, ChecksumFailureException exception)
74                  throws ChecksumFailureException {
75              callbacks.add(String.format("mismatch(%s, %s)", algorithm, kind));
76              if (inspectAll) {
77                  conclusion = exception;
78                  return;
79              }
80              throw exception;
81          }
82  
83          @Override
84          public void onChecksumError(String algorithm, ChecksumKind kind, ChecksumFailureException exception) {
85              callbacks.add(String.format(
86                      "error(%s, %s, %s)", algorithm, kind, exception.getCause().getMessage()));
87          }
88  
89          @Override
90          public void onNoMoreChecksums() throws ChecksumFailureException {
91              callbacks.add("noMore()");
92              if (conclusion instanceof ChecksumFailureException) {
93                  throw (ChecksumFailureException) conclusion;
94              } else if (!Boolean.TRUE.equals(conclusion)) {
95                  throw new ChecksumFailureException("no checksums");
96              }
97          }
98  
99          @Override
100         public void onTransferRetry() {
101             callbacks.add("retry()");
102         }
103 
104         @Override
105         public boolean onTransferChecksumFailure(ChecksumFailureException exception) {
106             callbacks.add(String.format("fail(%s)", exception.getMessage()));
107             return tolerateFailure;
108         }
109 
110         void assertCallbacks(String... callbacks) {
111             assertEquals(Arrays.asList(callbacks), this.callbacks);
112         }
113     }
114 
115     private static class StubChecksumFetcher implements ChecksumValidator.ChecksumFetcher {
116 
117         HashMap<URI, Object> checksums = new HashMap<>();
118 
119         ArrayList<Path> checksumFiles = new ArrayList<>();
120 
121         private final ArrayList<URI> fetchedFiles = new ArrayList<>();
122 
123         @Override
124         public boolean fetchChecksum(URI remote, Path local) throws Exception {
125             fetchedFiles.add(remote);
126             Object checksum = checksums.get(remote);
127             if (checksum == null) {
128                 return false;
129             }
130             if (checksum instanceof Exception) {
131                 throw (Exception) checksum;
132             }
133             TestFileUtils.writeString(local.toFile(), checksum.toString());
134             checksumFiles.add(local);
135             return true;
136         }
137 
138         void mock(String algo, Object value) {
139             checksums.put(toUri(algo), value);
140         }
141 
142         void assertFetchedFiles(String... algos) {
143             List<URI> expected = new ArrayList<>();
144             for (String algo : algos) {
145                 expected.add(toUri(algo));
146             }
147             assertEquals(expected, fetchedFiles);
148         }
149 
150         private static URI toUri(String algo) {
151             return newChecksum(algo).getLocation();
152         }
153     }
154 
155     private StubChecksumPolicy policy;
156 
157     private StubChecksumFetcher fetcher;
158 
159     private File dataFile;
160 
161     private static final TestChecksumAlgorithmSelector selector = new TestChecksumAlgorithmSelector();
162 
163     private List<ChecksumAlgorithmFactory> newChecksumAlgorithmFactories(String... factories) {
164         List<ChecksumAlgorithmFactory> checksums = new ArrayList<>();
165         for (String factory : factories) {
166             checksums.add(selector.select(factory));
167         }
168         return checksums;
169     }
170 
171     private static RepositoryLayout.ChecksumLocation newChecksum(String factory) {
172         return RepositoryLayout.ChecksumLocation.forLocation(URI.create("file"), selector.select(factory));
173     }
174 
175     private List<RepositoryLayout.ChecksumLocation> newChecksums(
176             List<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
177         List<RepositoryLayout.ChecksumLocation> checksums = new ArrayList<>();
178         for (ChecksumAlgorithmFactory factory : checksumAlgorithmFactories) {
179             checksums.add(RepositoryLayout.ChecksumLocation.forLocation(URI.create("file"), factory));
180         }
181         return checksums;
182     }
183 
184     private ChecksumValidator newValidator(String... factories) {
185         return newValidator(null, factories);
186     }
187 
188     private ChecksumValidator newValidator(Map<String, String> providedChecksums, String... factories) {
189         List<ChecksumAlgorithmFactory> checksumAlgorithmFactories = newChecksumAlgorithmFactories(factories);
190         return new ChecksumValidator(
191                 dataFile.toPath(),
192                 checksumAlgorithmFactories,
193                 new PathProcessorSupport(),
194                 new TestChecksumProcessor(),
195                 fetcher,
196                 policy,
197                 providedChecksums,
198                 newChecksums(checksumAlgorithmFactories));
199     }
200 
201     private Map<String, ?> checksums(String... algoDigestPairs) {
202         Map<String, Object> checksums = new LinkedHashMap<>();
203         for (int i = 0; i < algoDigestPairs.length; i += 2) {
204             String algo = algoDigestPairs[i];
205             String digest = algoDigestPairs[i + 1];
206             if (digest == null) {
207                 checksums.put(algo, new IOException("error"));
208             } else {
209                 checksums.put(algo, digest);
210             }
211         }
212         return checksums;
213     }
214 
215     @BeforeEach
216     void init() throws Exception {
217         dataFile = TestFileUtils.createTempFile("");
218         dataFile.delete();
219         policy = new StubChecksumPolicy();
220         fetcher = new StubChecksumFetcher();
221     }
222 
223     @Test
224     void testValidate_NullPolicy() throws Exception {
225         policy = null;
226         ChecksumValidator validator = newValidator(SHA1);
227         validator.validate(checksums(SHA1, "ignored"), null);
228         fetcher.assertFetchedFiles();
229     }
230 
231     @Test
232     void testValidate_AcceptOnFirstMatch() throws Exception {
233         ChecksumValidator validator = newValidator(SHA1);
234         fetcher.mock(SHA1, "foo");
235         validator.validate(checksums(SHA1, "foo"), null);
236         fetcher.assertFetchedFiles(SHA1);
237         policy.assertCallbacks("match(SHA-1, REMOTE_EXTERNAL)");
238     }
239 
240     @Test
241     void testValidate_FailOnFirstMismatch() {
242         ChecksumValidator validator = newValidator(SHA1);
243         fetcher.mock(SHA1, "foo");
244         try {
245             validator.validate(checksums(SHA1, "not-foo"), null);
246             fail("expected exception");
247         } catch (ChecksumFailureException e) {
248             assertEquals("foo", e.getExpected());
249             assertEquals(ChecksumKind.REMOTE_EXTERNAL.name(), e.getExpectedKind());
250             assertEquals("not-foo", e.getActual());
251             assertTrue(e.isRetryWorthy());
252         }
253         fetcher.assertFetchedFiles(SHA1);
254         policy.assertCallbacks("mismatch(SHA-1, REMOTE_EXTERNAL)");
255     }
256 
257     @Test
258     void testValidate_ProvidedAllComparableAlgorithmsMustMatch() {
259         // the weaker algorithm matches, the stronger one does not: the artifact must be rejected regardless
260         // of map iteration order (previously the first matching algorithm accepted the artifact and skipped
261         // the remaining, stronger checksums)
262         Map<String, String> provided = new LinkedHashMap<>();
263         provided.put(MD5, "bar");
264         provided.put(SHA1, "foo");
265         ChecksumValidator validator = newValidator(provided, SHA1, MD5);
266         try {
267             validator.validate(checksums(SHA1, "not-foo", MD5, "bar"), null);
268             fail("expected exception");
269         } catch (ChecksumFailureException e) {
270             assertEquals("foo", e.getExpected());
271             assertEquals(ChecksumKind.PROVIDED.name(), e.getExpectedKind());
272             assertEquals("not-foo", e.getActual());
273             assertTrue(e.isRetryWorthy());
274         }
275         fetcher.assertFetchedFiles();
276         policy.assertCallbacks("match(MD5, PROVIDED)", "mismatch(SHA-1, PROVIDED)");
277     }
278 
279     @Test
280     void testValidate_ProvidedAllMatchingAccepts() throws Exception {
281         Map<String, String> provided = new LinkedHashMap<>();
282         provided.put(SHA1, "foo");
283         provided.put(MD5, "bar");
284         ChecksumValidator validator = newValidator(provided, SHA1, MD5);
285         validator.validate(checksums(SHA1, "foo", MD5, "bar"), null);
286         fetcher.assertFetchedFiles();
287         policy.assertCallbacks("match(SHA-1, PROVIDED)", "match(MD5, PROVIDED)");
288     }
289 
290     @Test
291     void testValidate_IncludedWeakMatchDoesNotMaskStrongMismatch() {
292         ChecksumValidator validator = newValidator(SHA1, MD5);
293         try {
294             validator.validate(checksums(SHA1, "not-foo", MD5, "bar"), checksums(MD5, "bar", SHA1, "foo"));
295             fail("expected exception");
296         } catch (ChecksumFailureException e) {
297             assertEquals("foo", e.getExpected());
298             assertEquals(ChecksumKind.REMOTE_INCLUDED.name(), e.getExpectedKind());
299             assertEquals("not-foo", e.getActual());
300         }
301         fetcher.assertFetchedFiles();
302         policy.assertCallbacks("match(MD5, REMOTE_INCLUDED)", "mismatch(SHA-1, REMOTE_INCLUDED)");
303     }
304 
305     @Test
306     void testValidate_AcceptOnEnd() throws Exception {
307         policy.inspectAll = true;
308         ChecksumValidator validator = newValidator(SHA1, MD5);
309         fetcher.mock(SHA1, "foo");
310         fetcher.mock(MD5, "bar");
311         validator.validate(checksums(SHA1, "foo", MD5, "bar"), null);
312         fetcher.assertFetchedFiles(SHA1, MD5);
313         policy.assertCallbacks("match(SHA-1, REMOTE_EXTERNAL)", "match(MD5, REMOTE_EXTERNAL)", "noMore()");
314     }
315 
316     @Test
317     void testValidate_FailOnEnd() {
318         policy.inspectAll = true;
319         ChecksumValidator validator = newValidator(SHA1, MD5);
320         fetcher.mock(SHA1, "foo");
321         fetcher.mock(MD5, "bar");
322         try {
323             validator.validate(checksums(SHA1, "not-foo", MD5, "bar"), null);
324             fail("expected exception");
325         } catch (ChecksumFailureException e) {
326             assertEquals("foo", e.getExpected());
327             assertEquals(ChecksumKind.REMOTE_EXTERNAL.name(), e.getExpectedKind());
328             assertEquals("not-foo", e.getActual());
329             assertTrue(e.isRetryWorthy());
330         }
331         fetcher.assertFetchedFiles(SHA1, MD5);
332         policy.assertCallbacks("mismatch(SHA-1, REMOTE_EXTERNAL)", "match(MD5, REMOTE_EXTERNAL)", "noMore()");
333     }
334 
335     @Test
336     void testValidate_IncludedBeforeExternal() throws Exception {
337         policy.inspectAll = true;
338         HashMap<String, String> provided = new HashMap<>();
339         provided.put(SHA1, "foo");
340         ChecksumValidator validator = newValidator(provided, SHA1, MD5);
341         fetcher.mock(SHA1, "foo");
342         fetcher.mock(MD5, "bar");
343         validator.validate(checksums(SHA1, "foo", MD5, "bar"), checksums(SHA1, "foo", MD5, "bar"));
344         fetcher.assertFetchedFiles(SHA1, MD5);
345         policy.assertCallbacks(
346                 "match(SHA-1, PROVIDED)",
347                 "match(SHA-1, REMOTE_INCLUDED)",
348                 "match(MD5, REMOTE_INCLUDED)",
349                 "match(SHA-1, REMOTE_EXTERNAL)",
350                 "match(MD5, REMOTE_EXTERNAL)",
351                 "noMore()");
352     }
353 
354     @Test
355     void testValidate_CaseInsensitive() throws Exception {
356         policy.inspectAll = true;
357         ChecksumValidator validator = newValidator(SHA1);
358         fetcher.mock(SHA1, "FOO");
359         validator.validate(checksums(SHA1, "foo"), checksums(SHA1, "foo"));
360         policy.assertCallbacks("match(SHA-1, REMOTE_INCLUDED)", "match(SHA-1, REMOTE_EXTERNAL)", "noMore()");
361     }
362 
363     @Test
364     void testValidate_MissingRemoteChecksum() throws Exception {
365         ChecksumValidator validator = newValidator(SHA1, MD5);
366         fetcher.mock(MD5, "bar");
367         validator.validate(checksums(MD5, "bar"), null);
368         fetcher.assertFetchedFiles(SHA1, MD5);
369         policy.assertCallbacks("match(MD5, REMOTE_EXTERNAL)");
370     }
371 
372     @Test
373     void testValidate_InaccessibleRemoteChecksum() throws Exception {
374         ChecksumValidator validator = newValidator(SHA1, MD5);
375         fetcher.mock(SHA1, new IOException("inaccessible"));
376         fetcher.mock(MD5, "bar");
377         validator.validate(checksums(MD5, "bar"), null);
378         fetcher.assertFetchedFiles(SHA1, MD5);
379         policy.assertCallbacks("error(SHA-1, REMOTE_EXTERNAL, inaccessible)", "match(MD5, REMOTE_EXTERNAL)");
380     }
381 
382     @Test
383     void testValidate_InaccessibleLocalChecksum() throws Exception {
384         ChecksumValidator validator = newValidator(SHA1, MD5);
385         fetcher.mock(SHA1, "foo");
386         fetcher.mock(MD5, "bar");
387         validator.validate(checksums(SHA1, null, MD5, "bar"), null);
388         fetcher.assertFetchedFiles(MD5);
389         policy.assertCallbacks("error(SHA-1, REMOTE_EXTERNAL, error)", "match(MD5, REMOTE_EXTERNAL)");
390     }
391 
392     @Test
393     void testHandle_Accept() {
394         policy.tolerateFailure = true;
395         ChecksumValidator validator = newValidator(SHA1);
396         assertTrue(validator.handle(new ChecksumFailureException("accept")));
397         policy.assertCallbacks("fail(accept)");
398     }
399 
400     @Test
401     void testHandle_Reject() {
402         policy.tolerateFailure = false;
403         ChecksumValidator validator = newValidator(SHA1);
404         assertFalse(validator.handle(new ChecksumFailureException("reject")));
405         policy.assertCallbacks("fail(reject)");
406     }
407 
408     @Test
409     void testRetry_ResetPolicy() {
410         ChecksumValidator validator = newValidator(SHA1);
411         validator.retry();
412         policy.assertCallbacks("retry()");
413     }
414 
415     @Test
416     void testRetry_RemoveTempFiles() throws Exception {
417         ChecksumValidator validator = newValidator(SHA1);
418         fetcher.mock(SHA1, "foo");
419         validator.validate(checksums(SHA1, "foo"), null);
420         fetcher.assertFetchedFiles(SHA1);
421         assertEquals(1, fetcher.checksumFiles.size());
422         validator.retry();
423         for (Path file : fetcher.checksumFiles) {
424             assertFalse(Files.exists(file), file.toAbsolutePath().toString());
425         }
426     }
427 
428     @Test
429     void testCommit_SaveChecksumFiles() throws Exception {
430         policy.inspectAll = true;
431         ChecksumValidator validator = newValidator(SHA1, MD5);
432         fetcher.mock(MD5, "bar");
433         validator.validate(checksums(SHA1, "foo", MD5, "bar"), checksums(SHA1, "foo"));
434         assertEquals(1, fetcher.checksumFiles.size());
435         validator.commit();
436         File checksumFile = new File(dataFile.getPath() + ".sha1");
437         assertTrue(checksumFile.isFile(), checksumFile.getAbsolutePath());
438         assertEquals("foo", TestFileUtils.readString(checksumFile));
439         checksumFile = new File(dataFile.getPath() + ".md5");
440         assertTrue(checksumFile.isFile(), checksumFile.getAbsolutePath());
441         assertEquals("bar", TestFileUtils.readString(checksumFile));
442         for (Path file : fetcher.checksumFiles) {
443             assertFalse(Files.exists(file), file.toAbsolutePath().toString());
444         }
445     }
446 
447     @Test
448     void testNoCommit_NoTempFiles() throws Exception {
449         ChecksumValidator validator = newValidator(SHA1);
450         fetcher.mock(SHA1, "foo");
451         validator.validate(checksums(SHA1, "foo"), null);
452         fetcher.assertFetchedFiles(SHA1);
453         assertEquals(1, fetcher.checksumFiles.size());
454         for (Path file : fetcher.checksumFiles) {
455             assertFalse(Files.exists(file), file.toAbsolutePath().toString());
456         }
457     }
458 }