Skip to content

Commit d0f029d

Browse files
committed
add volume backup describe and describe tests
1 parent 0c54c96 commit d0f029d

File tree

2 files changed

+393
-0
lines changed

2 files changed

+393
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package describe
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/goccy/go-yaml"
9+
"github.com/spf13/cobra"
10+
"github.com/stackitcloud/stackit-cli/internal/cmd/params"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
18+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
19+
20+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
21+
)
22+
23+
const (
24+
backupIdArg = "BACKUP_ID"
25+
)
26+
27+
type inputModel struct {
28+
*globalflags.GlobalFlagModel
29+
BackupId string
30+
}
31+
32+
func NewCmd(params *params.CmdParams) *cobra.Command {
33+
cmd := &cobra.Command{
34+
Use: fmt.Sprintf("describe %s", backupIdArg),
35+
Short: "Describes a backup",
36+
Long: "Describes a backup by its ID.",
37+
Args: args.SingleArg(backupIdArg, utils.ValidateUUID),
38+
Example: examples.Build(
39+
examples.NewExample(
40+
`Get details of a backup`,
41+
"$ stackit volume backup describe xxx-xxx-xxx"),
42+
examples.NewExample(
43+
`Get details of a backup in JSON format`,
44+
"$ stackit volume backup describe xxx-xxx-xxx --output-format json"),
45+
),
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
ctx := context.Background()
48+
model, err := parseInput(params.Printer, cmd, args)
49+
if err != nil {
50+
return err
51+
}
52+
53+
// Configure API client
54+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
55+
if err != nil {
56+
return err
57+
}
58+
59+
// Call API
60+
req := buildRequest(ctx, model, apiClient)
61+
backup, err := req.Execute()
62+
if err != nil {
63+
return fmt.Errorf("get backup details: %w", err)
64+
}
65+
66+
return outputResult(params.Printer, model.OutputFormat, backup)
67+
},
68+
}
69+
return cmd
70+
}
71+
72+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
73+
backupId := inputArgs[0]
74+
75+
globalFlags := globalflags.Parse(p, cmd)
76+
if globalFlags.ProjectId == "" {
77+
return nil, &errors.ProjectIdError{}
78+
}
79+
80+
model := inputModel{
81+
GlobalFlagModel: globalFlags,
82+
BackupId: backupId,
83+
}
84+
85+
if p.IsVerbosityDebug() {
86+
modelStr, err := print.BuildDebugStrFromInputModel(model)
87+
if err != nil {
88+
p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err)
89+
} else {
90+
p.Debug(print.DebugLevel, "parsed input values: %s", modelStr)
91+
}
92+
}
93+
94+
return &model, nil
95+
}
96+
97+
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetBackupRequest {
98+
req := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId)
99+
return req
100+
}
101+
102+
func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) error {
103+
if backup == nil {
104+
return fmt.Errorf("backup response is empty")
105+
}
106+
107+
switch outputFormat {
108+
case print.JSONOutputFormat:
109+
details, err := json.MarshalIndent(backup, "", " ")
110+
if err != nil {
111+
return fmt.Errorf("marshal backup: %w", err)
112+
}
113+
p.Outputln(string(details))
114+
return nil
115+
116+
case print.YAMLOutputFormat:
117+
details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
118+
if err != nil {
119+
return fmt.Errorf("marshal backup: %w", err)
120+
}
121+
p.Outputln(string(details))
122+
return nil
123+
124+
default:
125+
table := tables.NewTable()
126+
table.AddRow(
127+
"ID",
128+
utils.PtrString(backup.Id),
129+
)
130+
table.AddRow(
131+
"NAME",
132+
utils.PtrString(backup.Name),
133+
)
134+
table.AddRow(
135+
"SIZE",
136+
utils.PtrByteSizeDefault((*int64)(backup.Size), ""),
137+
)
138+
table.AddRow(
139+
"STATUS",
140+
utils.PtrString(backup.Status),
141+
)
142+
table.AddRow(
143+
"SNAPSHOT ID",
144+
utils.PtrString(backup.SnapshotId),
145+
)
146+
table.AddRow(
147+
"VOLUME ID",
148+
utils.PtrString(backup.VolumeId),
149+
)
150+
table.AddRow(
151+
"AVAILABILITY ZONE",
152+
utils.PtrString(backup.AvailabilityZone),
153+
)
154+
table.AddRow(
155+
"LABELS",
156+
utils.PtrStringDefault(backup.Labels, ""),
157+
)
158+
table.AddRow(
159+
"CREATED AT",
160+
utils.ConvertTimePToDateTimeString(backup.CreatedAt),
161+
)
162+
table.AddRow(
163+
"UPDATED AT",
164+
utils.ConvertTimePToDateTimeString(backup.UpdatedAt),
165+
)
166+
167+
err := table.Display(p)
168+
if err != nil {
169+
return fmt.Errorf("render table: %w", err)
170+
}
171+
172+
return nil
173+
}
174+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package describe
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.ApiGetBackupRequest)) iaas.ApiGetBackupRequest {
63+
request := testClient.GetBackup(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.ApiGetBackupRequest
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+
}
186+
187+
func TestOutputResult(t *testing.T) {
188+
type args struct {
189+
outputFormat string
190+
backup *iaas.Backup
191+
}
192+
tests := []struct {
193+
name string
194+
args args
195+
wantErr bool
196+
}{
197+
{
198+
name: "empty",
199+
args: args{},
200+
wantErr: true,
201+
},
202+
{
203+
name: "backup as argument",
204+
args: args{
205+
backup: &iaas.Backup{},
206+
},
207+
wantErr: false,
208+
},
209+
}
210+
p := print.NewPrinter()
211+
p.Cmd = NewCmd(&params.CmdParams{Printer: p})
212+
for _, tt := range tests {
213+
t.Run(tt.name, func(t *testing.T) {
214+
if err := outputResult(p, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr {
215+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
216+
}
217+
})
218+
}
219+
}

0 commit comments

Comments
 (0)