diff --git a/application/handler/application.go b/application/handler/application.go index 65b9dbd..53493a8 100644 --- a/application/handler/application.go +++ b/application/handler/application.go @@ -1,35 +1,100 @@ +// Package handler provides functionalities for managing applications, including adding new applications and +// listing existing ones with their environments. +// It acts as the CMD handler layer, connecting the application logic to the user interface. package handler import ( "errors" + "fmt" + "sort" + "strings" "gofr.dev/pkg/gofr" + "gofr.dev/pkg/gofr/cmd/terminal" ) +// Errors returned by the handler package. var ( + // ErrorApplicationNameNotProvided indicates that the application name was not provided as a parameter. ErrorApplicationNameNotProvided = errors.New("please enter application name, -name=") ) +// Handler represents the HTTP handler responsible for managing applications. type Handler struct { - appAdd ApplicationAdder + appAdd ApplicationService } -func New(appAdd ApplicationAdder) *Handler { +// New creates a new instance of Handler. +// +// Parameters: +// - appAdd: An implementation of the ApplicationService interface used to manage applications. +// +// Returns: +// +// A pointer to a Handler instance. +func New(appAdd ApplicationService) *Handler { return &Handler{ appAdd: appAdd, } } +// Add handles the addition of a new application. +// +// Parameters: +// - ctx: The application context containing dependencies and utilities. +// +// Returns: +// +// A success message and an error, if any. func (h *Handler) Add(ctx *gofr.Context) (any, error) { name := ctx.Param("name") if name == "" { return nil, ErrorApplicationNameNotProvided } - err := h.appAdd.AddApplication(ctx, name) + err := h.appAdd.Add(ctx, name) if err != nil { return nil, err } return "Application " + name + " added successfully!", nil } + +// List retrieves and displays all applications along with their environments. +// +// Parameters: +// - ctx: The application context containing dependencies and utilities. +// +// Returns: +// +// A newline-separated string and an error, if any. +func (h *Handler) List(ctx *gofr.Context) (any, error) { + apps, err := h.appAdd.List(ctx) + if err != nil { + return nil, err + } + + ctx.Out.Println("Applications and their environments:\n") + + s := strings.Builder{} + + for i, app := range apps { + ctx.Out.Printf("%d.", i+1) + ctx.Out.SetColor(terminal.Cyan) + ctx.Out.Printf(" %s \n\t", app.Name) + ctx.Out.ResetColor() + + sort.Slice(app.Envs, func(i, j int) bool { return app.Envs[i].Order < app.Envs[j].Order }) + + for _, env := range app.Envs { + s.WriteString(fmt.Sprintf("%s > ", env.Name)) + } + + ctx.Out.SetColor(terminal.Green) + ctx.Out.Println(s.String()[:s.Len()-2]) + ctx.Out.ResetColor() + s.Reset() + } + + return "\n", nil +} diff --git a/application/handler/handler_test.go b/application/handler/handler_test.go index deb430b..b417817 100644 --- a/application/handler/handler_test.go +++ b/application/handler/handler_test.go @@ -8,7 +8,11 @@ import ( "go.uber.org/mock/gomock" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/cmd" + "gofr.dev/pkg/gofr/cmd/terminal" "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/testutil" + + svc "zop.dev/cli/zop/application/service" ) var errAPICall = errors.New("error in API call") @@ -17,7 +21,7 @@ func TestHandler_Add(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockAppAdder := NewMockApplicationAdder(ctrl) + mockAppAdder := NewMockApplicationService(ctrl) testCases := []struct { name string @@ -68,3 +72,60 @@ func TestHandler_Add(t *testing.T) { }) } } + +func TestHandler_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := NewMockApplicationService(ctrl) + + testCases := []struct { + name string + mockCalls []*gomock.Call + expected string + expErr error + }{ + { + name: "success", + mockCalls: []*gomock.Call{ + mockSvc.EXPECT().GetApplications(gomock.Any()). + Return([]svc.Application{ + {ID: 1, Name: "app1", + Envs: []svc.Environment{{Name: "env1", Order: 1}, {Name: "env2", Order: 2}}}, + {ID: 2, Name: "app2", + Envs: []svc.Environment{{Name: "dev", Order: 1}, {Name: "prod", Order: 2}}}, + }, nil), + }, + expected: "Applications and their environments:\n\n1.\x1b[38;5;6m app1 " + + "\n\t\x1b[0m\x1b[38;5;2menv1 > env2 \n\x1b[0m2.\x1b[38;5;6m app2 " + + "\n\t\x1b[0m\x1b[38;5;2mdev > prod \n\x1b[0m", + }, + { + name: "failure", + mockCalls: []*gomock.Call{ + mockSvc.EXPECT().GetApplications(gomock.Any()). + Return(nil, errAPICall), + }, + expErr: errAPICall, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + h := New(mockSvc) + out := testutil.StdoutOutputForFunc(func() { + ctx := &gofr.Context{ + Request: cmd.NewRequest([]string{""}), + Out: terminal.New(), + } + + _, err := h.List(ctx) + + require.Equal(t, tc.expErr, err) + }) + + require.Equal(t, tc.expected, out) + }) + } +} diff --git a/application/handler/interface.go b/application/handler/interface.go index fb29e0e..a389c9a 100644 --- a/application/handler/interface.go +++ b/application/handler/interface.go @@ -1,7 +1,29 @@ package handler -import "gofr.dev/pkg/gofr" +import ( + "gofr.dev/pkg/gofr" -type ApplicationAdder interface { - AddApplication(ctx *gofr.Context, name string) error + "zop.dev/cli/zop/application/service" +) + +// ApplicationService defines the methods required for application management. +type ApplicationService interface { + // Add adds a new application with the specified name. + // + // Parameters: + // - ctx: The application context containing dependencies and utilities. + // - name: The name of the application to be added. + // + // Returns: + // An error if the application could not be added. + Add(ctx *gofr.Context, name string) error + + // List retrieves the list of applications along with their environments. + // + // Parameters: + // - ctx: The application context containing dependencies and utilities. + // + // Returns: + // A slice of applications and an error, if any. + List(ctx *gofr.Context) ([]service.Application, error) } diff --git a/application/handler/mock_interface.go b/application/handler/mock_interface.go index 1435ad3..b5693a3 100644 --- a/application/handler/mock_interface.go +++ b/application/handler/mock_interface.go @@ -14,42 +14,58 @@ import ( gomock "go.uber.org/mock/gomock" gofr "gofr.dev/pkg/gofr" + service "zop.dev/cli/zop/application/service" ) -// MockApplicationAdder is a mock of ApplicationAdder interface. -type MockApplicationAdder struct { +// MockApplicationService is a mock of ApplicationService interface. +type MockApplicationService struct { ctrl *gomock.Controller - recorder *MockApplicationAdderMockRecorder + recorder *MockApplicationServiceMockRecorder isgomock struct{} } -// MockApplicationAdderMockRecorder is the mock recorder for MockApplicationAdder. -type MockApplicationAdderMockRecorder struct { - mock *MockApplicationAdder +// MockApplicationServiceMockRecorder is the mock recorder for MockApplicationService. +type MockApplicationServiceMockRecorder struct { + mock *MockApplicationService } -// NewMockApplicationAdder creates a new mock instance. -func NewMockApplicationAdder(ctrl *gomock.Controller) *MockApplicationAdder { - mock := &MockApplicationAdder{ctrl: ctrl} - mock.recorder = &MockApplicationAdderMockRecorder{mock} +// NewMockApplicationService creates a new mock instance. +func NewMockApplicationService(ctrl *gomock.Controller) *MockApplicationService { + mock := &MockApplicationService{ctrl: ctrl} + mock.recorder = &MockApplicationServiceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockApplicationAdder) EXPECT() *MockApplicationAdderMockRecorder { +func (m *MockApplicationService) EXPECT() *MockApplicationServiceMockRecorder { return m.recorder } // AddApplication mocks base method. -func (m *MockApplicationAdder) AddApplication(ctx *gofr.Context, name string) error { +func (m *MockApplicationService) Add(ctx *gofr.Context, name string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddApplication", ctx, name) + ret := m.ctrl.Call(m, "Add", ctx, name) ret0, _ := ret[0].(error) return ret0 } // AddApplication indicates an expected call of AddApplication. -func (mr *MockApplicationAdderMockRecorder) AddApplication(ctx, name any) *gomock.Call { +func (mr *MockApplicationServiceMockRecorder) AddApplication(ctx, name any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddApplication", reflect.TypeOf((*MockApplicationAdder)(nil).AddApplication), ctx, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockApplicationService)(nil).Add), ctx, name) +} + +// GetApplications mocks base method. +func (m *MockApplicationService) List(ctx *gofr.Context) ([]service.Application, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx) + ret0, _ := ret[0].([]service.Application) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetApplications indicates an expected call of GetApplications. +func (mr *MockApplicationServiceMockRecorder) GetApplications(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockApplicationService)(nil).List), ctx) } diff --git a/application/service/application.go b/application/service/application.go index afd0e64..eb0950e 100644 --- a/application/service/application.go +++ b/application/service/application.go @@ -1,21 +1,38 @@ +// Package service provides functionalities for managing applications and their environments. +// It includes methods for adding a new application and listing existing applications. package service import ( "encoding/json" "fmt" + "io" "net/http" "gofr.dev/pkg/gofr" ) -type Service struct { -} +// Service provides methods for managing applications. +type Service struct{} +// New creates a new instance of Service. +// +// Returns: +// +// A pointer to a Service instance. func New() *Service { return &Service{} } -func (*Service) AddApplication(ctx *gofr.Context, name string) error { +// Add adds a new application and optionally its environments. +// +// Parameters: +// - ctx: The application context containing dependencies and utilities. +// - name: The name of the application to be added. +// +// Returns: +// +// An error if the application or environments could not be added. +func (*Service) Add(ctx *gofr.Context, name string) error { var ( envs []Environment input string @@ -52,7 +69,9 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { app.Envs = envs body, _ := json.Marshal(app) - resp, err := api.PostWithHeaders(ctx, "application", nil, body, nil) + resp, err := api.PostWithHeaders(ctx, "applications", nil, body, map[string]string{ + "Content-Type": "application/json", + }) if err != nil { return err } @@ -65,3 +84,37 @@ func (*Service) AddApplication(ctx *gofr.Context, name string) error { return nil } + +// List retrieves all applications and their environments. +// +// Parameters: +// - ctx: The application context containing dependencies and utilities. +// +// Returns: +// +// A slice of applications and an error, if any. +func (*Service) List(ctx *gofr.Context) ([]Application, error) { + api := ctx.GetHTTPService("api-service") + + reps, err := api.Get(ctx, "applications", nil) + if err != nil { + return nil, err + } + defer reps.Body.Close() + + var apps struct { + Data []Application `json:"data"` + } + + body, _ := io.ReadAll(reps.Body) + + err = json.Unmarshal(body, &apps) + if err != nil { + return nil, &ErrAPIService{ + StatusCode: http.StatusInternalServerError, + Message: "Internal Server Error", + } + } + + return apps.Data, nil +} diff --git a/application/service/application_test.go b/application/service/application_test.go index 70c46ec..9834422 100644 --- a/application/service/application_test.go +++ b/application/service/application_test.go @@ -19,7 +19,7 @@ import ( var errAPICall = errors.New("error in API call") -func Test_AddApplication(t *testing.T) { +func Test_Add(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -36,29 +36,33 @@ func Test_AddApplication(t *testing.T) { testCases := []struct { name string + input string mockCalls []*gomock.Call expError error }{ { - name: "success Post call", + name: "success Post call", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(&http.Response{StatusCode: http.StatusCreated, Body: io.NopCloser(&errorReader{})}, nil), }, expError: nil, }, { - name: "error in Post call", + name: "error in Post call", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(nil, errAPICall), }, expError: errAPICall, }, { - name: "unexpected response", + name: "unexpected response", + input: "n\n", mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBuffer(b))}, nil), }, expError: &ErrAPIService{StatusCode: http.StatusInternalServerError, Message: "Something went wrong"}, @@ -69,14 +73,21 @@ func Test_AddApplication(t *testing.T) { t.Run(tt.name, func(t *testing.T) { s := New() - errSvc := s.AddApplication(ctx, "test") + r, w, _ := os.Pipe() + os.Stdin = r + _, _ = w.WriteString(tt.input) + + errSvc := s.Add(ctx, "test") require.Equal(t, tt.expError, errSvc) + + r.Close() + w.Close() }) } } -func Test_AddApplication_WithEnvs(t *testing.T) { +func Test_Add_WithEnvs(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -102,7 +113,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { {Name: "dev", Order: 2}, }, mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) @@ -121,7 +132,7 @@ func Test_AddApplication_WithEnvs(t *testing.T) { userInput: "n\n", expectedEnvs: []Environment{}, mockCalls: []*gomock.Call{ - mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "application", nil, gomock.Any(), nil). + mocks.HTTPService.EXPECT().PostWithHeaders(ctx, "applications", nil, gomock.Any(), gomock.Any()). DoAndReturn(func(_ *gofr.Context, _ string, _, body, _ interface{}) (*http.Response, error) { var app Application _ = json.Unmarshal(body.([]byte), &app) @@ -147,8 +158,64 @@ func Test_AddApplication_WithEnvs(t *testing.T) { defer func() { os.Stdin = oldStdin }() - errSvc := s.AddApplication(ctx, "test") + errSvc := s.Add(ctx, "test") + require.Equal(t, tt.expError, errSvc) + }) + } +} + +func Test_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCont, mocks := container.NewMockContainer(t, func(_ *container.Container, ctrl *gomock.Controller) any { + return service.NewMockHTTP(ctrl) + }) + + mockCont.Services["api-service"] = mocks.HTTPService + ctx := &gofr.Context{Container: mockCont, Out: terminal.New()} + + testCases := []struct { + name string + mockCalls []*gomock.Call + expError error + }{ + { + name: "success Get call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{ "data" : null }`))}, nil), + }, + expError: nil, + }, + { + name: "error in Get call", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(nil, errAPICall), + }, + expError: errAPICall, + }, + { + name: "unexpected response", + mockCalls: []*gomock.Call{ + mocks.HTTPService.EXPECT().Get(ctx, "applications", nil). + Return(&http.Response{StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBuffer(nil))}, nil), + }, + expError: &ErrAPIService{StatusCode: http.StatusInternalServerError, Message: "Internal Server Error"}, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := New() + + apps, errSvc := s.List(ctx) require.Equal(t, tt.expError, errSvc) + + if tt.expError == nil { + require.Empty(t, apps) + } }) } } diff --git a/application/service/models.go b/application/service/models.go index 44fe149..afbde97 100644 --- a/application/service/models.go +++ b/application/service/models.go @@ -1,3 +1,5 @@ +// Package service defines models and utility functions for error handling +// and data structures representing applications and their environments. package service import ( @@ -6,20 +8,35 @@ import ( "net/http" ) +// ErrAPIService represents an error returned by the API service. type ErrAPIService struct { - StatusCode int - Message string + StatusCode int // HTTP status code of the error + Message string // Message describing the error } +// Error returns the error message for ErrAPIService. +// +// Returns: +// +// A string describing the error. func (e *ErrAPIService) Error() string { return e.Message } +// Predefined internal error for API response issues. var errInternal = &ErrAPIService{ StatusCode: http.StatusInternalServerError, - Message: "error in POST /application zop-api, invalid response", + Message: "error in /applications zop-api, invalid response", } +// getAPIError extracts and constructs an ErrAPIService from an HTTP response. +// +// Parameters: +// - resp: The HTTP response containing the error details. +// +// Returns: +// +// A pointer to an ErrAPIService. func getAPIError(resp *http.Response) *ErrAPIService { var errResp struct { Error string `json:"error"` @@ -41,13 +58,16 @@ func getAPIError(resp *http.Response) *ErrAPIService { } } +// Environment represents an environment associated with an application. type Environment struct { - Name string `json:"name"` - Order int `json:"order"` - DeploymentSpace any `json:"deploymentSpace,omitempty"` + Name string `json:"name"` // Name of the environment + Order int `json:"order"` // Order or priority of the environment + DeploymentSpace any `json:"deploymentSpace,omitempty"` // Optional deployment space information } +// Application represents an application with its associated environments. type Application struct { - Name string `json:"name"` - Envs []Environment `json:"environments,omitempty"` + ID int `json:"id"` // Unique identifier of the application + Name string `json:"name"` // Name of the application + Envs []Environment `json:"environments,omitempty"` // List of associated environments } diff --git a/go.mod b/go.mod index 179cbf8..92e7d9d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.8 require ( github.com/mattn/go-sqlite3 v1.14.24 github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 gofr.dev v1.28.0 golang.org/x/oauth2 v0.24.0 @@ -62,7 +63,6 @@ require ( github.com/redis/go-redis/v9 v9.7.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/segmentio/kafka-go v0.4.47 // indirect - github.com/stretchr/testify v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect diff --git a/main.go b/main.go index 7713c0a..010aa3a 100644 --- a/main.go +++ b/main.go @@ -6,9 +6,9 @@ import ( "os" "path/filepath" + _ "github.com/mattn/go-sqlite3" "gofr.dev/pkg/gofr" "gofr.dev/pkg/gofr/service" - _ "modernc.org/sqlite" applicationHandler "zop.dev/cli/zop/application/handler" applicationSvc "zop.dev/cli/zop/application/service" @@ -56,6 +56,7 @@ func main() { appH := applicationHandler.New(appSvc) app.SubCommand("application add", appH.Add) + app.SubCommand("application list", appH.List) app.Run() }