package space.anyi.docker; import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.ServerSocket; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; /** * 镜像推送与 Registry 认证 API 的测试类 * 注意:需要本机 Docker daemon 运行中(会自动拉取 registry:2 镜像) * 测试会启动一个本地私有仓库,结束后自动清理 */ class RegistryPushAPITest { private String uniqueName(String prefix) { return prefix + "-" + UUID.randomUUID().toString().substring(0, 8); } /** * 动态获取一个空闲端口,避免与其他进程冲突 */ private int freePort() throws IOException { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); } } /** * 测试 docker login:对本地私有仓库认证应返回 Login Succeeded */ @Test void authToRegistryReturnsLoginSucceeded() throws IOException { RegistryPushAPI api = new RegistryPushAPI(); int port = freePort(); String containerId = api.startLocalRegistry(uniqueName("reg"), port); String registryAddress = "localhost:" + port; String status = api.authToRegistry(registryAddress); assertEquals("Login Succeeded", status, "未启用认证的私有仓库应返回 Login Succeeded"); api.stopAndRemoveRegistryContainer(containerId); } /** * 测试完整闭环:启动仓库 -> 打标签 -> 推送 -> 验证仓库有镜像 -> 删本地 -> 拉回 */ @Test void pushThenPullRoundTrip() throws IOException, InterruptedException { RegistryPushAPI api = new RegistryPushAPI(); int port = freePort(); String registryContainerId = api.startLocalRegistry(uniqueName("reg"), port); String repository = uniqueName("myapp"); String tag = "v1"; String registryAddress = "localhost:" + port; String remoteImage = registryAddress + "/" + repository + ":" + tag; try { // 1. 认证 assertEquals("Login Succeeded", api.authToRegistry(registryAddress)); // 2. 打标签并推送 boolean pushed = api.tagAndPushImage("nginx:latest", registryAddress, repository, tag); assertTrue(pushed, "镜像应推送成功"); // 3. 通过 Registry HTTP API 验证仓库中确实存在该镜像 // 等价命令: curl http://localhost:5000/v2/_catalog HttpResponse catalogResponse = queryRegistry(port); String catalogBody = catalogResponse.body(); assertTrue(catalogBody.contains(repository), "仓库目录中应包含推送的镜像,实际: " + catalogBody); // 4. 删除本地镜像副本(模拟镜像仅存在于仓库) api.removeImage(remoteImage); assertFalse(api.checkImageExists(remoteImage), "本地镜像应已被删除"); // 5. 从仓库拉回并验证 boolean pulled = api.pullFromRegistry(registryAddress, repository, tag); assertTrue(pulled, "镜像应能从仓库拉回"); assertTrue(api.checkImageExists(remoteImage), "拉回后本地应存在该镜像"); } finally { api.removeImage(remoteImage); api.stopAndRemoveRegistryContainer(registryContainerId); } } /** * 查询本地仓库目录(Registry HTTP API v2) * 等价命令: curl http://localhost:/v2/_catalog */ private HttpResponse queryRegistry(int port) throws IOException, InterruptedException { HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(3)) .build(); HttpRequest request = HttpRequest.newBuilder( URI.create("http://localhost:" + port + "/v2/_catalog")) .GET() .build(); HttpResponse response = null; for (int i = 0; i < 10; i++) { try { response = client.send(request, HttpResponse.BodyHandlers.ofString()); break; } catch (IOException | InterruptedException e) { Thread.sleep(1000); } } assertNotNull(response, "本地仓库未能及时响应"); return response; } }