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 }