package buildstep import ( "encoding/base64" "encoding/json" "errors" "strings" "testing" ) // Design 47 G A-254: one reader of a run's answer from a step-ended, inline or through the seat's verb output on the node that ran it. func TestStepEndedReadOutput(t *testing.T) { const ask = "m2kx9a1b3c4d5f" var key string var body map[string]any verb := func(answer string, err error) AskVerb { return func(k string, b any) (json.RawMessage, error) { key = k body, _ = b.(map[string]any) return json.RawMessage(answer), err } } for _, c := range []struct { name string end StepEnded ask AskVerb want string wantKey string fail string }{ {name: "inline", end: StepEnded{Ask: ask, Node: "ace", Output: `{"a":1}`}, want: `{"a":1}`}, {name: "an object is read through the verb on its node", end: StepEnded{Ask: ask, Node: "ace", OutputObject: OutputObjectName(ask)}, ask: verb(`{"ask":"`+ask+`","file":"eyJhIjoxfQ=="}`, nil), want: `{"a":1}`, wantKey: "seat:node-build-go.output@ace"}, {name: "no answer kept", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(`{"ask":"`+ask+`","chunks":0}`, nil), fail: "keeps no answer"}, {name: "another ask's answer", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(`{"ask":"x","file":"e30="}`, nil), fail: "answered the ask"}, {name: "the verb fails", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, ask: verb(``, errors.New("no seat")), fail: "no seat"}, {name: "no verb", end: StepEnded{Ask: ask, Node: "ace", OutputObject: "o"}, fail: "no verb"}, } { t.Run(c.name, func(t *testing.T) { key, body = "", nil got, err := c.end.ReadOutput("go", c.ask) if c.fail != "" { if err == nil || !strings.Contains(err.Error(), c.fail) { t.Fatalf("err %v, want %q", err, c.fail) } return } if err != nil || string(got) != c.want || key != c.wantKey { t.Fatalf("got %q, %v on %q, want %q on %q", got, err, key, c.want, c.wantKey) } if c.wantKey != "" && (body["ask"] != ask || len(body["call"].(string)) != 32) { t.Fatalf("asked with %v, want the ask and 128 random bits as call", body) } }) } } // A step-ended names its answer inline or as an object, never both. func TestStepEndedRefusesAnswerTwice(t *testing.T) { e := StepEnded{ID: "m2kx9a1b3c4d5f", Ask: "m2kx9a1b3c4d5f", Asker: "a", Node: "ace", Outcome: OutcomePassed, Output: "1", OutputObject: "o"} if _, err := EncodeStepEnded(e); err == nil || !strings.Contains(err.Error(), "inline and as an object") { t.Fatalf("err %v", err) } } // Design 47 G A-290: a step whose key names its answer as an object is read whole through the seat's verb output on its // node: a check's answer as its result, a run's output put back in its run answer; a key holding its answer is answered as it is. func TestStepWholeReadsTheObjectItsKeyNames(t *testing.T) { const ask = "m2kx9a1b3c4d5f" var key string verb := func(file string, err error) AskVerb { return func(k string, _ any) (json.RawMessage, error) { key = k return json.RawMessage(`{"ask":"` + ask + `","file":"` + file + `"}`), err } } b64 := func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) } named := Step{ID: ask, Node: "ace", State: StepPassed, OutputObject: OutputObjectName(ask)} for _, c := range []struct { name string step Step run bool ask AskVerb want, fail string }{ {name: "the key holds it", step: Step{ID: ask, Node: "ace", Result: json.RawMessage(`{"a":1}`)}, want: `{"a":1}`}, {name: "a check's answer", step: named, ask: verb(b64(`{"fetch":{"workspace":"w"}}`), nil), want: `{"fetch":{"workspace":"w"}}`}, {name: "a run's output", step: func() Step { s := named; s.Result = json.RawMessage(`{"built":true,"key":"k","exit":0}`); return s }(), run: true, ask: verb(b64(`{"verdict":"ok"}`), nil), want: `{"built":true,"key":"k","exit":0,"output":{"verdict":"ok"}}`}, {name: "a check's answer that is no JSON", step: named, ask: verb(b64(`not json`), nil), fail: "not JSON"}, {name: "the verb fails", step: named, ask: verb("", errors.New("no responders")), fail: "no responders"}, {name: "no verb", step: named, fail: "no verb"}, } { t.Run(c.name, func(t *testing.T) { key = "" got, err := c.step.Whole("go", c.run, c.ask) if c.fail != "" { if err == nil || !strings.Contains(err.Error(), c.fail) { t.Fatalf("err %v, want %q", err, c.fail) } return } if err != nil || string(got.Result) != c.want || got.OutputObject != "" { t.Fatalf("got %s (object %q), %v; want %s", got.Result, got.OutputObject, err, c.want) } if c.ask != nil && key != "seat:node-build-go.output@ace" { t.Fatalf("asked %q", key) } }) } }