diff --git a/apptrust/commands/flags.go b/apptrust/commands/flags.go index 0f38c9f..a5fdaa1 100644 --- a/apptrust/commands/flags.go +++ b/apptrust/commands/flags.go @@ -123,6 +123,7 @@ var commandFlags = map[string][]string{ IncludeFilterFlag, ExcludeFilterFlag, SpecVarsFlag, + DryRunFlag, }, VersionPromote: { url, diff --git a/apptrust/commands/version/create_app_version_cmd.go b/apptrust/commands/version/create_app_version_cmd.go index 5482d36..1952e5d 100644 --- a/apptrust/commands/version/create_app_version_cmd.go +++ b/apptrust/commands/version/create_app_version_cmd.go @@ -21,6 +21,7 @@ type createAppVersionCommand struct { serverDetails *coreConfig.ServerDetails requestPayload *model.CreateAppVersionRequest sync bool + dryRun bool } func (cv *createAppVersionCommand) Run() error { @@ -29,7 +30,7 @@ func (cv *createAppVersionCommand) Run() error { return err } - return cv.versionService.CreateAppVersion(ctx, cv.requestPayload, cv.sync) + return cv.versionService.CreateAppVersion(ctx, cv.requestPayload, cv.sync, cv.dryRun) } func (cv *createAppVersionCommand) ServerDetails() (*coreConfig.ServerDetails, error) { @@ -54,6 +55,7 @@ func (cv *createAppVersionCommand) prepareAndRunCommand(ctx *components.Context) if errorutils.CheckError(err) != nil { return err } + cv.dryRun = ctx.GetBoolFlagValue(commands.DryRunFlag) return commonCLiCommands.Exec(cv) } diff --git a/apptrust/commands/version/create_app_version_cmd_test.go b/apptrust/commands/version/create_app_version_cmd_test.go index fdbb4e4..7a28bda 100644 --- a/apptrust/commands/version/create_app_version_cmd_test.go +++ b/apptrust/commands/version/create_app_version_cmd_test.go @@ -18,6 +18,7 @@ func TestCreateAppVersionCommand(t *testing.T) { tests := []struct { name string request *model.CreateAppVersionRequest + dryRun bool shouldError bool errorMessage string }{ @@ -36,10 +37,28 @@ func TestCreateAppVersionCommand(t *testing.T) { }}, }, }, + dryRun: false, + }, + { + name: "success with dry-run", + request: &model.CreateAppVersionRequest{ + ApplicationKey: "app-key", + Version: "1.0.0", + Sources: &model.CreateVersionSources{ + Packages: []model.CreateVersionPackage{{ + Type: "type", + Name: "name", + Version: "1.0.0", + Repository: "repo", + }}, + }, + }, + dryRun: true, }, { name: "context error", request: &model.CreateAppVersionRequest{ApplicationKey: "app-key", Version: "1.0.0", Draft: false, Sources: &model.CreateVersionSources{Packages: []model.CreateVersionPackage{{Type: "type", Name: "name", Version: "1.0.0", Repository: "repo"}}}}, + dryRun: false, shouldError: true, errorMessage: "context error", }, @@ -57,10 +76,10 @@ func TestCreateAppVersionCommand(t *testing.T) { mockVersionService := mockversions.NewMockVersionService(ctrl) if tt.shouldError { - mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), tt.request, true). + mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), tt.request, true, tt.dryRun). Return(errors.New(tt.errorMessage)).Times(1) } else { - mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), tt.request, true). + mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), tt.request, true, tt.dryRun). Return(nil).Times(1) } @@ -69,6 +88,7 @@ func TestCreateAppVersionCommand(t *testing.T) { serverDetails: &config.ServerDetails{Url: "https://example.com"}, requestPayload: tt.request, sync: true, + dryRun: tt.dryRun, } err := cmd.Run() @@ -196,8 +216,8 @@ func TestCreateAppVersionCommand_FlagsSuite(t *testing.T) { var actualPayload *model.CreateAppVersionRequest mockVersionService := mockversions.NewMockVersionService(ctrl) if !tt.expectsError { - mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, _ bool) error { + mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, _ bool, _ bool) error { actualPayload = req return nil }).Times(1) @@ -807,8 +827,8 @@ func TestCreateAppVersionCommand_SpecFileSuite(t *testing.T) { var capturedSync bool mockVersionService := mockversions.NewMockVersionService(ctrl) if !tt.expectsError { - mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, sync bool) error { + mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, sync, dryRun bool) error { actualPayload = req capturedSync = sync return nil @@ -876,8 +896,8 @@ func TestCreateAppVersionCommand_SyncFlagSuite(t *testing.T) { var capturedSync bool mockVersionService := mockversions.NewMockVersionService(ctrl) - mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any()). - DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, sync bool) error { + mockVersionService.EXPECT().CreateAppVersion(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ interface{}, req *model.CreateAppVersionRequest, sync, dryRun bool) error { capturedSync = sync return nil }).Times(1) diff --git a/apptrust/http/http_utils.go b/apptrust/http/http_utils.go new file mode 100644 index 0000000..a4bcfc0 --- /dev/null +++ b/apptrust/http/http_utils.go @@ -0,0 +1,5 @@ +package http + +func IsSuccessStatusCode(statusCode int) bool { + return statusCode >= 200 && statusCode < 300 +} diff --git a/apptrust/service/versions/mocks/version_service_mock.go b/apptrust/service/versions/mocks/version_service_mock.go index bc7834b..3424137 100644 --- a/apptrust/service/versions/mocks/version_service_mock.go +++ b/apptrust/service/versions/mocks/version_service_mock.go @@ -42,17 +42,17 @@ func (m *MockVersionService) EXPECT() *MockVersionServiceMockRecorder { } // CreateAppVersion mocks base method. -func (m *MockVersionService) CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync bool) error { +func (m *MockVersionService) CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync, dryRun bool) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAppVersion", ctx, request, sync) + ret := m.ctrl.Call(m, "CreateAppVersion", ctx, request, sync, dryRun) ret0, _ := ret[0].(error) return ret0 } // CreateAppVersion indicates an expected call of CreateAppVersion. -func (mr *MockVersionServiceMockRecorder) CreateAppVersion(ctx, request, sync any) *gomock.Call { +func (mr *MockVersionServiceMockRecorder) CreateAppVersion(ctx, request, sync, dryRun any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAppVersion", reflect.TypeOf((*MockVersionService)(nil).CreateAppVersion), ctx, request, sync) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAppVersion", reflect.TypeOf((*MockVersionService)(nil).CreateAppVersion), ctx, request, sync, dryRun) } // DeleteAppVersion mocks base method. diff --git a/apptrust/service/versions/version_service.go b/apptrust/service/versions/version_service.go index 4a60493..c126197 100644 --- a/apptrust/service/versions/version_service.go +++ b/apptrust/service/versions/version_service.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" + apphttp "github.com/jfrog/jfrog-cli-application/apptrust/http" "github.com/jfrog/jfrog-cli-application/apptrust/service" "github.com/jfrog/jfrog-client-go/utils/log" @@ -14,7 +15,7 @@ import ( ) type VersionService interface { - CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync bool) error + CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync, dryRun bool) error PromoteAppVersion(ctx service.Context, applicationKey string, version string, payload *model.PromoteAppVersionRequest, sync bool) error ReleaseAppVersion(ctx service.Context, applicationKey string, version string, request *model.ReleaseAppVersionRequest, sync bool) error RollbackAppVersion(ctx service.Context, applicationKey string, version string, request *model.RollbackAppVersionRequest, sync bool) error @@ -29,24 +30,20 @@ func NewVersionService() VersionService { return &versionService{} } -func (vs *versionService) CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync bool) error { +func (vs *versionService) CreateAppVersion(ctx service.Context, request *model.CreateAppVersionRequest, sync, dryRun bool) error { endpoint := fmt.Sprintf("/v1/applications/%s/versions/", request.ApplicationKey) - response, responseBody, err := ctx.GetHttpClient().Post(endpoint, request, map[string]string{"async": strconv.FormatBool(!sync)}) + response, responseBody, err := ctx.GetHttpClient().Post(endpoint, request, + map[string]string{"async": strconv.FormatBool(!sync), "dry_run": strconv.FormatBool(dryRun)}) if err != nil { return err } - expectedStatusCode := http.StatusCreated - if !sync { - expectedStatusCode = http.StatusAccepted - } - - if response.StatusCode != expectedStatusCode { + if !apphttp.IsSuccessStatusCode(response.StatusCode) { return fmt.Errorf("failed to create app version. Status code: %d. \n%s", response.StatusCode, responseBody) } - log.Info("Application version created successfully.") + logSuccessMessage(sync, request, dryRun) log.Output(string(responseBody)) return nil } @@ -58,7 +55,7 @@ func (vs *versionService) PromoteAppVersion(ctx service.Context, applicationKey, return err } - if response.StatusCode >= http.StatusBadRequest { + if !apphttp.IsSuccessStatusCode(response.StatusCode) { return fmt.Errorf("failed to promote app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -74,7 +71,7 @@ func (vs *versionService) ReleaseAppVersion(ctx service.Context, applicationKey, return err } - if response.StatusCode >= http.StatusBadRequest { + if !apphttp.IsSuccessStatusCode(response.StatusCode) { return fmt.Errorf("failed to release app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -90,13 +87,7 @@ func (vs *versionService) RollbackAppVersion(ctx service.Context, applicationKey return err } - // Validate status code based on sync mode - expectedStatusCode := http.StatusAccepted - if sync { - expectedStatusCode = http.StatusOK - } - - if response.StatusCode != expectedStatusCode { + if !apphttp.IsSuccessStatusCode(response.StatusCode) { return fmt.Errorf("failed to rollback app version. Status code: %d. \n%s", response.StatusCode, responseBody) } @@ -165,3 +156,13 @@ func (vs *versionService) UpdateAppVersionSources(ctx service.Context, applicati log.Output(string(responseBody)) return nil } + +func logSuccessMessage(sync bool, request *model.CreateAppVersionRequest, dryRun bool) { + if !sync { + log.Info(fmt.Sprintf("Application version creation initiated: %s:%s", request.ApplicationKey, request.Version)) + } else if dryRun { + log.Info(fmt.Sprintf("Dry run successful for application version: %s:%s", request.ApplicationKey, request.Version)) + } else { + log.Info(fmt.Sprintf("Application version created successfully: %s:%s", request.ApplicationKey, request.Version)) + } +} diff --git a/apptrust/service/versions/version_service_test.go b/apptrust/service/versions/version_service_test.go index b5b4534..043d34c 100644 --- a/apptrust/service/versions/version_service_test.go +++ b/apptrust/service/versions/version_service_test.go @@ -24,6 +24,7 @@ func TestCreateAppVersion(t *testing.T) { name string request *model.CreateAppVersionRequest sync bool + dryRun bool mockResponse *http.Response mockResponseBody string mockError error @@ -33,15 +34,37 @@ func TestCreateAppVersion(t *testing.T) { name: "success", request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, sync: true, + dryRun: false, mockResponse: &http.Response{StatusCode: 201}, mockResponseBody: "{}", mockError: nil, expectedError: "", }, + { + name: "success with dry-run (200 OK)", + request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, + sync: true, + dryRun: true, + mockResponse: &http.Response{StatusCode: 200}, + mockResponseBody: "{\"validation\": \"passed\"}", + mockError: nil, + expectedError: "", + }, { name: "success with sync=false", request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, sync: false, + dryRun: false, + mockResponse: &http.Response{StatusCode: 202}, + mockResponseBody: "{}", + mockError: nil, + expectedError: "", + }, + { + name: "success with sync=false & dryRun=true", + request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, + sync: false, + dryRun: true, mockResponse: &http.Response{StatusCode: 202}, mockResponseBody: "{}", mockError: nil, @@ -51,6 +74,7 @@ func TestCreateAppVersion(t *testing.T) { name: "failure", request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, sync: true, + dryRun: false, mockResponse: &http.Response{StatusCode: 400}, mockResponseBody: "error", mockError: nil, @@ -60,6 +84,7 @@ func TestCreateAppVersion(t *testing.T) { name: "http client error", request: &model.CreateAppVersionRequest{ApplicationKey: "test-app", Version: "1.0.0"}, sync: true, + dryRun: false, mockResponse: nil, mockResponseBody: "", mockError: errors.New("http client error"), @@ -70,13 +95,13 @@ func TestCreateAppVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockHttpClient := mockhttp.NewMockApptrustHttpClient(ctrl) - mockHttpClient.EXPECT().Post("/v1/applications/test-app/versions/", tt.request, map[string]string{"async": strconv.FormatBool(!tt.sync)}). + mockHttpClient.EXPECT().Post("/v1/applications/test-app/versions/", tt.request, map[string]string{"async": strconv.FormatBool(!tt.sync), "dry_run": strconv.FormatBool(tt.dryRun)}). Return(tt.mockResponse, []byte(tt.mockResponseBody), tt.mockError).Times(1) mockCtx := mockservice.NewMockContext(ctrl) mockCtx.EXPECT().GetHttpClient().Return(mockHttpClient).Times(1) - err := service.CreateAppVersion(mockCtx, tt.request, tt.sync) + err := service.CreateAppVersion(mockCtx, tt.request, tt.sync, tt.dryRun) if tt.expectedError == "" { assert.NoError(t, err) } else { @@ -476,28 +501,6 @@ func TestRollbackAppVersion(t *testing.T) { expectedStatus: http.StatusBadRequest, expectedError: true, }, - { - name: "failed rollback - sync=true but got 202", - applicationKey: "video-encoder", - version: "1.5.0", - payload: &model.RollbackAppVersionRequest{ - FromStage: "qa", - }, - sync: true, - expectedStatus: http.StatusAccepted, - expectedError: true, - }, - { - name: "failed rollback - sync=false but got 200", - applicationKey: "video-encoder", - version: "1.5.0", - payload: &model.RollbackAppVersionRequest{ - FromStage: "prod", - }, - sync: false, - expectedStatus: http.StatusOK, - expectedError: true, - }, } for _, tt := range tests { diff --git a/e2e/version_test.go b/e2e/version_test.go index 683c1a5..b37555f 100644 --- a/e2e/version_test.go +++ b/e2e/version_test.go @@ -91,6 +91,38 @@ func TestCreateVersion_ApplicationVersion(t *testing.T) { assertVersionContent(t, testPackage, versionContent, statusCode, targetAppKey, targetVersion) } +func TestCreateVersion_ApplicationVersion_DryRun(t *testing.T) { + // Prepare - create source application with a version + sourceAppKey := utils.GenerateUniqueKey("app-version-create-app-version-dryrun") + utils.CreateBasicApplication(t, sourceAppKey) + defer utils.DeleteApplication(t, sourceAppKey) + + testPackage := utils.GetTestPackage(t) + sourceVersion := "1.0.2" + packageFlag := fmt.Sprintf("--source-type-packages=type=%s, name=%s, version=%s, repo-key=%s", + testPackage.PackageType, testPackage.PackageName, testPackage.PackageVersion, testPackage.RepoKey) + err := utils.AppTrustCli.Exec("version-create", sourceAppKey, sourceVersion, packageFlag) + require.NoError(t, err) + defer utils.DeleteApplicationVersion(t, sourceAppKey, sourceVersion) + + // Prepare - create target application + targetAppKey := utils.GenerateUniqueKey("app-target-version-dryrun") + utils.CreateBasicApplication(t, targetAppKey) + defer utils.DeleteApplication(t, targetAppKey) + + targetVersion := "1.0.3" + + // Execute with dry-run flag + appVersionFlag := fmt.Sprintf("--source-type-application-versions=application-key=%s, version=%s", sourceAppKey, sourceVersion) + err = utils.AppTrustCli.Exec("version-create", targetAppKey, targetVersion, appVersionFlag, "--dry-run") + require.NoError(t, err) + + // Assert - version should not exist since it was a dry run + _, statusCode, err := utils.GetApplicationVersion(targetAppKey, targetVersion) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, statusCode) +} + func TestCreateVersion_ReleaseBundle(t *testing.T) { // Prepare appKey := utils.GenerateUniqueKey("app-version-create-release-bundle")