Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kosli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func newListCmd(out io.Writer) *cobra.Command {
newListTrailsCmd(out),
newListPoliciesCmd(out),
newListAttestationTypesCmd(out),
newListReposCmd(out),
)

return cmd
Expand Down
93 changes: 93 additions & 0 deletions cmd/kosli/listRepos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/requests"
"github.com/spf13/cobra"
)

const listReposDesc = `List repos for an org.`

type listReposOptions struct {
listOptions
}

func newListReposCmd(out io.Writer) *cobra.Command {
o := new(listReposOptions)
cmd := &cobra.Command{
Use: "repos",
Hidden: true,
Short: listReposDesc,
Long: listReposDesc,
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return o.validate(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
},
}

addListFlags(cmd, &o.listOptions)

return cmd
}

func (o *listReposOptions) run(out io.Writer) error {
url := fmt.Sprintf("%s/api/v2/repos/%s?page=%d&per_page=%d", global.Host, global.Org, o.pageNumber, o.pageLimit)

reqParams := &requests.RequestParams{
Method: http.MethodGet,
URL: url,
Token: global.ApiToken,
}
response, err := kosliClient.Do(reqParams)
if err != nil {
return err
}

return output.FormattedPrint(response.Body, o.listOptions.output, out, o.pageNumber,
map[string]output.FormatOutputFunc{
"table": printReposListAsTable,
"json": output.PrintJson,
})
}

func printReposListAsTable(raw string, out io.Writer, page int) error {
var repos []map[string]any
var response struct {
Embedded struct {
Repos []map[string]any `json:"repos"`
} `json:"_embedded"`
}

err := json.Unmarshal([]byte(raw), &response)
if err != nil {
return err
}
repos = response.Embedded.Repos

if len(repos) == 0 {
logger.Info("No repos were found.")
return nil
}

header := []string{"NAME", "URL", "LAST_ACTIVITY"}
rows := []string{}
for _, repo := range repos {
row := fmt.Sprintf("%s\t%s\t%s", repo["name"], repo["url"], repo["latest_activity"])
rows = append(rows, row)
}
tabFormattedPrint(out, header, rows)

return nil
}
105 changes: 105 additions & 0 deletions cmd/kosli/listRepos_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/suite"
)

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type ListReposCommandTestSuite struct {
suite.Suite
defaultKosliArguments string
acmeOrgKosliArguments string
}

func (suite *ListReposCommandTestSuite) SetupTest() {
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

global.Org = "acme-org"
global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c"
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
CreateFlowWithTemplate("list-repos", "testdata/valid_template.yml", suite.Suite.T())
SetEnvVars(map[string]string{
"GITHUB_RUN_NUMBER": "1234",
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_REPOSITORY": "kosli-dev/cli",
"GITHUB_REPOSITORY_ID": "1234567890",
}, suite.Suite.T())
BeginTrail("trail-name", "list-repos", "", suite.Suite.T())
}

func (suite *ListReposCommandTestSuite) TearDownTest() {
UnSetEnvVars(map[string]string{
"GITHUB_RUN_NUMBER": "",
"GITHUB_SERVER_URL": "",
"GITHUB_REPOSITORY": "",
"GITHUB_REPOSITORY_ID": "",
}, suite.Suite.T())
}

