package main import ( "context" "os" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/forgekey" ) // 1703d/AC (ADR 0487 ยง5): a candidate's build leaves the registry unchanged: nothing is pushed, nothing is asked of it. func TestACandidatesBuildLeavesTheRegistryUnchanged(t *testing.T) { m, f, states := setup(t, same) res, st := runBuild(t, m, states, nil) // no publish if st.State != delivery.StepPassed || res.Pushed { t.Fatalf("step = %+v result = %+v", st, res) } if len(f.called("push")) != 0 || len(f.registry) != 0 || f.pushes != 0 { t.Errorf("a candidate's build reached the registry: pushes %d, registry %v, calls %v", f.pushes, f.registry, f.lines()) } } // 1703d/AC: a trunk build adds exactly one digest, under / and a tag of its own (the digest's first 12 // hex digits), after the build has ended, with TLS verification off (plain HTTP) and no credential; the key carries the // digest the registry serves and the reference pinned by it. func TestATrunkBuildPushesOnceAndAnswersTheRegistrysDigest(t *testing.T) { m, f, states := setup(t, same) res, st := runBuild(t, m, states, map[string]any{"publish": true}) if st.State != delivery.StepPassed { t.Fatalf("step = %+v", st) } tag := "111111111111" if len(f.registry) != 1 || f.registry["fixture-image/server:"+tag] != imageDigest || f.pushes != 1 { t.Fatalf("registry = %v after %d pushes, want exactly fixture-image/server:%s", f.registry, f.pushes, tag) } if !res.Pushed || res.Digest != imageDigest || res.Reference != f.address()+"/fixture-image/server@"+imageDigest { t.Errorf("result = %+v", res) } push := f.called("push") if len(push) != 1 || !strings.Contains(push[0].line, "--tls-verify=false") || !strings.Contains(push[0].line, "docker://"+f.address()+"/fixture-image/server:"+tag) { t.Fatalf("push = %v", push) } for _, bad := range []string{"--creds", "--authfile", "--sign"} { if strings.Contains(push[0].line, bad) { t.Errorf("the push carries %s", bad) } } // The push comes after the build has ended: bud, then images, then push. order := []string{} for _, l := range f.lines() { for _, v := range []string{" bud ", " images ", " push ", " rmi "} { if strings.Contains(l, "buildah") && strings.Contains(l, v) { order = append(order, strings.TrimSpace(v)) } } } if strings.Join(order, ",") != "bud,images,push,rmi" { t.Errorf("order = %v", order) } } // A tag the call gives is the one used; one that is not a tag is refused. func TestATagOfTheCallsOwn(t *testing.T) { m, f, states := setup(t, same) if _, st := runBuild(t, m, states, map[string]any{"publish": true, "tag": "fp-abc123"}); st.State != delivery.StepPassed { t.Fatalf("step = %+v", st) } if _, ok := f.registry["fixture-image/server:fp-abc123"]; !ok || len(f.registry) != 1 { t.Errorf("registry = %v", f.registry) } m2, f2, states2 := setup(t, same) if _, st := runBuild(t, m2, states2, map[string]any{"publish": true, "tag": "a/b:c"}); st.State != delivery.StepFailed || len(f2.called("bud")) != 0 { t.Errorf("step = %+v", st) } } // issue 457: a registry held still (connection refused) is waited for, and the push then goes through once. func TestAPushWaitsForARegistryHeldStill(t *testing.T) { m, f, states := setup(t, same) f.refuse = 2 res, st := runBuild(t, m, states, map[string]any{"publish": true}) if st.State != delivery.StepPassed || !res.Pushed { t.Fatalf("step = %+v result = %+v", st, res) } if f.pushes != 1 || len(f.registry) != 1 || len(f.called("push")) != 3 { t.Errorf("pushes %d, registry %v, push calls %d; want two refused and one made", f.pushes, f.registry, len(f.called("push"))) } } // A registry that stays down past the wait fails the step saying how long it was refused; nothing is claimed pushed. func TestARegistryDownPastTheWaitFailsTheStep(t *testing.T) { m, f, states := setup(t, same) f.refuse = 1 << 20 m.RegistryWait = 50 * time.Millisecond _, st := runBuild(t, m, states, map[string]any{"publish": true}) if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "refused every connection") { t.Errorf("step = %+v", st) } if len(f.registry) != 0 { t.Errorf("registry = %v", f.registry) } } // The digest the registry serves is the answer: when it differs from the one the push reported, the step fails. func TestAnAnswerThatDisagreesWithThePushFails(t *testing.T) { m, f, states := setup(t, same) run := m.Run m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { r := run(ctx, env, name, args...) if name == "buildah" && strings.Contains(strings.Join(args, " "), " push ") { f.mu.Lock() for k := range f.registry { f.registry[k] = "sha256:2222222222222222222222222222222222222222222222222222222222222222" } f.mu.Unlock() } return r } _, st := runBuild(t, m, states, map[string]any{"publish": true}) if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "the registry serves") { t.Errorf("step = %+v", st) } } func TestTheRegistrysAddressMustBeBound(t *testing.T) { for _, bad := range []string{"", "${bound:artifact-store:at}:5000", ":5000"} { m, _, _ := setup(t, same) writeFile(t, m.RegistryFile, bad) if _, err := m.registry(); err == nil { t.Errorf("%q was taken as the registry's address", bad) } } } func writeFile(t *testing.T, path, body string) { t.Helper() if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } } // A push that succeeded wrote its digest file: one missing, unreadable or empty is a fault, and the registry's digest is // never answered unchecked (review round 1). func TestAPushWithoutAReadableDigestFileFailsTheStep(t *testing.T) { for _, kind := range []string{"none", "empty"} { m, f, states := setup(t, same) f.digestFile = kind _, st := runBuild(t, m, states, map[string]any{"publish": true}) if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "digest") { t.Errorf("%s: step = %+v, want failed naming the digest file", kind, st) } } }