package main import ( "bytes" "encoding/json" "errors" "reflect" "strings" "testing" reg "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" "distribution/internal/holder" ) func jsonFields(v any) map[string]bool { out := map[string]bool{} t := reflect.TypeOf(v) for i := 0; i < t.NumField(); i++ { name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",") out[name] = true } return out } // TestTheVerbsSpeakTheSeatsContract holds distribution's seat verbs to the contracts mesh-contracts generates from the // two seats' definitions (ADR 0620 *How it is checked*): every argument a verb's tool takes is a field of the // contract's input, and every event body decodes into the contract's type with no field left over. func TestTheVerbsSpeakTheSeatsContract(t *testing.T) { inputs := map[string]any{reg.VerbPublish: reg.PublishInput{}, reg.VerbPublishPart: reg.PublishPartInput{}, reg.VerbVersions: reg.VersionsInput{}, reg.VerbRemove: reg.RemoveInput{}, reg.VerbHold: reg.HoldInput{}, reg.VerbRelease: reg.ReleaseInput{}, reg.VerbMirror: reg.MirrorInput{}, reg.VerbTags: reg.TagsInput{}} seen := 0 for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, func() (holder.Callers, error) { return holder.DefaultCallers(), nil }) { seat, verb, ok := strings.Cut(tool.Name, ".") in, isVerb := inputs[verb] if !ok || !isVerb || (seat != holder.ImageSeat && seat != holder.ArtifactSeat) { continue } seen++ fields := jsonFields(in) props, _ := tool.Input["properties"].(map[string]any) for p := range props { if !fields[p] { t.Errorf("%s takes %q, which the contract's input does not name", tool.Name, p) } } } if seen != 14 { t.Errorf("%d seat verbs served, want 14 (8 on the image seat, 6 on the artifact seat)", seen) } for event, want := range map[string]any{holder.EventPublished: ®.Outcome{}, holder.EventHeld: ®.Hold{}, holder.EventReleased: ®.Hold{}} { b, _ := json.Marshal(eventBody(event, holder.Event{Name: "m/a", Kind: holder.KindArchive, Digest: "sha256:aa", Holder: holder.Self, By: "x.artifact-store-tools"})) dec := json.NewDecoder(bytes.NewReader(b)) dec.DisallowUnknownFields() if err := dec.Decode(want); err != nil { t.Errorf("%s's body %s is not the contract's: %v", event, b, err) } } // The caller list is read on each call: one that cannot be read answers no verb, and registry_callers says it as read. unread := func() (holder.Callers, error) { return nil, errors.New("config refused") } for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, unread) { if tool.Name == holder.ImageSeat+"."+holder.VerbVersions { if _, err := tool.Run(map[string]any{"name": "m/a"}); err == nil || !strings.Contains(err.Error(), "caller list") { t.Errorf("versions with an unread caller list = %v, want refused naming the caller list", err) } } } answered := false for _, tool := range tools(&holder.Verbs{}, kept[holder.Window]{}, func() (holder.Callers, error) { return holder.Callers{holder.VerbPublish: {"mesh-build-go"}}, nil }) { if tool.Name == holder.CallersTool { answered = true got, err := tool.Run(map[string]any{}) if b, _ := json.Marshal(got); err != nil || string(b) != `{"callers":{"publish":["mesh-build-go"]}}` { t.Errorf("%s = %s, %v", holder.CallersTool, b, err) } } } if !answered { t.Errorf("no tool %s: the program could not read the caller list", holder.CallersTool) } if a := publishAnswer(holder.PublishAnswer{Transfer: "t", Missing: []string{"sha256:bb"}}); !reflect.DeepEqual(a.Lacks, []string{"sha256:bb"}) || a.Held { t.Errorf("publish answers %+v", a) } if r, err := partOf(reg.PublishPartInput{Transfer: "t", Part: "sha256:bb", Data: "aGk=", Last: true}); err != nil || string(r.Data) != "hi" || r.Digest != "sha256:bb" { t.Errorf("publish-part read %+v, %v", r, err) } }