001/* 002 * The contents of this file are subject to the license and copyright detailed 003 * in the LICENSE and NOTICE files at the root of the source tree. 004 */ 005package org.duraspace.bagit; 006 007import static org.duraspace.bagit.BagConfig.ACCESS_KEY; 008import static org.duraspace.bagit.BagConfig.BAGGING_DATE_KEY; 009import static org.duraspace.bagit.BagConfig.BAG_SIZE_KEY; 010import static org.duraspace.bagit.BagConfig.CONTACT_EMAIL_KEY; 011import static org.duraspace.bagit.BagConfig.CONTACT_NAME_KEY; 012import static org.duraspace.bagit.BagConfig.CONTACT_PHONE_KEY; 013import static org.duraspace.bagit.BagConfig.EXTERNAL_DESCRIPTION_KEY; 014import static org.duraspace.bagit.BagConfig.ORGANIZATION_ADDRESS_KEY; 015import static org.duraspace.bagit.BagConfig.PAYLOAD_OXUM_KEY; 016import static org.duraspace.bagit.BagConfig.SOURCE_ORGANIZATION_KEY; 017import static org.duraspace.bagit.BagConfig.TITLE_KEY; 018import static org.duraspace.bagit.BagProfileConstants.ACCEPT_BAGIT_VERSION; 019import static org.duraspace.bagit.BagProfileConstants.ACCEPT_SERIALIZATION; 020import static org.duraspace.bagit.BagProfileConstants.BAGIT_PROFILE_IDENTIFIER; 021import static org.duraspace.bagit.BagProfileConstants.BAGIT_PROFILE_INFO; 022import static org.duraspace.bagit.BagProfileConstants.BAGIT_PROFILE_VERSION; 023import static org.duraspace.bagit.BagProfileConstants.BAG_INFO; 024import static org.duraspace.bagit.BagProfileConstants.MANIFESTS_REQUIRED; 025import static org.duraspace.bagit.BagProfileConstants.PROFILE_VERSION; 026import static org.duraspace.bagit.BagProfileConstants.TAG_FILES_REQUIRED; 027import static org.duraspace.bagit.BagProfileConstants.TAG_MANIFESTS_REQUIRED; 028import static org.junit.Assert.assertEquals; 029import static org.junit.Assert.assertFalse; 030import static org.junit.Assert.assertTrue; 031 032import java.io.IOException; 033import java.net.URISyntaxException; 034import java.net.URL; 035import java.nio.file.Files; 036import java.nio.file.Path; 037import java.nio.file.Paths; 038import java.util.Arrays; 039import java.util.Collection; 040import java.util.Collections; 041import java.util.List; 042import java.util.Map; 043import java.util.Objects; 044import java.util.Set; 045 046import gov.loc.repository.bagit.domain.Bag; 047import gov.loc.repository.bagit.domain.FetchItem; 048import gov.loc.repository.bagit.domain.Manifest; 049import gov.loc.repository.bagit.domain.Version; 050import gov.loc.repository.bagit.hash.StandardSupportedAlgorithms; 051import org.junit.Assert; 052import org.junit.Before; 053import org.junit.Test; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057/** 058 * @author escowles 059 * @since 2016-12-13 060 */ 061public class BagProfileTest { 062 063 private final String testValue = "test-value"; 064 private final String defaultBag = "bag"; 065 066 // profile locations 067 private final String defaultProfilePath = "profiles/profile.json"; 068 private final String extraTagsPath = "profiles/profileWithExtraTags.json"; 069 private final String invalidPath = "profiles/invalidProfile.json"; 070 private final String invalidSerializationPath = "profiles/invalidProfileSerializationError.json"; 071 072 // config locations 073 private final String bagitConfig = "configs/bagit-config.yml"; 074 private final String bagitConfigBadAccess = "configs/bagit-config-bad-access.yml"; 075 private final String bagitConfigMissingAccess = "configs/bagit-config-missing-access.yml"; 076 private final String bagitConfigNoAptrust = "configs/bagit-config-no-aptrust.yml"; 077 078 private final Version defaultVersion = new Version(1, 0); 079 private Path targetDir; 080 081 private final Logger logger = LoggerFactory.getLogger(BagProfileTest.class); 082 083 @Before 084 public void setup() throws URISyntaxException { 085 final URL url = this.getClass().getClassLoader().getResource("sample"); 086 targetDir = Paths.get(Objects.requireNonNull(url).toURI()); 087 } 088 089 @Test 090 public void testBasicProfileFromFile() throws Exception { 091 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(defaultProfilePath))); 092 093 final String md5 = BagItDigest.MD5.bagitName(); 094 final String sha1 = BagItDigest.SHA1.bagitName(); 095 final String sha256 = BagItDigest.SHA256.bagitName(); 096 final String sha512 = BagItDigest.SHA512.bagitName(); 097 assertTrue(profile.getPayloadDigestAlgorithms().contains(md5)); 098 assertTrue(profile.getPayloadDigestAlgorithms().contains(sha1)); 099 assertTrue(profile.getPayloadDigestAlgorithms().contains(sha256)); 100 assertTrue(profile.getPayloadDigestAlgorithms().contains(sha512)); 101 102 assertFalse(profile.getTagDigestAlgorithms().contains(md5)); 103 assertTrue(profile.getTagDigestAlgorithms().contains(sha1)); 104 assertTrue(profile.getTagDigestAlgorithms().contains(sha256)); 105 assertTrue(profile.getTagDigestAlgorithms().contains(sha512)); 106 107 assertTrue(profile.getMetadataFields().get(SOURCE_ORGANIZATION_KEY).isRequired()); 108 assertTrue(profile.getMetadataFields().get(ORGANIZATION_ADDRESS_KEY).isRequired()); 109 assertTrue(profile.getMetadataFields().get(CONTACT_NAME_KEY).isRequired()); 110 assertTrue(profile.getMetadataFields().get(CONTACT_PHONE_KEY).isRequired()); 111 assertTrue(profile.getMetadataFields().get(BAG_SIZE_KEY).isRequired()); 112 assertTrue(profile.getMetadataFields().get(BAGGING_DATE_KEY).isRequired()); 113 assertTrue(profile.getMetadataFields().get(PAYLOAD_OXUM_KEY).isRequired()); 114 assertFalse(profile.getMetadataFields().get(CONTACT_EMAIL_KEY).isRequired()); 115 116 assertTrue(profile.getSectionNames().stream().allMatch(t -> t.equalsIgnoreCase(BAG_INFO))); 117 118 assertFalse(profile.isAllowFetch()); 119 assertEquals(BagProfile.Serialization.OPTIONAL, profile.getSerialization()); 120 assertTrue(profile.getAcceptedBagItVersions().contains("0.97")); 121 assertTrue(profile.getAcceptedBagItVersions().contains("1.0")); 122 assertTrue(profile.getAcceptedSerializations().contains("application/tar")); 123 assertTrue(profile.getTagFilesAllowed().contains("*")); 124 assertTrue(profile.getTagFilesRequired().isEmpty()); 125 assertTrue(profile.getAllowedTagAlgorithms().isEmpty()); 126 assertTrue(profile.getAllowedPayloadAlgorithms().isEmpty()); 127 } 128 129 @Test 130 public void testExtendedProfile() throws Exception { 131 final String aptrustInfo = "APTrust-Info"; 132 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(extraTagsPath))); 133 134 assertTrue(profile.getSectionNames().stream().anyMatch(t -> t.equalsIgnoreCase(BAG_INFO))); 135 assertTrue(profile.getSectionNames().stream().anyMatch(t -> t.equals(aptrustInfo))); 136 assertTrue(profile.getSectionNames().stream().noneMatch(t -> t.equals("Wrong-Tags"))); 137 assertTrue(profile.getMetadataFields(aptrustInfo).containsKey(TITLE_KEY)); 138 assertTrue(profile.getMetadataFields(aptrustInfo).containsKey(ACCESS_KEY)); 139 assertTrue(profile.getMetadataFields(aptrustInfo).get(ACCESS_KEY).getValues().contains("Consortia")); 140 assertTrue(profile.getMetadataFields(aptrustInfo).get(ACCESS_KEY).getValues().contains("Institution")); 141 assertTrue(profile.getMetadataFields(aptrustInfo).get(ACCESS_KEY).getValues().contains("Restricted")); 142 143 } 144 145 146 @Test 147 public void testGoodConfig() throws Exception { 148 final BagConfig config = new BagConfig(resolveResourcePath(bagitConfig).toFile()); 149 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(extraTagsPath))); 150 profile.validateConfig(config); 151 } 152 153 @Test(expected = RuntimeException.class) 154 public void testBadAccessValue() throws Exception { 155 final BagConfig config = new BagConfig(resolveResourcePath(bagitConfigBadAccess).toFile()); 156 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(extraTagsPath))); 157 profile.validateConfig(config); 158 } 159 160 @Test(expected = RuntimeException.class) 161 public void testMissingAccessValue() throws Exception { 162 final BagConfig config = new BagConfig(resolveResourcePath(bagitConfigMissingAccess).toFile()); 163 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(extraTagsPath))); 164 profile.validateConfig(config); 165 } 166 167 @Test 168 public void testMissingSectionNotNeeded() throws Exception { 169 final BagConfig config = new BagConfig(resolveResourcePath(bagitConfigNoAptrust).toFile()); 170 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(defaultProfilePath))); 171 profile.validateConfig(config); 172 } 173 174 @Test(expected = RuntimeException.class) 175 public void testMissingSectionRequired() throws Exception { 176 final BagConfig config = new BagConfig(resolveResourcePath(bagitConfigNoAptrust).toFile()); 177 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(extraTagsPath))); 178 profile.validateConfig(config); 179 } 180 181 @Test 182 public void testAllProfilesPassValidation() throws IOException { 183 final Path profiles = Paths.get("src/main/resources/profiles"); 184 185 Files.list(profiles).forEach(path -> { 186 String profileIdentifier = path.getFileName().toString(); 187 profileIdentifier = profileIdentifier.substring(0, profileIdentifier.indexOf(".")); 188 logger.debug("Validating {}", profileIdentifier); 189 BagProfile profile = null; 190 try { 191 profile = new BagProfile(BagProfile.BuiltIn.from(profileIdentifier)); 192 } catch (IOException e) { 193 Assert.fail(e.getMessage()); 194 } 195 196 validateProfile(Objects.requireNonNull(profile)); 197 }); 198 199 } 200 201 @Test 202 public void testInvalidBagProfile() throws IOException, URISyntaxException { 203 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(invalidPath))); 204 try { 205 validateProfile(profile); 206 Assert.fail("Should throw an exception"); 207 } catch (RuntimeException e) { 208 final String message = e.getMessage(); 209 // check that the error message contains each failed section 210 Assert.assertTrue(message.contains(BAGIT_PROFILE_INFO)); 211 Assert.assertTrue(message.contains(BAGIT_PROFILE_IDENTIFIER)); 212 Assert.assertTrue(message.contains(ACCEPT_SERIALIZATION)); 213 Assert.assertTrue(message.contains(MANIFESTS_REQUIRED)); 214 Assert.assertTrue(message.contains(TAG_MANIFESTS_REQUIRED)); 215 Assert.assertTrue(message.contains(TAG_FILES_REQUIRED)); 216 Assert.assertTrue(message.contains(ACCEPT_BAGIT_VERSION)); 217 } 218 } 219 220 @Test 221 public void testInvalidBagProfileSerializationTypo() throws IOException, URISyntaxException { 222 final BagProfile profile = new BagProfile(Files.newInputStream(resolveResourcePath(invalidSerializationPath))); 223 try { 224 validateProfile(profile); 225 Assert.fail("Should throw an exception"); 226 } catch (RuntimeException e) { 227 final String message = e.getMessage(); 228 // check that the serialization field failed to parse 229 Assert.assertTrue(message.contains("Unknown Serialization")); 230 } 231 } 232 233 /** 234 * Validates this {@link BagProfile} according to the BagIt Profiles specification found at 235 * https://bagit-profiles.github.io/bagit-profiles-specification/ 236 * 237 * This checks the following fields: 238 * 239 * BagIt-Profile-Info 240 * Existence of the Source-Organization, External-Description, Version, BagIt-Profile-Identifier, and 241 * BagIt-Profile-Version fields 242 * 243 * Serialization 244 * Is equal to one of "forbidden", "required", or "optional" 245 * 246 * Accept-Serialization 247 * If serialization has a value of required or optional, at least one value is needed. 248 * 249 * Manifests-Allowed 250 * If specified, the {@link BagProfile#getPayloadDigestAlgorithms()} must be a subset of 251 * {@link BagProfile#getAllowedPayloadAlgorithms()} 252 * 253 * Tag-Manifests-Allowed 254 * If specified, the {@link BagProfile#getTagDigestAlgorithms()} must be a subset of 255 * {@link BagProfile#getAllowedTagAlgorithms()} 256 * 257 * Tag-Files-Allowed 258 * If specified, the {@link BagProfile#getTagFilesRequired()} must be a subset of 259 * {@link BagProfile#getTagFilesAllowed()}. If not specified, all tags must match the '*' glob 260 * 261 * Accept-BagIt-Version 262 * At least one version is required 263 */ 264 private void validateProfile(final BagProfile profile) { 265 final StringBuilder errors = new StringBuilder(); 266 267 // Bag-Profile-Info 268 final List<String> expectedInfoFields = Arrays.asList(SOURCE_ORGANIZATION_KEY, 269 EXTERNAL_DESCRIPTION_KEY, 270 PROFILE_VERSION, 271 BAGIT_PROFILE_IDENTIFIER, 272 BAGIT_PROFILE_VERSION); 273 final Map<String, String> bagInfo = profile.getProfileMetadata(); 274 for (final String expected : expectedInfoFields) { 275 if (!bagInfo.containsKey(expected)) { 276 if (errors.length() == 0) { 277 errors.append("Error(s) in BagIt-Profile-Info:\n"); 278 } 279 errors.append(" * Missing key ").append(expected).append("\n"); 280 } 281 } 282 283 // Serialization / Accept-Serialization 284 final BagProfile.Serialization serialization = profile.getSerialization(); 285 if (serialization == BagProfile.Serialization.REQUIRED || serialization == BagProfile.Serialization.OPTIONAL) { 286 if (profile.getAcceptedSerializations().isEmpty()) { 287 errors.append("Serialization value of ").append(serialization) 288 .append(" requires at least one value in the Accept-Serialization field!\n"); 289 } 290 } else if(serialization == BagProfile.Serialization.UNKNOWN) { 291 errors.append("Unknown Serialization value ").append(serialization) 292 .append(". Allowed values are forbidden, required, or optional.\n"); 293 } 294 295 // Manifests-Allowed / Manifests-Required 296 final Set<String> allowedPayloadAlgorithms = profile.getAllowedPayloadAlgorithms(); 297 final Set<String> payloadDigestAlgorithms = profile.getPayloadDigestAlgorithms(); 298 if (!(allowedPayloadAlgorithms.isEmpty() || isSubset(payloadDigestAlgorithms, allowedPayloadAlgorithms))) { 299 errors.append("Manifests-Required must be a subset of Manifests-Allowed!\n"); 300 } 301 302 // Tag-Manifests-Allowed / Tag-Manifests-Required 303 final Set<String> allowedTagAlgorithms = profile.getAllowedTagAlgorithms(); 304 final Set<String> tagDigestAlgorithms = profile.getTagDigestAlgorithms(); 305 if (!(allowedTagAlgorithms.isEmpty() || isSubset(tagDigestAlgorithms, allowedTagAlgorithms))) { 306 errors.append("Tag-Manifests-Required must be a subset of Tag-Manifests-Allowed!\n"); 307 } 308 309 // Tag-Files-Allowed / Tag-Files-Required 310 final Set<String> tagFilesAllowed = profile.getTagFilesAllowed(); 311 final Set<String> tagFilesRequired = profile.getTagFilesRequired(); 312 if (!(tagFilesAllowed.isEmpty() || isSubset(tagFilesRequired, tagFilesAllowed))) { 313 errors.append("Tag-Files-Required must be a subset of Tag-Files-Allowed!\n"); 314 } 315 316 if (profile.getAcceptedBagItVersions().isEmpty()) { 317 errors.append("Accept-BagIt-Version requires at least one entry!"); 318 } 319 320 if (errors.length() > 0) { 321 errors.insert(0, "Bag Profile json does not conform to BagIt Profiles specification! " + 322 "The following errors occurred:\n"); 323 throw new RuntimeException(errors.toString()); 324 } 325 } 326 327 /** 328 * Check to see if a collection (labelled as {@code subCollection}) is a subset of the {@code superCollection} 329 * 330 * @param subCollection the sub collection to iterate against and check if elements are contained within 331 * {@code superCollection} 332 * @param superCollection the super collection containing all the elements 333 * @param <T> the type of each collection 334 * @return true if all elements of {@code subCollection} are contained within {@code superCollection} 335 */ 336 private <T> boolean isSubset(final Collection<T> subCollection, final Collection<T> superCollection) { 337 for (T t : subCollection) { 338 if (!superCollection.contains(t)) { 339 return false; 340 } 341 } 342 343 return true; 344 } 345 346 @Test 347 public void testValidateBag() throws IOException, URISyntaxException { 348 final Bag bag = new Bag(); 349 bag.setVersion(defaultVersion); 350 bag.setRootDir(targetDir.resolve(defaultBag)); 351 final BagProfile bagProfile = new BagProfile(Files.newInputStream(resolveResourcePath(defaultProfilePath))); 352 353 putRequiredBagInfo(bag, bagProfile); 354 putRequiredManifests(bag.getTagManifests(), bagProfile.getTagDigestAlgorithms()); 355 putRequiredManifests(bag.getPayLoadManifests(), bagProfile.getPayloadDigestAlgorithms()); 356 putRequiredTags(bag, bagProfile); 357 358 bagProfile.validateBag(bag); 359 } 360 361 @Test 362 public void testValidateBagFailure() throws IOException { 363 final Long fetchLength = 0L; 364 final Path fetchFile = Paths.get("data/fetch.txt"); 365 final URL fetchUrl = new URL("http://localhost/data/fetch.txt"); 366 367 final Bag bag = new Bag(); 368 bag.setItemsToFetch(Collections.singletonList(new FetchItem(fetchUrl, fetchLength, fetchFile))); 369 bag.setVersion(new Version(0, 0)); 370 bag.setRootDir(targetDir.resolve(defaultBag)); 371 final BagProfile bagProfile = new BagProfile(BagProfile.BuiltIn.APTRUST); 372 373 putRequiredBagInfo(bag, bagProfile); 374 putRequiredManifests(bag.getPayLoadManifests(), bagProfile.getPayloadDigestAlgorithms()); 375 putRequiredTags(bag, bagProfile); 376 377 try { 378 bagProfile.validateBag(bag); 379 Assert.fail("Validation did not throw an exception"); 380 } catch (RuntimeException e) { 381 final String message = e.getMessage(); 382 Assert.assertTrue(message.contains("Profile does not allow a fetch.txt")); 383 Assert.assertTrue(message.contains("No tag manifest")); 384 Assert.assertTrue(message.contains("Required tag file \"aptrust-info.txt\" does not exist")); 385 Assert.assertTrue(message.contains("Could not read info from \"aptrust-info.txt\"")); 386 Assert.assertTrue(message.contains("BagIt version incompatible")); 387 388 Assert.assertFalse(message.contains("Missing tag manifest algorithm")); 389 } 390 } 391 392 /** 393 * Add required tag files to a Bag from a BagProfile 394 * 395 * @param bag the Bag 396 * @param bagProfile the BagProfile defining the required files 397 */ 398 private void putRequiredTags(final Bag bag, final BagProfile bagProfile) { 399 final List<String> tagManifestExpected = Arrays.asList("manifest-sha1.txt", "bag-info.txt", "bagit.txt"); 400 401 // Always populate with the files we expect to see 402 for (String expected : tagManifestExpected) { 403 final Path required = Paths.get(expected); 404 for (Manifest manifest : bag.getTagManifests()) { 405 manifest.getFileToChecksumMap().put(required, testValue); 406 } 407 } 408 409 for (String requiredTag : bagProfile.getTagFilesRequired()) { 410 final Path requiredPath = Paths.get(requiredTag); 411 for (Manifest manifest : bag.getTagManifests()) { 412 manifest.getFileToChecksumMap().put(requiredPath, testValue); 413 } 414 } 415 } 416 417 /** 418 * 419 * @param manifests the manifests to add algorithms to 420 * @param algorithms the algorithms to add 421 */ 422 private void putRequiredManifests(final Set<Manifest> manifests, final Set<String> algorithms) { 423 for (String algorithm : algorithms) { 424 manifests.add(new Manifest(StandardSupportedAlgorithms.valueOf(algorithm.toUpperCase()))); 425 } 426 } 427 428 /** 429 * 430 * @param bag the Bag to set info fields for 431 * @param profile the BagProfile defining the required info fields 432 */ 433 private void putRequiredBagInfo(final Bag bag, final BagProfile profile) { 434 final Map<String, ProfileFieldRule> bagInfoMeta = profile.getMetadataFields(BAG_INFO); 435 for (Map.Entry<String, ProfileFieldRule> entry : bagInfoMeta.entrySet()) { 436 if (entry.getValue().isRequired()) { 437 bag.getMetadata().add(entry.getKey(), testValue); 438 } 439 } 440 } 441 442 private Path resolveResourcePath(final String resource) throws URISyntaxException { 443 final URL url = this.getClass().getClassLoader().getResource(resource); 444 return Paths.get(Objects.requireNonNull(url).toURI()); 445 } 446 447}