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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 85 additions & 7 deletions apptrust/commands/application/create_app_cmd.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package application

import (
"encoding/json"

pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"

"github.com/jfrog/jfrog-cli-application/apptrust/commands/utils"
Expand All @@ -9,7 +11,9 @@ import (
commonCLiCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"

"github.com/jfrog/jfrog-cli-application/apptrust/app"
"github.com/jfrog/jfrog-cli-application/apptrust/commands"
Expand Down Expand Up @@ -42,12 +46,31 @@ func (cac *createAppCommand) CommandName() string {

func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*model.AppDescriptor, error) {
applicationKey := ctx.Arguments[0]
applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag)
if applicationName == "" {
// Default to the application key if application name is not provided
applicationName = applicationKey

var appDescriptor *model.AppDescriptor
var err error

if ctx.IsFlagSet(commands.SpecFlag) {
appDescriptor, err = cac.loadFromSpec(ctx)
} else {
appDescriptor, err = cac.buildFromFlags(ctx)
}

if err != nil {
return nil, err
}

appDescriptor.ApplicationKey = applicationKey
if appDescriptor.ApplicationName == "" {
appDescriptor.ApplicationName = applicationKey
}

return appDescriptor, nil
}

func (cac *createAppCommand) buildFromFlags(ctx *components.Context) (*model.AppDescriptor, error) {
applicationName := ctx.GetStringFlagValue(commands.ApplicationNameFlag)

project := ctx.GetStringFlagValue(commands.ProjectFlag)
if project == "" {
return nil, errorutils.CheckErrorf("--%s is mandatory", commands.ProjectFlag)
Expand Down Expand Up @@ -83,7 +106,6 @@ func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*mode

return &model.AppDescriptor{
ApplicationName: applicationName,
ApplicationKey: applicationKey,
Description: description,
ProjectKey: project,
MaturityLevel: maturityLevel,
Expand All @@ -94,9 +116,34 @@ func (cac *createAppCommand) buildRequestPayload(ctx *components.Context) (*mode
}, nil
}

func (cac *createAppCommand) loadFromSpec(ctx *components.Context) (*model.AppDescriptor, error) {
specFilePath := ctx.GetStringFlagValue(commands.SpecFlag)
spec := new(model.AppDescriptor)
specVars := coreutils.SpecVarsStringToMap(ctx.GetStringFlagValue(commands.SpecVarsFlag))
content, err := fileutils.ReadFile(specFilePath)
if errorutils.CheckError(err) != nil {
return nil, err
}

if len(specVars) > 0 {
content = coreutils.ReplaceVars(content, specVars)
}

err = json.Unmarshal(content, spec)
if errorutils.CheckError(err) != nil {
return nil, err
}

if spec.ProjectKey == "" {
return nil, errorutils.CheckErrorf("project_key is mandatory in spec file")
}

return spec, nil
}

func (cac *createAppCommand) prepareAndRunCommand(ctx *components.Context) error {
if len(ctx.Arguments) != 1 {
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
if err := validateCreateAppContext(ctx); err != nil {
return err
}

var err error
Expand All @@ -113,6 +160,37 @@ func (cac *createAppCommand) prepareAndRunCommand(ctx *components.Context) error
return commonCLiCommands.Exec(cac)
}

func validateCreateAppContext(ctx *components.Context) error {
if err := validateNoSpecAndFlagsTogether(ctx); err != nil {
return err
}
if len(ctx.Arguments) != 1 {
return pluginsCommon.WrongNumberOfArgumentsHandler(ctx)
}
return nil
}

func validateNoSpecAndFlagsTogether(ctx *components.Context) error {
if ctx.IsFlagSet(commands.SpecFlag) {
otherAppFlags := []string{
commands.ApplicationNameFlag,
commands.ProjectFlag,
commands.DescriptionFlag,
commands.BusinessCriticalityFlag,
commands.MaturityLevelFlag,
commands.LabelsFlag,
commands.UserOwnersFlag,
commands.GroupOwnersFlag,
}
for _, flag := range otherAppFlags {
if ctx.IsFlagSet(flag) {
return errorutils.CheckErrorf("the flag --%s is not allowed when --spec is provided.", flag)
}
}
}
return nil
}

func GetCreateAppCommand(appContext app.Context) components.Command {
cmd := &createAppCommand{
applicationService: appContext.GetApplicationService(),
Expand Down
230 changes: 223 additions & 7 deletions apptrust/commands/application/create_app_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,47 @@ import (
"go.uber.org/mock/gomock"
)

func TestCreateAppCommand_Run(t *testing.T) {
func TestCreateAppCommand_Run_Flags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

serverDetails := &config.ServerDetails{Url: "https://example.com"}
ctx := &components.Context{
Arguments: []string{"app-key"},
}
ctx.AddStringFlag("application-name", "test-app")
ctx.AddStringFlag("project", "test-project")
ctx.AddStringFlag("desc", "Test application")
ctx.AddStringFlag("business-criticality", "high")
ctx.AddStringFlag("maturity-level", "production")
ctx.AddStringFlag("labels", "env=prod;region=us-east")
ctx.AddStringFlag("user-owners", "john.doe;jane.smith")
ctx.AddStringFlag("group-owners", "devops;security")
ctx.AddStringFlag("url", "https://example.com")

requestPayload := &model.AppDescriptor{
ApplicationKey: "app-key",
ApplicationName: "app-name",
ProjectKey: "proj-key",
ApplicationKey: "app-key",
ApplicationName: "test-app",
ProjectKey: "test-project",
Description: "Test application",
BusinessCriticality: "high",
MaturityLevel: "production",
Labels: map[string]string{
"env": "prod",
"region": "us-east",
},
UserOwners: []string{"john.doe", "jane.smith"},
GroupOwners: []string{"devops", "security"},
}

mockAppService := mockapps.NewMockApplicationService(ctrl)
mockAppService.EXPECT().CreateApplication(gomock.Any(), requestPayload).Return(nil).Times(1)

cmd := &createAppCommand{
applicationService: mockAppService,
serverDetails: serverDetails,
requestBody: requestPayload,
}

err := cmd.Run()
err := cmd.prepareAndRunCommand(ctx)
assert.NoError(t, err)
}

Expand Down Expand Up @@ -84,3 +104,199 @@ func TestCreateAppCommand_WrongNumberOfArguments(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "Wrong number of arguments")
}

func TestCreateAppCommand_MissingProjectFlag(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

ctx := &components.Context{
Arguments: []string{"app-key"},
}
ctx.AddStringFlag("application-name", "test-app")
ctx.AddStringFlag("url", "https://example.com")
mockAppService := mockapps.NewMockApplicationService(ctrl)

cmd := &createAppCommand{
applicationService: mockAppService,
}

err := cmd.prepareAndRunCommand(ctx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "--project is mandatory")
}

func TestCreateAppCommand_Run_SpecFile(t *testing.T) {
tests := []struct {
name string
specPath string
args []string
expectsError bool
errorContains string
expectsPayload *model.AppDescriptor
}{
{
name: "minimal spec file",
specPath: "./testfiles/minimal-spec.json",
args: []string{"app-min"},
expectsPayload: &model.AppDescriptor{
ApplicationKey: "app-min",
ApplicationName: "app-min",
ProjectKey: "test-project",
},
},
{
name: "full spec file",
specPath: "./testfiles/full-spec.json",
args: []string{"app-full"},
expectsPayload: &model.AppDescriptor{
ApplicationKey: "app-full",
ApplicationName: "test-app-full",
ProjectKey: "test-project",
Description: "A comprehensive test application",
MaturityLevel: "production",
BusinessCriticality: "high",
Labels: map[string]string{
"environment": "production",
"region": "us-east-1",
"team": "devops",
},
UserOwners: []string{"john.doe", "jane.smith"},
GroupOwners: []string{"devops-team", "security-team"},
},
},
{
name: "invalid spec file",
specPath: "./testfiles/invalid-spec.json",
args: []string{"app-invalid"},
expectsError: true,
errorContains: "unexpected end of JSON input",
},
{
name: "missing project key",
specPath: "./testfiles/missing-project-spec.json",
args: []string{"app-no-project"},
expectsError: true,
errorContains: "project_key is mandatory in spec file",
},
{
name: "non-existent spec file",
specPath: "./testfiles/non-existent.json",
args: []string{"app-nonexistent"},
expectsError: true,
errorContains: "no such file or directory",
},
{
name: "spec with application_key that should be ignored",
specPath: "./testfiles/spec-with-app-key.json",
args: []string{"command-line-app-key"},
expectsPayload: &model.AppDescriptor{
ApplicationKey: "command-line-app-key",
ApplicationName: "test-app",
ProjectKey: "test-project",
Description: "A test application with application_key that should be ignored",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

ctx := &components.Context{
Arguments: tt.args,
}
ctx.AddStringFlag("url", "https://example.com")
ctx.AddStringFlag("spec", tt.specPath)

var actualPayload *model.AppDescriptor
mockAppService := mockapps.NewMockApplicationService(ctrl)
if !tt.expectsError {
mockAppService.EXPECT().CreateApplication(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ interface{}, req *model.AppDescriptor) error {
actualPayload = req
return nil
}).Times(1)
}

cmd := &createAppCommand{
applicationService: mockAppService,
}

err := cmd.prepareAndRunCommand(ctx)
if tt.expectsError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectsPayload, actualPayload)
}
})
}
}

func TestCreateAppCommand_Run_SpecVars(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

expectedPayload := &model.AppDescriptor{
ApplicationKey: "app-with-vars",
ApplicationName: "test-app",
ProjectKey: "test-project",
Description: "A test application for production",
MaturityLevel: "production",
BusinessCriticality: "high",
Labels: map[string]string{
"environment": "production",
"region": "us-east-1",
},
}

ctx := &components.Context{
Arguments: []string{"app-with-vars"},
}
ctx.AddStringFlag("spec", "./testfiles/with-vars-spec.json")
ctx.AddStringFlag("spec-vars", "PROJECT_KEY=test-project;APP_NAME=test-app;ENVIRONMENT=production;MATURITY_LEVEL=production;CRITICALITY=high;REGION=us-east-1")
ctx.AddStringFlag("url", "https://example.com")

var actualPayload *model.AppDescriptor
mockAppService := mockapps.NewMockApplicationService(ctrl)
mockAppService.EXPECT().CreateApplication(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ interface{}, req *model.AppDescriptor) error {
actualPayload = req
return nil
}).Times(1)

cmd := &createAppCommand{
applicationService: mockAppService,
}

err := cmd.prepareAndRunCommand(ctx)
assert.NoError(t, err)
assert.Equal(t, expectedPayload, actualPayload)
}

func TestCreateAppCommand_Error_SpecAndFlags(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

testSpecPath := "./testfiles/minimal-spec.json"
ctx := &components.Context{
Arguments: []string{"app-key"},
}
ctx.AddStringFlag("spec", testSpecPath)
ctx.AddStringFlag("project", "test-project")
ctx.AddStringFlag("url", "https://example.com")

mockAppService := mockapps.NewMockApplicationService(ctrl)

cmd := &createAppCommand{
applicationService: mockAppService,
}

err := cmd.prepareAndRunCommand(ctx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "the flag --project is not allowed when --spec is provided")
}
Loading
Loading