diff --git a/commands/common/cmd_api.go b/commands/common/cmd_api.go index 403f794..f5b465b 100644 --- a/commands/common/cmd_api.go +++ b/commands/common/cmd_api.go @@ -43,16 +43,17 @@ func apiError(status int, message string, args ...any) *APIError { } type APICallParams struct { - Method string - ServerURL string - ServerToken string - Body []byte - Query map[string]string - Path []string - ProjectKey string - APIVersion apiVersion - OkStatuses []int - OnContent APIContentHandler + Method string + ServerURL string + ServerToken string + Body []byte + Query map[string]string + Path []string + ProjectKey string + APIVersion apiVersion + OkStatuses []int + OnContent APIContentHandler + CaptureStatus *int } func CallWorkerAPI(c model.IntFlagProvider, params APICallParams) error { @@ -113,6 +114,10 @@ func CallWorkerAPI(c model.IntFlagProvider, params APICallParams) error { return apiError(res.StatusCode, "command %s %s returned an unexpected status code %d", params.Method, apiEndpoint, res.StatusCode) } + if params.CaptureStatus != nil { + *params.CaptureStatus = res.StatusCode + } + return processAPIResponse(res, params.OnContent) } diff --git a/commands/common/cmd_io.go b/commands/common/cmd_io.go index 8e738d8..937b456 100644 --- a/commands/common/cmd_io.go +++ b/commands/common/cmd_io.go @@ -42,6 +42,32 @@ func PrintJSON(data []byte) error { return err } +// PrintJSONValue marshals v to indented JSON and writes it to the CLI output. +// Use instead of json.Marshal + PrintJSON when the struct is already available. +func PrintJSONValue(v any) error { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + _, err = cliOut.Write(data) + return err +} + +// PrintJSONOrStatus prints contentBytes as JSON when it is valid JSON, +// otherwise prints {"status_code": statusCode, "content": ""}. +func PrintJSONOrStatus(statusCode int, contentBytes []byte) error { + if len(contentBytes) > 0 && json.Valid(contentBytes) { + return PrintJSON(contentBytes) + } + return PrintJSONValue(struct { + StatusCode int `json:"status_code"` + Content string `json:"content"` + }{ + StatusCode: statusCode, + Content: string(contentBytes), + }) +} + func printJSONOrLogError(data []byte) error { if _, writeErr := cliOut.Write(PrettifyJSON(data)); writeErr != nil { log.Warn(fmt.Sprintf("Write error: %+v (data:%s)", writeErr, string(data))) diff --git a/commands/common/errors.go b/commands/common/errors.go new file mode 100644 index 0000000..039e400 --- /dev/null +++ b/commands/common/errors.go @@ -0,0 +1,16 @@ +package common + +import ( + "fmt" + "strings" + + "github.com/jfrog/jfrog-cli-core/v2/common/format" +) + +func ErrUnsupportedFormat(format format.OutputFormat, supportedFormats ...format.OutputFormat) error { + supportedFormatsStr := make([]string, len(supportedFormats)) + for i, f := range supportedFormats { + supportedFormatsStr[i] = string(f) + } + return fmt.Errorf("unsupported format '%s'. Accepted values: %s", format, strings.Join(supportedFormatsStr, ", ")) +} diff --git a/commands/common/test_worker_server.go b/commands/common/test_worker_server.go index f6bba98..9faa56a 100644 --- a/commands/common/test_worker_server.go +++ b/commands/common/test_worker_server.go @@ -85,16 +85,17 @@ type queryParamStub struct { 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"` + WorkerKey string `json:"workerKey"` + WorkerType string `json:"workerType"` + WorkerProjectKey string `json:"workerProjectKey"` + ExecutionStatus string `json:"executionStatus"` + StartTimeMillis int64 `json:"startTimeMillis"` + EndTimeMillis int64 `json:"endTimeMillis"` + TriggeredBy string `json:"triggeredBy"` + TestRun bool `json:"testRun"` + ExecutedVersion string `json:"executedVersion"` + TraceID string `json:"traceId"` } type ExecutionHistoryStub []*ExecutionHistoryEntryStub diff --git a/commands/deploy_cmd.go b/commands/deploy_cmd.go index d8fa210..e0bf7dd 100644 --- a/commands/deploy_cmd.go +++ b/commands/deploy_cmd.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "slices" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" @@ -28,11 +30,23 @@ type deployRequest struct { Version *model.Version `json:"version,omitempty"` } +type deployCommandHandler struct { + ctx *components.Context + manifest *model.Manifest + actionMeta *model.ActionMetadata + version *model.Version + serverURL string + token string + encodeSourceCodeInBase64 bool + outputFormat format.OutputFormat +} + func GetDeployCommand() components.Command { return components.Command{ - Name: "deploy", - Description: "Deploy a worker", - Aliases: []string{"d"}, + Name: "deploy", + Description: "Deploy a worker", + Aliases: []string{"d"}, + SupportedFormats: []format.OutputFormat{format.Json}, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -43,6 +57,15 @@ func GetDeployCommand() components.Command { model.GetBase64Flag(), }, Action: func(c *components.Context) error { + var outputFormat format.OutputFormat + if slices.Contains(c.FlagsUsed, format.FlagName) { + var fmtErr error + outputFormat, fmtErr = c.GetOutputFormat() + if fmtErr != nil { + return fmtErr + } + } + server, err := model.GetServerDetails(c) if err != nil { return err @@ -102,18 +125,28 @@ func GetDeployCommand() components.Command { } else { encodeSourceCodeInBase64 = *options.ShouldEncodeSourceCodeInBase64 || c.GetBoolFlagValue(model.FlagBase64) } - return runDeployCommand(c, manifest, actionMeta, version, server.GetUrl(), server.GetAccessToken(), encodeSourceCodeInBase64) + + return (&deployCommandHandler{ + ctx: c, + manifest: manifest, + actionMeta: actionMeta, + version: version, + serverURL: server.GetUrl(), + token: server.GetAccessToken(), + encodeSourceCodeInBase64: encodeSourceCodeInBase64, + outputFormat: outputFormat, + }).run() }, } } -func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, serverURL string, token string, encodeSourceCodeInBase64 bool) error { - existingWorker, err := common.FetchWorkerDetails(ctx, serverURL, token, manifest.Name, manifest.ProjectKey) +func (h *deployCommandHandler) run() error { + existingWorker, err := common.FetchWorkerDetails(h.ctx, h.serverURL, h.token, h.manifest.Name, h.manifest.ProjectKey) if err != nil { return err } - body, err := prepareDeployRequest(ctx, manifest, actionMeta, version, existingWorker, encodeSourceCodeInBase64) + body, err := h.prepareRequest(existingWorker) if err != nil { return err } @@ -123,70 +156,81 @@ func runDeployCommand(ctx *components.Context, manifest *model.Manifest, actionM return err } + var responseStatus int + var contentHandler common.APIContentHandler + if h.outputFormat != format.None { + contentHandler = func(body []byte) error { + return common.PrintJSONOrStatus(responseStatus, body) + } + } + if existingWorker == nil { - log.Info(fmt.Sprintf("Deploying worker '%s'", manifest.Name)) - err = common.CallWorkerAPI(ctx, common.APICallParams{ - Method: http.MethodPost, - ServerURL: serverURL, - ServerToken: token, - Body: bodyBytes, - OkStatuses: []int{http.StatusCreated}, - Path: []string{"workers"}, - APIVersion: common.APIVersionV2, + log.Info(fmt.Sprintf("Deploying worker '%s'", h.manifest.Name)) + err = common.CallWorkerAPI(h.ctx, common.APICallParams{ + Method: http.MethodPost, + ServerURL: h.serverURL, + ServerToken: h.token, + Body: bodyBytes, + OkStatuses: []int{http.StatusCreated}, + Path: []string{"workers"}, + APIVersion: common.APIVersionV2, + OnContent: contentHandler, + CaptureStatus: &responseStatus, }) if err == nil { - log.Info(fmt.Sprintf("Worker '%s' deployed", manifest.Name)) + log.Info(fmt.Sprintf("Worker '%s' deployed", h.manifest.Name)) + } + } else { + log.Info(fmt.Sprintf("Updating worker '%s'", h.manifest.Name)) + err = common.CallWorkerAPI(h.ctx, common.APICallParams{ + Method: http.MethodPut, + ServerURL: h.serverURL, + ServerToken: h.token, + Body: bodyBytes, + OkStatuses: []int{http.StatusNoContent}, + Path: []string{"workers"}, + APIVersion: common.APIVersionV2, + OnContent: contentHandler, + CaptureStatus: &responseStatus, + }) + if err == nil { + log.Info(fmt.Sprintf("Worker '%s' updated", h.manifest.Name)) } - return err - } - - log.Info(fmt.Sprintf("Updating worker '%s'", manifest.Name)) - err = common.CallWorkerAPI(ctx, common.APICallParams{ - Method: http.MethodPut, - ServerURL: serverURL, - ServerToken: token, - Body: bodyBytes, - OkStatuses: []int{http.StatusNoContent}, - Path: []string{"workers"}, - APIVersion: common.APIVersionV2, - }) - if err == nil { - log.Info(fmt.Sprintf("Worker '%s' updated", manifest.Name)) } return err } -func prepareDeployRequest(ctx *components.Context, manifest *model.Manifest, actionMeta *model.ActionMetadata, version *model.Version, existingWorker *model.WorkerDetails, encodeSourceCodeInBase64 bool) (*deployRequest, error) { - sourceCode, err := common.ReadSourceCode(manifest) +func (h *deployCommandHandler) prepareRequest(existingWorker *model.WorkerDetails) (*deployRequest, error) { + sourceCode, err := common.ReadSourceCode(h.manifest) if err != nil { return nil, err } sourceCode = common.CleanImports(sourceCode) - if encodeSourceCodeInBase64 { + if h.encodeSourceCodeInBase64 { sourceCode = "base64:" + base64.StdEncoding.EncodeToString([]byte(sourceCode)) } var secrets []*model.Secret - - if !ctx.GetBoolFlagValue(model.FlagNoSecrets) { - secrets = common.PrepareSecretsUpdate(manifest, existingWorker) + if !h.ctx.GetBoolFlagValue(model.FlagNoSecrets) { + secrets = common.PrepareSecretsUpdate(h.manifest, existingWorker) } + payload := &deployRequest{ - Key: manifest.Name, - Action: actionMeta.Action, - Description: manifest.Description, - Enabled: manifest.Enabled, - Debug: manifest.Debug, + Key: h.manifest.Name, + Action: h.actionMeta.Action, + Description: h.manifest.Description, + Enabled: h.manifest.Enabled, + Debug: h.manifest.Debug, SourceCode: sourceCode, Secrets: secrets, - ProjectKey: manifest.ProjectKey, - Version: version, + ProjectKey: h.manifest.ProjectKey, + Version: h.version, } - if actionMeta.MandatoryFilter { - payload.FilterCriteria = manifest.FilterCriteria + if h.actionMeta.MandatoryFilter { + payload.FilterCriteria = h.manifest.FilterCriteria } return payload, nil } diff --git a/commands/deploy_cmd_test.go b/commands/deploy_cmd_test.go index 6c2e459..e1b96b1 100644 --- a/commands/deploy_cmd_test.go +++ b/commands/deploy_cmd_test.go @@ -4,13 +4,16 @@ package commands import ( + "bytes" "encoding/base64" "encoding/json" "errors" "fmt" + "os" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/stretchr/testify/assert" @@ -344,3 +347,49 @@ func getExpectedDeployRequestForAction( return r } + +func setupDeployFormatTest(t *testing.T) (func(args ...string) error, *bytes.Buffer) { + t.Helper() + + serverStub := common.NewServerStub(t). + WithDefaultActionsMetadataEndpoint(). + WithGetOneEndpoint(). + WithOptionsEndpoint(). + WithCreateEndpoint(nil) + common.NewMockWorkerServer(t, serverStub) + + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetDeployCommand()) + + _, workerName := common.PrepareWorkerDirForTest(t) + require.NoError(t, runCmd("worker", "init", "BEFORE_UPLOAD", workerName)) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + return runCmd, &out +} + +func TestWorkerDeploy_FormatJSON(t *testing.T) { + runCmd, out := setupDeployFormatTest(t) + + require.NoError(t, runCmd("worker", "deploy", "--"+format.FlagName, "json")) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) + assert.Contains(t, out.String(), "status_code") + assert.Contains(t, out.String(), "content") +} + +func TestWorkerDeploy_FormatTableRejected(t *testing.T) { + runCmd, _ := setupDeployFormatTest(t) + + err := runCmd("worker", "deploy", "--"+format.FlagName, "table") + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} + +func TestWorkerDeploy_NoFormat(t *testing.T) { + runCmd, out := setupDeployFormatTest(t) + + require.NoError(t, runCmd("worker", "deploy")) + assert.Empty(t, out.String(), "expected no JSON output when --format is not set, got: %s", out.String()) +} diff --git a/commands/dry_run_cmd.go b/commands/dry_run_cmd.go index 4c37291..be7e87d 100644 --- a/commands/dry_run_cmd.go +++ b/commands/dry_run_cmd.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/jfrog/jfrog-client-go/utils/log" @@ -28,9 +29,11 @@ type dryRunRequest struct { func GetDryRunCommand() components.Command { return components.Command{ - Name: "test-run", - Description: "Dry run a worker", - Aliases: []string{"dry-run", "dr", "tr"}, + Name: "test-run", + Description: "Dry run a worker", + Aliases: []string{"dry-run", "dr", "tr"}, + SupportedFormats: []format.OutputFormat{format.Json, format.Table}, + DefaultFormat: format.Json, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -40,6 +43,11 @@ func GetDryRunCommand() components.Command { model.GetJSONPayloadArgument(), }, Action: func(c *components.Context) error { + outputFormat, err := c.GetOutputFormat() + if err != nil { + return err + } + h := &dryRunHandler{c} manifest, err := common.ReadManifest() @@ -74,16 +82,25 @@ func GetDryRunCommand() components.Command { } } - return h.run(manifest, server.GetUrl(), server.GetAccessToken(), data) + return h.run(manifest, server.GetUrl(), server.GetAccessToken(), data, outputFormat) }, } } -func (c *dryRunHandler) run(manifest *model.Manifest, serverURL string, token string, data map[string]any) error { +func (c *dryRunHandler) run(manifest *model.Manifest, serverURL string, token string, data map[string]any, outputFormat format.OutputFormat) error { body, err := c.preparePayload(manifest, serverURL, token, data) if err != nil { return err } + + var contentHandler func([]byte) error + switch outputFormat { + case format.Json: + contentHandler = common.PrintJSON + case format.Table: + contentHandler = printDryRunResponseAsTable + } + return common.CallWorkerAPI(c.ctx, common.APICallParams{ Method: http.MethodPost, ServerURL: serverURL, @@ -95,10 +112,26 @@ func (c *dryRunHandler) run(manifest *model.Manifest, serverURL string, token st }, OkStatuses: []int{http.StatusOK}, Path: []string{"test", manifest.Name}, - OnContent: common.PrintJSON, + OnContent: contentHandler, }) } +func printDryRunResponseAsTable(responseBytes []byte) error { + var data map[string]any + if err := json.Unmarshal(responseBytes, &data); err != nil { + return common.PrintJSON(responseBytes) + } + + writer := common.NewCsvWriter() + for k, v := range data { + if err := writer.Write([]string{k, fmt.Sprint(v)}); err != nil { + return err + } + } + writer.Flush() + return writer.Error() +} + func (c *dryRunHandler) preparePayload(manifest *model.Manifest, serverURL string, token string, data map[string]any) ([]byte, error) { payload := &dryRunRequest{Action: manifest.Action, Data: data} diff --git a/commands/dry_run_cmd_test.go b/commands/dry_run_cmd_test.go index fb0c299..7ec77ef 100644 --- a/commands/dry_run_cmd_test.go +++ b/commands/dry_run_cmd_test.go @@ -5,13 +5,17 @@ package commands import ( "bytes" + "encoding/json" "os" + "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" - "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestDryRun(t *testing.T) { @@ -197,3 +201,59 @@ func validateTestPayloadData(data any) common.BodyValidator { return gotData }) } + +const workerKeyForDryRunTest = "test-worker" + +func setupDryRunFormatTest(t *testing.T) (func(args ...string) error, *bytes.Buffer) { + t.Helper() + + workerKey := workerKeyForDryRunTest + serverStub := common.NewServerStub(t). + WithWorkers(&model.WorkerDetails{Key: workerKey}). + WithDefaultActionsMetadataEndpoint(). + WithGetOneEndpoint(). + WithTestEndpoint(nil, map[string]any{"status": "OK", "result": "done"}) + common.NewMockWorkerServer(t, serverStub) + + common.PrepareWorkerDirForTest(t) + + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetDryRunCommand()) + require.NoError(t, runCmd("worker", "init", "BEFORE_DOWNLOAD", workerKey)) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + return runCmd, &out +} + +func TestWorkerDryRun_FormatJSON(t *testing.T) { + runCmd, out := setupDryRunFormatTest(t) + + require.NoError(t, runCmd("worker", "dry-run", "--"+format.FlagName, "json", `{}`)) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) +} + +func TestWorkerDryRun_FormatTable(t *testing.T) { + runCmd, out := setupDryRunFormatTest(t) + + require.NoError(t, runCmd("worker", "dry-run", "--"+format.FlagName, "table", `{}`)) + outputStr := out.String() + assert.True(t, strings.Contains(outputStr, "status") || strings.Contains(outputStr, "result"), + "expected table output to contain response fields, got: %s", outputStr) +} + +func TestWorkerDryRun_FormatDefault(t *testing.T) { + runCmd, out := setupDryRunFormatTest(t) + + require.NoError(t, runCmd("worker", "dry-run", `{}`)) + assert.True(t, json.Valid(out.Bytes()), "default output should be valid JSON, got: %s", out.String()) +} + +func TestWorkerDryRun_FormatUnsupported(t *testing.T) { + runCmd, _ := setupDryRunFormatTest(t) + + err := runCmd("worker", "dry-run", "--"+format.FlagName, "sarif", `{}`) + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} diff --git a/commands/execute_cmd.go b/commands/execute_cmd.go index c9f6d5d..5b08d1f 100644 --- a/commands/execute_cmd.go +++ b/commands/execute_cmd.go @@ -2,8 +2,10 @@ package commands import ( "encoding/json" + "fmt" "net/http" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/jfrog/jfrog-client-go/utils/log" @@ -16,9 +18,11 @@ import ( func GetExecuteCommand() components.Command { return components.Command{ - Name: "execute", - Description: "Execute a GENERIC_EVENT worker", - Aliases: []string{"exec", "e"}, + Name: "execute", + Description: "Execute a GENERIC_EVENT worker", + Aliases: []string{"exec", "e"}, + SupportedFormats: []format.OutputFormat{format.Json, format.Table}, + DefaultFormat: format.Json, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -33,6 +37,19 @@ func GetExecuteCommand() components.Command { } func runExecuteCommand(c *components.Context) error { + outputFormat, err := c.GetOutputFormat() + if err != nil { + return err + } + + var contentHandler func([]byte) error + switch outputFormat { + case format.Json: + contentHandler = common.PrintJSON + case format.Table: + contentHandler = printExecuteResponseAsTable + } + workerKey, projectKey, err := common.ExtractProjectAndKeyFromCommandContext(c, c.Arguments, 1, true) if err != nil { return err @@ -67,6 +84,22 @@ func runExecuteCommand(c *components.Context) error { Body: body, ProjectKey: projectKey, Path: []string{"execute", workerKey}, - OnContent: common.PrintJSON, + OnContent: contentHandler, }) } + +func printExecuteResponseAsTable(responseBytes []byte) error { + var data map[string]any + if err := json.Unmarshal(responseBytes, &data); err != nil { + return common.PrintJSON(responseBytes) + } + + writer := common.NewCsvWriter() + for k, v := range data { + if err := writer.Write([]string{k, fmt.Sprint(v)}); err != nil { + return err + } + } + writer.Flush() + return writer.Error() +} diff --git a/commands/execute_cmd_test.go b/commands/execute_cmd_test.go index 23cb3c0..c122474 100644 --- a/commands/execute_cmd_test.go +++ b/commands/execute_cmd_test.go @@ -5,13 +5,17 @@ package commands import ( "bytes" + "encoding/json" "os" + "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" - "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExecute(t *testing.T) { @@ -201,3 +205,60 @@ func TestExecute(t *testing.T) { }) } } + +func setupExecuteFormatTest(t *testing.T) (func(args ...string) error, *bytes.Buffer) { + t.Helper() + + workerKey := "test-worker" + serverStub := common.NewServerStub(t). + WithWorkers(&model.WorkerDetails{Key: workerKey}). + WithDefaultActionsMetadataEndpoint(). + WithGetOneEndpoint(). + WithExecuteEndpoint(nil, map[string]any{"status": "OK", "result": "done"}) + common.NewMockWorkerServer(t, serverStub) + + _, dir := common.PrepareWorkerDirForTest(t) + _ = dir + + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetExecuteCommand()) + require.NoError(t, runCmd("worker", "init", "GENERIC_EVENT", workerKey)) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + return runCmd, &out +} + +func TestWorkerExecute_FormatJSON(t *testing.T) { + runCmd, out := setupExecuteFormatTest(t) + + require.NoError(t, runCmd("worker", "execute", workerKeyForExecuteTest, "--"+format.FlagName, "json", `{}`)) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) +} + +func TestWorkerExecute_FormatTable(t *testing.T) { + runCmd, out := setupExecuteFormatTest(t) + + require.NoError(t, runCmd("worker", "execute", workerKeyForExecuteTest, "--"+format.FlagName, "table", `{}`)) + outputStr := out.String() + assert.True(t, strings.Contains(outputStr, "status") || strings.Contains(outputStr, "result"), + "expected table output to contain response fields, got: %s", outputStr) +} + +func TestWorkerExecute_FormatDefault(t *testing.T) { + runCmd, out := setupExecuteFormatTest(t) + + require.NoError(t, runCmd("worker", "execute", workerKeyForExecuteTest, `{}`)) + assert.True(t, json.Valid(out.Bytes()), "default output should be valid JSON, got: %s", out.String()) +} + +func TestWorkerExecute_FormatUnsupported(t *testing.T) { + runCmd, _ := setupExecuteFormatTest(t) + + err := runCmd("worker", "execute", workerKeyForExecuteTest, "--"+format.FlagName, "sarif", `{}`) + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} + +const workerKeyForExecuteTest = "test-worker" diff --git a/commands/list_cmd.go b/commands/list_cmd.go index 449f6d9..f4ff509 100644 --- a/commands/list_cmd.go +++ b/commands/list_cmd.go @@ -7,10 +7,12 @@ import ( "slices" "strings" - "github.com/jfrog/jfrog-cli-platform-services/commands/common" - + "github.com/jfrog/jfrog-cli-core/v2/common/format" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + "github.com/jfrog/jfrog-client-go/utils/log" + + "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/jfrog/jfrog-cli-platform-services/model" ) @@ -20,12 +22,14 @@ type getAllResponse struct { func GetListCommand() components.Command { return components.Command{ - Name: "list", - Description: "List workers. The default output is a CSV format with columns ,,,.", - Aliases: []string{"ls"}, + Name: "list", + Description: "List workers. The default output is a CSV format with columns ,,,.", + Aliases: []string{"ls"}, + SupportedFormats: []format.OutputFormat{format.Json, format.Table}, + DefaultFormat: format.Table, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), - model.GetJSONOutputFlag("Use JSON instead of CSV as output"), + model.GetJSONOutputFlag("Deprecated: use --format json instead."), model.GetTimeoutFlag(), model.GetProjectKeyFlag(), }, @@ -58,9 +62,21 @@ func runListCommand(ctx *components.Context, serverURL string, token string) err params["action"] = action } - contentHandler := printWorkerDetailsAsCsv + outputFormat, err := ctx.GetOutputFormat() + if err != nil { + return err + } if ctx.GetBoolFlagValue(model.FlagJSONOutput) { + log.Warn("--json is deprecated, use --format json instead.") + outputFormat = format.Json + } + + var contentHandler func([]byte) error + switch outputFormat { + case format.Json: contentHandler = common.PrintJSON + case format.Table: + contentHandler = printWorkerDetailsAsCsv } return common.CallWorkerAPI(ctx, common.APICallParams{ diff --git a/commands/list_cmd_test.go b/commands/list_cmd_test.go index a27c9da..7aba9ff 100644 --- a/commands/list_cmd_test.go +++ b/commands/list_cmd_test.go @@ -8,9 +8,11 @@ import ( "encoding/json" "os" "sort" + "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/stretchr/testify/assert" @@ -19,6 +21,23 @@ import ( "github.com/jfrog/jfrog-cli-platform-services/model" ) +var testWorkers = []*model.WorkerDetails{ + { + Key: "wk-0", + Action: "AFTER_CREATE", + Description: "run wk-0", + Enabled: true, + SourceCode: "export default async () => ({ 'S': 'OK'})", + }, + { + Key: "wk-1", + Action: "GENERIC_EVENT", + Description: "run wk-1", + Enabled: false, + SourceCode: "export default async () => ({ 'S': 'OK'})", + }, +} + func TestListCommand(t *testing.T) { tests := []struct { name string @@ -65,33 +84,9 @@ func TestListCommand(t *testing.T) { }...), assert: common.AssertOutputText("wk-0,AFTER_CREATE,run wk-0,true", "invalid csv received"), }, - { - name: "list for JSON", - commandArgs: []string{"--" + model.FlagJSONOutput}, - serverStub: common.NewServerStub(t). - WithGetAllEndpoint(). - WithWorkers([]*model.WorkerDetails{ - { - Key: "wk-1", - Action: "GENERIC_EVENT", - Description: "run wk-1", - Enabled: false, - SourceCode: "export default async () => ({ 'S': 'OK'})", - }, - }...), - assert: assertWorkerListOutput([]*model.WorkerDetails{ - { - Key: "wk-1", - Action: "GENERIC_EVENT", - Description: "run wk-1", - Enabled: false, - SourceCode: "export default async () => ({ 'S': 'OK'})", - }, - }), - }, { name: "projectKey is passed to the request", - commandArgs: []string{"--" + model.FlagProjectKey, "my-project", "--" + model.FlagJSONOutput}, + commandArgs: []string{"--" + model.FlagProjectKey, "my-project", "--" + format.FlagName, "json"}, serverStub: common.NewServerStub(t).WithProjectKey("my-project").WithGetAllEndpoint(), assert: common.AssertOutputJson(map[string]any{"workers": []any{}}), }, @@ -133,6 +128,77 @@ func TestListCommand(t *testing.T) { } } +func TestWorkerList_FormatJSON(t *testing.T) { + serverStub := common.NewServerStub(t).WithGetAllEndpoint().WithWorkers(testWorkers...) + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListCommand()) + require.NoError(t, runCmd("worker", "list", "--"+format.FlagName, "json")) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) +} + +func TestWorkerList_FormatTable(t *testing.T) { + serverStub := common.NewServerStub(t).WithGetAllEndpoint().WithWorkers(testWorkers...) + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListCommand()) + require.NoError(t, runCmd("worker", "list", "--"+format.FlagName, "table")) + + outputStr := out.String() + // CSV output should contain worker keys and action names + assert.True(t, strings.Contains(outputStr, "wk-0"), "expected wk-0 in table output, got: %s", outputStr) + assert.True(t, strings.Contains(outputStr, "AFTER_CREATE"), "expected AFTER_CREATE in table output, got: %s", outputStr) + assert.True(t, strings.Contains(outputStr, "wk-1"), "expected wk-1 in table output, got: %s", outputStr) + assert.True(t, strings.Contains(outputStr, "GENERIC_EVENT"), "expected GENERIC_EVENT in table output, got: %s", outputStr) +} + +func TestWorkerList_FormatDefault(t *testing.T) { + serverStub := common.NewServerStub(t).WithGetAllEndpoint().WithWorkers(testWorkers...) + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + var outDefault bytes.Buffer + common.SetCliOut(&outDefault) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListCommand()) + require.NoError(t, runCmd("worker", "list")) + + // Default output (no --format flag) should be CSV table format (not JSON) + assert.False(t, json.Valid(outDefault.Bytes()), "default output should not be JSON, got: %s", outDefault.String()) + assert.True(t, strings.Contains(outDefault.String(), "wk-0"), "expected wk-0 in default output") +} + +func TestWorkerList_FormatUnsupported(t *testing.T) { + serverStub := common.NewServerStub(t).WithGetAllEndpoint() + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + runCmd := common.CreateCliRunner(t, GetListCommand()) + err := runCmd("worker", "list", "--"+format.FlagName, "sarif") + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} + +func TestWorkerList_LegacyJSONFlagDeprecated(t *testing.T) { + serverStub := common.NewServerStub(t).WithGetAllEndpoint() + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListCommand()) + require.NoError(t, runCmd("worker", "list", "--"+model.FlagJSONOutput)) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) +} + func assertWorkerListOutput(want []*model.WorkerDetails) common.AssertOutputFunc { return func(t *testing.T, output []byte, err error) { require.NoError(t, err) diff --git a/commands/list_event_cmd.go b/commands/list_event_cmd.go index 70bffb4..e08fcdc 100644 --- a/commands/list_event_cmd.go +++ b/commands/list_event_cmd.go @@ -3,19 +3,21 @@ package commands import ( "strings" - "github.com/jfrog/jfrog-cli-platform-services/commands/common" - + "github.com/jfrog/jfrog-cli-core/v2/common/format" 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/commands/common" "github.com/jfrog/jfrog-cli-platform-services/model" ) func GetListEventsCommand() components.Command { return components.Command{ - Name: "list-event", - Description: "List available events.", - Aliases: []string{"le"}, + Name: "list-event", + Description: "List available events.", + Aliases: []string{"le"}, + SupportedFormats: []format.OutputFormat{format.Json, format.Table}, + DefaultFormat: format.None, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -29,17 +31,44 @@ func GetListEventsCommand() components.Command { projectKey := c.GetStringFlagValue(model.FlagProjectKey) - actionsMeta, err := common.FetchActions(c, server.Url, server.AccessToken, projectKey) + outputFormat, err := c.GetOutputFormat() if err != nil { return err } - var actions []string - for _, md := range actionsMeta { - actions = append(actions, md.Action.Name) + actionsMeta, err := common.FetchActions(c, server.Url, server.AccessToken, projectKey) + if err != nil { + return err } - return common.Print("%s", strings.Join(actions, ", ")) + if outputFormat == format.None { + // Old behavior: no --format flag + return common.Print("%s", strings.Join(actionsMeta.ActionsNames(), ", ")) + } + switch outputFormat { + case format.Json: + return common.PrintJSONValue(actionsMeta) + default: + return printListEventTable(actionsMeta) + } }, } } + +func printListEventTable(actionsMeta common.ActionsMetadata) error { + writer := common.NewCsvWriter() + if err := writer.Write([]string{"NAME", "APPLICATION", "DESCRIPTION"}); err != nil { + return err + } + for _, action := range actionsMeta { + if err := writer.Write([]string{ + action.Action.Name, + action.Action.Application, + action.Description, + }); err != nil { + return err + } + } + writer.Flush() + return writer.Error() +} diff --git a/commands/list_event_cmd_test.go b/commands/list_event_cmd_test.go index d324eb3..f42fd05 100644 --- a/commands/list_event_cmd_test.go +++ b/commands/list_event_cmd_test.go @@ -5,13 +5,16 @@ package commands import ( "bytes" + "encoding/json" "os" "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,3 +67,62 @@ func TestListEventCommand(t *testing.T) { }) } } + +func TestWorkerListEvent_FormatJSON(t *testing.T) { + serverStub := common.NewServerStub(t).WithDefaultActionsMetadataEndpoint() + common.NewMockWorkerServer(t, serverStub) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListEventsCommand()) + require.NoError(t, runCmd("worker", "list-event", "--"+format.FlagName, "json")) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) +} + +func TestWorkerListEvent_FormatTable(t *testing.T) { + serverStub := common.NewServerStub(t).WithDefaultActionsMetadataEndpoint() + common.NewMockWorkerServer(t, serverStub) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListEventsCommand()) + require.NoError(t, runCmd("worker", "list-event", "--"+format.FlagName, "table")) + + outputStr := out.String() + sampleEvents := common.LoadSampleActionEvents(t) + for _, event := range sampleEvents { + assert.True(t, strings.Contains(outputStr, event), "expected event %q in table output, got: %s", event, outputStr) + } +} + +func TestWorkerListEvent_FormatDefault(t *testing.T) { + serverStub := common.NewServerStub(t).WithDefaultActionsMetadataEndpoint() + common.NewMockWorkerServer(t, serverStub) + + var outDefault bytes.Buffer + common.SetCliOut(&outDefault) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + runCmd := common.CreateCliRunner(t, GetListEventsCommand()) + require.NoError(t, runCmd("worker", "list-event")) + + // Default output (no --format flag) should be plain text (not JSON) + assert.False(t, json.Valid(outDefault.Bytes()), "default output should not be JSON, got: %s", outDefault.String()) + + sampleEvents := common.LoadSampleActionEvents(t) + assert.Equal(t, strings.Join(sampleEvents, ", "), strings.TrimSpace(outDefault.String())) +} + +func TestWorkerListEvent_FormatUnsupported(t *testing.T) { + serverStub := common.NewServerStub(t).WithDefaultActionsMetadataEndpoint() + common.NewMockWorkerServer(t, serverStub) + + runCmd := common.CreateCliRunner(t, GetListEventsCommand()) + err := runCmd("worker", "list-event", "--"+format.FlagName, "sarif") + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} diff --git a/commands/remove_cmd.go b/commands/remove_cmd.go index 8dccb06..1a78ace 100644 --- a/commands/remove_cmd.go +++ b/commands/remove_cmd.go @@ -3,7 +3,9 @@ package commands import ( "fmt" "net/http" + "slices" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" plugins_common "github.com/jfrog/jfrog-cli-core/v2/plugins/common" @@ -15,9 +17,10 @@ import ( func GetRemoveCommand() components.Command { return components.Command{ - Name: "undeploy", - Description: "Undeploy a worker", - Aliases: []string{"rm"}, + Name: "undeploy", + Description: "Undeploy a worker", + Aliases: []string{"rm"}, + SupportedFormats: []format.OutputFormat{format.Json}, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -40,18 +43,33 @@ func runRemoveCommand(c *components.Context) error { return err } + var responseStatus int + var contentHandler common.APIContentHandler + if slices.Contains(c.FlagsUsed, format.FlagName) { + if _, fmtErr := c.GetOutputFormat(); fmtErr != nil { + return fmtErr + } + contentHandler = func(body []byte) error { + return common.PrintJSONOrStatus(responseStatus, body) + } + } + log.Info(fmt.Sprintf("Removing worker '%s' ...", workerKey)) err = common.CallWorkerAPI(c, common.APICallParams{ - Method: http.MethodDelete, - ServerURL: server.GetUrl(), - ServerToken: server.GetAccessToken(), - OkStatuses: []int{http.StatusNoContent}, - Path: []string{"workers", workerKey}, + Method: http.MethodDelete, + ServerURL: server.GetUrl(), + ServerToken: server.GetAccessToken(), + OkStatuses: []int{http.StatusNoContent}, + Path: []string{"workers", workerKey}, + OnContent: contentHandler, + CaptureStatus: &responseStatus, }) - if err == nil { - log.Info(fmt.Sprintf("Worker '%s' removed", workerKey)) + if err != nil { + return err } - return err + log.Info(fmt.Sprintf("Worker '%s' removed", workerKey)) + + return nil } diff --git a/commands/remove_cmd_test.go b/commands/remove_cmd_test.go index aeb88e9..15473f4 100644 --- a/commands/remove_cmd_test.go +++ b/commands/remove_cmd_test.go @@ -5,10 +5,12 @@ package commands import ( "bytes" + "encoding/json" "os" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" "github.com/stretchr/testify/assert" @@ -102,3 +104,49 @@ func TestRemoveCommand(t *testing.T) { }) } } + +func setupRemoveFormatTest(t *testing.T) (func(args ...string) error, *bytes.Buffer) { + t.Helper() + + serverStub := common.NewServerStub(t). + WithDefaultActionsMetadataEndpoint(). + WithWorkers(&model.WorkerDetails{Key: "wk-0"}). + WithDeleteEndpoint() + common.NewMockWorkerServer(t, serverStub) + + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetRemoveCommand()) + + _, workerName := common.PrepareWorkerDirForTest(t) + workerName = "wk-0" + require.NoError(t, runCmd("worker", "init", "BEFORE_UPLOAD", workerName)) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + return runCmd, &out +} + +func TestWorkerRemove_FormatJSON(t *testing.T) { + runCmd, out := setupRemoveFormatTest(t) + + require.NoError(t, runCmd("worker", "undeploy", "--"+format.FlagName, "json")) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) + assert.Contains(t, out.String(), "status_code") + assert.Contains(t, out.String(), "content") +} + +func TestWorkerRemove_FormatTableRejected(t *testing.T) { + runCmd, _ := setupRemoveFormatTest(t) + + err := runCmd("worker", "undeploy", "--"+format.FlagName, "table") + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} + +func TestWorkerRemove_NoFormat(t *testing.T) { + runCmd, out := setupRemoveFormatTest(t) + + require.NoError(t, runCmd("worker", "undeploy")) + assert.Empty(t, out.String(), "expected no JSON output when --format is not set, got: %s", out.String()) +} diff --git a/commands/show_execution_history_cmd.go b/commands/show_execution_history_cmd.go index 87d20b4..501b4ae 100644 --- a/commands/show_execution_history_cmd.go +++ b/commands/show_execution_history_cmd.go @@ -1,21 +1,38 @@ package commands import ( + "encoding/json" + "fmt" "net/http" + "time" - "github.com/jfrog/jfrog-cli-platform-services/commands/common" - + "github.com/jfrog/jfrog-cli-core/v2/common/format" 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/commands/common" "github.com/jfrog/jfrog-cli-platform-services/model" ) +type executionHistoryEntry struct { + WorkerKey string `json:"workerKey"` + WorkerType string `json:"workerType"` + WorkerProjectKey string `json:"workerProjectKey"` + ExecutionStatus string `json:"executionStatus"` + StartTimeMillis int64 `json:"startTimeMillis"` + EndTimeMillis int64 `json:"endTimeMillis"` + TriggeredBy string `json:"triggeredBy"` + TestRun bool `json:"testRun"` + ExecutedVersion string `json:"executedVersion"` + TraceID string `json:"traceId"` +} + func GetShowExecutionHistoryCommand() components.Command { return components.Command{ - Name: "execution-history", - Description: "Show a worker execution history.", - Aliases: []string{"exec-hist", "eh"}, + Name: "execution-history", + Description: "Show a worker execution history.", + Aliases: []string{"exec-hist", "eh"}, + SupportedFormats: []format.OutputFormat{format.Json, format.Table}, + DefaultFormat: format.Json, Flags: []components.Flag{ plugins_common.GetServerIdFlag(), model.GetTimeoutFlag(), @@ -30,6 +47,19 @@ func GetShowExecutionHistoryCommand() components.Command { model.GetWorkerKeyArgument(), }, Action: func(c *components.Context) error { + outputFormat, err := c.GetOutputFormat() + if err != nil { + return err + } + + var contentHandler func([]byte) error + switch outputFormat { + case format.Json: + contentHandler = common.PrintJSON + case format.Table: + contentHandler = printExecutionHistoryTable + } + workerKey, projectKey, err := common.ExtractProjectAndKeyFromCommandContext(c, c.Arguments, 1, false) if err != nil { return err @@ -56,8 +86,53 @@ func GetShowExecutionHistoryCommand() components.Command { ProjectKey: projectKey, Query: query, Path: []string{"execution_history"}, - OnContent: common.PrintJSON, + OnContent: contentHandler, }) }, } } + +func printExecutionHistoryTable(responseBytes []byte) error { + var entries []executionHistoryEntry + if err := json.Unmarshal(responseBytes, &entries); err != nil { + return err + } + + writer := common.NewCsvWriter() + + if err := writer.Write([]string{ + "Worker Key", + "Worker Type", + "Project Key", + "Status", + "Started At", + "Ended At", + "Triggered By", + "Test Run", + "Executed Version", + "Trace ID", + }); err != nil { + return err + } + + for _, entry := range entries { + startedAt := time.UnixMilli(entry.StartTimeMillis).UTC().Format(time.RFC3339) + endedAt := time.UnixMilli(entry.EndTimeMillis).UTC().Format(time.RFC3339) + if err := writer.Write([]string{ + entry.WorkerKey, + entry.WorkerType, + entry.WorkerProjectKey, + entry.ExecutionStatus, + startedAt, + endedAt, + entry.TriggeredBy, + fmt.Sprint(entry.TestRun), + entry.ExecutedVersion, + entry.TraceID, + }); err != nil { + return err + } + } + writer.Flush() + return writer.Error() +} diff --git a/commands/show_execution_history_cmd_test.go b/commands/show_execution_history_cmd_test.go index ebb0370..9b4365e 100644 --- a/commands/show_execution_history_cmd_test.go +++ b/commands/show_execution_history_cmd_test.go @@ -5,33 +5,34 @@ package commands import ( "bytes" + "encoding/json" "os" + "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-platform-services/commands/common" - "github.com/jfrog/jfrog-cli-platform-services/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExecutionHistory(t *testing.T) { + now := time.Now() 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", - }, + WorkerKey: "my-worker", + ExecutionStatus: "OK", + StartTimeMillis: now.UnixMilli(), + EndTimeMillis: now.Add(5 * time.Second).UnixMilli(), + TestRun: false, } 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", - }, + WorkerKey: "my-worker", + ExecutionStatus: "KO", + StartTimeMillis: now.Add(-24 * time.Hour).UnixMilli(), + EndTimeMillis: now.Add(-23 * time.Hour).UnixMilli(), + TestRun: true, } workerHistory := common.ExecutionHistoryStub{entry, testRunEntry} @@ -153,3 +154,78 @@ func TestExecutionHistory(t *testing.T) { }) } } + +func testExecutionHistoryWorkerHistory(t *testing.T) (*common.ExecutionHistoryEntryStub, common.ExecutionHistoryStub) { + entry := &common.ExecutionHistoryEntryStub{ + WorkerKey: testExecHistoryWorkerKey, + ExecutionStatus: "OK", + StartTimeMillis: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC).UnixMilli(), + EndTimeMillis: time.Date(2024, 1, 15, 10, 0, 5, 0, time.UTC).UnixMilli(), + TestRun: false, + } + return entry, common.ExecutionHistoryStub{entry} +} + +const testExecHistoryWorkerKey = "my-format-test-worker" + +func setupExecutionHistoryFormatTest(t *testing.T, workerHistory common.ExecutionHistoryStub) func(args ...string) error { + serverStub := common.NewServerStub(t). + WithWorkerExecutionHistory(testExecHistoryWorkerKey, workerHistory). + WithQueryParam("workerKey", testExecHistoryWorkerKey, common.EndpointExecutionHistory). + WithGetExecutionHistoryEndpoint() + common.NewMockWorkerServer(t, serverStub.WithDefaultActionsMetadataEndpoint()) + + runCmd := common.CreateCliRunner(t, GetInitCommand(), GetShowExecutionHistoryCommand()) + common.PrepareWorkerDirForTest(t) + require.NoError(t, runCmd("worker", "init", "GENERIC_EVENT", testExecHistoryWorkerKey)) + return runCmd +} + +func TestWorkerExecutionHistory_FormatJSON(t *testing.T) { + entry, workerHistory := testExecutionHistoryWorkerHistory(t) + runCmd := setupExecutionHistoryFormatTest(t, workerHistory) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + require.NoError(t, runCmd("worker", "execution-history", "--"+format.FlagName, "json")) + assert.True(t, json.Valid(out.Bytes()), "expected valid JSON output, got: %s", out.String()) + assert.Contains(t, out.String(), entry.ExecutionStatus) +} + +func TestWorkerExecutionHistory_FormatTable(t *testing.T) { + _, workerHistory := testExecutionHistoryWorkerHistory(t) + runCmd := setupExecutionHistoryFormatTest(t, workerHistory) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + require.NoError(t, runCmd("worker", "execution-history", "--"+format.FlagName, "table")) + outputStr := out.String() + assert.False(t, json.Valid([]byte(strings.TrimSpace(outputStr))), "table output should not be JSON, got: %s", outputStr) + assert.Contains(t, outputStr, "OK", "expected execution status in table output, got: %s", outputStr) + assert.Contains(t, outputStr, testExecHistoryWorkerKey, "expected worker key in table output, got: %s", outputStr) +} + +func TestWorkerExecutionHistory_FormatDefault(t *testing.T) { + _, workerHistory := testExecutionHistoryWorkerHistory(t) + runCmd := setupExecutionHistoryFormatTest(t, workerHistory) + + var out bytes.Buffer + common.SetCliOut(&out) + t.Cleanup(func() { common.SetCliOut(os.Stdout) }) + + require.NoError(t, runCmd("worker", "execution-history")) + assert.True(t, json.Valid(out.Bytes()), "default output should be JSON, got: %s", out.String()) +} + +func TestWorkerExecutionHistory_FormatUnsupported(t *testing.T) { + _, workerHistory := testExecutionHistoryWorkerHistory(t) + runCmd := setupExecutionHistoryFormatTest(t, workerHistory) + + err := runCmd("worker", "execution-history", "--"+format.FlagName, "sarif") + require.Error(t, err) + assert.Contains(t, err.Error(), "only the following output formats are supported") +} diff --git a/go.mod b/go.mod index 6c35756..aee0b91 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/jfrog/jfrog-cli-platform-services require ( github.com/google/uuid v1.6.0 github.com/jfrog/go-mockhttp v0.3.1 - github.com/jfrog/jfrog-cli-core/v2 v2.60.0 - github.com/jfrog/jfrog-client-go v1.55.0 + github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430091103-6242ecf15d29 + github.com/jfrog/jfrog-client-go v1.55.1-0.20251223101502-1a13a993b0c7 github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 @@ -18,7 +18,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect @@ -31,8 +31,8 @@ require ( github.com/forPelevin/gomoji v1.4.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.5 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-git/v5 v5.18.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -42,7 +42,7 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedib0t/go-pretty/v6 v6.7.8 // indirect github.com/jfrog/archiver/v3 v3.6.3 // indirect - github.com/jfrog/build-info-go v1.13.0 // indirect + github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 // indirect github.com/jfrog/gofrog v1.7.6 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/compress v1.18.4 // indirect diff --git a/go.sum b/go.sum index 3105673..b0f1cbf 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -64,12 +64,12 @@ 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.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= 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.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= @@ -95,16 +95,16 @@ github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.0 h1:bHedp1Gl+a8eR71xxP5JvkqwDj2X3r6e5NiIwNcIwRM= -github.com/jfrog/build-info-go v1.13.0/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= +github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 h1:EH0h86KwGvNHWyEBQoHoU9WfMMKy1GJ6jJQNmfy6E0U= +github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= github.com/jfrog/go-mockhttp v0.3.1/go.mod h1:LmKHex73SUZswM8ANS8kPxLihTOvtq44HVcCoTJKuqc= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= -github.com/jfrog/jfrog-cli-core/v2 v2.60.0 h1:PsxCX5xZZl50psOORpehMP2P47We9xjocva0KRrUxew= -github.com/jfrog/jfrog-cli-core/v2 v2.60.0/go.mod h1:h52WS70v79WLNUAMIsDGURPvAdLRrrUCLyjsj32GORo= -github.com/jfrog/jfrog-client-go v1.55.0 h1:dZq7sLjUJMps8X1I5coVUChprtR7xklp7oSfmZnI48w= -github.com/jfrog/jfrog-client-go v1.55.0/go.mod h1:/e2kaF1oZTmSRgMIk7wYna5xMtNY7Xk8ahpSNZQ2d3s= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430091103-6242ecf15d29 h1:J5+08rOpv/avgt53jNFZ+j5gU8mllcj7Dcfja5Ewodw= +github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260430091103-6242ecf15d29/go.mod h1:bjAkVD8c2W+jg4whqy10bSXDC/c+Se8/ll/GPp5F/+0= +github.com/jfrog/jfrog-client-go v1.55.1-0.20251223101502-1a13a993b0c7 h1:5JUiqmBV9ikFOZEH+ZgvJLHshT1aAuw08bfdJOLHbzQ= +github.com/jfrog/jfrog-client-go v1.55.1-0.20251223101502-1a13a993b0c7/go.mod h1:USb7bfWSE7bGKsJ4nR0lxGILvmtnCcR5OO4biSUItMs= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -146,8 +146,8 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= diff --git a/model/tz_generated.go b/model/tz_generated.go index 8ca8d13..fb422ec 100644 --- a/model/tz_generated.go +++ b/model/tz_generated.go @@ -3,602 +3,602 @@ package model var TimeZones = []string{ - "Africa/Abidjan", - "Africa/Accra", - "Africa/Addis_Ababa", - "Africa/Algiers", - "Africa/Asmara", - "Africa/Asmera", - "Africa/Bamako", - "Africa/Bangui", - "Africa/Banjul", - "Africa/Bissau", - "Africa/Blantyre", - "Africa/Brazzaville", - "Africa/Bujumbura", - "Africa/Cairo", - "Africa/Casablanca", - "Africa/Ceuta", - "Africa/Conakry", - "Africa/Dakar", - "Africa/Dar_es_Salaam", - "Africa/Djibouti", - "Africa/Douala", - "Africa/El_Aaiun", - "Africa/Freetown", - "Africa/Gaborone", - "Africa/Harare", - "Africa/Johannesburg", - "Africa/Juba", - "Africa/Kampala", - "Africa/Khartoum", - "Africa/Kigali", - "Africa/Kinshasa", - "Africa/Lagos", - "Africa/Libreville", - "Africa/Lome", - "Africa/Luanda", - "Africa/Lubumbashi", - "Africa/Lusaka", - "Africa/Malabo", - "Africa/Maputo", - "Africa/Maseru", - "Africa/Mbabane", - "Africa/Mogadishu", - "Africa/Monrovia", - "Africa/Nairobi", - "Africa/Ndjamena", - "Africa/Niamey", - "Africa/Nouakchott", - "Africa/Ouagadougou", - "Africa/Porto-Novo", - "Africa/Sao_Tome", - "Africa/Timbuktu", - "Africa/Tripoli", - "Africa/Tunis", - "Africa/Windhoek", - "America/Adak", - "America/Anchorage", - "America/Anguilla", - "America/Antigua", - "America/Araguaina", - "America/Argentina/Buenos_Aires", - "America/Argentina/Catamarca", - "America/Argentina/ComodRivadavia", - "America/Argentina/Cordoba", - "America/Argentina/Jujuy", - "America/Argentina/La_Rioja", - "America/Argentina/Mendoza", - "America/Argentina/Rio_Gallegos", - "America/Argentina/Salta", - "America/Argentina/San_Juan", - "America/Argentina/San_Luis", - "America/Argentina/Tucuman", - "America/Argentina/Ushuaia", - "America/Aruba", - "America/Asuncion", - "America/Atikokan", - "America/Atka", - "America/Bahia", - "America/Bahia_Banderas", - "America/Barbados", - "America/Belem", - "America/Belize", - "America/Blanc-Sablon", - "America/Boa_Vista", - "America/Bogota", - "America/Boise", - "America/Buenos_Aires", - "America/Cambridge_Bay", - "America/Campo_Grande", - "America/Cancun", - "America/Caracas", - "America/Catamarca", - "America/Cayenne", - "America/Cayman", - "America/Chicago", - "America/Chihuahua", - "America/Ciudad_Juarez", - "America/Coral_Harbour", - "America/Cordoba", - "America/Costa_Rica", - "America/Coyhaique", - "America/Creston", - "America/Cuiaba", - "America/Curacao", - "America/Danmarkshavn", - "America/Dawson", - "America/Dawson_Creek", - "America/Denver", - "America/Detroit", - "America/Dominica", - "America/Edmonton", - "America/Eirunepe", - "America/El_Salvador", - "America/Ensenada", - "America/Fort_Nelson", - "America/Fort_Wayne", - "America/Fortaleza", - "America/Glace_Bay", - "America/Godthab", - "America/Goose_Bay", - "America/Grand_Turk", - "America/Grenada", - "America/Guadeloupe", - "America/Guatemala", - "America/Guayaquil", - "America/Guyana", - "America/Halifax", - "America/Havana", - "America/Hermosillo", - "America/Indiana/Indianapolis", - "America/Indiana/Knox", - "America/Indiana/Marengo", - "America/Indiana/Petersburg", - "America/Indiana/Tell_City", - "America/Indiana/Vevay", - "America/Indiana/Vincennes", - "America/Indiana/Winamac", - "America/Indianapolis", - "America/Inuvik", - "America/Iqaluit", - "America/Jamaica", - "America/Jujuy", - "America/Juneau", - "America/Kentucky/Louisville", - "America/Kentucky/Monticello", - "America/Knox_IN", - "America/Kralendijk", - "America/La_Paz", - "America/Lima", - "America/Los_Angeles", - "America/Louisville", - "America/Lower_Princes", - "America/Maceio", - "America/Managua", - "America/Manaus", - "America/Marigot", - "America/Martinique", - "America/Matamoros", - "America/Mazatlan", - "America/Mendoza", - "America/Menominee", - "America/Merida", - "America/Metlakatla", - "America/Mexico_City", - "America/Miquelon", - "America/Moncton", - "America/Monterrey", - "America/Montevideo", - "America/Montreal", - "America/Montserrat", - "America/Nassau", - "America/New_York", - "America/Nipigon", - "America/Nome", - "America/Noronha", - "America/North_Dakota/Beulah", - "America/North_Dakota/Center", - "America/North_Dakota/New_Salem", - "America/Nuuk", - "America/Ojinaga", - "America/Panama", - "America/Pangnirtung", - "America/Paramaribo", - "America/Phoenix", - "America/Port-au-Prince", - "America/Port_of_Spain", - "America/Porto_Acre", - "America/Porto_Velho", - "America/Puerto_Rico", - "America/Punta_Arenas", - "America/Rainy_River", - "America/Rankin_Inlet", - "America/Recife", - "America/Regina", - "America/Resolute", - "America/Rio_Branco", - "America/Rosario", - "America/Santa_Isabel", - "America/Santarem", - "America/Santiago", - "America/Santo_Domingo", - "America/Sao_Paulo", - "America/Scoresbysund", - "America/Shiprock", - "America/Sitka", - "America/St_Barthelemy", - "America/St_Johns", - "America/St_Kitts", - "America/St_Lucia", - "America/St_Thomas", - "America/St_Vincent", - "America/Swift_Current", - "America/Tegucigalpa", - "America/Thule", - "America/Thunder_Bay", - "America/Tijuana", - "America/Toronto", - "America/Tortola", - "America/Vancouver", - "America/Virgin", - "America/Whitehorse", - "America/Winnipeg", - "America/Yakutat", - "America/Yellowknife", - "Antarctica/Casey", - "Antarctica/Davis", - "Antarctica/DumontDUrville", - "Antarctica/Macquarie", - "Antarctica/Mawson", - "Antarctica/McMurdo", - "Antarctica/Palmer", - "Antarctica/Rothera", - "Antarctica/South_Pole", - "Antarctica/Syowa", - "Antarctica/Troll", - "Antarctica/Vostok", - "Arctic/Longyearbyen", - "Asia/Aden", - "Asia/Almaty", - "Asia/Amman", - "Asia/Anadyr", - "Asia/Aqtau", - "Asia/Aqtobe", - "Asia/Ashgabat", - "Asia/Ashkhabad", - "Asia/Atyrau", - "Asia/Baghdad", - "Asia/Bahrain", - "Asia/Baku", - "Asia/Bangkok", - "Asia/Barnaul", - "Asia/Beirut", - "Asia/Bishkek", - "Asia/Brunei", - "Asia/Calcutta", - "Asia/Chita", - "Asia/Choibalsan", - "Asia/Chongqing", - "Asia/Chungking", - "Asia/Colombo", - "Asia/Dacca", - "Asia/Damascus", - "Asia/Dhaka", - "Asia/Dili", - "Asia/Dubai", - "Asia/Dushanbe", - "Asia/Famagusta", - "Asia/Gaza", - "Asia/Harbin", - "Asia/Hebron", - "Asia/Ho_Chi_Minh", - "Asia/Hong_Kong", - "Asia/Hovd", - "Asia/Irkutsk", - "Asia/Istanbul", - "Asia/Jakarta", - "Asia/Jayapura", - "Asia/Jerusalem", - "Asia/Kabul", - "Asia/Kamchatka", - "Asia/Karachi", - "Asia/Kashgar", - "Asia/Kathmandu", - "Asia/Katmandu", - "Asia/Khandyga", - "Asia/Kolkata", - "Asia/Krasnoyarsk", - "Asia/Kuala_Lumpur", - "Asia/Kuching", - "Asia/Kuwait", - "Asia/Macao", - "Asia/Macau", - "Asia/Magadan", - "Asia/Makassar", - "Asia/Manila", - "Asia/Muscat", - "Asia/Nicosia", - "Asia/Novokuznetsk", - "Asia/Novosibirsk", - "Asia/Omsk", - "Asia/Oral", - "Asia/Phnom_Penh", - "Asia/Pontianak", - "Asia/Pyongyang", - "Asia/Qatar", - "Asia/Qostanay", - "Asia/Qyzylorda", - "Asia/Rangoon", - "Asia/Riyadh", - "Asia/Saigon", - "Asia/Sakhalin", - "Asia/Samarkand", - "Asia/Seoul", - "Asia/Shanghai", - "Asia/Singapore", - "Asia/Srednekolymsk", - "Asia/Taipei", - "Asia/Tashkent", - "Asia/Tbilisi", - "Asia/Tehran", - "Asia/Tel_Aviv", - "Asia/Thimbu", - "Asia/Thimphu", - "Asia/Tokyo", - "Asia/Tomsk", - "Asia/Ujung_Pandang", - "Asia/Ulaanbaatar", - "Asia/Ulan_Bator", - "Asia/Urumqi", - "Asia/Ust-Nera", - "Asia/Vientiane", - "Asia/Vladivostok", - "Asia/Yakutsk", - "Asia/Yangon", - "Asia/Yekaterinburg", - "Asia/Yerevan", - "Atlantic/Azores", - "Atlantic/Bermuda", - "Atlantic/Canary", - "Atlantic/Cape_Verde", - "Atlantic/Faeroe", - "Atlantic/Faroe", - "Atlantic/Jan_Mayen", - "Atlantic/Madeira", - "Atlantic/Reykjavik", - "Atlantic/South_Georgia", - "Atlantic/St_Helena", - "Atlantic/Stanley", - "Australia/ACT", - "Australia/Adelaide", - "Australia/Brisbane", - "Australia/Broken_Hill", - "Australia/Canberra", - "Australia/Currie", - "Australia/Darwin", - "Australia/Eucla", - "Australia/Hobart", - "Australia/LHI", - "Australia/Lindeman", - "Australia/Lord_Howe", - "Australia/Melbourne", - "Australia/NSW", - "Australia/North", - "Australia/Perth", - "Australia/Queensland", - "Australia/South", - "Australia/Sydney", - "Australia/Tasmania", - "Australia/Victoria", - "Australia/West", - "Australia/Yancowinna", - "Brazil/Acre", - "Brazil/DeNoronha", - "Brazil/East", - "Brazil/West", - "CET", - "CST6CDT", - "Canada/Atlantic", - "Canada/Central", - "Canada/Eastern", - "Canada/Mountain", - "Canada/Newfoundland", - "Canada/Pacific", - "Canada/Saskatchewan", - "Canada/Yukon", - "Chile/Continental", - "Chile/EasterIsland", - "Cuba", - "EET", - "EST", - "EST5EDT", - "Egypt", - "Eire", - "GMT", - "GMT+0", - "GMT+1", - "GMT+10", - "GMT+11", - "GMT+12", - "GMT+2", - "GMT+3", - "GMT+4", - "GMT+5", - "GMT+6", - "GMT+7", - "GMT+8", - "GMT+9", - "GMT-0", - "GMT-1", - "GMT-10", - "GMT-11", - "GMT-12", - "GMT-13", - "GMT-14", - "GMT-2", - "GMT-3", - "GMT-4", - "GMT-5", - "GMT-6", - "GMT-7", - "GMT-8", - "GMT-9", - "GMT0", - "Greenwich", - "UCT", - "UTC", - "Universal", - "Zulu", - "Europe/Amsterdam", - "Europe/Andorra", - "Europe/Astrakhan", - "Europe/Athens", - "Europe/Belfast", - "Europe/Belgrade", - "Europe/Berlin", - "Europe/Bratislava", - "Europe/Brussels", - "Europe/Bucharest", - "Europe/Budapest", - "Europe/Busingen", - "Europe/Chisinau", - "Europe/Copenhagen", - "Europe/Dublin", - "Europe/Gibraltar", - "Europe/Guernsey", - "Europe/Helsinki", - "Europe/Isle_of_Man", - "Europe/Istanbul", - "Europe/Jersey", - "Europe/Kaliningrad", - "Europe/Kiev", - "Europe/Kirov", - "Europe/Kyiv", - "Europe/Lisbon", - "Europe/Ljubljana", - "Europe/London", - "Europe/Luxembourg", - "Europe/Madrid", - "Europe/Malta", - "Europe/Mariehamn", - "Europe/Minsk", - "Europe/Monaco", - "Europe/Moscow", - "Europe/Nicosia", - "Europe/Oslo", - "Europe/Paris", - "Europe/Podgorica", - "Europe/Prague", - "Europe/Riga", - "Europe/Rome", - "Europe/Samara", - "Europe/San_Marino", - "Europe/Sarajevo", - "Europe/Saratov", - "Europe/Simferopol", - "Europe/Skopje", - "Europe/Sofia", - "Europe/Stockholm", - "Europe/Tallinn", - "Europe/Tirane", - "Europe/Tiraspol", - "Europe/Ulyanovsk", - "Europe/Uzhgorod", - "Europe/Vaduz", - "Europe/Vatican", - "Europe/Vienna", - "Europe/Vilnius", - "Europe/Volgograd", - "Europe/Warsaw", - "Europe/Zagreb", - "Europe/Zaporozhye", - "Europe/Zurich", - "Factory", - "GB", - "GB-Eire", - "GMT", - "GMT+0", - "GMT-0", - "GMT0", - "Greenwich", - "HST", - "Hongkong", - "Iceland", - "Indian/Antananarivo", - "Indian/Chagos", - "Indian/Christmas", - "Indian/Cocos", - "Indian/Comoro", - "Indian/Kerguelen", - "Indian/Mahe", - "Indian/Maldives", - "Indian/Mauritius", - "Indian/Mayotte", - "Indian/Reunion", - "Iran", - "Israel", - "Jamaica", - "Japan", - "Kwajalein", - "Libya", - "MET", - "MST", - "MST7MDT", - "Mexico/BajaNorte", - "Mexico/BajaSur", - "Mexico/General", - "NZ", - "NZ-CHAT", - "Navajo", - "PRC", - "PST8PDT", - "Pacific/Apia", - "Pacific/Auckland", - "Pacific/Bougainville", - "Pacific/Chatham", - "Pacific/Chuuk", - "Pacific/Easter", - "Pacific/Efate", - "Pacific/Enderbury", - "Pacific/Fakaofo", - "Pacific/Fiji", - "Pacific/Funafuti", - "Pacific/Galapagos", - "Pacific/Gambier", - "Pacific/Guadalcanal", - "Pacific/Guam", - "Pacific/Honolulu", - "Pacific/Johnston", - "Pacific/Kanton", - "Pacific/Kiritimati", - "Pacific/Kosrae", - "Pacific/Kwajalein", - "Pacific/Majuro", - "Pacific/Marquesas", - "Pacific/Midway", - "Pacific/Nauru", - "Pacific/Niue", - "Pacific/Norfolk", - "Pacific/Noumea", - "Pacific/Pago_Pago", - "Pacific/Palau", - "Pacific/Pitcairn", - "Pacific/Pohnpei", - "Pacific/Ponape", - "Pacific/Port_Moresby", - "Pacific/Rarotonga", - "Pacific/Saipan", - "Pacific/Samoa", - "Pacific/Tahiti", - "Pacific/Tarawa", - "Pacific/Tongatapu", - "Pacific/Truk", - "Pacific/Wake", - "Pacific/Wallis", - "Pacific/Yap", - "Poland", - "Portugal", - "ROC", - "ROK", - "Singapore", - "Turkey", - "UCT", - "US/Alaska", - "US/Aleutian", - "US/Arizona", - "US/Central", - "US/East-Indiana", - "US/Eastern", - "US/Hawaii", - "US/Indiana-Starke", - "US/Michigan", - "US/Mountain", - "US/Pacific", - "US/Samoa", - "UTC", - "Universal", - "W-SU", - "WET", - "Zulu", + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Ciudad_Juarez", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Coyhaique", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "GMT", + "GMT+0", + "GMT+1", + "GMT+10", + "GMT+11", + "GMT+12", + "GMT+2", + "GMT+3", + "GMT+4", + "GMT+5", + "GMT+6", + "GMT+7", + "GMT+8", + "GMT+9", + "GMT-0", + "GMT-1", + "GMT-10", + "GMT-11", + "GMT-12", + "GMT-13", + "GMT-14", + "GMT-2", + "GMT-3", + "GMT-4", + "GMT-5", + "GMT-6", + "GMT-7", + "GMT-8", + "GMT-9", + "GMT0", + "Greenwich", + "UCT", + "UTC", + "Universal", + "Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Kyiv", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "Factory", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu", }