func (suite *ListReposCommandTestSuite) TestListReposCmd() {
tests := []cmdTestCase{
// THIS TEST IS FLAKY IN CI SINCE CI VARIABLES ARE SET THERE AND REPOS MAY EXIST FROM OTHER TESTS
// {
// name: "01-listing repos works when there are repos",
// cmd: fmt.Sprintf(`list repos %s`, suite.defaultKosliArguments),
// golden: "No repos were found.\n",
// },
{
name: "02-listing repos works when there are no repos",
cmd: fmt.Sprintf(`list repos %s`, suite.acmeOrgKosliArguments),
goldenRegex: ".*\nkosli-dev/cli https://github.com/kosli-dev/cli Trail Started at.*",
},
{
name: "03-listing repos with --output json works when there are repos",
cmd: fmt.Sprintf(`list repos --output json %s`, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
},
// THIS TEST IS FLAKY IN CI SINCE CI VARIABLES ARE SET THERE AND REPOS MAY EXIST FROM OTHER TESTS
// {
// name: "04-listing repos with --output json works when there are no repos",
// cmd: fmt.Sprintf(`list repos --output json %s`, suite.defaultKosliArguments),
// goldenJson: []jsonCheck{{"_embedded.repos", "[]"}},
// },
{
wantError: true,
name: "05-providing an argument causes an error",
cmd: fmt.Sprintf(`list repos xxx %s`, suite.defaultKosliArguments),
golden: "Error: unknown command \"xxx\" for \"kosli list repos\"\n",
},
{
wantError: true,
name: "06-negative page limit causes an error",
cmd: fmt.Sprintf(`list repos --page-limit -1 %s`, suite.defaultKosliArguments),
golden: "Error: flag '--page-limit' has value '-1' which is illegal\n",
},
{
wantError: true,
name: "07-negative page number causes an error",
cmd: fmt.Sprintf(`list repos --page -1 %s`, suite.defaultKosliArguments),
golden: "Error: flag '--page' has value '-1' which is illegal\n",
},
{
name: "08-can list repos with pagination",
cmd: fmt.Sprintf(`list repos --page-limit 15 --page 2 %s`, suite.defaultKosliArguments),
golden: "",
},
}

runTestCmd(suite.Suite.T(), tests)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestListReposCommandTestSuite(t *testing.T) {
suite.Run(t, new(ListReposCommandTestSuite))
}
95 changes: 52 additions & 43 deletions cmd/kosli/testHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

type jsonCheck struct {
Path string
Want interface{}
Want any
}

// cmdTestCase describes a cmd test case.
Expand Down Expand Up @@ -106,38 +106,53 @@ func goldenPath(filename string) string {
return filepath.Join("testdata", filename)
}

func goldenJsonContains(t *testing.T, output string, path string, want interface{}) {
var data interface{}
func goldenJsonContains(t *testing.T, output string, path string, want any) {
var data any
err := json.Unmarshal([]byte(output), &data)
require.NoError(t, err, "invalid JSON in command output")

// Handle empty path - check root value directly
if path == "" {
// Special case: check for empty array
if want == "[]" || want == "empty" {
list, ok := data.([]interface{})
require.True(t, ok, "expected array at root")
require.Equal(t, 0, len(list), "expected empty array")
return
}
// Special case: check for non-empty array
if want == "non-empty" {
list, ok := data.([]interface{})
require.True(t, ok, "expected array at root")
require.Greater(t, len(list), 0, "expected non-empty array")
return
}
// Special case: check for empty object
if want == "{}" {
obj, ok := data.(map[string]interface{})
require.True(t, ok, "expected object at root")
require.Equal(t, 0, len(obj), "expected empty object")
return
}
require.Equal(t, want, data, "unexpected value at root")
if path != "" {
data = parseJsonData(data, path, t)
}

// Special case: check for empty array
if want == "[]" || want == "empty" {
list, ok := data.([]any)
require.True(t, ok, "expected array at root")
require.Equal(t, 0, len(list), "expected empty array")
return
}
// Special case: check for non-empty array
if want == "non-empty" {
list, ok := data.([]any)
require.True(t, ok, "expected array at root")
require.Greater(t, len(list), 0, "expected non-empty array")
return
}
// Special case: check for empty object
if want == "{}" {
obj, ok := data.(map[string]any)
require.True(t, ok, "expected object at root")
require.Equal(t, 0, len(obj), "expected empty object")
return
}

// Special case: check array length
if wantStr, ok := want.(string); ok && strings.HasPrefix(wantStr, "length:") {
lengthStr := strings.TrimPrefix(wantStr, "length:")
expectedLength, err := strconv.Atoi(lengthStr)
require.NoError(t, err, "invalid length specification: %s", wantStr)

list, ok := data.([]any)
require.True(t, ok, "expected array at path %s", path)
require.Equal(t, expectedLength, len(list), "unexpected array length at path %s", path)
return
}

require.Equal(t, want, data, "unexpected value at path %s", path)
}

func parseJsonData(data any, path string, t *testing.T) any {
current := data
segments := strings.Split(path, ".")
for _, seg := range segments {
Expand All @@ -147,34 +162,21 @@ func goldenJsonContains(t *testing.T, output string, path string, want interface
idx, err := strconv.Atoi(idxStr)
require.NoError(t, err, "invalid array index in path: %s", seg)

list, ok := current.([]interface{})
list, ok := current.([]any)
require.True(t, ok, "expected array at %s", seg)
require.True(t, idx < len(list), "index %d out of range", idx)
current = list[idx]
} else {
// map lookup
m, ok := current.(map[string]interface{})
m, ok := current.(map[string]any)
require.True(t, ok, "expected object at %s", seg)

val, exists := m[seg]
require.True(t, exists, "missing key %s", seg)
current = val
}
}

// Special case: check array length
if wantStr, ok := want.(string); ok && strings.HasPrefix(wantStr, "length:") {
lengthStr := strings.TrimPrefix(wantStr, "length:")
expectedLength, err := strconv.Atoi(lengthStr)
require.NoError(t, err, "invalid length specification: %s", wantStr)

list, ok := current.([]interface{})
require.True(t, ok, "expected array at path %s", path)
require.Equal(t, expectedLength, len(list), "unexpected array length at path %s", path)
return
}

require.Equal(t, want, current, "unexpected value at path %s", path)
return current
}

func compareTwoFiles(actualFilename, expectedFilename string) error {
Expand Down Expand Up @@ -329,6 +331,13 @@ func BeginTrail(trailName, flowName, templatePath string, t *testing.T) {
payload: TrailPayload{
Name: trailName,
Description: "test trail",
GitRepoInfo: &gitview.GitRepoInfo{
URL: "https://github.com/kosli-dev/cli",
Name: "main",
ID: "1234567890",
Description: "test description",
Provider: "github",
},
},
templateFile: templatePath,
flow: flowName,
Expand Down