github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/argparser/flags_test.go (about) 1 package argparser_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 "testing" 10 11 "github.com/fastly/go-fastly/v9/fastly" 12 13 "github.com/fastly/cli/pkg/argparser" 14 "github.com/fastly/cli/pkg/mock" 15 "github.com/fastly/cli/pkg/testutil" 16 ) 17 18 func TestOptionalServiceVersionParse(t *testing.T) { 19 cases := map[string]struct { 20 flagValue string 21 flagOmitted bool 22 wantVersion int 23 errExpected bool 24 }{ 25 "latest": { 26 flagValue: "latest", 27 wantVersion: 3, 28 }, 29 "active": { 30 flagValue: "active", 31 wantVersion: 1, 32 }, 33 // NOTE: Default behaviour for an empty flag value (or no flag at all) is to 34 // get the active version, and if no active version return the latest. 35 "empty": { 36 flagValue: "", 37 wantVersion: 1, 38 }, 39 "omitted": { 40 flagOmitted: true, 41 wantVersion: 1, 42 }, 43 "specific version OK": { 44 flagValue: "2", 45 wantVersion: 2, 46 }, 47 "specific version ERR": { 48 flagValue: "4", 49 errExpected: true, // there is no version 4 50 }, 51 } 52 53 for name, c := range cases { 54 t.Run(name, func(t *testing.T) { 55 sv := &argparser.OptionalServiceVersion{} 56 57 if !c.flagOmitted { 58 sv.OptionalString = argparser.OptionalString{ 59 Value: c.flagValue, 60 } 61 } 62 63 v, err := sv.Parse("123", mock.API{ 64 ListVersionsFn: listVersions, 65 }) 66 if err != nil { 67 if c.errExpected { 68 return 69 } 70 t.Fatalf("unexpected error: %v", err) 71 } 72 if err == nil { 73 if c.errExpected { 74 t.Fatalf("expected error, have %v", v) 75 } 76 } 77 78 want := c.wantVersion 79 have := fastly.ToValue(v.Number) 80 if have != want { 81 t.Errorf("wanted %d, have %d", want, have) 82 } 83 }) 84 } 85 } 86 87 // listVersions returns a list of service versions in different states. 88 // 89 // The first element is active, the second is locked, the third is editable. 90 func listVersions(i *fastly.ListVersionsInput) ([]*fastly.Version, error) { 91 return []*fastly.Version{ 92 { 93 ServiceID: fastly.ToPointer(i.ServiceID), 94 Number: fastly.ToPointer(1), 95 Active: fastly.ToPointer(true), 96 UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z"), 97 }, 98 { 99 ServiceID: fastly.ToPointer(i.ServiceID), 100 Number: fastly.ToPointer(2), 101 Active: fastly.ToPointer(false), 102 Locked: fastly.ToPointer(true), 103 UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z"), 104 }, 105 { 106 ServiceID: fastly.ToPointer(i.ServiceID), 107 Number: fastly.ToPointer(3), 108 Active: fastly.ToPointer(false), 109 UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-03T01:00:00Z"), 110 }, 111 }, nil 112 } 113 114 func TestGetLatestActiveVersion(t *testing.T) { 115 for _, testcase := range []struct { 116 name string 117 inputVersions []*fastly.Version 118 wantVersion int 119 wantError string 120 }{ 121 { 122 name: "active", 123 inputVersions: []*fastly.Version{ 124 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 125 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z")}, 126 }, 127 wantVersion: 2, 128 }, 129 { 130 name: "draft", 131 inputVersions: []*fastly.Version{ 132 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 133 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z")}, 134 }, 135 wantVersion: 1, 136 }, 137 { 138 name: "locked", 139 inputVersions: []*fastly.Version{ 140 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 141 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(false), Locked: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z")}, 142 }, 143 wantVersion: 1, 144 }, 145 { 146 name: "no active", 147 inputVersions: []*fastly.Version{ 148 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 149 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z")}, 150 {Number: fastly.ToPointer(3), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-03T01:00:00Z")}, 151 }, 152 wantError: "no active service version found", 153 }, 154 } { 155 t.Run(testcase.name, func(t *testing.T) { 156 // NOTE: this is a duplicate of the sorting algorithm in 157 // cmd/command.go to make the test as realistic as possible 158 sort.Slice(testcase.inputVersions, func(i, j int) bool { 159 return fastly.ToValue(testcase.inputVersions[i].Number) > fastly.ToValue(testcase.inputVersions[j].Number) 160 }) 161 162 v, err := argparser.GetActiveVersion(testcase.inputVersions) 163 if err != nil { 164 if testcase.wantError != "" { 165 testutil.AssertString(t, testcase.wantError, err.Error()) 166 } else { 167 t.Errorf("unexpected error returned: %v", err) 168 } 169 } else if fastly.ToValue(v.Number) != testcase.wantVersion { 170 t.Errorf("wanted version %d, got %d", testcase.wantVersion, v.Number) 171 } 172 }) 173 } 174 } 175 176 func TestGetSpecifiedVersion(t *testing.T) { 177 for _, testcase := range []struct { 178 name string 179 inputVersions []*fastly.Version 180 wantVersion int 181 wantError string 182 }{ 183 { 184 name: "success", 185 inputVersions: []*fastly.Version{ 186 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(false), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 187 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-02T01:00:00Z")}, 188 }, 189 wantVersion: 1, 190 }, 191 { 192 name: "no version available", 193 inputVersions: []*fastly.Version{ 194 {Number: fastly.ToPointer(1), Active: fastly.ToPointer(false), Locked: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-01-01T01:00:00Z")}, 195 {Number: fastly.ToPointer(2), Active: fastly.ToPointer(false), Locked: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-02-02T01:00:00Z")}, 196 {Number: fastly.ToPointer(3), Active: fastly.ToPointer(true), UpdatedAt: testutil.MustParseTimeRFC3339("2000-03-03T01:00:00Z")}, 197 }, 198 wantVersion: 4, 199 wantError: "specified service version not found: 4", 200 }, 201 } { 202 t.Run(testcase.name, func(t *testing.T) { 203 // NOTE: this is a duplicate of the sorting algorithm in 204 // cmd/command.go to make the test as realistic as possible 205 sort.Slice(testcase.inputVersions, func(i, j int) bool { 206 return fastly.ToValue(testcase.inputVersions[i].Number) > fastly.ToValue(testcase.inputVersions[j].Number) 207 }) 208 209 v, err := argparser.GetSpecifiedVersion(testcase.inputVersions, strconv.Itoa(testcase.wantVersion)) 210 if err != nil { 211 if testcase.wantError != "" { 212 testutil.AssertString(t, testcase.wantError, err.Error()) 213 } else { 214 t.Errorf("unexpected error returned: %v", err) 215 } 216 } else if fastly.ToValue(v.Number) != testcase.wantVersion { 217 t.Errorf("wanted version %d, got %d", testcase.wantVersion, v.Number) 218 } 219 }) 220 } 221 } 222 223 func TestOptionalAutoCloneParse(t *testing.T) { 224 cases := map[string]struct { 225 version *fastly.Version 226 flagOmitted bool 227 wantVersion int 228 errExpected bool 229 expectEditable bool 230 }{ 231 "version is editable": { 232 version: &fastly.Version{ 233 Number: fastly.ToPointer(1), 234 }, 235 wantVersion: 1, 236 expectEditable: true, 237 }, 238 "version is locked": { 239 version: &fastly.Version{ 240 Number: fastly.ToPointer(1), 241 Locked: fastly.ToPointer(true), 242 }, 243 wantVersion: 2, 244 }, 245 "version is active": { 246 version: &fastly.Version{ 247 Number: fastly.ToPointer(1), 248 Active: fastly.ToPointer(true), 249 }, 250 wantVersion: 2, 251 }, 252 "version is locked but flag omitted": { 253 version: &fastly.Version{ 254 Number: fastly.ToPointer(1), 255 Locked: fastly.ToPointer(true), 256 }, 257 flagOmitted: true, 258 errExpected: true, 259 }, 260 "version is active but flag omitted": { 261 version: &fastly.Version{ 262 Number: fastly.ToPointer(1), 263 Active: fastly.ToPointer(true), 264 }, 265 flagOmitted: true, 266 errExpected: true, 267 }, 268 } 269 270 for name, c := range cases { 271 t.Run(name, func(t *testing.T) { 272 var ( 273 acv *argparser.OptionalAutoClone 274 bs []byte 275 ) 276 buf := bytes.NewBuffer(bs) 277 278 if c.flagOmitted { 279 acv = &argparser.OptionalAutoClone{} 280 } else { 281 acv = &argparser.OptionalAutoClone{ 282 OptionalBool: argparser.OptionalBool{ 283 Value: true, 284 }, 285 } 286 } 287 288 verboseMode := true 289 v, err := acv.Parse(c.version, "123", verboseMode, buf, mock.API{ 290 CloneVersionFn: cloneVersionResult(fastly.ToValue(c.version.Number) + 1), 291 }) 292 if err != nil { 293 if c.errExpected && errMatches(fastly.ToValue(c.version.Number), err) { 294 return 295 } 296 t.Fatalf("unexpected error: %v", err) 297 } 298 if err == nil { 299 if c.errExpected { 300 t.Fatalf("expected error, have %v", v) 301 } 302 } 303 304 want := c.wantVersion 305 have := fastly.ToValue(v.Number) 306 if have != want { 307 t.Errorf("wanted %d, have %d", want, have) 308 } 309 310 if !c.expectEditable { 311 want := fmt.Sprintf("Service version %d is not editable, so it was automatically cloned because --autoclone is enabled. Now operating on version %d.", fastly.ToValue(c.version.Number), fastly.ToValue(v.Number)) 312 have := strings.Trim(strings.ReplaceAll(buf.String(), "\n", " "), " ") 313 if !strings.Contains(have, want) { 314 t.Errorf("wanted %s, have %s", want, have) 315 } 316 } 317 }) 318 } 319 } 320 321 // cloneVersionResult returns a function which returns a specific cloned version. 322 func cloneVersionResult(version int) func(i *fastly.CloneVersionInput) (*fastly.Version, error) { 323 return func(i *fastly.CloneVersionInput) (*fastly.Version, error) { 324 return &fastly.Version{ 325 ServiceID: fastly.ToPointer(i.ServiceID), 326 Number: fastly.ToPointer(version), 327 }, nil 328 } 329 } 330 331 // errMatches validates that the error message is what we expect when given a 332 // service version that is either locked or active, while also not providing 333 // the --autoclone flag. 334 func errMatches(version int, err error) bool { 335 return err.Error() == fmt.Sprintf("service version %d is not editable", version) 336 }