github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/test/integration/publish_int_test.go (about)

     1  package integration
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"regexp"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/google/uuid"
    11  	"gopkg.in/yaml.v2"
    12  
    13  	"github.com/ActiveState/cli/internal/constants"
    14  	"github.com/ActiveState/cli/internal/fileutils"
    15  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    16  	"github.com/ActiveState/cli/internal/strutils"
    17  	"github.com/ActiveState/cli/internal/testhelpers/e2e"
    18  	"github.com/ActiveState/cli/internal/testhelpers/suite"
    19  	"github.com/ActiveState/cli/internal/testhelpers/tagsuite"
    20  	"github.com/ActiveState/cli/pkg/platform/api/graphql/request"
    21  )
    22  
    23  var editorFileRx = regexp.MustCompile(`file:\s*?(.*?)\.\s`)
    24  
    25  type PublishIntegrationTestSuite struct {
    26  	tagsuite.Suite
    27  }
    28  
    29  func (suite *PublishIntegrationTestSuite) TestPublish() {
    30  	suite.OnlyRunForTags(tagsuite.Publish)
    31  
    32  	// For development convenience, should not be committed without commenting out..
    33  	// os.Setenv(constants.APIHostEnvVarName, "pr13375.activestate.build")
    34  
    35  	type input struct {
    36  		args          []string
    37  		metafile      *string
    38  		editorValue   *string
    39  		confirmUpload bool
    40  	}
    41  
    42  	type expect struct {
    43  		confirmPrompt    []string
    44  		immediateOutput  string
    45  		exitBeforePrompt bool
    46  		exitCode         int
    47  		parseMeta        bool
    48  	}
    49  
    50  	type invocation struct {
    51  		input  input
    52  		expect expect
    53  	}
    54  
    55  	tempFile := fileutils.TempFilePathUnsafe("", "*.zip")
    56  	defer os.Remove(tempFile)
    57  
    58  	tempFileInvalid := fileutils.TempFilePathUnsafe("", "*.notzip")
    59  	defer os.Remove(tempFileInvalid)
    60  
    61  	ts := e2e.New(suite.T(), false)
    62  	defer ts.Close()
    63  
    64  	if apiHost := os.Getenv(constants.APIHostEnvVarName); apiHost != "" {
    65  		ts.Env = append(ts.Env, constants.APIHostEnvVarName+"="+apiHost)
    66  	}
    67  
    68  	ts.LoginAsPersistentUser()
    69  
    70  	namespaceUUID, err := uuid.NewRandom()
    71  	suite.Require().NoError(err, "unable generate new random UUID")
    72  	namespace := "private/ActiveState-CLI-Testing/" + namespaceUUID.String()
    73  
    74  	tests := []struct {
    75  		name                string
    76  		ingredientName      string
    77  		ingredientNamespace string
    78  		ingredientVersion   string
    79  		invocations         []invocation
    80  	}{
    81  		{
    82  			"New ingredient with file arg and flags",
    83  			"im-a-name-test1",
    84  			namespace,
    85  			"2.3.4",
    86  			[]invocation{
    87  				{
    88  					input{
    89  						[]string{
    90  							tempFile,
    91  							"--name", "{{.Name}}",
    92  							"--namespace", "{{.Namespace}}",
    93  							"--version", "2.3.4",
    94  							"--description", "im-a-description",
    95  							"--author", "author-name <author-email@domain.tld>",
    96  							"--depend", "language:python@>=3",
    97  							"--depend", "builder:python-module-builder@>=0",
    98  							"--depend-test", "language:python@>=3",
    99  							"--depend-build", "language:python@>=3",
   100  							"--depend-runtime", "language:python@>=3",
   101  						},
   102  						nil,
   103  						nil,
   104  						true,
   105  					},
   106  					expect{
   107  						[]string{
   108  							`name: {{.Name}}`,
   109  							`namespace: {{.Namespace}}`,
   110  							`version: 2.3.4`,
   111  							`description: im-a-description`,
   112  							`name: author-name`,
   113  							`email: author-email@domain.tld`,
   114  							`publish this ingredient?`,
   115  						},
   116  						"",
   117  						false,
   118  						0,
   119  						false,
   120  					},
   121  				},
   122  			},
   123  		},
   124  		{
   125  			"New ingredient with invalid filename",
   126  			"",
   127  			"",
   128  			"",
   129  			[]invocation{{input{
   130  				[]string{tempFileInvalid},
   131  				nil,
   132  				nil,
   133  				true,
   134  			},
   135  				expect{
   136  					[]string{},
   137  					"Expected file extension to be either",
   138  					true,
   139  					1,
   140  					true,
   141  				},
   142  			},
   143  			},
   144  		},
   145  		{
   146  			"New ingredient with meta file",
   147  			"im-a-name-test2",
   148  			namespace,
   149  			"2.3.4",
   150  			[]invocation{{
   151  				input{
   152  					[]string{"--meta", "{{.MetaFile}}", tempFile},
   153  					ptr.To(`
   154  name: {{.Name}}
   155  namespace: {{.Namespace}}
   156  version: 2.3.4
   157  description: im-a-description
   158  authors:
   159    - name: author-name
   160      email: author-email@domain.tld
   161  `),
   162  					nil,
   163  					true,
   164  				},
   165  				expect{
   166  					[]string{
   167  						`name: {{.Name}}`,
   168  						`namespace: {{.Namespace}}`,
   169  						`version: 2.3.4`,
   170  						`description: im-a-description`,
   171  						`name: author-name`,
   172  						`email: author-email@domain.tld`,
   173  						`publish this ingredient?`,
   174  					},
   175  					"",
   176  					false,
   177  					0,
   178  					true,
   179  				},
   180  			}},
   181  		},
   182  		{
   183  			"New ingredient with meta file and flags",
   184  			"im-a-name-from-flag",
   185  			namespace,
   186  			"2.3.4",
   187  			[]invocation{{
   188  				input{
   189  					[]string{"--meta", "{{.MetaFile}}", tempFile, "--name", "{{.Name}}", "--author", "author-name-from-flag <author-email-from-flag@domain.tld>"},
   190  					ptr.To(`
   191  name: {{.Name}}
   192  namespace: {{.Namespace}}
   193  version: 2.3.4
   194  description: im-a-description
   195  authors:
   196    - name: author-name
   197      email: author-email@domain.tld
   198  `),
   199  					nil,
   200  					true,
   201  				},
   202  				expect{
   203  					[]string{
   204  						`name: {{.Name}}`,
   205  						`namespace: {{.Namespace}}`,
   206  						`version: 2.3.4`,
   207  						`description: im-a-description`,
   208  						`name: author-name-from-flag`,
   209  						`email: author-email-from-flag@domain.tld`,
   210  						`publish this ingredient?`,
   211  					},
   212  					"",
   213  					false,
   214  					0,
   215  					true,
   216  				},
   217  			}},
   218  		},
   219  		{
   220  			"New ingredient with editor flag",
   221  			"im-a-name-test3",
   222  			namespace,
   223  			"2.3.4",
   224  			[]invocation{{
   225  				input{
   226  					[]string{tempFile, "--editor"},
   227  					nil,
   228  					ptr.To(`
   229  name: {{.Name}}
   230  namespace: {{.Namespace}}
   231  version: 2.3.4
   232  description: im-a-description
   233  authors:
   234    - name: author-name
   235      email: author-email@domain.tld
   236  `),
   237  					true,
   238  				},
   239  				expect{
   240  					[]string{
   241  						`name: {{.Name}}`,
   242  						`namespace: {{.Namespace}}`,
   243  						`version: 2.3.4`,
   244  						`description: im-a-description`,
   245  						`name: author-name`,
   246  						`email: author-email@domain.tld`,
   247  						`publish this ingredient?`,
   248  					},
   249  					"",
   250  					false,
   251  					0,
   252  					true,
   253  				},
   254  			}},
   255  		},
   256  		{
   257  			"Cancel Publish",
   258  			"bogus",
   259  			namespace,
   260  			"2.3.4",
   261  			[]invocation{{
   262  				input{
   263  					[]string{tempFile, "--name", "{{.Name}}", "--namespace", "{{.Namespace}}"},
   264  					nil,
   265  					nil,
   266  					false,
   267  				},
   268  				expect{
   269  					[]string{`name: {{.Name}}`},
   270  					"",
   271  					false,
   272  					0,
   273  					true,
   274  				},
   275  			}},
   276  		},
   277  		{
   278  			"Edit ingredient without file arg and with flags",
   279  			"editable",
   280  			namespace,
   281  			"1.0.1",
   282  			[]invocation{
   283  				{ // Create ingredient
   284  					input{
   285  						[]string{tempFile,
   286  							"--name", "{{.Name}}",
   287  							"--namespace", "{{.Namespace}}",
   288  							"--version", "1.0.0",
   289  						},
   290  						nil,
   291  						nil,
   292  						true,
   293  					},
   294  					expect{
   295  						[]string{
   296  							`name: {{.Name}}`,
   297  							`publish this ingredient?`,
   298  						},
   299  						"",
   300  						false,
   301  						0,
   302  						true,
   303  					},
   304  				},
   305  				{ // Edit ingredient
   306  					input{
   307  						[]string{
   308  							tempFile,
   309  							"--edit",
   310  							"--name", "{{.Name}}",
   311  							"--namespace", "{{.Namespace}}",
   312  							"--version", "1.0.1",
   313  							"--author", "author-name-edited <author-email-edited@domain.tld>",
   314  						},
   315  						nil,
   316  						nil,
   317  						true,
   318  					},
   319  					expect{
   320  						[]string{
   321  							`name: {{.Name}}`,
   322  							`namespace: {{.Namespace}}`,
   323  							`version: 1.0.1`,
   324  							`name: author-name-edited`,
   325  							`email: author-email-edited@domain.tld`,
   326  							`publish this ingredient?`,
   327  						},
   328  						"",
   329  						false,
   330  						0,
   331  						true,
   332  					},
   333  				},
   334  				{ // description editing not supported
   335  					input{
   336  						[]string{
   337  							"--edit",
   338  							"--name", "{{.Name}}",
   339  							"--description", "foo",
   340  						},
   341  						nil,
   342  						nil,
   343  						false,
   344  					},
   345  					expect{
   346  						[]string{
   347  							`Editing an ingredient description is not yet supported`,
   348  						},
   349  						"",
   350  						true,
   351  						1,
   352  						true,
   353  					},
   354  				},
   355  			},
   356  		},
   357  	}
   358  	for n, tt := range tests {
   359  		suite.Run(tt.name, func() {
   360  			templateVars := map[string]interface{}{
   361  				"Name":      tt.ingredientName,
   362  				"Namespace": tt.ingredientNamespace,
   363  			}
   364  
   365  			for _, inv := range tt.invocations {
   366  				suite.Run(fmt.Sprintf("%s invocation %d", tt.name, n), func() {
   367  					ts.T = suite.T() // This differs per subtest
   368  					if inv.input.metafile != nil {
   369  						inputMetaParsed, err := strutils.ParseTemplate(*inv.input.metafile, templateVars, nil)
   370  						suite.Require().NoError(err)
   371  						metafile, err := fileutils.WriteTempFile("metafile.yaml", []byte(inputMetaParsed))
   372  						suite.Require().NoError(err)
   373  						templateVars["MetaFile"] = metafile
   374  					}
   375  
   376  					args := make([]string, len(inv.input.args))
   377  					copy(args, inv.input.args)
   378  
   379  					for k, v := range args {
   380  						vp, err := strutils.ParseTemplate(v, templateVars, nil)
   381  						suite.Require().NoError(err)
   382  						args[k] = vp
   383  					}
   384  
   385  					cp := ts.SpawnWithOpts(
   386  						e2e.OptArgs(append([]string{"publish"}, args...)...),
   387  					)
   388  
   389  					if inv.expect.immediateOutput != "" {
   390  						cp.Expect(inv.expect.immediateOutput)
   391  					}
   392  
   393  					// Send custom input via --editor
   394  					if inv.input.editorValue != nil {
   395  						cp.Expect("Press enter when done editing")
   396  						snapshot := cp.Snapshot()
   397  						match := editorFileRx.FindSubmatch([]byte(snapshot))
   398  						if len(match) != 2 {
   399  							suite.Fail("Could not match rx in snapshot: %s", editorFileRx.String())
   400  						}
   401  						fpath := match[1]
   402  						inputEditorValue, err := strutils.ParseTemplate(*inv.input.editorValue, templateVars, nil)
   403  						suite.Require().NoError(err)
   404  						suite.Require().NoError(fileutils.WriteFile(string(fpath), []byte(inputEditorValue)))
   405  						time.Sleep(100 * time.Millisecond) // wait for disk write to happen
   406  						cp.SendLine("")
   407  					}
   408  
   409  					if inv.expect.exitBeforePrompt {
   410  						cp.ExpectExitCode(inv.expect.exitCode)
   411  						return
   412  					}
   413  
   414  					for _, value := range inv.expect.confirmPrompt {
   415  						v, err := strutils.ParseTemplate(value, templateVars, nil)
   416  						suite.Require().NoError(err)
   417  						cp.Expect(v)
   418  					}
   419  
   420  					cp.Expect("Y/n")
   421  
   422  					var (
   423  						name      = tt.ingredientName
   424  						namespace = tt.ingredientNamespace
   425  						version   = tt.ingredientVersion
   426  					)
   427  
   428  					if inv.expect.parseMeta {
   429  						snapshot := cp.Snapshot()
   430  						rx := regexp.MustCompile(`(?s)Prepared the following ingredient:(.*)Do you want to publish this ingredient\?`)
   431  						match := rx.FindSubmatch([]byte(snapshot))
   432  						suite.Require().NotNil(match, fmt.Sprintf("Could not match '%s' against: %s", rx.String(), snapshot))
   433  
   434  						meta := request.PublishVariables{}
   435  						err := yaml.Unmarshal(match[1], &meta)
   436  						if err == nil {
   437  							name = meta.Name
   438  							namespace = meta.Namespace
   439  							version = meta.Version
   440  						}
   441  					}
   442  
   443  					if inv.input.confirmUpload {
   444  						cp.SendLine("Y")
   445  					} else {
   446  						cp.SendLine("n")
   447  						cp.Expect("Publish cancelled")
   448  						return
   449  					}
   450  
   451  					cp.Expect("Successfully published")
   452  					cp.Expect("Name:")
   453  					cp.Expect(name)
   454  					cp.Expect("Namespace:")
   455  					cp.Expect(namespace)
   456  					cp.Expect("Version:")
   457  					cp.Expect(version)
   458  					cp.ExpectExitCode(inv.expect.exitCode)
   459  
   460  					cp = ts.Spawn("search", namespace+":"+name, "--ts=now")
   461  					cp.Expect(version)
   462  					time.Sleep(time.Second)
   463  					cp.Send("q")
   464  					cp.ExpectExitCode(0)
   465  				})
   466  			}
   467  		})
   468  	}
   469  
   470  	ts.IgnoreLogErrors() // ignore intentional failures like omitted filename, cannot edit description, etc.
   471  }
   472  
   473  func TestPublishIntegrationTestSuite(t *testing.T) {
   474  	suite.Run(t, new(PublishIntegrationTestSuite))
   475  }