import java.nio.file.*; import java.time.Duration; import java.util.*; /** Dependency-free behavior tests: java ci/Ci.java test. */ class CiTests { static final String SHA = "a".repeat(40); static int count; interface Test { void run() throws Exception; } static void test(String name, Test test) throws Exception { test.run(); count++; System.out.println("PASS " + name); } static void equal(Object expected, Object actual) { if (!Objects.equals(expected,actual)) throw new AssertionError("Expected " + expected + ", got " + actual); } static void failure(Test action) throws Exception { try { action.run(); } catch (Exception expected) { return; } throw new AssertionError("Expected failure"); } static void run() throws Exception { count = 0; test("JSON preserves configuration values and escaped text", () -> { String json = "{\"text\":\"line\\nquote\\\"\\u20ac\",\"values\":[null,true,false,1,-2.5e2],\"nested\":{}}"; Object data = Json.parse(json); equal(data,Json.parse(Json.write(data))); equal("line\nquote\"€",Json.object(data).get("text")); }); test("JSON rejects malformed/ambiguous input", () -> { for (String value : List.of("{\"a\":null,\"a\":1}","{} []","[1,]","01","\"\\uZZZZ\"","\"line\n\"","truefalse","+1")) failure(() -> Json.parse(value)); }); test("Docker configuration merge is idempotent and preserves backup", () -> { Path dir = Files.createTempDirectory("ci-docker-test-"); try { Path file = dir.resolve("daemon.json"); String original = "{\"features\":{\"buildkit\":true},\"insecure-registries\":[\"existing:5000\"]}"; Files.writeString(file,original); Setup.configureDocker(file); String first = Files.readString(file); Setup.configureDocker(file); equal(first,Files.readString(file)); equal(List.of("existing:5000","192.168.2.28:3000"),Json.object(Json.parse(first)).get("insecure-registries")); equal(original,Files.readString(dir.resolve("daemon.json.before-agiledancers"))); } finally { Pipeline.deleteTree(dir); } }); test("Invalid Docker configuration is unchanged", () -> { Path dir = Files.createTempDirectory("ci-docker-test-"); try { Path file = dir.resolve("daemon.json"); String original = "{\"insecure-registries\":\"invalid\"}"; Files.writeString(file,original); failure(() -> Setup.configureDocker(file)); equal(original,Files.readString(file)); } finally { Pipeline.deleteTree(dir); } }); test("Feature branches never contact registry", () -> { try (Fixture f = new Fixture()) { f.pipeline.env.put("CI_REF","refs/heads/feature/test"); f.pipeline.publish(); equal(0L,f.calls("login")); equal(0L,f.calls("push")); } }); test("Unverified commit cannot publish", () -> { try (Fixture f = new Fixture()) { Files.delete(f.root.resolve("ci/artifacts/verified-commit")); failure(f.pipeline::publish); equal(0L,f.calls("push")); } }); test("Publication pullback and rerun preserve commit tags", () -> { try (Fixture f = new Fixture()) { f.pipeline.publish(); equal(4L,f.calls("push")); f.pipeline.publish(); equal(6L,f.calls("push")); equal(8L,f.calls("pull")); } }); test("Partial publication is completed without overwriting backend", () -> { try (Fixture f = new Fixture()) { f.fake.pushFailure = "frontend:" + SHA; failure(f.pipeline::publish); equal(0L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith(":main")).count()); f.fake.pushFailure = ""; f.pipeline.publish(); equal(1L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith("backend:" + SHA)).count()); } }); test("Authentication error is not a missing tag", () -> { try (Fixture f = new Fixture()) { f.fake.manifestError = true; failure(f.pipeline::publish); equal(0L,f.calls("push")); } }); test("Pull failure blocks moving tags", () -> { try (Fixture f = new Fixture()) { f.fake.pullFailure = "backend:" + SHA; failure(f.pipeline::publish); equal(0L,f.fake.calls.stream().filter(a -> a.contains("push") && a.getLast().endsWith(":main")).count()); } }); test("Older commit cannot move main", () -> { try (Fixture f = new Fixture()) { f.fake.head = "c".repeat(40); f.pipeline.publish(); equal(2L,f.calls("push")); } }); test("Unavailable Docker fails before build with a concrete cause", () -> { try (Fixture f = new Fixture()) { f.fake.dockerAvailable = false; try { f.pipeline.verify(); throw new AssertionError("Expected failure"); } catch (IllegalArgumentException error) { equal(true,error.getMessage().contains("Docker is unavailable")); } equal(0L,f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).count()); } }); test("Architecture mismatch blocks publication", () -> { try (Fixture f = new Fixture()) { f.pipeline.env.put("TARGET_PLATFORM","linux/arm64"); failure(f.pipeline::publish); equal(0L,f.calls("push")); } }); test("Failed verification removes success marker and preserves exit status", () -> { try (Fixture f = new Fixture()) { f.fake.mavenFailure = true; try { f.pipeline.verify(); throw new AssertionError("Expected failure"); } catch (Commands.Failure error) { equal(42,error.exitCode); } equal(false,Files.exists(f.root.resolve("ci/artifacts/verified-commit"))); equal(0L,f.calls("build")); equal(0L,f.calls("push")); } }); test("Process execution enforces its deadline", () -> { Path dir = Files.createTempDirectory("ci-process-test-"); try { Path file = dir.resolve("Sleep.java"); Files.writeString(file,"class Sleep { public static void main(String[] args) throws Exception { Thread.sleep(10000); } }"); Commands runner = new Commands(Duration.ofMillis(100)); try { runner.execute(List.of(System.getProperty("java.home") + "/bin/java",file.toString()),dir,System.getenv(),null,false); throw new AssertionError("Expected timeout"); } catch (Commands.Failure error) { equal(124,error.exitCode); } } finally { Pipeline.deleteTree(dir); } }); System.out.println(count + " CI tests passed"); } static class Fixture implements AutoCloseable { final Path root = Files.createTempDirectory("ci-publication-test-"); final FakeCommands fake = new FakeCommands(); final Pipeline pipeline; Fixture() throws Exception { Map env = new HashMap<>(System.getenv()); env.putAll(Map.of("CI_COMMIT",SHA,"CI_RUN_ID","test","CI_RUN_ATTEMPT","1","CI_REF","refs/heads/main", "CI_REPOSITORY","wanjakrah/agiledancers","TARGET_PLATFORM","linux/amd64","GITEA_SERVER_URL","http://192.168.2.28:3000", "REGISTRY_USER","test","REGISTRY_TOKEN","dummy-token","CI_PUBLISH_LOCK_DIR",root.resolve("lock").toString())); pipeline = new Pipeline(root,env,fake) { @Override String mainHead() { return fake.head; } }; Files.writeString(root.resolve("ci/artifacts/verified-commit"),SHA + "\n"); } long calls(String verb) { return fake.calls.stream().filter(a -> a.size()>1 && a.getFirst().equals("docker") && a.get(1).equals(verb)).count(); } public void close() throws Exception { pipeline.closeDockerConfig(); Pipeline.deleteTree(root); } } static class FakeCommands extends Commands { final List> calls = new ArrayList<>(); final Map images = new HashMap<>(), tags = new HashMap<>(); String head = SHA, pushFailure = "", pullFailure = ""; boolean manifestError, mavenFailure; boolean dockerAvailable = true; FakeCommands() { super(Duration.ofMinutes(1)); } @Override Result execute(List args, Path cwd, Map env, String input, boolean echo) { calls.add(List.copyOf(args)); if (args.getFirst().equals("git")) return new Result(0,SHA); if (args.getFirst().equals("mvn") && mavenFailure) return new Result(42,"deliberate failure"); if (!args.getFirst().equals("docker")) return new Result(0,""); String command = args.get(1), ref = args.getLast(); if (command.equals("info")) return new Result(dockerAvailable ? 0 : 1,args.contains("{{.Architecture}}") ? "amd64" : "linux"); if (command.equals("context")) return new Result(0,"unix:///tmp/test-docker.sock"); if (command.equals("manifest")) { if (manifestError) return new Result(1,"unauthorized"); return images.containsKey(ref) ? new Result(0,"{}") : new Result(1,"manifest unknown"); } if (command.equals("tag")) tags.put(ref,images.getOrDefault(args.get(2),SHA)); if (command.equals("push")) { if (!pushFailure.isEmpty() && ref.contains(pushFailure)) return new Result(1,"push failed"); images.put(ref,tags.getOrDefault(ref,SHA)); } if (command.equals("pull") && !pullFailure.isEmpty() && ref.contains(pullFailure)) return new Result(1,"pull failed"); if (command.equals("image")) return new Result(0,args.stream().anyMatch(a -> a.contains("RepoDigests")) ? ref.substring(0,ref.lastIndexOf(':')) + "@sha256:" + "b".repeat(64) : images.getOrDefault(ref,"missing")); if (command.equals("container") || command.equals("network") && args.contains("inspect")) return new Result(1,""); return new Result(0,""); } } }