|
17 | 17 | package com.adobe.testing.s3mock.service |
18 | 18 |
|
19 | 19 | import com.adobe.testing.s3mock.S3Exception |
| 20 | +import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration |
| 21 | +import com.adobe.testing.s3mock.dto.ObjectLockConfiguration |
| 22 | +import com.adobe.testing.s3mock.dto.ObjectLockEnabled |
20 | 23 | import com.adobe.testing.s3mock.dto.ObjectOwnership |
21 | 24 | import com.adobe.testing.s3mock.dto.VersioningConfiguration |
22 | 25 | import com.adobe.testing.s3mock.dto.VersioningConfiguration.Status |
23 | 26 | import com.adobe.testing.s3mock.store.BucketMetadata |
24 | 27 | import com.adobe.testing.s3mock.store.MultipartStore |
| 28 | +import com.adobe.testing.s3mock.store.S3ObjectMetadata |
25 | 29 | import org.assertj.core.api.Assertions.assertThat |
26 | 30 | import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy |
27 | 31 | import org.junit.jupiter.api.Test |
@@ -474,25 +478,190 @@ internal class BucketServiceTest : ServiceTestBase() { |
474 | 478 |
|
475 | 479 | // After setting, BucketStore should have been invoked; we simulate by making metadata return the configuration |
476 | 480 | whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn( |
477 | | - BucketMetadata( |
| 481 | + bucketMetadata( |
478 | 482 | bucketName, |
479 | | - bucketMetadata.creationDate(), |
480 | | - cfg, |
481 | | - bucketMetadata.objectLockConfiguration(), |
482 | | - bucketMetadata.bucketLifecycleConfiguration(), |
483 | | - bucketMetadata.objectOwnership(), |
484 | | - bucketMetadata.path(), |
485 | | - bucketMetadata.bucketRegion(), |
486 | | - bucketMetadata.bucketInfo(), |
487 | | - bucketMetadata.locationInfo() |
| 483 | + bucketMetadata, |
| 484 | + versioningConfiguration = cfg, |
488 | 485 | ) |
489 | 486 | ) |
490 | 487 |
|
491 | 488 | val out = iut.getVersioningConfiguration(bucketName) |
492 | 489 | assertThat(out.status()).isEqualTo(Status.ENABLED) |
493 | 490 | } |
494 | 491 |
|
| 492 | + @Test |
| 493 | + fun testObjectLockConfiguration_getThrowsWhenAbsent_thenSetAndGet() { |
| 494 | + val bucketName = "bucket-lock" |
| 495 | + val bucketMetadata = givenBucket(bucketName) |
| 496 | + |
| 497 | + // Absent -> throws |
| 498 | + assertThatThrownBy { iut.getObjectLockConfiguration(bucketName) } |
| 499 | + .isEqualTo(S3Exception.NOT_FOUND_BUCKET_OBJECT_LOCK) |
| 500 | + |
| 501 | + // Set configuration |
| 502 | + val cfg = ObjectLockConfiguration( |
| 503 | + ObjectLockEnabled.ENABLED, |
| 504 | + null |
| 505 | + ) |
| 506 | + iut.setObjectLockConfiguration(bucketName, cfg) |
| 507 | + |
| 508 | + // Return metadata updated with configuration |
| 509 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn( |
| 510 | + bucketMetadata( |
| 511 | + bucketName, |
| 512 | + bucketMetadata, |
| 513 | + cfg, |
| 514 | + ) |
| 515 | + ) |
| 516 | + |
| 517 | + val out = iut.getObjectLockConfiguration(bucketName) |
| 518 | + assertThat(out.objectLockEnabled()).isEqualTo(ObjectLockEnabled.ENABLED) |
| 519 | + } |
| 520 | + |
| 521 | + @Test |
| 522 | + fun testBucketLifecycleConfiguration_setGetDelete() { |
| 523 | + val bucketName = "bucket-lc" |
| 524 | + val bucketMetadata = givenBucket(bucketName) |
| 525 | + |
| 526 | + // Absent -> throws |
| 527 | + assertThatThrownBy { iut.getBucketLifecycleConfiguration(bucketName) } |
| 528 | + .isEqualTo(S3Exception.NO_SUCH_LIFECYCLE_CONFIGURATION) |
| 529 | + |
| 530 | + // Set lifecycle configuration |
| 531 | + val lc = BucketLifecycleConfiguration(emptyList()) |
| 532 | + iut.setBucketLifecycleConfiguration(bucketName, lc) |
| 533 | + |
| 534 | + // Simulate store returning updated metadata |
| 535 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn( |
| 536 | + bucketMetadata( |
| 537 | + bucketName, |
| 538 | + bucketMetadata, |
| 539 | + bucketLifecycleConfiguration = lc, |
| 540 | + ) |
| 541 | + ) |
| 542 | + |
| 543 | + val read = iut.getBucketLifecycleConfiguration(bucketName) |
| 544 | + assertThat(read.rules()).isEmpty() |
| 545 | + |
| 546 | + // Delete configuration and ensure it's gone |
| 547 | + iut.deleteBucketLifecycleConfiguration(bucketName) |
| 548 | + |
| 549 | + // After delete, simulate metadata without lifecycle configuration again |
| 550 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn( |
| 551 | + bucketMetadata(bucketName, bucketMetadata) |
| 552 | + ) |
| 553 | + |
| 554 | + assertThatThrownBy { iut.getBucketLifecycleConfiguration(bucketName) } |
| 555 | + .isEqualTo(S3Exception.NO_SUCH_LIFECYCLE_CONFIGURATION) |
| 556 | + } |
| 557 | + |
| 558 | + @Test |
| 559 | + fun testDeleteBucket_nonEmptyWithNonDeleteMarker_throws() { |
| 560 | + val bucketName = "bucket-del" |
| 561 | + val meta = givenBucket(bucketName) |
| 562 | + val key = "k1" |
| 563 | + val id = meta.addKey(key) |
| 564 | + |
| 565 | + // First call returns metadata with one object, second call also returns non-empty -> triggers exception |
| 566 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn(meta, meta) |
| 567 | + |
| 568 | + // Object metadata without delete marker |
| 569 | + whenever(objectStore.getS3ObjectMetadata(meta, id, null)).thenReturn(s3ObjectMetadata(id, key)) |
| 570 | + |
| 571 | + assertThatThrownBy { iut.deleteBucket(bucketName) } |
| 572 | + .isInstanceOf(IllegalStateException::class.java) |
| 573 | + .hasMessageContaining("Bucket is not empty: $bucketName") |
| 574 | + } |
| 575 | + |
| 576 | + @Test |
| 577 | + fun testDeleteBucket_onlyDeleteMarkersAreRemoved_andBucketDeleted() { |
| 578 | + val bucketName = "bucket-del-markers" |
| 579 | + val metaInitial = givenBucket(bucketName) |
| 580 | + val key = "k1" |
| 581 | + val id = metaInitial.addKey(key) |
| 582 | + |
| 583 | + // Metadata before deletion: contains one key |
| 584 | + // After removing delete marker, metadata is empty |
| 585 | + val metaAfter = bucketMetadata(bucketName, metaInitial) |
| 586 | + |
| 587 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn(metaInitial, metaAfter) |
| 588 | + |
| 589 | + // Return S3ObjectMetadata marked as delete marker |
| 590 | + val dm = s3ObjectMetadata(id, key) |
| 591 | + // mark it as a delete marker using helper |
| 592 | + val dmMeta = S3ObjectMetadata.deleteMarker(dm, "v1") |
| 593 | + |
| 594 | + whenever(objectStore.getS3ObjectMetadata(metaInitial, id, null)).thenReturn(dmMeta) |
| 595 | + |
| 596 | + // bucketStore.deleteBucket should be called and return true |
| 597 | + whenever(bucketStore.deleteBucket(bucketName)).thenReturn(true) |
| 598 | + |
| 599 | + val deleted = iut.deleteBucket(bucketName) |
| 600 | + assertThat(deleted).isTrue() |
| 601 | + // ensure we removed the key from the bucket |
| 602 | + verify(bucketStore).removeFromBucket(key, bucketName) |
| 603 | + verify(objectStore).doDeleteObject(metaInitial, id) |
| 604 | + } |
| 605 | + |
| 606 | + @Test |
| 607 | + fun testDeleteBucket_nonExistingBucket_throws() { |
| 608 | + val bucketName = "no-such-bucket" |
| 609 | + // Return null metadata to trigger the else-branch in service |
| 610 | + whenever(bucketStore.getBucketMetadata(bucketName)).thenReturn(null) |
| 611 | + assertThatThrownBy { iut.deleteBucket(bucketName) } |
| 612 | + .isInstanceOf(IllegalStateException::class.java) |
| 613 | + .hasMessageContaining("Requested Bucket does not exist: $bucketName") |
| 614 | + } |
| 615 | + |
| 616 | + @Test |
| 617 | + fun testListVersions_versioningDisabled_returnsCurrentVersionsOnly() { |
| 618 | + val bucketName = "bucket-versions" |
| 619 | + val prefix = "" |
| 620 | + val delimiter = "" |
| 621 | + val encodingType = "url" |
| 622 | + val maxKeys = 100 |
| 623 | + val keyMarker = "" |
| 624 | + val versionIdMarker = "" |
| 625 | + |
| 626 | + givenBucketWithContents(bucketName, prefix) |
| 627 | + |
| 628 | + val out = iut.listVersions( |
| 629 | + bucketName, |
| 630 | + prefix, |
| 631 | + delimiter, |
| 632 | + encodingType, |
| 633 | + maxKeys, |
| 634 | + keyMarker, |
| 635 | + versionIdMarker |
| 636 | + ) |
| 637 | + |
| 638 | + // With versioning disabled, entries are mapped 1:1 without delete markers |
| 639 | + assertThat(out.deleteMarkers()).isEmpty() |
| 640 | + assertThat(out.objectVersions()).isNotEmpty() |
| 641 | + } |
| 642 | + |
495 | 643 | companion object { |
496 | 644 | private const val TEST_BUCKET_NAME = "test-bucket" |
| 645 | + |
| 646 | + private fun bucketMetadata( |
| 647 | + bucketName: String, |
| 648 | + bucketMetadata: BucketMetadata, |
| 649 | + objectLockConfiguration: ObjectLockConfiguration? = bucketMetadata.objectLockConfiguration(), |
| 650 | + bucketLifecycleConfiguration: BucketLifecycleConfiguration? = bucketMetadata.bucketLifecycleConfiguration(), |
| 651 | + versioningConfiguration: VersioningConfiguration? = bucketMetadata.versioningConfiguration() |
| 652 | + ): BucketMetadata { |
| 653 | + return BucketMetadata( |
| 654 | + bucketName, |
| 655 | + bucketMetadata.creationDate(), |
| 656 | + versioningConfiguration, |
| 657 | + objectLockConfiguration, |
| 658 | + bucketLifecycleConfiguration, |
| 659 | + bucketMetadata.objectOwnership(), |
| 660 | + bucketMetadata.path(), |
| 661 | + bucketMetadata.bucketRegion(), |
| 662 | + bucketMetadata.bucketInfo(), |
| 663 | + bucketMetadata.locationInfo() |
| 664 | + ) |
| 665 | + } |
497 | 666 | } |
498 | 667 | } |
0 commit comments