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  }