diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index ec3b2e322..ad3392579 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -116,7 +116,7 @@ func (o *getTrailOptions) printTrailAsMarkdown(raw string, out io.Writer, page i if err != nil { return err } - sha := mdCell(commitInfo["sha1"]) + sha := mdCell(stringOrNA(commitInfo["sha1"])) if commitURL, ok := commitInfo["url"].(string); ok && commitURL != "" { sha = fmt.Sprintf("[%s](%s)", sha, commitURL) } @@ -124,9 +124,9 @@ func (o *getTrailOptions) printTrailAsMarkdown(raw string, out io.Writer, page i b.WriteString("| | |\n") b.WriteString("| --- | --- |\n") fmt.Fprintf(&b, "| Sha1 | %s |\n", sha) - fmt.Fprintf(&b, "| Author | %s |\n", mdCell(commitInfo["author"])) + fmt.Fprintf(&b, "| Author | %s |\n", mdCell(stringOrNA(commitInfo["author"]))) fmt.Fprintf(&b, "| Timestamp | %s |\n", mdCell(commitTimestamp)) - fmt.Fprintf(&b, "| Message | %s |\n", mdCell(firstLine(commitInfo["message"]))) + fmt.Fprintf(&b, "| Message | %s |\n", mdCell(stringOrNA(firstLine(commitInfo["message"])))) } writeAttestationStatuses(&b, trail["compliance_status"], trailURL) @@ -368,17 +368,17 @@ func printTrailAsTable(raw string, out io.Writer, page int) error { rows = append(rows, fmt.Sprintf("Last modified at:\t%s", lastModifiedAt)) if commitInfo, ok := trail["git_commit_info"].(map[string]any); ok { rows = append(rows, "Git commit:\t") - rows = append(rows, fmt.Sprintf(" Sha1:\t%s", commitInfo["sha1"].(string))) - rows = append(rows, fmt.Sprintf(" Author:\t%s", commitInfo["author"].(string))) - commitTimestamp, err := formattedTimestamp(commitInfo["timestamp"].(float64), false) + rows = append(rows, fmt.Sprintf(" Sha1:\t%s", stringOrNA(commitInfo["sha1"]))) + rows = append(rows, fmt.Sprintf(" Author:\t%s", stringOrNA(commitInfo["author"]))) + commitTimestamp, err := formattedTimestamp(commitInfo["timestamp"], false) if err != nil { return err } rows = append(rows, fmt.Sprintf(" Timestamp:\t%s", commitTimestamp)) - if url, ok := commitInfo["url"]; ok { + if url, ok := commitInfo["url"].(string); ok && url != "" { rows = append(rows, fmt.Sprintf(" url:\t%s", url)) } - rows = append(rows, fmt.Sprintf(" message:\t%s", prefixEachLine(commitInfo["message"].(string), "\t"))) + rows = append(rows, fmt.Sprintf(" message:\t%s", prefixEachLine(stringOrNA(commitInfo["message"]), "\t"))) } rows = append(rows, "Events:\n") @@ -508,3 +508,11 @@ func eventFields(event any) (trailEventFields, error) { attestationRef: eventAttestationRef, }, nil } + +// stringOrNA renders an optional string field, since commit info may carry only a sha1. +func stringOrNA(v any) string { + if s, ok := v.(string); ok && s != "" { + return s + } + return "N/A" +} diff --git a/cmd/kosli/getTrail_markdown_test.go b/cmd/kosli/getTrail_markdown_test.go index a961bcb1d..da0ba8894 100644 --- a/cmd/kosli/getTrail_markdown_test.go +++ b/cmd/kosli/getTrail_markdown_test.go @@ -413,3 +413,22 @@ func TestPrintTrailAsMarkdownEnvironmentLinks(t *testing.T) { "artifact 'artifact' stopped running in ['prod-aws'](https://app.kosli.com/my-org/environments/prod-aws/)", "without a snapshot index the link falls back to the environment page") } + +func TestPrintTrailAsMarkdownPartialCommitInfo(t *testing.T) { + t.Setenv("KOSLI_TESTS", "true") + global = &GlobalOpts{Host: "http://localhost:8001", Org: "docs-cmd-test-user-shared"} + o := &getTrailOptions{flowName: "get-trail"} + raw := `{"name": "t1", "compliance_state": "INCOMPLETE", "last_modified_at": 1452902400, + "git_commit_info": {"sha1": "abc1234def"}, "events": []}` + + var buf bytes.Buffer + require.NoError(t, o.printTrailAsMarkdown(raw, &buf, 0)) + require.Contains(t, buf.String(), "| Author | N/A |") + require.Contains(t, buf.String(), "| Message | N/A |") + + missingSha := `{"name": "t1", "compliance_state": "INCOMPLETE", "last_modified_at": 1452902400, + "git_commit_info": {"author": "Jane "}, "events": []}` + buf.Reset() + require.NoError(t, o.printTrailAsMarkdown(missingSha, &buf, 0)) + require.Contains(t, buf.String(), "| Sha1 | N/A |") +} diff --git a/cmd/kosli/getTrail_table_test.go b/cmd/kosli/getTrail_table_test.go new file mode 100644 index 000000000..2852057d7 --- /dev/null +++ b/cmd/kosli/getTrail_table_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPrintTrailAsTableCommitInfo checks that the table renderer tolerates +// commit info with only some of its fields, which the API accepts for trails. +func TestPrintTrailAsTableCommitInfo(t *testing.T) { + t.Setenv("KOSLI_TESTS", "true") // makes formattedTimestamp use a fixed time + + for _, tc := range []struct { + name string + commitInfo string + want []string + notWant []string + }{ + { + name: "only sha1", + commitInfo: `{"sha1": "abc1234def"}`, + want: []string{`Sha1:\s+abc1234def`, `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, + }, + { + name: "sha1 and branch", + commitInfo: `{"sha1": "abc1234def", "branch": "main"}`, + want: []string{"abc1234def", `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, + }, + { + // the server returns absent commit fields as explicit nulls + name: "only sha1 with null fields", + commitInfo: `{"sha1": "abc1234def", "message": null, "author": null, "branch": null, "timestamp": null, "url": null}`, + want: []string{`Sha1:\s+abc1234def`, `Author:\s+N/A`, `Timestamp:\s+N/A`, `message:\s+N/A`}, + notWant: []string{"%!", "url:"}, + }, + { + name: "full commit info", + commitInfo: `{"sha1": "abc1234def", "author": "Jane ", "timestamp": 1452902400, "url": "https://example.com/c/abc1234def", "message": "first line\nsecond line", "branch": "main"}`, + want: []string{"abc1234def", `Jane `, `Timestamp:\s+Sat, 16 Jan 2016`, `url:\s+https://example\.com/c/abc1234def`, `message:\s+first line\n\s+second line`}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + raw := `{"name": "t1", "description": "d", "compliance_state": "INCOMPLETE", + "last_modified_at": 1452902400, "git_commit_info": ` + tc.commitInfo + `, "events": []}` + + var buf bytes.Buffer + require.NoError(t, printTrailAsTable(raw, &buf, 0)) + for _, w := range tc.want { + require.Regexp(t, w, buf.String()) + } + for _, nw := range tc.notWant { + require.NotContains(t, buf.String(), nw) + } + }) + } +}