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