diff --git a/cli/cli.go b/cli/cli.go index 5b9f868..66ca70e 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -30,6 +30,7 @@ func getWorkerNamespace() components.Namespace { commands.GetAddSecretCommand(), commands.GetListEventsCommand(), commands.GetEditScheduleCommand(), + commands.GetShowExecutionHistoryCommand(), }, } } diff --git a/commands/common/test_commons.go b/commands/common/test_commons.go index f1aaa91..c30403f 100644 --- a/commands/common/test_commons.go +++ b/commands/common/test_commons.go @@ -217,12 +217,10 @@ func AssertOutputJson[T any](wantResponse T) AssertOutputFunc { return func(t *testing.T, output []byte, err error) { require.NoError(t, err) - outputData := new(T) - - err = json.Unmarshal(output, outputData) + wantJson, err := json.Marshal(wantResponse) require.NoError(t, err) - assert.Equal(t, wantResponse, *outputData) + assert.JSONEq(t, string(wantJson), string(output)) } } diff --git a/commands/common/test_worker_server.go b/commands/common/test_worker_server.go index 5f0d2ee..ee5b559 100644 --- a/commands/common/test_worker_server.go +++ b/commands/common/test_worker_server.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "regexp" + "slices" "strings" "testing" "time" @@ -19,6 +20,10 @@ import ( "github.com/stretchr/testify/require" ) +const ( + EndpointExecutionHistory = "/worker/api/v1/execution_history" +) + type BodyValidator func(t require.TestingT, content []byte) func ValidateJson(expected any) BodyValidator { @@ -62,22 +67,43 @@ func NewMockWorkerServer(t *testing.T, stubs ...*ServerStub) (*mockhttp.Server, return server, token } +type queryParamStub struct { + value string + paths []string +} + +type ExecutionHistoryResultEntryStub struct { + Result string `json:"result"` + Logs string `json:"logs"` +} + +type ExecutionHistoryEntryStub struct { + Start time.Time `json:"start"` + End time.Time `json:"end"` + TestRun bool `json:"testRun"` + Result ExecutionHistoryResultEntryStub `json:"entries"` +} + +type ExecutionHistoryStub []*ExecutionHistoryEntryStub + func NewServerStub(t *testing.T) *ServerStub { return &ServerStub{ - test: t, - workers: map[string]*model.WorkerDetails{}, - queryParams: map[string]string{}, + test: t, + workers: map[string]*model.WorkerDetails{}, + queryParams: map[string]queryParamStub{}, + executionHistory: map[string]ExecutionHistoryStub{}, } } type ServerStub struct { - test *testing.T - waitFor time.Duration - token string - projectKey string - workers map[string]*model.WorkerDetails - endpoints []mockhttp.ServerEndpoint - queryParams map[string]string + test *testing.T + waitFor time.Duration + token string + projectKey *queryParamStub + workers map[string]*model.WorkerDetails + executionHistory map[string]ExecutionHistoryStub + endpoints []mockhttp.ServerEndpoint + queryParams map[string]queryParamStub } func (s *ServerStub) WithT(t *testing.T) *ServerStub { @@ -95,13 +121,16 @@ func (s *ServerStub) WithToken(token string) *ServerStub { return s } -func (s *ServerStub) WithProjectKey(projectKey string) *ServerStub { - s.projectKey = projectKey +func (s *ServerStub) WithProjectKey(projectKey string, paths ...string) *ServerStub { + s.projectKey = &queryParamStub{ + value: projectKey, + paths: paths, + } return s } -func (s *ServerStub) WithQueryParam(name, value string) *ServerStub { - s.queryParams[name] = value +func (s *ServerStub) WithQueryParam(name, value string, paths ...string) *ServerStub { + s.queryParams[name] = queryParamStub{value: value, paths: paths} return s } @@ -112,6 +141,11 @@ func (s *ServerStub) WithWorkers(workers ...*model.WorkerDetails) *ServerStub { return s } +func (s *ServerStub) WithWorkerExecutionHistory(workerKey string, history ExecutionHistoryStub) *ServerStub { + s.executionHistory[workerKey] = history + return s +} + func (s *ServerStub) WithCreateEndpoint(validateBody BodyValidator) *ServerStub { s.endpoints = append(s.endpoints, mockhttp.NewServerEndpoint(). @@ -222,6 +256,19 @@ func (s *ServerStub) WithGetAllEndpoint() *ServerStub { return s } +func (s *ServerStub) WithGetExecutionHistoryEndpoint() *ServerStub { + s.endpoints = append(s.endpoints, + mockhttp.NewServerEndpoint(). + When( + mockhttp.Request(). + Method(http.MethodGet). + Path(EndpointExecutionHistory), + ). + HandleWith(s.handleGetExecutionHistory), + ) + return s +} + func (s *ServerStub) handleGetAll(res http.ResponseWriter, req *http.Request) { s.applyDelay() @@ -284,6 +331,48 @@ func (s *ServerStub) handleGetOne(res http.ResponseWriter, req *http.Request) { require.NoError(s.test, err) } +func (s *ServerStub) handleGetExecutionHistory(res http.ResponseWriter, req *http.Request) { + s.applyDelay() + + if !s.validateToken(res, req) { + return + } + + if !s.validateProjectKey(res, req) { + return + } + + if !s.validateQueryParams(res, req) { + return + } + + workerKey := req.URL.Query().Get("workerKey") + + executionHistory, hasHistory := s.executionHistory[workerKey] + if !hasHistory { + executionHistory = ExecutionHistoryStub{} + } + + showTestRun := req.URL.Query().Get("showTestRun") == "true" + + newHistory := make(ExecutionHistoryStub, 0, len(executionHistory)) + for _, entry := range executionHistory { + if entry.TestRun { + if showTestRun { + newHistory = append(newHistory, entry) + } + } else { + newHistory = append(newHistory, entry) + } + } + executionHistory = newHistory + + res.Header().Set("Content-Type", "application/json") + + _, err := res.Write([]byte(MustJsonMarshal(s.test, executionHistory))) + require.NoError(s.test, err) +} + func (s *ServerStub) handleDelete(res http.ResponseWriter, req *http.Request) { s.applyDelay() @@ -408,9 +497,9 @@ func (s *ServerStub) validateToken(res http.ResponseWriter, req *http.Request) b } func (s *ServerStub) validateProjectKey(res http.ResponseWriter, req *http.Request) bool { - if s.projectKey != "" { + if s.projectKey != nil && (len(s.projectKey.paths) == 0 || slices.Contains(s.projectKey.paths, req.URL.Path)) { gotProjectKey := req.URL.Query().Get("projectKey") - if s.projectKey == gotProjectKey { + if s.projectKey.value == gotProjectKey { return true } res.WriteHeader(http.StatusForbidden) @@ -421,13 +510,21 @@ func (s *ServerStub) validateProjectKey(res http.ResponseWriter, req *http.Reque } func (s *ServerStub) validateQueryParams(res http.ResponseWriter, req *http.Request) bool { - for key, value := range s.queryParams { + for key, stub := range s.queryParams { + if len(stub.paths) > 0 && !slices.Contains(stub.paths, req.URL.Path) { + // We only check the query param if the path is in the list + continue + } + gotValue := req.URL.Query().Get(key) - if value == gotValue { - return true + if stub.value == gotValue { + continue } + res.WriteHeader(http.StatusBadRequest) - assert.FailNow(s.test, fmt.Sprintf("Invalid query params %s want=%s, got=%s", key, value, gotValue)) + + assert.FailNow(s.test, fmt.Sprintf("Invalid query params %s want=%s, got=%s", key, stub, gotValue)) + return false } return true diff --git a/commands/show_execution_history_cmd.go b/commands/show_execution_history_cmd.go new file mode 100644 index 0000000..4cb218a --- /dev/null +++ b/commands/show_execution_history_cmd.go @@ -0,0 +1,63 @@ +package commands + +import ( + "net/http" + + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + + plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +func GetShowExecutionHistoryCommand() components.Command { + return components.Command{ + Name: "execution-history", + Description: "Show a worker execution history.", + Aliases: []string{"exec-hist", "eh"}, + Flags: []components.Flag{ + plugins_common.GetServerIdFlag(), + model.GetTimeoutFlag(), + model.GetProjectKeyFlag(), + components.NewBoolFlag( + "with-test-runs", + "Whether to include test-runs entries.", + components.WithBoolDefaultValue(false), + ), + }, + Arguments: []components.Argument{ + model.GetWorkerKeyArgument(), + }, + Action: func(c *components.Context) error { + workerKey, projectKey, err := common.ExtractProjectAndKeyFromCommandContext(c, c.Arguments, 1, false) + if err != nil { + return err + } + + server, err := model.GetServerDetails(c) + if err != nil { + return err + } + + query := map[string]string{ + "workerKey": workerKey, + } + + if c.GetBoolFlagValue("with-test-runs") { + query["showTestRun"] = "true" + } + + return common.CallWorkerApi(c, common.ApiCallParams{ + Method: http.MethodGet, + ServerUrl: server.GetUrl(), + ServerToken: server.GetAccessToken(), + OkStatuses: []int{http.StatusOK}, + ProjectKey: projectKey, + Query: query, + Path: []string{"execution_history"}, + OnContent: common.PrintJson, + }) + }, + } +} diff --git a/commands/show_execution_history_cmd_test.go b/commands/show_execution_history_cmd_test.go new file mode 100644 index 0000000..ebb0370 --- /dev/null +++ b/commands/show_execution_history_cmd_test.go @@ -0,0 +1,155 @@ +//go:build test +// +build test + +package commands + +import ( + "bytes" + "os" + "testing" + "time" + + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + + "github.com/jfrog/jfrog-cli-platform-services/model" +) + +func TestExecutionHistory(t *testing.T) { + entry := &common.ExecutionHistoryEntryStub{ + Start: time.Now(), + End: time.Now().Add(5 * time.Second), + TestRun: false, + Result: common.ExecutionHistoryResultEntryStub{ + Result: "OK", + Logs: "not a test run", + }, + } + testRunEntry := &common.ExecutionHistoryEntryStub{ + Start: time.Now().Add(-24 * time.Hour), + End: time.Now().Add(-23 * time.Hour), + TestRun: true, + Result: common.ExecutionHistoryResultEntryStub{ + Result: "KO", + Logs: "test run", + }, + } + + workerHistory := common.ExecutionHistoryStub{entry, testRunEntry} + + tests := []struct { + name string + commandArgs []string + initExtraArgs []string + assert common.AssertOutputFunc + action string + workerKey string + // The server behavior + serverStub *common.ServerStub + patchManifest func(mf *model.Manifest) + }{ + { + name: "show execution history", + workerKey: "my-worker", + serverStub: common.NewServerStub(t). + WithWorkerExecutionHistory("my-worker", workerHistory). + WithQueryParam("workerKey", "my-worker", common.EndpointExecutionHistory). + WithGetExecutionHistoryEndpoint(), + commandArgs: []string{"my-worker"}, + assert: common.AssertOutputJson(common.ExecutionHistoryStub{entry}), + }, + { + name: "show execution history with test runs", + workerKey: "my-worker", + serverStub: common.NewServerStub(t). + WithWorkerExecutionHistory("my-worker", workerHistory). + WithQueryParam("workerKey", "my-worker", common.EndpointExecutionHistory). + WithGetExecutionHistoryEndpoint(), + commandArgs: []string{"--with-test-runs", "my-worker"}, + assert: common.AssertOutputJson(workerHistory), + }, + { + name: "should read workerKey and projectKey from manifest", + workerKey: "my-worker", + serverStub: common.NewServerStub(t). + WithWorkerExecutionHistory("my-worker", workerHistory). + WithProjectKey("my-project"). + WithQueryParam("workerKey", "my-worker", common.EndpointExecutionHistory). + WithGetExecutionHistoryEndpoint(), + initExtraArgs: []string{"--" + model.FlagProjectKey, "my-project"}, + commandArgs: []string{}, + assert: common.AssertOutputJson(common.ExecutionHistoryStub{entry}), + }, + { + name: "fails if not OK status", + serverStub: common.NewServerStub(t). + WithToken("invalid-token"). + WithWorkerExecutionHistory("a-worker", workerHistory). + WithQueryParam("workerKey", "a-worker", common.EndpointExecutionHistory). + WithGetExecutionHistoryEndpoint(), + commandArgs: []string{"a-worker"}, + assert: common.AssertOutputErrorRegexp(`command.+returned\san\sunexpected\sstatus\scode\s403`), + }, + { + name: "fails if timeout exceeds", + commandArgs: []string{"--" + model.FlagTimeout, "500"}, + serverStub: common.NewServerStub(t).WithDelay(2 * time.Second).WithGetExecutionHistoryEndpoint(), + assert: common.AssertOutputError("request timed out after 500ms"), + }, + { + name: "fails if invalid timeout", + commandArgs: []string{"--" + model.FlagTimeout, "abc", `{}`}, + assert: common.AssertOutputError("invalid timeout provided"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetShowExecutionHistoryCommand()) + + _, workerName := common.PrepareWorkerDirForTest(t) + + if tt.workerKey != "" { + workerName = tt.workerKey + } + + action := "GENERIC_EVENT" + if tt.action != "" { + action = tt.action + } + + if tt.serverStub == nil { + tt.serverStub = common.NewServerStub(t) + } + + common.NewMockWorkerServer(t, + tt.serverStub. + WithT(t). + WithDefaultActionsMetadataEndpoint(), + ) + + initCmd := append([]string{"worker", "init"}, tt.initExtraArgs...) + err := runCmd(append(initCmd, action, workerName)...) + if err != nil { + tt.assert(t, nil, err) + return + } + + if tt.patchManifest != nil { + common.PatchManifest(t, tt.patchManifest) + } + + var output bytes.Buffer + + common.SetCliOut(&output) + t.Cleanup(func() { + common.SetCliOut(os.Stdout) + }) + + cmd := append([]string{"worker", "execution-history"}, tt.commandArgs...) + + err = runCmd(cmd...) + + tt.assert(t, output.Bytes(), err) + }) + } +} diff --git a/qa-plugin/go.mod b/qa-plugin/go.mod index da0d328..0df5bee 100644 --- a/qa-plugin/go.mod +++ b/qa-plugin/go.mod @@ -12,22 +12,22 @@ require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/CycloneDX/cyclonedx-go v0.9.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.1.2 // indirect + github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/forPelevin/gomoji v1.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-git/go-billy/v5 v5.6.0 // indirect + github.com/go-git/go-git/v5 v5.13.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect @@ -66,7 +66,7 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -80,14 +80,14 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.35.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.27.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/qa-plugin/go.sum b/qa-plugin/go.sum index c6bcce5..cdb8c68 100644 --- a/qa-plugin/go.sum +++ b/qa-plugin/go.sum @@ -7,8 +7,8 @@ github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKc github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= -github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -38,8 +38,8 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -47,8 +47,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= +github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/forPelevin/gomoji v1.2.0 h1:9k4WVSSkE1ARO/BWywxgEUBvR/jMnao6EZzrql5nxJ8= @@ -57,16 +57,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= +github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= +github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -143,8 +143,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= @@ -176,8 +176,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -226,18 +226,18 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -257,14 +257,14 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= diff --git a/qa-plugin/main.go b/qa-plugin/main.go index 838f4a1..50ca753 100644 --- a/qa-plugin/main.go +++ b/qa-plugin/main.go @@ -30,5 +30,6 @@ func getCommands() []components.Command { commands.GetAddSecretCommand(), commands.GetListEventsCommand(), commands.GetEditScheduleCommand(), + commands.GetShowExecutionHistoryCommand(), } } diff --git a/test/commands/show_execution_history_test.go b/test/commands/show_execution_history_test.go new file mode 100644 index 0000000..1e2eb80 --- /dev/null +++ b/test/commands/show_execution_history_test.go @@ -0,0 +1,97 @@ +//go:build itest + +package commands + +import ( + "encoding/json" + "testing" + + "github.com/jfrog/jfrog-cli-platform-services/commands/common" + "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/jfrog/jfrog-cli-platform-services/test/infra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShowExecutionHistory(t *testing.T) { + tests := []struct { + name string + test func(it *infra.Test, workerKey string) + }{ + { + name: "nominal", + test: testShowExecutionHistory(false, 2), + }, + { + name: "with test runs", + test: testShowExecutionHistory(true, 3), + }, + } + + infra.RunITests([]infra.TestDefinition{ + { + Name: "Execution History", + CaptureOutput: true, + Test: func(it *infra.Test) { + dir, workerKey := it.PrepareWorkerTestDir() + + err := it.RunCommand(infra.AppName, "init", "GENERIC_EVENT", workerKey) + require.NoError(it, err) + + common.PatchManifest(it, func(mf *model.Manifest) { + mf.Enabled = true + mf.Debug = true + }, dir) + + err = it.RunCommand(infra.AppName, "deploy") + require.NoError(it, err) + if err == nil { + // We make sure to undeploy our worker + it.Cleanup(func() { + it.DeleteWorker(workerKey) + }) + } + + payload := map[string]any{"my": "payload"} + + it.ExecuteWorker(workerKey, payload) + it.ExecuteWorker(workerKey, payload) + it.TestRunWorker( + workerKey, + "worker", + "GENERIC_EVENT", + `export default async () => ({"message":"OK"});`, + payload, + true, + ) + + for _, tt := range tests { + it.Run(tt.name, func(t *infra.Test) { + it.ResetOutput() + tt.test(t, workerKey) + }) + } + }, + }, + }, t) +} + +func testShowExecutionHistory(showTestRun bool, wantEntries int) func(it *infra.Test, workerKey string) { + return func(it *infra.Test, workerKey string) { + cmd := []string{infra.AppName, "execution-history"} + + if showTestRun { + cmd = append(cmd, "--with-test-runs") + } + + err := it.RunCommand(cmd...) + require.NoError(it, err) + + var content any + + err = json.Unmarshal(it.CapturedOutput(), &content) + require.NoError(it, err) + + assert.Len(it, content, wantEntries) + } +} diff --git a/test/infra/http_utils.go b/test/infra/http_utils.go index 8692877..5b9d8e3 100644 --- a/test/infra/http_utils.go +++ b/test/infra/http_utils.go @@ -29,6 +29,7 @@ type HttpRequest struct { basicAuth *basicAuth bodyReader io.Reader headers map[string]string + query url.Values reqContext context.Context url string } @@ -48,9 +49,14 @@ type HttpExecutor struct { func (h *HttpRequest) getUrl(endpoint string) string { baseUrl := h.url if h.url == "" { - return "http://localhost:8082" + baseUrl = "http://localhost:8082" } - return strings.TrimSuffix(baseUrl, "/") + "/" + strings.TrimPrefix(endpoint, "/") + u := strings.TrimSuffix(baseUrl, "/") + "/" + strings.TrimPrefix(endpoint, "/") + query := h.query.Encode() + if query != "" { + u += "?" + query + } + return u } func (h *HttpRequest) Get(endpoint string) *HttpExecutor { @@ -151,6 +157,14 @@ func (h *HttpRequest) WithBasicAuth(username string, password string) *HttpReque return h } +func (h *HttpRequest) WithQueryParam(name, value string) *HttpRequest { + if h.query == nil { + h.query = url.Values{} + } + h.query.Add(name, value) + return h +} + func (h *HttpRequest) WithFormData(params map[string]string) *HttpRequest { values := url.Values{} for k, v := range params { @@ -171,7 +185,7 @@ func (h *HttpRequest) WithJsonBytes(jsonBytes []byte) *HttpRequest { } func (h *HttpRequest) WithJsonString(jsonString string) *HttpRequest { - h.headers["Content-Type"] = "application/json; charset=UTF-8" + h.headers["Content-Type"] = "application/json" return h.WithBody(strings.NewReader(jsonString)) } diff --git a/test/infra/itest_runner.go b/test/infra/itest_runner.go index ef58c8d..0acf333 100644 --- a/test/infra/itest_runner.go +++ b/test/infra/itest_runner.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "os" "path" @@ -227,6 +228,46 @@ func (it *Test) DeleteWorker(workerKey string) { } } +func (it *Test) ResetOutput() { + if it.output != nil { + it.output.Reset() + } +} + +func (it *Test) ExecuteWorker(workerKey string, payload any) { + ctx, cancelCtx := context.WithTimeout(context.Background(), requestTimeout) + defer cancelCtx() + + it.NewHttpRequestWithContext(ctx). + WithAccessToken(). + WithJsonData(payload). + Post("/worker/api/v1/execute/" + workerKey). + Do(). + IsOk() +} + +func (it *Test) TestRunWorker(workerKey string, application, event string, code string, payload any, debug bool) { + ctx, cancelCtx := context.WithTimeout(context.Background(), requestTimeout) + defer cancelCtx() + + it.Logf("Executing worker '%s'", workerKey) + + it.NewHttpRequestWithContext(ctx). + WithAccessToken(). + WithQueryParam("debug", fmt.Sprint(debug)). + WithJsonData(map[string]any{ + "code": code, + "data": payload, + "action": map[string]string{ + "application": application, + "name": event, + }, + }). + Post("/worker/api/v2/test/" + workerKey). + Do(). + IsOk() +} + func (it *Test) DeleteAllWorkers() { for _, wk := range it.GetAllWorkers() { it.DeleteWorker(wk.Key) diff --git a/test/infra/test_app.go b/test/infra/test_app.go index 69f4848..8cdb9a7 100644 --- a/test/infra/test_app.go +++ b/test/infra/test_app.go @@ -24,6 +24,7 @@ func getApp() components.App { commands.GetAddSecretCommand(), commands.GetListEventsCommand(), commands.GetEditScheduleCommand(), + commands.GetShowExecutionHistoryCommand(), } return app }