Skip to content

Commit 9874f6f

Browse files
feat(account): delete subcommand (#287)
1 parent 082ddef commit 9874f6f

15 files changed

Lines changed: 313 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add `gateway` commands (`delete`, `list`) for Network gateway management.
1313
- In machine readable outputs of server list, add support for `--show-ip-addresses` parameter.
14+
- Support for sub-account deletion via `account delete` command
1415

1516
## [3.3.0] - 2024-01-23
1617

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package account
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/completion"
8+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
9+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/resolver"
10+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request"
11+
)
12+
13+
// DeleteCommand creates the "account delete" command
14+
func DeleteCommand() commands.Command {
15+
return &deleteCommand{
16+
BaseCommand: commands.New(
17+
"delete",
18+
"Delete a sub-account",
19+
"upctl account delete my-sub-account",
20+
),
21+
}
22+
}
23+
24+
type deleteCommand struct {
25+
*commands.BaseCommand
26+
resolver.CachingAccount
27+
completion.Account
28+
}
29+
30+
// Execute implements commands.MultipleArgumentCommand
31+
func (s *deleteCommand) Execute(exec commands.Executor, arg string) (output.Output, error) {
32+
svc := exec.All()
33+
msg := fmt.Sprintf("Deleting sub-account %v", arg)
34+
exec.PushProgressStarted(msg)
35+
36+
err := svc.DeleteSubaccount(exec.Context(), &request.DeleteSubaccountRequest{
37+
Username: arg,
38+
})
39+
if err != nil {
40+
return commands.HandleError(exec, fmt.Sprintf("%s: failed", msg), err)
41+
}
42+
43+
exec.PushProgressSuccess(msg)
44+
45+
return output.None{}, nil
46+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package account
2+
3+
import (
4+
"testing"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/config"
8+
smock "github.com/UpCloudLtd/upcloud-cli/v3/internal/mock"
9+
10+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud"
11+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request"
12+
"github.com/gemalto/flume"
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestDeleteCommand(t *testing.T) {
17+
targetMethod := "DeleteSubaccount"
18+
19+
account := upcloud.Account{
20+
UserName: "my-sub-account",
21+
}
22+
23+
for _, test := range []struct {
24+
name string
25+
arg string
26+
error string
27+
req request.DeleteSubaccountRequest
28+
}{
29+
{
30+
name: "delete with username",
31+
arg: account.UserName,
32+
req: request.DeleteSubaccountRequest{Username: account.UserName},
33+
},
34+
} {
35+
t.Run(test.name, func(t *testing.T) {
36+
mService := smock.Service{}
37+
mService.On(targetMethod, &test.req).Return(nil)
38+
39+
conf := config.New()
40+
c := commands.BuildCommand(DeleteCommand(), nil, conf)
41+
42+
_, err := c.(commands.MultipleArgumentCommand).Execute(commands.NewExecutor(conf, &mService, flume.New("test")), test.arg)
43+
44+
if test.error != "" {
45+
assert.EqualError(t, err, test.error)
46+
} else {
47+
assert.NoError(t, err)
48+
mService.AssertNumberOfCalls(t, targetMethod, 1)
49+
}
50+
})
51+
}
52+
}

internal/commands/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
106106
accountCommand := commands.BuildCommand(account.BaseAccountCommand(), rootCmd, conf)
107107
commands.BuildCommand(account.ShowCommand(), accountCommand.Cobra(), conf)
108108
commands.BuildCommand(account.ListCommand(), accountCommand.Cobra(), conf)
109+
commands.BuildCommand(account.DeleteCommand(), accountCommand.Cobra(), conf)
109110

110111
// Account permissions
111112
permissionsCommand := commands.BuildCommand(permissions.BasePermissionsCommand(), accountCommand.Cobra(), conf)

internal/completion/account.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package completion
2+
3+
import (
4+
"context"
5+
6+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/service"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// Account implements argument completion for accounts by username.
11+
type Account struct{}
12+
13+
// make sure Account implements the interface
14+
var _ Provider = Account{}
15+
16+
// CompleteArgument implements completion.Provider
17+
func (s Account) CompleteArgument(ctx context.Context, svc service.AllServices, toComplete string) ([]string, cobra.ShellCompDirective) {
18+
accounts, err := svc.GetAccountList(ctx)
19+
if err != nil {
20+
return None(toComplete)
21+
}
22+
var vals []string
23+
for _, v := range accounts {
24+
vals = append(vals, v.Username)
25+
}
26+
return MatchStringPrefix(vals, toComplete, true), cobra.ShellCompDirectiveNoFileComp
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package completion_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/completion"
9+
smock "github.com/UpCloudLtd/upcloud-cli/v3/internal/mock"
10+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud"
11+
"github.com/spf13/cobra"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
)
15+
16+
var mockAccounts = upcloud.AccountList{
17+
{Username: "mock1"},
18+
{Username: "mock2"},
19+
{Username: "bock1"},
20+
{Username: "bock2"},
21+
{Username: "dock1"},
22+
}
23+
24+
func TestAccount_CompleteArgument(t *testing.T) {
25+
for _, test := range []struct {
26+
name string
27+
complete string
28+
expectedMatches []string
29+
expectedDirective cobra.ShellCompDirective
30+
}{
31+
{name: "basic username", complete: "dock", expectedMatches: []string{"dock1"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
32+
{name: "multiple usernames", complete: "mock", expectedMatches: []string{"mock1", "mock2"}, expectedDirective: cobra.ShellCompDirectiveNoFileComp},
33+
} {
34+
t.Run(test.name, func(t *testing.T) {
35+
mService := new(smock.Service)
36+
mService.On("GetAccountList", mock.Anything).Return(mockAccounts, nil)
37+
accounts, directive := completion.Account{}.CompleteArgument(context.TODO(), mService, test.complete)
38+
assert.Equal(t, test.expectedMatches, accounts)
39+
assert.Equal(t, test.expectedDirective, directive)
40+
})
41+
}
42+
}
43+
44+
func TestAccount_CompleteArgumentServiceFail(t *testing.T) {
45+
mService := new(smock.Service)
46+
mService.On("GetAccountList", mock.Anything).Return(nil, fmt.Errorf("MOCKFAIL"))
47+
ips, directive := completion.Account{}.CompleteArgument(context.TODO(), mService, "127")
48+
assert.Nil(t, ips)
49+
assert.Equal(t, cobra.ShellCompDirectiveDefault, directive)
50+
}

internal/mock/mock.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,7 @@ func (m *Service) ModifySubaccount(_ context.Context, r *request.ModifySubaccoun
573573
// DeleteSubaccount implements service.Account.DeleteSubaccount
574574
func (m *Service) DeleteSubaccount(_ context.Context, r *request.DeleteSubaccountRequest) error {
575575
args := m.Called(r)
576-
if args[0] == nil {
577-
return args.Error(1)
578-
}
579-
return nil
576+
return args.Error(0)
580577
}
581578

582579
func (m *Service) CancelManagedDatabaseConnection(_ context.Context, r *request.CancelManagedDatabaseConnection) error {

internal/resolver/account.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package resolver
2+
3+
import (
4+
"context"
5+
6+
internal "github.com/UpCloudLtd/upcloud-cli/v3/internal/service"
7+
)
8+
9+
// CachingAccount implements resolver for servers, caching the results
10+
type CachingAccount struct{}
11+
12+
// make sure we implement the ResolutionProvider interface
13+
var _ ResolutionProvider = CachingAccount{}
14+
15+
// Get implements ResolutionProvider.Get
16+
func (s CachingAccount) Get(ctx context.Context, svc internal.AllServices) (Resolver, error) {
17+
accounts, err := svc.GetAccountList(ctx)
18+
if err != nil {
19+
return nil, err
20+
}
21+
return func(arg string) (uuid string, err error) {
22+
rv := ""
23+
for _, account := range accounts {
24+
if MatchArgWithWhitespace(arg, account.Username) {
25+
if rv != "" {
26+
return "", AmbiguousResolutionError(arg)
27+
}
28+
rv = account.Username
29+
}
30+
}
31+
if rv != "" {
32+
return rv, nil
33+
}
34+
return "", NotFoundError(arg)
35+
}, nil
36+
}
37+
38+
// PositionalArgumentHelp implements resolver.ResolutionProvider
39+
func (s CachingAccount) PositionalArgumentHelp() string {
40+
return "<Username...>"
41+
}

internal/resolver/account_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package resolver_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
smock "github.com/UpCloudLtd/upcloud-cli/v3/internal/mock"
9+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/resolver"
10+
11+
"github.com/UpCloudLtd/upcloud-go-api/v6/upcloud"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func TestAccountResolution(t *testing.T) {
16+
Account1 := upcloud.AccountListItem{
17+
Username: "account-1",
18+
}
19+
20+
Account2 := upcloud.AccountListItem{
21+
Username: "account-2",
22+
}
23+
24+
Account3 := upcloud.AccountListItem{
25+
Username: "account-3",
26+
}
27+
28+
Account4 := upcloud.AccountListItem{
29+
Username: "account-4",
30+
}
31+
32+
allAccounts := upcloud.AccountList{
33+
Account1,
34+
Account2,
35+
Account3,
36+
Account4,
37+
}
38+
39+
t.Run("resolve username", func(t *testing.T) {
40+
mService := &smock.Service{}
41+
mService.On("GetAccountList").Return(allAccounts, nil)
42+
res := resolver.CachingAccount{}
43+
argResolver, err := res.Get(context.TODO(), mService)
44+
assert.NoError(t, err)
45+
for _, account := range allAccounts {
46+
resolved, err := argResolver(account.Username)
47+
assert.NoError(t, err)
48+
assert.Equal(t, account.Username, resolved)
49+
}
50+
// make sure caching works, eg. we didn't call GetAccountList more than once
51+
mService.AssertNumberOfCalls(t, "GetAccountList", 1)
52+
})
53+
54+
t.Run("failure situations", func(t *testing.T) {
55+
mService := &smock.Service{}
56+
mService.On("GetAccountList").Return(allAccounts, nil)
57+
58+
res := resolver.CachingAccount{}
59+
argResolver, err := res.Get(context.TODO(), mService)
60+
assert.NoError(t, err)
61+
62+
// not found
63+
resolved, err := argResolver("notfound")
64+
if !assert.Error(t, err) {
65+
t.FailNow()
66+
}
67+
assert.ErrorIs(t, err, resolver.NotFoundError("notfound"))
68+
assert.Equal(t, "", resolved)
69+
70+
// make sure caching works, eg. we didn't call GetAccountList more than once
71+
mService.AssertNumberOfCalls(t, "GetAccountList", 1)
72+
})
73+
}
74+
75+
func TestFailingAccountResolution(t *testing.T) {
76+
mService := &smock.Service{}
77+
mService.On("GetAccountList").Return(upcloud.AccountList{}, errors.New("MOCKERROR"))
78+
res := resolver.CachingAccount{}
79+
argResolver, err := res.Get(context.TODO(), mService)
80+
assert.EqualError(t, err, "MOCKERROR")
81+
assert.Nil(t, argResolver)
82+
}

internal/resolver/ipaddress_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ func TestIPAddressResolution(t *testing.T) {
139139
assert.Equal(t, "", resolved)
140140

141141
// not found
142-
resolved, err = argResolver("notfounf")
142+
resolved, err = argResolver("notfound")
143143
if !assert.Error(t, err) {
144144
t.FailNow()
145145
}
146-
assert.ErrorIs(t, err, resolver.NotFoundError("notfounf"))
146+
assert.ErrorIs(t, err, resolver.NotFoundError("notfound"))
147147
assert.Equal(t, "", resolved)
148148

149149
// make sure caching works, eg. we didn't call GetServers more than once

0 commit comments

Comments
 (0)