diff --git a/internal/runbits/runtime/requirements/requirements.go b/internal/runbits/runtime/requirements/requirements.go index f7625b5e1d..e041052df6 100644 --- a/internal/runbits/runtime/requirements/requirements.go +++ b/internal/runbits/runtime/requirements/requirements.go @@ -608,12 +608,12 @@ func resolvePkgAndNamespace(prompt prompt.Prompter, packageName string, nsType m choices := []string{} values := map[string][]string{} for _, i := range ingredients { - language := model.LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) + language := model.LanguageFromNamespace(i.Namespace.Namespace) // Generate ingredient choices to present to the user - name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, language) + name := fmt.Sprintf("%s (%s)", i.Name, language) choices = append(choices, name) - values[name] = []string{*i.Ingredient.Name, language} + values[name] = []string{i.Name, language} } if len(choices) == 0 { @@ -644,7 +644,7 @@ func resolvePkgAndNamespace(prompt prompt.Prompter, packageName string, nsType m } func getSuggestions(ns model.Namespace, name string, auth *authentication.Auth) ([]string, error) { - results, err := model.SearchIngredients(ns.String(), name, false, nil, auth) + results, err := model.SearchIngredientsLatest(ns.String(), name, false, true, nil, auth) if err != nil { return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", name) } @@ -656,7 +656,7 @@ func getSuggestions(ns model.Namespace, name string, auth *authentication.Auth) suggestions := make([]string, 0, maxResults+1) for _, result := range results { - suggestions = append(suggestions, fmt.Sprintf(" - %s", *result.Ingredient.Name)) + suggestions = append(suggestions, fmt.Sprintf(" - %s", result.Name)) } return suggestions, nil diff --git a/internal/runners/packages/info.go b/internal/runners/packages/info.go index fb559b8ecc..cb0cbc88a8 100644 --- a/internal/runners/packages/info.go +++ b/internal/runners/packages/info.go @@ -13,12 +13,11 @@ import ( "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/rtutils/ptr" "github.com/ActiveState/cli/internal/runbits/commits_runbit" - "github.com/ActiveState/cli/pkg/platform/api/inventory/inventory_models" + hsInventoryModel "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory/model" "github.com/ActiveState/cli/pkg/platform/api/vulnerabilities/request" "github.com/ActiveState/cli/pkg/platform/authentication" "github.com/ActiveState/cli/pkg/platform/model" "github.com/ActiveState/cli/pkg/project" - "github.com/go-openapi/strfmt" ) // InfoRunParams tracks the info required for running Info. @@ -90,16 +89,18 @@ func (i *Info) Run(params InfoRunParams, nstype model.NamespaceType) error { } pkg := packages[0] - ingredientVersion := pkg.LatestVersion + ingredientVersion := pkg.Versions[0] // latest version if params.Package.Version != "" { - ingredientVersion, err = specificIngredientVersion(pkg.Ingredient.IngredientID, params.Package.Version, i.auth) - if err != nil { - return locale.WrapExternalError(err, "info_err_version_not_found", "Could not find version {{.V0}} for package {{.V1}}", params.Package.Version, params.Package.Name) + for _, v := range pkg.Versions { + if v.Version == params.Package.Version { + ingredientVersion = v + break + } } } - authors, err := model.FetchAuthors(pkg.Ingredient.IngredientID, ingredientVersion.IngredientVersionID, i.auth) + authors, err := model.FetchAuthors(&pkg.IngredientID, &ingredientVersion.IngredientVersionID, i.auth) if err != nil { return locale.WrapError(err, "package_err_cannot_obtain_authors_info", "Cannot obtain authors info") } @@ -109,8 +110,8 @@ func (i *Info) Run(params InfoRunParams, nstype model.NamespaceType) error { vulnerabilityIngredients := make([]*request.Ingredient, len(pkg.Versions)) for i, p := range pkg.Versions { vulnerabilityIngredients[i] = &request.Ingredient{ - Name: *pkg.Ingredient.Name, - Namespace: *pkg.Ingredient.PrimaryNamespace, + Name: pkg.Name, + Namespace: pkg.Namespace.Namespace, Version: p.Version, } } @@ -122,8 +123,8 @@ func (i *Info) Run(params InfoRunParams, nstype model.NamespaceType) error { } i.out.Print(&infoOutput{i.out, structuredOutput{ - pkg.Ingredient, - ingredientVersion, + pkg.SearchIngredient, + &ingredientVersion, authors, pkg.Versions, vulns, @@ -132,21 +133,6 @@ func (i *Info) Run(params InfoRunParams, nstype model.NamespaceType) error { return nil } -func specificIngredientVersion(ingredientID *strfmt.UUID, version string, auth *authentication.Auth) (*inventory_models.IngredientVersion, error) { - ingredientVersions, err := model.FetchIngredientVersions(ingredientID, auth) - if err != nil { - return nil, locale.WrapError(err, "info_err_cannot_obtain_version", "Could not retrieve ingredient version information") - } - - for _, iv := range ingredientVersions { - if iv.Version != nil && *iv.Version == version { - return iv, nil - } - } - - return nil, locale.NewInputError("err_no_ingredient_version_found", "No ingredient version found") -} - // PkgDetailsTable describes package details. type PkgDetailsTable struct { Description string `opts:"omitEmpty" locale:"package_description,[HEADING]Description[/RESET]" json:"description"` @@ -171,25 +157,13 @@ func newInfoResult(so structuredOutput) *infoResult { PkgDetailsTable: &PkgDetailsTable{}, } - if so.Ingredient.Name != nil { - res.name = *so.Ingredient.Name - } - - if so.IngredientVersion.Version != nil { - res.version = *so.IngredientVersion.Version - } - - if so.Ingredient.Description != nil { - res.PkgDetailsTable.Description = *so.Ingredient.Description - } - - if so.Ingredient.Website != "" { - res.PkgDetailsTable.Website = so.Ingredient.Website.String() - } - - if so.IngredientVersion.LicenseExpression != nil { - res.PkgDetailsTable.License = fmt.Sprintf("[CYAN]%s[/RESET]", *so.IngredientVersion.LicenseExpression) + res.name = so.Ingredient.Name + res.version = so.IngredientVersion.Version + res.PkgDetailsTable.Description = so.Ingredient.Description + if so.Ingredient.Website != nil { + res.PkgDetailsTable.Website = *so.Ingredient.Website } + res.PkgDetailsTable.License = fmt.Sprintf("[CYAN]%s[/RESET]", so.IngredientVersion.LicenseExpression) for _, version := range so.Versions { res.plainVersions = append(res.plainVersions, version.Version) @@ -286,11 +260,11 @@ func newInfoResult(so structuredOutput) *infoResult { } type structuredOutput struct { - Ingredient *inventory_models.Ingredient `json:"ingredient"` - IngredientVersion *inventory_models.IngredientVersion `json:"ingredient_version"` - Authors model.Authors `json:"authors"` - Versions []*inventory_models.SearchIngredientsResponseVersion `json:"versions"` - Vulnerabilities []*model.VulnerabilityIngredient `json:"vulnerabilities,omitempty"` + Ingredient *hsInventoryModel.SearchIngredient `json:"ingredient"` + IngredientVersion *hsInventoryModel.IngredientVersion `json:"ingredient_version"` + Authors model.Authors `json:"authors"` + Versions []hsInventoryModel.IngredientVersion `json:"versions"` + Vulnerabilities []*model.VulnerabilityIngredient `json:"vulnerabilities,omitempty"` } type infoOutput struct { diff --git a/internal/runners/packages/search.go b/internal/runners/packages/search.go index ed6ad2e4f0..fde109fe23 100644 --- a/internal/runners/packages/search.go +++ b/internal/runners/packages/search.go @@ -68,7 +68,7 @@ func (s *Search) Run(params SearchRunParams, nstype model.NamespaceType) error { if params.ExactTerm { packages, err = model.SearchIngredientsLatestStrict(ns.String(), params.Ingredient.Name, true, true, &ts, s.auth) } else { - packages, err = model.SearchIngredientsLatest(ns.String(), params.Ingredient.Name, true, &ts, s.auth) + packages, err = model.SearchIngredientsLatest(ns.String(), params.Ingredient.Name, true, false, &ts, s.auth) } if err != nil { return locale.WrapError(err, "package_err_cannot_obtain_search_results") @@ -142,8 +142,8 @@ func (s *Search) getVulns(packages []*model.IngredientAndVersion) ([]*model.Vuln var ingredients []*request.Ingredient for _, pkg := range packages { ingredients = append(ingredients, &request.Ingredient{ - Name: *pkg.Ingredient.Name, - Namespace: *pkg.Ingredient.PrimaryNamespace, + Name: pkg.Name, + Namespace: pkg.Namespace.Namespace, Version: pkg.Version, }) } diff --git a/internal/runners/packages/searchresult.go b/internal/runners/packages/searchresult.go index c60366409f..56a4e8b4cd 100644 --- a/internal/runners/packages/searchresult.go +++ b/internal/runners/packages/searchresult.go @@ -28,10 +28,10 @@ func createSearchResults(packages []*model.IngredientAndVersion, vulns []*model. var packageNames []string for _, pkg := range packages { result := &searchResult{} - result.Name = ptr.From(pkg.Ingredient.Name, "") - result.Description = ptr.From(pkg.Ingredient.Description, "") - result.Website = pkg.Ingredient.Website.String() - result.License = ptr.From(pkg.LatestVersion.LicenseExpression, "") + result.Name = pkg.Name + result.Description = pkg.Description + result.Website = ptr.From(pkg.Website, "") + result.License = pkg.Versions[0].LicenseExpression // latest version var versions []string for _, v := range pkg.Versions { @@ -44,8 +44,8 @@ func createSearchResults(packages []*model.IngredientAndVersion, vulns []*model. var ingredientVulns *model.VulnerabilityIngredient for _, v := range vulns { - if strings.EqualFold(v.Name, *pkg.Ingredient.Name) && - strings.EqualFold(v.PrimaryNamespace, *pkg.Ingredient.PrimaryNamespace) && + if strings.EqualFold(v.Name, pkg.Name) && + strings.EqualFold(v.PrimaryNamespace, pkg.Namespace.Namespace) && strings.EqualFold(v.Version, pkg.Version) { ingredientVulns = v break @@ -56,7 +56,7 @@ func createSearchResults(packages []*model.IngredientAndVersion, vulns []*model. result.Vulnerabilities = ingredientVulns.Vulnerabilities.Count() } - packageNames = append(packageNames, *pkg.Ingredient.Name) + packageNames = append(packageNames, pkg.Name) results = append(results, result) } diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index d75084e25d..b831913fac 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -173,9 +173,25 @@ func (r *Runner) Run(params *Params) error { if err != nil && !errors.As(err, &errSearch404) { // 404 means either the ingredient or the namespace was not found, which is fine return locale.WrapError(err, "err_uploadingredient_search", "Could not search for ingredient") } + if len(ingredients) > 0 { - i := ingredients[0].LatestVersion - ingredient = &ParentIngredient{*i.IngredientID, *i.IngredientVersionID, *i.Version, i.Dependencies} + i := ingredients[0] + + // Attempt to find the ingredient's dependencies. + var dependencies []inventory_models.Dependency + ingredientVersions, err := model.FetchIngredientVersions(&i.IngredientID, r.auth) + if err != nil { + return locale.WrapError(err, "err_uploadingredient_fetch_versions", "Could not retrieve ingredient version information") + } + for _, iv := range ingredientVersions { + if iv.Version != nil && *iv.Version == i.Version { + dependencies = iv.Dependencies + break + } + } + + ingredientVersionID := i.Versions[0].IngredientVersionID // latest version + ingredient = &ParentIngredient{i.IngredientID, ingredientVersionID, i.Version, dependencies} if params.Version == "" { isRevision = true } diff --git a/pkg/platform/api/hasura_inventory/model/inventory.go b/pkg/platform/api/hasura_inventory/model/inventory.go index 689cc89ddf..265be023d8 100644 --- a/pkg/platform/api/hasura_inventory/model/inventory.go +++ b/pkg/platform/api/hasura_inventory/model/inventory.go @@ -11,3 +11,27 @@ type LastIngredientRevisionTime struct { type LatestRevisionResponse struct { RevisionTimes []LastIngredientRevisionTime `json:"last_ingredient_revision_time"` } + +type Namespace struct { + Namespace string `json:"namespace"` +} + +type IngredientVersion struct { + Version string `json:"version"` + IngredientVersionID strfmt.UUID `json:"ingredient_version_id"` + LicenseExpression string `json:"license_expression"` +} + +type SearchIngredient struct { + Name string `json:"name"` + NormalizedName string `json:"normalized_name"` + Namespace Namespace `json:"namespace"` + IngredientID strfmt.UUID `json:"ingredient_id"` + Description string `json:"description"` + Website *string `json:"website"` + Versions []IngredientVersion `json:"versions"` +} + +type SearchIngredientsResponse struct { + SearchIngredients []SearchIngredient `json:"search_ingredients"` +} diff --git a/pkg/platform/api/hasura_inventory/request/search_ingredients.go b/pkg/platform/api/hasura_inventory/request/search_ingredients.go new file mode 100644 index 0000000000..4b91c747f5 --- /dev/null +++ b/pkg/platform/api/hasura_inventory/request/search_ingredients.go @@ -0,0 +1,53 @@ +package request + +import ( + "fmt" + "strings" + "time" +) + +func SearchIngredients(namespaces []string, name string, exact bool, time time.Time, limit, offset int) *searchIngredients { + return &searchIngredients{map[string]interface{}{ + "namespaces": fmt.Sprintf("{%s}", strings.Join(namespaces, ",")), // API requires enclosure in {} + "name": name, + "exact": exact, + "time": time, + "limit": limit, + "offset": offset, + }} +} + +type searchIngredients struct { + vars map[string]interface{} +} + +func (s *searchIngredients) Query() string { + return ` +query ($namespaces: _non_empty_citext, $name: non_empty_citext, $exact: Boolean!, $time: timestamptz!, $limit: Int!, $offset: Int!) { + search_ingredients( + args: {namespaces: $namespaces, name_: $name, exact: $exact, timestamp_: $time, limit_: $limit, offset_: $offset} + ) { + name + normalized_name + namespace { + namespace + } + ingredient_id + description + website + versions(order_by:{sortable_version:desc}) { + version + ingredient_version_id + license_expression + } + } +}` +} + +func (s *searchIngredients) Vars() (map[string]interface{}, error) { + return s.vars, nil +} + +func (s *searchIngredients) SetOffset(offset int) { + s.vars["offset"] = offset +} diff --git a/pkg/platform/model/inventory.go b/pkg/platform/model/inventory.go index 5e976b9c68..ba54524d5b 100644 --- a/pkg/platform/model/inventory.go +++ b/pkg/platform/model/inventory.go @@ -17,6 +17,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" configMediator "github.com/ActiveState/cli/internal/mediators/config" + "github.com/ActiveState/cli/internal/profile" "github.com/ActiveState/cli/pkg/platform/api" hsInventory "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory" hsInventoryModel "github.com/ActiveState/cli/pkg/platform/api/hasura_inventory/model" @@ -48,9 +49,8 @@ func (e ErrNoMatchingPlatform) Error() string { type ErrSearch404 struct{ *locale.LocalizedError } -// IngredientAndVersion is a sane version of whatever the hell it is go-swagger thinks it's doing type IngredientAndVersion struct { - *inventory_models.SearchIngredientsResponseItem + *hsInventoryModel.SearchIngredient Version string } @@ -83,16 +83,10 @@ func GetIngredientByNameAndVersion(namespace string, name string, version string return response.Payload, nil } -// SearchIngredients will return all ingredients+ingredientVersions that fuzzily -// match the ingredient name. -func SearchIngredients(namespace string, name string, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { - return searchIngredientsNamespace(namespace, name, includeVersions, false, ts, auth) -} - // SearchIngredientsStrict will return all ingredients+ingredientVersions that // strictly match the ingredient name. func SearchIngredientsStrict(namespace string, name string, caseSensitive bool, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { - results, err := searchIngredientsNamespace(namespace, name, includeVersions, true, ts, auth) + results, err := searchIngredientsNamespace(namespace, name, includeVersions, true, false, ts, auth) if err != nil { return nil, err } @@ -104,9 +98,7 @@ func SearchIngredientsStrict(namespace string, name string, caseSensitive bool, ingredients := results[:0] for _, ing := range results { var ingName string - if ing.Ingredient.Name != nil { - ingName = *ing.Ingredient.Name - } + ingName = ing.Name if !caseSensitive { ingName = strings.ToLower(ingName) } @@ -121,8 +113,9 @@ func SearchIngredientsStrict(namespace string, name string, caseSensitive bool, // SearchIngredientsLatest will return all ingredients+ingredientVersions that // fuzzily match the ingredient name, but only the latest version of each // ingredient. -func SearchIngredientsLatest(namespace string, name string, includeVersions bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { - results, err := searchIngredientsNamespace(namespace, name, includeVersions, false, ts, auth) +// Returns an error if there are too many matches unless `partial` is true. +func SearchIngredientsLatest(namespace string, name string, includeVersions bool, partial bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { + results, err := searchIngredientsNamespace(namespace, name, includeVersions, false, partial, ts, auth) if err != nil { return nil, err } @@ -146,14 +139,11 @@ func processLatestIngredients(ingredients []*IngredientAndVersion) []*Ingredient seen := make(map[string]bool) var processedIngredients []*IngredientAndVersion for _, ing := range ingredients { - if ing.Ingredient.Name == nil { - continue - } - if seen[*ing.Ingredient.Name] { + if seen[ing.Name] { continue } processedIngredients = append(processedIngredients, ing) - seen[*ing.Ingredient.Name] = true + seen[ing.Name] = true } return processedIngredients } @@ -189,63 +179,49 @@ type ErrTooManyMatches struct { Query string } -func searchIngredientsNamespace(ns string, name string, includeVersions bool, exactOnly bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { - limit := int64(100) - offset := int64(0) +func searchIngredientsNamespace(ns string, name string, includeVersions bool, exactOnly bool, partial bool, ts *time.Time, auth *authentication.Auth) ([]*IngredientAndVersion, error) { + defer profile.Measure("searchIngredientsNamespace", time.Now()) + limit := 1000 + offset := 0 - client := inventory.Get(auth) - - params := inventory_operations.NewSearchIngredientsParams() - params.SetQ(&name) - if exactOnly { - params.SetExactOnly(&exactOnly) - } - if ns != "" { - params.SetNamespaces(&ns) + if ts == nil { + platformTime, err := FetchLatestTimeStamp(auth) + if err != nil { + return nil, errs.Wrap(err, "Unable to fetch latest platform timestamp") + } + ts = &platformTime } - params.SetLimit(&limit) - params.SetHTTPClient(api.NewHTTPClient()) - if ts != nil { - dt := strfmt.DateTime(*ts) - params.SetStateAt(&dt) - } + client := hsInventory.New(auth) + request := hsInventoryRequest.SearchIngredients([]string{ns}, name, exactOnly, *ts, limit, offset) var ingredients []*IngredientAndVersion - var entries []*inventory_models.SearchIngredientsResponseItem - for offset == 0 || len(entries) == int(limit) { - if offset > (limit * 10) { // at most we will get 10 pages of ingredients (that's ONE THOUSAND ingredients) + for { + response := hsInventoryModel.SearchIngredientsResponse{} + if offset > 0 { // Guard against queries that match TOO MANY ingredients return nil, &ErrTooManyMatches{locale.NewInputError("err_searchingredient_toomany", "", name), name} } - params.SetOffset(&offset) - results, err := client.SearchIngredients(params, auth.ClientAuth()) + request.SetOffset(offset) + err := client.Run(request, &response) if err != nil { - if sidErr, ok := err.(*inventory_operations.SearchIngredientsDefault); ok { - errv := locale.NewError(*sidErr.Payload.Message) - if sidErr.Code() == 404 { - return nil, &ErrSearch404{errv} - } - return nil, errv - } return nil, errs.Wrap(err, "SearchIngredients failed") } - entries = results.Payload.Ingredients - for _, res := range entries { - if res.Ingredient.PrimaryNamespace == nil { - continue // Shouldn't ever happen, but this at least guards around nil pointer panics - } + for _, res := range response.SearchIngredients { if includeVersions { for _, v := range res.Versions { - ingredients = append(ingredients, &IngredientAndVersion{res, v.Version}) + ingredients = append(ingredients, &IngredientAndVersion{&res, v.Version}) } } else { - ingredients = append(ingredients, &IngredientAndVersion{res, ""}) + ingredients = append(ingredients, &IngredientAndVersion{&res, res.Versions[0].Version}) } } + if len(response.SearchIngredients) < limit || partial { + break + } offset += limit } diff --git a/pkg/platform/model/vcs.go b/pkg/platform/model/vcs.go index 845b576a95..a6c75d822a 100644 --- a/pkg/platform/model/vcs.go +++ b/pkg/platform/model/vcs.go @@ -226,7 +226,7 @@ func FilterSupportedIngredients(supported []model.SupportedLanguage, ingredients var res []*IngredientAndVersion for _, i := range ingredients { - language := LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) + language := LanguageFromNamespace(i.Namespace.Namespace) for _, l := range supported { if l.Name != language { diff --git a/test/integration/package_int_test.go b/test/integration/package_int_test.go index d7243b1167..93dab1d641 100644 --- a/test/integration/package_int_test.go +++ b/test/integration/package_int_test.go @@ -151,7 +151,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_searchSimple() { suite.PrepareActiveStateYAML(ts) // Note that the expected strings might change due to inventory changes - cp := ts.Spawn("search", "requests") + cp := ts.Spawn("search", "requests2") expectations := []string{ "requests2", "2.16.0", @@ -207,8 +207,8 @@ func (suite *PackageIntegrationTestSuite) TestPackage_searchWithLang() { cp := ts.Spawn("search", "Moose", "--language=perl") cp.Expect("Name") cp.Expect("Moose") - cp.Expect("Moose-Autobox") - cp.Expect("MooseFS") + cp.Expect("IO-Moose") + cp.Expect("MooseX") cp.Send("q") cp.ExpectExitCode(0) } @@ -246,7 +246,7 @@ func (suite *PackageIntegrationTestSuite) TestPackage_searchWithBadLang() { suite.PrepareActiveStateYAML(ts) cp := ts.Spawn("search", "numpy", "--language=bad") - cp.Expect("Cannot obtain search") + cp.Expect("No packages in our catalog match") cp.ExpectExitCode(1) ts.IgnoreLogErrors() } @@ -279,8 +279,8 @@ func (suite *PackageIntegrationTestSuite) TestPackage_detached_operation() { suite.Run("install non-existing", func() { cp := ts.Spawn("install", "json") - cp.Expect("No results found for search term") - cp.Expect("json2") + cp.Expect(`No results found for search term "json". Did you mean:`) + cp.Expect("json") // suggestions include packages with json in the name cp.Wait() }) @@ -665,8 +665,11 @@ func (suite *PackageIntegrationTestSuite) TestCVE_NoPrompt() { cp := ts.Spawn("config", "set", constants.AsyncRuntimeConfig, "true") cp.ExpectExitCode(0) + // Note: this version has 2 known vulnerabilities, but since the number of indirect + // vulnerabilities is variable, we need to craft our expectations accordingly. cp = ts.Spawn("install", "urllib3@2.0.2") - cp.Expect("Warning: Dependency has 2 known vulnerabilities", e2e.RuntimeSourcingTimeoutOpt) + cp.Expect("Warning: Dependency has 2") + cp.Expect("known vulnerabilities") cp.ExpectExitCode(0) } @@ -688,8 +691,11 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Prompt() { cp = ts.Spawn("config", "set", constants.SecurityPromptConfig, "true") cp.ExpectExitCode(0) + // Note: this version has 2 known vulnerabilities, but since the number of indirect + // vulnerabilities is variable, we need to craft our expectations accordingly. cp = ts.Spawn("install", "urllib3@2.0.2") - cp.Expect("Warning: Dependency has 2 known vulnerabilities") + cp.Expect("Warning: Dependency has 2") + cp.Expect("known vulnerabilities") cp.Expect("Do you want to continue") cp.SendLine("y") cp.ExpectExitCode(0) @@ -711,7 +717,7 @@ func (suite *PackageIntegrationTestSuite) TestCVE_Indirect() { cp.ExpectExitCode(0) cp = ts.Spawn("install", "private/ActiveState-CLI-Testing/language/python/django_dep", "--ts=now") - cp.ExpectRe(`Warning: Dependency has \d indirect known vulnerabilities`) + cp.ExpectRe(`Warning: Dependency has \d+ indirect known vulnerabilities`) cp.Expect("Do you want to continue") cp.SendLine("n") cp.ExpectExitCode(1)