diff --git a/pom.xml b/pom.xml index 38a9a0a2..b81d777f 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,6 @@ false false - 17 - 2.0.16 3.4.1 @@ -48,6 +46,11 @@ 3.5.0 3.11.2 + + 17 + 17 + 17 + diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index 2c8db658..6ee4cef4 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -562,6 +562,7 @@ private boolean switchTokenAuth(OrasHttpClient.ResponseWrapper response) private void handleError(OrasHttpClient.ResponseWrapper responseWrapper) { if (responseWrapper.statusCode() >= 400) { if (responseWrapper.response() instanceof String) { + LOG.debug("Response: {}", responseWrapper.response()); throw new OrasException((OrasHttpClient.ResponseWrapper) responseWrapper); } throw new OrasException(new OrasHttpClient.ResponseWrapper<>("", responseWrapper.statusCode(), Map.of())); diff --git a/src/test/java/land/oras/AnnotationsTest.java b/src/test/java/land/oras/AnnotationsTest.java index 1006f490..5176d75a 100644 --- a/src/test/java/land/oras/AnnotationsTest.java +++ b/src/test/java/land/oras/AnnotationsTest.java @@ -4,7 +4,10 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +@Execution(ExecutionMode.CONCURRENT) public class AnnotationsTest { @Test diff --git a/src/test/java/land/oras/ContainerRefTest.java b/src/test/java/land/oras/ContainerRefTest.java index 8530ec28..bb18d8a0 100644 --- a/src/test/java/land/oras/ContainerRefTest.java +++ b/src/test/java/land/oras/ContainerRefTest.java @@ -4,7 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +@Execution(ExecutionMode.CONCURRENT) public class ContainerRefTest { @Test diff --git a/src/test/java/land/oras/LayerTest.java b/src/test/java/land/oras/LayerTest.java index 46877591..de3eed1e 100644 --- a/src/test/java/land/oras/LayerTest.java +++ b/src/test/java/land/oras/LayerTest.java @@ -3,7 +3,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +@Execution(ExecutionMode.CONCURRENT) public class LayerTest { @Test diff --git a/src/test/java/land/oras/ManifestTest.java b/src/test/java/land/oras/ManifestTest.java index cd5a69ed..4ae4d3de 100644 --- a/src/test/java/land/oras/ManifestTest.java +++ b/src/test/java/land/oras/ManifestTest.java @@ -3,11 +3,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Execution(ExecutionMode.CONCURRENT) public class ManifestTest { /** diff --git a/src/test/java/land/oras/RegistryContainerTest.java b/src/test/java/land/oras/RegistryContainerTest.java new file mode 100644 index 00000000..89dc008b --- /dev/null +++ b/src/test/java/land/oras/RegistryContainerTest.java @@ -0,0 +1,243 @@ +package land.oras; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import land.oras.utils.Const; +import land.oras.utils.JsonUtils; +import land.oras.utils.RegistryContainer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@WireMockTest +@Execution(ExecutionMode.CONCURRENT) +public class RegistryContainerTest { + + private static final Logger LOG = LoggerFactory.getLogger(RegistryContainerTest.class); + + @Container + private final RegistryContainer registry = new RegistryContainer().withStartupAttempts(3); + + /** + * Blob temporary dir + */ + @TempDir + private Path blobDir; + + @TempDir + private Path artifactDir; + + @BeforeEach + void before() { + registry.withFollowOutput(); + } + + @Test + void shouldListTags(WireMockRuntimeInfo wmRuntimeInfo) { + + // Return data from wiremock + WireMock wireMock = wmRuntimeInfo.getWireMock(); + wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/artifact-text/tags/list")) + .willReturn(WireMock.okJson(JsonUtils.toJson(new Tags("artifact-text", List.of("latest", "0.1.1")))))); + + // Insecure registry + Registry registry = Registry.Builder.builder().withInsecure(true).build(); + + // Test + List tags = registry.getTags(ContainerRef.parse("%s/library/artifact-text" + .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))); + + // Assert + assertEquals(2, tags.size()); + assertEquals("latest", tags.get(0)); + assertEquals("0.1.1", tags.get(1)); + } + + @Test + void shouldPushAndGetBlobThenDelete() { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-text".formatted(this.registry.getRegistry())); + Layer layer = registry.pushBlob(containerRef, "hello".getBytes()); + assertEquals("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", layer.getDigest()); + byte[] blob = registry.getBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")); + assertEquals("hello", new String(blob)); + registry.pushBlob(containerRef, "hello".getBytes()); + registry.deleteBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")); + + // Ensure the blob is deleted + assertThrows(OrasException.class, () -> { + registry.getBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")); + }); + } + + @Test + void shouldUploadAndFetchBlobThenDelete() throws IOException { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-text".formatted(this.registry.getRegistry())); + Files.createFile(blobDir.resolve("temp.txt")); + Files.writeString(blobDir.resolve("temp.txt"), "hello"); + Layer layer = registry.uploadBlob(containerRef, blobDir.resolve("temp.txt")); + assertEquals("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", layer.getDigest()); + + registry.fetchBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"), + blobDir.resolve("temp.txt")); + + try (InputStream is = registry.fetchBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"))) { + assertEquals("hello", new String(is.readAllBytes())); + } + + assertEquals("hello", Files.readString(blobDir.resolve("temp.txt"))); + registry.deleteBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")); + + // Ensure the blob is deleted + assertThrows(OrasException.class, () -> { + registry.getBlob( + containerRef.withDigest("sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")); + }); + } + + @Test + void shouldPushAndGetManifestThenDelete() { + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + + // Empty manifest + ContainerRef containerRef = + ContainerRef.parse("%s/library/empty-manifest".formatted(this.registry.getRegistry())); + Layer emptyLayer = registry.pushBlob(containerRef, Layer.empty().getDataBytes()); + Manifest emptyManifest = Manifest.empty().withLayers(List.of(Layer.fromDigest(emptyLayer.getDigest(), 2))); + String location = registry.pushManifest(containerRef, emptyManifest); + assertEquals( + "http://%s/v2/library/empty-manifest/manifests/sha256:f570eb29564f04e73d15cc2a2bb4153d488b9e8428c7f5108b895baa379750bd" + .formatted(this.registry.getRegistry()), + location); + Manifest manifest = registry.getManifest(containerRef); + + // Assert + assertEquals(2, manifest.getSchemaVersion()); + assertEquals(Const.DEFAULT_MANIFEST_MEDIA_TYPE, manifest.getMediaType()); + assertEquals(Config.empty().getDigest(), manifest.getConfig().getDigest()); + assertEquals(1, manifest.getLayers().size()); // One empty layer + Layer layer = manifest.getLayers().get(0); + + // An empty layer + assertEquals(2, layer.getSize()); + assertEquals(Const.DEFAULT_EMPTY_MEDIA_TYPE, layer.getMediaType()); + + assertNull(manifest.getArtifactType()); + assertTrue(manifest.getAnnotations().isEmpty()); + + // Push again + registry.pushManifest(containerRef, manifest); + + // Delete manifest + registry.deleteManifest(containerRef); + // Ensure the blob is deleted + assertThrows(OrasException.class, () -> { + registry.getManifest(containerRef); + }); + } + + @Test + void testShouldPushAndPullMinimalArtifact() throws IOException { + + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + + Path file1 = blobDir.resolve("file1.txt"); + Files.writeString(file1, "foobar"); + + // Upload + Manifest manifest = registry.pushArtifact(containerRef, file1); + assertEquals(1, manifest.getLayers().size()); + + Layer layer = manifest.getLayers().get(0); + + // A test file layer + assertEquals(6, layer.getSize()); + assertEquals("text/plain", layer.getMediaType()); + assertEquals("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", layer.getDigest()); + + Map annotations = layer.getAnnotations(); + + // Assert annotations of the layer + assertEquals(1, annotations.size()); + assertEquals("file1.txt", annotations.get(Const.ANNOTATION_TITLE)); + + // Pull + registry.pullArtifact(containerRef, artifactDir, true); + assertEquals("foobar", Files.readString(artifactDir.resolve("file1.txt"))); + } + + @Test + void testShouldPushCompressedDirectory() throws IOException { + + Registry registry = Registry.Builder.builder() + .withInsecure(true) + .withSkipTlsVerify(true) + .build(); + ContainerRef containerRef = + ContainerRef.parse("%s/library/artifact-full".formatted(this.registry.getRegistry())); + + Path file1 = blobDir.resolve("file1.txt"); + Path file2 = blobDir.resolve("file2.txt"); + Path file3 = blobDir.resolve("file3.txt"); + Files.writeString(file1, "foobar"); + Files.writeString(file2, "test1234"); + Files.writeString(file3, "barfoo"); + + // Upload blob dir + Manifest manifest = registry.pushArtifact(containerRef, blobDir); + assertEquals(1, manifest.getLayers().size()); + + Layer layer = manifest.getLayers().get(0); + + // A compressed directory file + assertEquals(Const.DEFAULT_BLOB_DIR_MEDIA_TYPE, layer.getMediaType()); + Map annotations = layer.getAnnotations(); + + // Assert annotations of the layer + assertEquals(2, annotations.size()); + assertEquals(blobDir.getFileName().toString(), annotations.get(Const.ANNOTATION_TITLE)); + assertEquals("true", annotations.get(Const.ANNOTATION_ORAS_UNPACK)); + } +} diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index a5ff2bb2..b29acf2e 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -5,9 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -15,18 +12,19 @@ import java.util.List; import java.util.Map; import land.oras.utils.Const; -import land.oras.utils.JsonUtils; import land.oras.utils.RegistryContainer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers -@WireMockTest +@Execution(ExecutionMode.CONCURRENT) public class RegistryTest { private static final Logger LOG = LoggerFactory.getLogger(RegistryTest.class); @@ -48,27 +46,6 @@ void before() { registry.withFollowOutput(); } - @Test - void shouldListTags(WireMockRuntimeInfo wmRuntimeInfo) { - - // Return data from wiremock - WireMock wireMock = wmRuntimeInfo.getWireMock(); - wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/artifact-text/tags/list")) - .willReturn(WireMock.okJson(JsonUtils.toJson(new Tags("artifact-text", List.of("latest", "0.1.1")))))); - - // Insecure registry - Registry registry = Registry.Builder.builder().withInsecure(true).build(); - - // Test - List tags = registry.getTags(ContainerRef.parse("%s/library/artifact-text" - .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))); - - // Assert - assertEquals(2, tags.size()); - assertEquals("latest", tags.get(0)); - assertEquals("0.1.1", tags.get(1)); - } - @Test void shouldPushAndGetBlobThenDelete() { Registry registry = Registry.Builder.builder() diff --git a/src/test/java/land/oras/RegistryWireMockTest.java b/src/test/java/land/oras/RegistryWireMockTest.java new file mode 100644 index 00000000..3cbb62c0 --- /dev/null +++ b/src/test/java/land/oras/RegistryWireMockTest.java @@ -0,0 +1,39 @@ +package land.oras; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.List; +import land.oras.utils.JsonUtils; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@WireMockTest +public class RegistryWireMockTest { + + private static final Logger LOG = LoggerFactory.getLogger(RegistryWireMockTest.class); + + @Test + void shouldListTags(WireMockRuntimeInfo wmRuntimeInfo) { + + // Return data from wiremock + WireMock wireMock = wmRuntimeInfo.getWireMock(); + wireMock.register(WireMock.get(WireMock.urlEqualTo("/v2/library/artifact-text/tags/list")) + .willReturn(WireMock.okJson(JsonUtils.toJson(new Tags("artifact-text", List.of("latest", "0.1.1")))))); + + // Insecure registry + Registry registry = Registry.Builder.builder().withInsecure(true).build(); + + // Test + List tags = registry.getTags(ContainerRef.parse("%s/library/artifact-text" + .formatted(wmRuntimeInfo.getHttpBaseUrl().replace("http://", "")))); + + // Assert + assertEquals(2, tags.size()); + assertEquals("latest", tags.get(0)); + assertEquals("0.1.1", tags.get(1)); + } +} diff --git a/src/test/java/land/oras/auth/BearerTokenProviderTest.java b/src/test/java/land/oras/auth/BearerTokenProviderTest.java index e8d74d53..4b22680b 100644 --- a/src/test/java/land/oras/auth/BearerTokenProviderTest.java +++ b/src/test/java/land/oras/auth/BearerTokenProviderTest.java @@ -15,12 +15,15 @@ import land.oras.utils.OrasHttpClient; import land.oras.utils.RegistryContainer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import org.mockito.Mockito; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @Testcontainers @WireMockTest +@Execution(ExecutionMode.SAME_THREAD) public class BearerTokenProviderTest { @Container diff --git a/src/test/java/land/oras/auth/EnvironmentPasswordProviderTest.java b/src/test/java/land/oras/auth/EnvironmentPasswordProviderTest.java index 23990d2d..a4e55e06 100644 --- a/src/test/java/land/oras/auth/EnvironmentPasswordProviderTest.java +++ b/src/test/java/land/oras/auth/EnvironmentPasswordProviderTest.java @@ -4,11 +4,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; import uk.org.webcompere.systemstubs.jupiter.SystemStub; import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; @ExtendWith(SystemStubsExtension.class) +@Execution(ExecutionMode.CONCURRENT) public class EnvironmentPasswordProviderTest { @SystemStub diff --git a/src/test/java/land/oras/utils/DigestUtilsTest.java b/src/test/java/land/oras/utils/DigestUtilsTest.java index 50439300..6d86a394 100644 --- a/src/test/java/land/oras/utils/DigestUtilsTest.java +++ b/src/test/java/land/oras/utils/DigestUtilsTest.java @@ -7,7 +7,10 @@ import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +@Execution(ExecutionMode.CONCURRENT) public class DigestUtilsTest { /** diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 00000000..9e63e3e8 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.mode.classes.default=concurrent