Skip to content

Commit 0c54c96

Browse files
committed
add volume backup delete and delete tests
1 parent a0f7113 commit 0c54c96

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package delete
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/spinner"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
17+
18+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
19+
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
20+
)
21+
22+
const (
23+
backupIdArg = "BACKUP_ID"
24+
)
25+
26+
type inputModel struct {
27+
*globalflags.GlobalFlagModel
28+
BackupId string
29+
}
30+
31+
func NewCmd(params *params.CmdParams) *cobra.Command {
32+
cmd := &cobra.Command{
33+
Use: fmt.Sprintf("delete %s", backupIdArg),
34+
Short: "Deletes a backup",
35+
Long: "Deletes a backup by its ID.",
36+
Args: args.SingleArg(backupIdArg, utils.ValidateUUID),
37+
Example: examples.Build(
38+
examples.NewExample(
39+
`Delete a backup`,
40+
"$ stackit volume backup delete xxx-xxx-xxx"),
41+
examples.NewExample(
42+
`Delete a backup and wait for deletion to be completed`,
43+
"$ stackit volume backup delete xxx-xxx-xxx --async=false"),
44+
),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
ctx := context.Background()
47+
model, err := parseInput(params.Printer, cmd, args)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Configure API client
53+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
54+
if err != nil {
55+
return err
56+
}
57+
58+
backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute()
59+
if err != nil {
60+
params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err)
61+
}
62+
backupLabel := model.BackupId
63+
if backup != nil && backup.Name != nil {
64+
backupLabel = *backup.Name
65+
}
66+
67+
if !model.AssumeYes {
68+
prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel)
69+
err = params.Printer.PromptForConfirmation(prompt)
70+
if err != nil {
71+
return err
72+
}
73+
}
74+
75+
// Call API
76+
req := buildRequest(ctx, model, apiClient)
77+
err = req.Execute()
78+
if err != nil {
79+
return fmt.Errorf("delete backup: %w", err)
80+
}
81+
82+
// Wait for async operation, if async mode not enabled
83+
if !model.Async {
84+
s := spinner.New(params.Printer)
85+
s.Start("Deleting backup")
86+
_, err = wait.DeleteBackupWaitHandler(ctx, apiClient, model.ProjectId, model.BackupId).WaitWithContext(ctx)
87+
if err != nil {
88+
return fmt.Errorf("wait for backup deletion: %w", err)
89+
}
90+
s.Stop()
91+
}
92+
93+
if model.Async {
94+
params.Printer.Info("Triggered deletion of backup %q\n", backupLabel)
95+
} else {
96+
params.Printer.Info("Deleted backup %q\n", backupLabel)
97+
}
98+
return nil
99+
},
100+
}
101+
return cmd
102+
}
103+
104+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
105+
backupId := inputArgs[0]
106+
107+
globalFlags := globalflags.Parse(p, cmd)
108+
if globalFlags.ProjectId == "" {
109+
return nil, &errors.ProjectIdError{}
110+
}
111+
112+
model := inputModel{
113+
GlobalFlagModel: globalFlags,
114+
BackupId: backupId,
115+
}
116+
117+
if p.IsVerbosityDebug() {
118+
modelStr, err := print.BuildDebugStrFromInputModel(model)
119+
if err != nil {
120+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
121+
} else {
122+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
123+
}
124+
}
125+
126+
return &model, nil
127+
}
128+
129+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteBackupRequest {
130+
req := apiClient.DeleteBackup(ctx, model.ProjectId, model.BackupId)
131+
return req
132+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package delete
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
8+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
13+
"github.com/google/uuid"
14+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
15+
)
16+
17+
var projectIdFlag = globalflags.ProjectIdFlag
18+
19+
type testCtxKey struct{}
20+
21+
var (
22+
testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
23+
testClient = &iaas.APIClient{}
24+
testProjectId = uuid.NewString()
25+
testBackupId = uuid.NewString()
26+
)
27+
28+
func fixtureArgValues(mods ...func(argValues []string)) []string {
29+
argValues := []string{
30+
testBackupId,
31+
}
32+
for _, mod := range mods {
33+
mod(argValues)
34+
}
35+
return argValues
36+
}
37+
38+
func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
39+
flagValues := map[string]string{
40+
projectIdFlag: testProjectId,
41+
}
42+
for _, mod := range mods {
43+
mod(flagValues)
44+
}
45+
return flagValues
46+
}
47+
48+
func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
49+
model := &inputModel{
50+
GlobalFlagModel: &globalflags.GlobalFlagModel{
51+
ProjectId: testProjectId,
52+
Verbosity: globalflags.VerbosityDefault,
53+
},
54+
BackupId: testBackupId,
55+
}
56+
for _, mod := range mods {
57+
mod(model)
58+
}
59+
return model
60+
}
61+
62+
func fixtureRequest(mods ...func(request *iaas.ApiDeleteBackupRequest)) iaas.ApiDeleteBackupRequest {
63+
request := testClient.DeleteBackup(testCtx, testProjectId, testBackupId)
64+
for _, mod := range mods {
65+
mod(&request)
66+
}
67+
return request
68+
}
69+
70+
func TestParseInput(t *testing.T) {
71+
tests := []struct {
72+
description string
73+
argValues []string
74+
flagValues map[string]string
75+
isValid bool
76+
expectedModel *inputModel
77+
}{
78+
{
79+
description: "base",
80+
argValues: fixtureArgValues(),
81+
flagValues: fixtureFlagValues(),
82+
isValid: true,
83+
expectedModel: fixtureInputModel(),
84+
},
85+
{
86+
description: "no values",
87+
argValues: []string{},
88+
flagValues: map[string]string{},
89+
isValid: false,
90+
},
91+
{
92+
description: "no arg values",
93+
argValues: []string{},
94+
flagValues: fixtureFlagValues(),
95+
isValid: false,
96+
},
97+
{
98+
description: "no flag values",
99+
argValues: fixtureArgValues(),
100+
flagValues: map[string]string{},
101+
isValid: false,
102+
},
103+
{
104+
description: "project id missing",
105+
argValues: fixtureArgValues(),
106+
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
107+
delete(flagValues, projectIdFlag)
108+
}),
109+
isValid: false,
110+
},
111+
}
112+
113+
for _, tt := range tests {
114+
t.Run(tt.description, func(t *testing.T) {
115+
p := print.NewPrinter()
116+
cmd := NewCmd(&params.CmdParams{Printer: p})
117+
err := globalflags.Configure(cmd.Flags())
118+
if err != nil {
119+
t.Fatalf("configure global flags: %v", err)
120+
}
121+
122+
for flag, value := range tt.flagValues {
123+
err := cmd.Flags().Set(flag, value)
124+
if err != nil {
125+
if !tt.isValid {
126+
return
127+
}
128+
t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
129+
}
130+
}
131+
132+
err = cmd.ValidateArgs(tt.argValues)
133+
if err != nil {
134+
if !tt.isValid {
135+
return
136+
}
137+
t.Fatalf("error validating args: %v", err)
138+
}
139+
140+
model, err := parseInput(p, cmd, tt.argValues)
141+
if err != nil {
142+
if !tt.isValid {
143+
return
144+
}
145+
t.Fatalf("error parsing input: %v", err)
146+
}
147+
148+
if !tt.isValid {
149+
t.Fatalf("did not fail on invalid input")
150+
}
151+
diff := cmp.Diff(model, tt.expectedModel)
152+
if diff != "" {
153+
t.Fatalf("Data does not match: %s", diff)
154+
}
155+
})
156+
}
157+
}
158+
159+
func TestBuildRequest(t *testing.T) {
160+
tests := []struct {
161+
description string
162+
model *inputModel
163+
expectedRequest iaas.ApiDeleteBackupRequest
164+
}{
165+
{
166+
description: "base",
167+
model: fixtureInputModel(),
168+
expectedRequest: fixtureRequest(),
169+
},
170+
}
171+
172+
for _, tt := range tests {
173+
t.Run(tt.description, func(t *testing.T) {
174+
request := buildRequest(testCtx, tt.model, testClient)
175+
176+
diff := cmp.Diff(request, tt.expectedRequest,
177+
cmp.AllowUnexported(tt.expectedRequest),
178+
cmpopts.EquateComparable(testCtx),
179+
)
180+
if diff != "" {
181+
t.Fatalf("Data does not match: %s", diff)
182+
}
183+
})
184+
}
185+
}

0 commit comments

Comments
 (0)