From dd3bb37d0cf7b9c18b4aaef1cf0d76235c62e397 Mon Sep 17 00:00:00 2001 From: Federico Maggi <7142570+fredmaggiowski@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:55:29 +0200 Subject: [PATCH] feat: get extension by id (#209) --- CHANGELOG.md | 4 + docs/30_commands.md | 19 +++- internal/cmd/extensions/client.go | 37 +++++--- internal/cmd/extensions/client_test.go | 94 ++++++++++++++++++- internal/cmd/extensions/cmd.go | 1 + internal/cmd/extensions/delete.go | 1 - internal/cmd/extensions/getone.go | 64 +++++++++++++ internal/cmd/extensions/getone_test.go | 30 ++++++ internal/cmd/extensions/list.go | 2 +- .../resources/extensibility/extensionInfo.go | 64 +++++++++++++ 10 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 internal/cmd/extensions/getone.go create mode 100644 internal/cmd/extensions/getone_test.go create mode 100644 internal/resources/extensibility/extensionInfo.go diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf64791..9daf1a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `extensions get` command + ## [0.13.0] - 2024-06-26 ### Added diff --git a/docs/30_commands.md b/docs/30_commands.md index a912143d..c1d0af9a 100644 --- a/docs/30_commands.md +++ b/docs/30_commands.md @@ -570,6 +570,7 @@ Available subcommands are the following ones: ```sh list List registered extensions + get Retrieve data of a specific extension apply Create or update an extension activate Activate an extension deactivate Deactivate an extension @@ -590,6 +591,22 @@ Available flags for the command: - `--company-id` to set the ID of the desired Company +### get + +The `extensions get` command helps you gathering information about a specific extension in your Company + +Usage: + +```sh +miactl extensions get [flags] +``` + +Available flags for the command: + +- `--company-id` to set the ID of the desired Company +- `--extension-id` to set the ID of the desired extension. +- `--output=json|yaml` to control the printed output format. + ### apply The `extensions apply` command can be used to register new extensions or update an existing one. @@ -664,7 +681,7 @@ Available flags for the command: - `--company-id` to set the ID of the desired Company - `--file-path` (`-f`) **required** to specify the path to the extension manifest -- `--extension-id` to set the ID of the extension Company, required for updating an existing extension. +- `--extension-id` to set the ID of the extension, required for updating an existing extension. :::tip In order to specify whether a create or an update is needed you have to use the `--extension-id` diff --git a/internal/cmd/extensions/client.go b/internal/cmd/extensions/client.go index beede266..629c55dc 100644 --- a/internal/cmd/extensions/client.go +++ b/internal/cmd/extensions/client.go @@ -26,20 +26,18 @@ import ( ) const ( - extensibilityAPIPrefix = "/api/extensibility" - tenantsExtensionsAPIPrefix = extensibilityAPIPrefix + "/tenants/%s/extensions" - - listAPIFmt = tenantsExtensionsAPIPrefix - applyAPIFmt = tenantsExtensionsAPIPrefix - deleteAPIFmt = tenantsExtensionsAPIPrefix + "/%s" - activationAPIFmt = tenantsExtensionsAPIPrefix + "/%s/activation" - deactivationAPIFmt = tenantsExtensionsAPIPrefix + "/%s/%s/%s/activation" + extensibilityAPIPrefix = "/api/extensibility" + tenantsExtensionsAPIFmt = extensibilityAPIPrefix + "/tenants/%s/extensions" + tenantsExtensionsByIDAPIFmt = tenantsExtensionsAPIFmt + "/%s" + activationAPIFmt = tenantsExtensionsByIDAPIFmt + "/activation" + deactivationAPIFmt = tenantsExtensionsByIDAPIFmt + "/%s/%s/activation" ) const IFrameExtensionType = "iframe" type IE11yClient interface { List(ctx context.Context, companyID string) ([]*extensibility.Extension, error) + GetOne(ctx context.Context, companyID string, extensionID string) (*extensibility.ExtensionInfo, error) Apply(ctx context.Context, companyID string, extensionData *extensibility.Extension) (string, error) Delete(ctx context.Context, companyID string, extensionID string) error Activate(ctx context.Context, companyID string, extensionID string, scope ActivationScope) error @@ -55,7 +53,7 @@ func New(c *client.APIClient) IE11yClient { } func (e *E11yClient) List(ctx context.Context, companyID string) ([]*extensibility.Extension, error) { - apiPath := fmt.Sprintf(listAPIFmt, companyID) + apiPath := fmt.Sprintf(tenantsExtensionsAPIFmt, companyID) resp, err := e.c.Get().APIPath(apiPath).Do(ctx) if err != nil { return nil, fmt.Errorf("error executing request: %w", err) @@ -73,12 +71,29 @@ func (e *E11yClient) List(ctx context.Context, companyID string) ([]*extensibili return extensions, nil } +func (e *E11yClient) GetOne(ctx context.Context, companyID string, extensionID string) (*extensibility.ExtensionInfo, error) { + apiPath := fmt.Sprintf(tenantsExtensionsByIDAPIFmt, companyID, extensionID) + resp, err := e.c.Get().APIPath(apiPath).Do(ctx) + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + if err := e.assertSuccessResponse(resp); err != nil { + return nil, err + } + + var extension *extensibility.ExtensionInfo + if err := resp.ParseResponse(&extension); err != nil { + return nil, fmt.Errorf("error parsing response body: %w", err) + } + return extension, nil +} + type ApplyResponseBody struct { ExtensionID string `json:"extensionId"` } func (e *E11yClient) Apply(ctx context.Context, companyID string, extensionData *extensibility.Extension) (string, error) { - apiPath := fmt.Sprintf(applyAPIFmt, companyID) + apiPath := fmt.Sprintf(tenantsExtensionsAPIFmt, companyID) body, err := resources.EncodeResourceToJSON(extensionData) if err != nil { return "", fmt.Errorf("error serializing request body: %s", err.Error()) @@ -105,7 +120,7 @@ func (e *E11yClient) Apply(ctx context.Context, companyID string, extensionData } func (e *E11yClient) Delete(ctx context.Context, companyID string, extensionID string) error { - apiPath := fmt.Sprintf(deleteAPIFmt, companyID, extensionID) + apiPath := fmt.Sprintf(tenantsExtensionsByIDAPIFmt, companyID, extensionID) resp, err := e.c.Delete().APIPath(apiPath).Do(ctx) if err != nil { return fmt.Errorf("error executing request: %w", err) diff --git a/internal/cmd/extensions/client_test.go b/internal/cmd/extensions/client_test.go index 0e4d63c3..aaccb959 100644 --- a/internal/cmd/extensions/client_test.go +++ b/internal/cmd/extensions/client_test.go @@ -52,7 +52,7 @@ func TestE11yClientList(t *testing.T) { "valid response": { companyID: "company-1", server: mockServer(t, ExpectedRequest{ - path: fmt.Sprintf(listAPIFmt, "company-1"), + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions", "company-1"), verb: http.MethodGet, }, MockResponse{ statusCode: http.StatusOK, @@ -62,7 +62,7 @@ func TestE11yClientList(t *testing.T) { "invalid response": { companyID: "company-1", server: mockServer(t, ExpectedRequest{ - path: fmt.Sprintf(listAPIFmt, "company-1"), + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions", "company-1"), verb: http.MethodGet, }, MockResponse{ statusCode: http.StatusInternalServerError, @@ -106,6 +106,92 @@ func TestE11yClientList(t *testing.T) { } } +func TestE11yClientGetOne(t *testing.T) { + validBodyString := `{ + "extensionId": "mocked-id", + "extensionName": "mocked-name", + "entry": "http://example.com/", + "type": "iframe", + "destination": {"id": "project"}, + "description": "some description", + "activationContexts": ["project"], + "permissions": ["perm1"], + "visibilities": [{"contextType": "project", "contextId": "prjId"}], + "menu": {"id": "routeId", "labelIntl": {"en":"some label", "it": "qualche etichetta"}}, + "category": {"id": "some-category"} + }` + + testCases := map[string]struct { + companyID string + server *httptest.Server + err bool + }{ + "valid response": { + companyID: "company-1", + server: mockServer(t, ExpectedRequest{ + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions/%s", "company-1", "ext-1"), + verb: http.MethodGet, + }, MockResponse{ + statusCode: http.StatusOK, + respBody: validBodyString, + }), + }, + "invalid response": { + companyID: "company-1", + server: mockServer(t, ExpectedRequest{ + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions/%s", "company-1", "ext-1"), + verb: http.MethodGet, + }, MockResponse{ + statusCode: http.StatusInternalServerError, + err: true, + }), + err: true, + }, + } + + for testName, testCase := range testCases { + t.Run(testName, func(t *testing.T) { + defer testCase.server.Close() + clientConfig := &client.Config{ + Transport: http.DefaultTransport, + Host: testCase.server.URL, + } + + client, err := client.APIClientForConfig(clientConfig) + require.NoError(t, err) + + data, err := New(client).GetOne(context.TODO(), testCase.companyID, "ext-1") + if testCase.err { + require.Error(t, err) + require.Nil(t, data) + } else { + require.NoError(t, err) + require.Equal(t, &extensibility.ExtensionInfo{ + ExtensionID: "mocked-id", + ExtensionName: "mocked-name", + Entry: "http://example.com/", + Type: "iframe", + Destination: extensibility.DestinationArea{ID: "project"}, + Description: "some description", + ActivationContexts: []extensibility.Context{"project"}, + Permissions: []string{"perm1"}, + Visibilities: []extensibility.Visibility{{ContextType: "project", ContextID: "prjId"}}, + Menu: extensibility.Menu{ + ID: "routeId", + LabelIntl: extensibility.IntlMessages{ + "en": "some label", + "it": "qualche etichetta", + }, + }, + Category: extensibility.Category{ + ID: "some-category", + }, + }, data) + } + }) + } +} + func TestE11yClientApply(t *testing.T) { testCases := map[string]struct { companyID string @@ -188,7 +274,7 @@ func TestE11yClientDelete(t *testing.T) { companyID: "company-1", extensionID: "ext-1", server: mockServer(t, ExpectedRequest{ - path: fmt.Sprintf(deleteAPIFmt, "company-1", "ext-1"), + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions/%s", "company-1", "ext-1"), verb: http.MethodDelete, }, MockResponse{ statusCode: http.StatusNoContent, @@ -198,7 +284,7 @@ func TestE11yClientDelete(t *testing.T) { companyID: "company-1", extensionID: "ext-1", server: mockServer(t, ExpectedRequest{ - path: fmt.Sprintf(deleteAPIFmt, "company-1", "ext-1"), + path: fmt.Sprintf("/api/extensibilty/tenants/%s/extensions/%s", "company-1", "ext-1"), verb: http.MethodDelete, }, MockResponse{ statusCode: http.StatusInternalServerError, diff --git a/internal/cmd/extensions/cmd.go b/internal/cmd/extensions/cmd.go index abe6865c..ccd7b2d1 100644 --- a/internal/cmd/extensions/cmd.go +++ b/internal/cmd/extensions/cmd.go @@ -43,6 +43,7 @@ func NewCommand(o *clioptions.CLIOptions) *cobra.Command { cmd.AddCommand( ListCmd(o), + GetOneCmd(o), ApplyCmd(o), DeleteCmd(o), ActivateCmd(o), diff --git a/internal/cmd/extensions/delete.go b/internal/cmd/extensions/delete.go index 2424cffc..824352b5 100644 --- a/internal/cmd/extensions/delete.go +++ b/internal/cmd/extensions/delete.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" ) -// DeleteCmd return a new cobra command for listing companies func DeleteCmd(options *clioptions.CLIOptions) *cobra.Command { cmd := &cobra.Command{ Use: "delete", diff --git a/internal/cmd/extensions/getone.go b/internal/cmd/extensions/getone.go new file mode 100644 index 00000000..f0b80b53 --- /dev/null +++ b/internal/cmd/extensions/getone.go @@ -0,0 +1,64 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extensions + +import ( + "fmt" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/encoding" + + "github.com/spf13/cobra" +) + +// GetOneCmd return a new cobra command to get a single extension +func GetOneCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get a registered extension", + Long: "Get details for a single registered extension for the company.", + RunE: func(cmd *cobra.Command, _ []string) error { + restConfig, err := options.ToRESTConfig() + cobra.CheckErr(err) + + client, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + + if restConfig.CompanyID == "" { + return ErrRequiredCompanyID + } + if options.EntityID == "" { + return ErrRequiredExtensionID + } + + extensibilityClient := New(client) + extension, err := extensibilityClient.GetOne(cmd.Context(), restConfig.CompanyID, options.EntityID) + cobra.CheckErr(err) + + serialized, err := encoding.MarshalData(extension, options.OutputFormat, encoding.MarshalOptions{Indent: true}) + if err != nil { + return err + } + fmt.Println(string(serialized)) + return nil + }, + } + + options.AddOutputFormatFlag(cmd.Flags(), encoding.JSON) + addExtensionIDFlag(options, cmd) + return cmd +} diff --git a/internal/cmd/extensions/getone_test.go b/internal/cmd/extensions/getone_test.go new file mode 100644 index 00000000..9a3f9cd6 --- /dev/null +++ b/internal/cmd/extensions/getone_test.go @@ -0,0 +1,30 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extensions + +import ( + "testing" + + "github.com/mia-platform/miactl/internal/clioptions" + + "github.com/stretchr/testify/require" +) + +func TestGetOneCmdCommandBuilder(t *testing.T) { + opts := clioptions.NewCLIOptions() + cmd := GetOneCmd(opts) + require.NotNil(t, cmd) +} diff --git a/internal/cmd/extensions/list.go b/internal/cmd/extensions/list.go index b9e85732..f31ee958 100644 --- a/internal/cmd/extensions/list.go +++ b/internal/cmd/extensions/list.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" ) -// ListCmd return a new cobra command for listing companies func ListCmd(options *clioptions.CLIOptions) *cobra.Command { return &cobra.Command{ Use: "list", @@ -50,6 +49,7 @@ func ListCmd(options *clioptions.CLIOptions) *cobra.Command { }, } } + func printExtensionsList(extensions []*extensibility.Extension, p printer.IPrinter) { p.Keys("ID", "Name", "Description") for _, extension := range extensions { diff --git a/internal/resources/extensibility/extensionInfo.go b/internal/resources/extensibility/extensionInfo.go new file mode 100644 index 00000000..c685b33c --- /dev/null +++ b/internal/resources/extensibility/extensionInfo.go @@ -0,0 +1,64 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package extensibility + +type Order float64 + +type Context string + +type DestinationArea struct { + ID string `json:"id" yaml:"id"` +} +type Languages string + +// TODO: Constraint type on these values +const ( + En Languages = "en" + It Languages = "it" +) + +type IntlMessages map[Languages]string + +type Visibility struct { + ContextType Context `json:"contextType" yaml:"contextType"` + ContextID string `json:"contextId" yaml:"contextId"` +} + +type Category struct { + ID string `json:"id" yaml:"id"` + LabelIntl IntlMessages `json:"labelIntl,omitempty" yaml:"labelIntl,omitempty"` +} + +type Menu struct { + ID string `json:"id" yaml:"id"` + LabelIntl IntlMessages `json:"labelIntl" yaml:"labelIntl"` + Order *Order `json:"order,omitempty" yaml:"order,omitempty"` +} + +type ExtensionInfo struct { + ExtensionID string `json:"extensionId" yaml:"extensionId"` + ExtensionName string `json:"extensionName" yaml:"extensionName"` + Entry string `json:"entry" yaml:"entry"` + Type string `json:"type" yaml:"type"` + Destination DestinationArea `json:"destination" yaml:"destination"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + IconName string `json:"iconName,omitempty" yaml:"iconName,omitempty"` + ActivationContexts []Context `json:"activationContexts" yaml:"activationContexts"` + Permissions []string `json:"permissions,omitempty" yaml:"permissions,omitempty"` + Visibilities []Visibility `json:"visibilities,omitempty" yaml:"visibilities,omitempty"` + Category Category `json:"category,omitempty" yaml:"category,omitempty"` + Menu Menu `json:"menu" yaml:"menu"` +}