github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/suggest/maven_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package suggest
    16  
    17  import (
    18  	"reflect"
    19  	"sort"
    20  	"testing"
    21  
    22  	"deps.dev/util/maven"
    23  	"deps.dev/util/resolve"
    24  	"deps.dev/util/resolve/dep"
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    27  	mavenmanifest "github.com/google/osv-scalibr/guidedremediation/internal/manifest/maven"
    28  	"github.com/google/osv-scalibr/guidedremediation/options"
    29  	"github.com/google/osv-scalibr/guidedremediation/result"
    30  	"github.com/google/osv-scalibr/guidedremediation/upgrade"
    31  )
    32  
    33  var (
    34  	depMgmt           = depTypeWithOrigin("management")
    35  	depParent         = depTypeWithOrigin("parent")
    36  	depPlugin         = depTypeWithOrigin("plugin@org.plugin:plugin")
    37  	depProfileOne     = depTypeWithOrigin("profile@profile-one")
    38  	depProfileTwoMgmt = depTypeWithOrigin("profile@profile-two@management")
    39  )
    40  
    41  func depTypeWithOrigin(origin string) dep.Type {
    42  	var result dep.Type
    43  	result.AddAttr(dep.MavenDependencyOrigin, origin)
    44  
    45  	return result
    46  }
    47  
    48  func mavenReqKey(t *testing.T, name, artifactType, classifier string) manifest.RequirementKey {
    49  	t.Helper()
    50  	var typ dep.Type
    51  	if artifactType != "" {
    52  		typ.AddAttr(dep.MavenArtifactType, artifactType)
    53  	}
    54  	if classifier != "" {
    55  		typ.AddAttr(dep.MavenClassifier, classifier)
    56  	}
    57  
    58  	return mavenmanifest.MakeRequirementKey(resolve.RequirementVersion{
    59  		VersionKey: resolve.VersionKey{
    60  			PackageKey: resolve.PackageKey{
    61  				Name:   name,
    62  				System: resolve.Maven,
    63  			},
    64  		},
    65  		Type: typ,
    66  	})
    67  }
    68  
    69  type testManifest struct {
    70  	filePath          string
    71  	root              resolve.Version
    72  	system            resolve.System
    73  	requirements      []resolve.RequirementVersion
    74  	groups            map[manifest.RequirementKey][]string
    75  	ecosystemSpecific mavenmanifest.ManifestSpecific
    76  }
    77  
    78  // FilePath returns the path to the manifest file.
    79  func (m testManifest) FilePath() string {
    80  	return m.filePath
    81  }
    82  
    83  // Root returns the Version representing this package.
    84  func (m testManifest) Root() resolve.Version {
    85  	return m.root
    86  }
    87  
    88  // System returns the ecosystem of this manifest.
    89  func (m testManifest) System() resolve.System {
    90  	return m.system
    91  }
    92  
    93  // Requirements returns all direct requirements (including dev).
    94  func (m testManifest) Requirements() []resolve.RequirementVersion {
    95  	return m.requirements
    96  }
    97  
    98  // Groups returns the dependency groups that the direct requirements belong to.
    99  func (m testManifest) Groups() map[manifest.RequirementKey][]string {
   100  	return m.groups
   101  }
   102  
   103  // LocalManifests returns Manifests of any local packages.
   104  func (m testManifest) LocalManifests() []manifest.Manifest {
   105  	return nil
   106  }
   107  
   108  // EcosystemSpecific returns any ecosystem-specific information for this manifest.
   109  func (m testManifest) EcosystemSpecific() any {
   110  	return m.ecosystemSpecific
   111  }
   112  
   113  // EcosystemSpecific returns any ecosystem-specific information for this manifest.
   114  func (m testManifest) PatchRequirement(req resolve.RequirementVersion) error {
   115  	return nil
   116  }
   117  
   118  // EcosystemSpecific returns any ecosystem-specific information for this manifest.
   119  func (m testManifest) Clone() manifest.Manifest {
   120  	return nil
   121  }
   122  
   123  func TestMavenSuggester_Suggest(t *testing.T) {
   124  	ctx := t.Context()
   125  	client := resolve.NewLocalClient()
   126  	addVersions := func(sys resolve.System, name string, versions []string) {
   127  		for _, version := range versions {
   128  			client.AddVersion(resolve.Version{
   129  				VersionKey: resolve.VersionKey{
   130  					PackageKey: resolve.PackageKey{
   131  						System: sys,
   132  						Name:   name,
   133  					},
   134  					VersionType: resolve.Concrete,
   135  					Version:     version,
   136  				}}, nil)
   137  		}
   138  	}
   139  	addVersions(resolve.Maven, "com.mycompany.app:parent-pom", []string{"1.0.0"})
   140  	addVersions(resolve.Maven, "junit:junit", []string{"4.11", "4.12", "4.13", "4.13.2"})
   141  	addVersions(resolve.Maven, "org.example:abc", []string{"1.0.0", "1.0.1", "1.0.2"})
   142  	addVersions(resolve.Maven, "org.example:no-updates", []string{"9.9.9", "10.0.0"})
   143  	addVersions(resolve.Maven, "org.example:property", []string{"1.0.0", "1.0.1"})
   144  	addVersions(resolve.Maven, "org.example:same-property", []string{"1.0.0", "1.0.1"})
   145  	addVersions(resolve.Maven, "org.example:another-property", []string{"1.0.0", "1.1.0"})
   146  	addVersions(resolve.Maven, "org.example:property-no-update", []string{"1.9.0", "2.0.0"})
   147  	addVersions(resolve.Maven, "org.example:xyz", []string{"2.0.0", "2.0.1"})
   148  	addVersions(resolve.Maven, "org.profile:abc", []string{"1.2.3", "1.2.4"})
   149  	addVersions(resolve.Maven, "org.profile:def", []string{"2.3.4", "2.3.5"})
   150  	addVersions(resolve.Maven, "org.import:xyz", []string{"6.6.6", "6.7.0", "7.0.0"})
   151  	addVersions(resolve.Maven, "org.dep:plugin-dep", []string{"2.3.1", "2.3.2", "2.3.3", "2.3.4"})
   152  
   153  	suggester, err := NewSuggester(resolve.Maven)
   154  	if err != nil {
   155  		t.Fatalf("failed to get Maven suggester: %v", err)
   156  	}
   157  
   158  	depProfileTwoMgmt.AddAttr(dep.MavenArtifactType, "pom")
   159  	depProfileTwoMgmt.AddAttr(dep.Scope, "import")
   160  
   161  	mf := testManifest{
   162  		filePath: "pom.xml",
   163  		root: resolve.Version{
   164  			VersionKey: resolve.VersionKey{
   165  				PackageKey: resolve.PackageKey{
   166  					System: resolve.Maven,
   167  					Name:   "com.mycompany.app:my-app",
   168  				},
   169  				VersionType: resolve.Concrete,
   170  				Version:     "1.0.0",
   171  			},
   172  		},
   173  		requirements: []resolve.RequirementVersion{
   174  			{
   175  				// Test dependencies are not updated.
   176  				VersionKey: resolve.VersionKey{
   177  					PackageKey: resolve.PackageKey{
   178  						System: resolve.Maven,
   179  						Name:   "junit:junit",
   180  					},
   181  					VersionType: resolve.Requirement,
   182  					Version:     "4.12",
   183  				},
   184  				Type: dep.NewType(dep.Test),
   185  			},
   186  			{
   187  				VersionKey: resolve.VersionKey{
   188  					PackageKey: resolve.PackageKey{
   189  						System: resolve.Maven,
   190  						Name:   "org.example:abc",
   191  					},
   192  					VersionType: resolve.Requirement,
   193  					Version:     "1.0.1",
   194  				},
   195  			},
   196  			{
   197  				// A package is specified to disallow updates.
   198  				VersionKey: resolve.VersionKey{
   199  					PackageKey: resolve.PackageKey{
   200  						System: resolve.Maven,
   201  						Name:   "org.example:no-updates",
   202  					},
   203  					VersionType: resolve.Requirement,
   204  					Version:     "9.9.9",
   205  				},
   206  			},
   207  			{
   208  				// The universal property should be updated.
   209  				VersionKey: resolve.VersionKey{
   210  					PackageKey: resolve.PackageKey{
   211  						System: resolve.Maven,
   212  						Name:   "org.example:property",
   213  					},
   214  					VersionType: resolve.Requirement,
   215  					Version:     "1.0.0",
   216  				},
   217  			},
   218  			{
   219  				// Property cannot be updated, so update the dependency directly.
   220  				VersionKey: resolve.VersionKey{
   221  					PackageKey: resolve.PackageKey{
   222  						System: resolve.Maven,
   223  						Name:   "org.example:property-no-update",
   224  					},
   225  					VersionType: resolve.Requirement,
   226  					Version:     "1.9",
   227  				},
   228  			},
   229  			{
   230  				// The property is updated to the same value.
   231  				VersionKey: resolve.VersionKey{
   232  					PackageKey: resolve.PackageKey{
   233  						System: resolve.Maven,
   234  						Name:   "org.example:same-property",
   235  					},
   236  					VersionType: resolve.Requirement,
   237  					Version:     "1.0.0",
   238  				},
   239  			},
   240  			{
   241  				// Property needs to be updated to a different value,
   242  				// so update dependency directly.
   243  				VersionKey: resolve.VersionKey{
   244  					PackageKey: resolve.PackageKey{
   245  						System: resolve.Maven,
   246  						Name:   "org.example:another-property",
   247  					},
   248  					VersionType: resolve.Requirement,
   249  					Version:     "1.0.0",
   250  				},
   251  			},
   252  			{
   253  				VersionKey: resolve.VersionKey{
   254  					PackageKey: resolve.PackageKey{
   255  						System: resolve.Maven,
   256  						Name:   "org.example:xyz",
   257  					},
   258  					VersionType: resolve.Requirement,
   259  					Version:     "2.0.0",
   260  				},
   261  				Type: depMgmt,
   262  			},
   263  		},
   264  		groups: map[manifest.RequirementKey][]string{
   265  			mavenReqKey(t, "junit:junit", "", ""):    {"test"},
   266  			mavenReqKey(t, "org.import:xyz", "", ""): {"import"},
   267  		},
   268  		ecosystemSpecific: mavenmanifest.ManifestSpecific{
   269  			RequirementsForUpdates: []resolve.RequirementVersion{
   270  				{
   271  					VersionKey: resolve.VersionKey{
   272  						PackageKey: resolve.PackageKey{
   273  							System: resolve.Maven,
   274  							Name:   "com.mycompany.app:parent-pom",
   275  						},
   276  						VersionType: resolve.Requirement,
   277  						Version:     "1.0.0",
   278  					},
   279  					Type: depParent,
   280  				},
   281  				{
   282  					VersionKey: resolve.VersionKey{
   283  						PackageKey: resolve.PackageKey{
   284  							System: resolve.Maven,
   285  							Name:   "org.profile:abc",
   286  						},
   287  						VersionType: resolve.Requirement,
   288  						Version:     "1.2.3",
   289  					},
   290  					Type: depProfileOne,
   291  				},
   292  				{
   293  					VersionKey: resolve.VersionKey{
   294  						PackageKey: resolve.PackageKey{
   295  							System: resolve.Maven,
   296  							Name:   "org.profile:def",
   297  						},
   298  						VersionType: resolve.Requirement,
   299  						Version:     "2.3.4",
   300  					},
   301  					Type: depProfileOne,
   302  				},
   303  				{
   304  					// A package is specified to ignore major updates.
   305  					VersionKey: resolve.VersionKey{
   306  						PackageKey: resolve.PackageKey{
   307  							System: resolve.Maven,
   308  							Name:   "org.import:xyz",
   309  						},
   310  						VersionType: resolve.Requirement,
   311  						Version:     "6.6.6",
   312  					},
   313  					Type: depProfileTwoMgmt,
   314  				},
   315  				{
   316  					VersionKey: resolve.VersionKey{
   317  						PackageKey: resolve.PackageKey{
   318  							System: resolve.Maven,
   319  							Name:   "org.dep:plugin-dep",
   320  						},
   321  						VersionType: resolve.Requirement,
   322  						Version:     "2.3.3",
   323  					},
   324  					Type: depPlugin,
   325  				},
   326  			},
   327  			LocalRequirements: []mavenmanifest.DependencyWithOrigin{
   328  				{
   329  					Dependency: maven.Dependency{GroupID: "org.parent", ArtifactID: "parent-pom", Version: "1.2.0", Type: "pom"},
   330  					Origin:     "parent",
   331  				},
   332  				{
   333  					Dependency: maven.Dependency{GroupID: "junit", ArtifactID: "junit", Version: "${junit.version}", Scope: "test"},
   334  				},
   335  				{
   336  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "abc", Version: "1.0.1"},
   337  				},
   338  				{
   339  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-updates", Version: "9.9.9"},
   340  				},
   341  				{
   342  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version"},
   343  				},
   344  				{
   345  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "property", Version: "${property.version}"},
   346  				},
   347  				{
   348  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "property-no-update", Version: "1.${no.update.minor}"},
   349  				},
   350  				{
   351  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "same-property", Version: "${property.version}"},
   352  				},
   353  				{
   354  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "another-property", Version: "${property.version}"},
   355  				},
   356  				{
   357  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version", Version: "2.0.0"},
   358  					Origin:     "management",
   359  				},
   360  				{
   361  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "xyz", Version: "2.0.0"},
   362  					Origin:     "management",
   363  				},
   364  				{
   365  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "abc", Version: "1.2.3"},
   366  					Origin:     "profile@profile-one",
   367  				},
   368  				{
   369  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "def", Version: "${def.version}"},
   370  					Origin:     "profile@profile-one",
   371  				},
   372  				{
   373  					Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "xyz", Version: "6.6.6", Scope: "import", Type: "pom"},
   374  					Origin:     "profile@profile-two@management",
   375  				},
   376  				{
   377  					Dependency: maven.Dependency{GroupID: "org.dep", ArtifactID: "plugin-dep", Version: "2.3.3"},
   378  					Origin:     "plugin@org.plugin:plugin",
   379  				},
   380  			},
   381  		},
   382  	}
   383  
   384  	got, err := suggester.Suggest(ctx, mf, options.UpdateOptions{
   385  		ResolveClient: client,
   386  		IgnoreDev:     true, // Do no update test dependencies.
   387  		UpgradeConfig: upgrade.Config{
   388  			"org.example:no-updates": upgrade.None,
   389  			"org.import:xyz":         upgrade.Minor,
   390  		},
   391  	})
   392  	if err != nil {
   393  		t.Fatalf("failed to suggest Patch: %v", err)
   394  	}
   395  
   396  	want := result.Patch{
   397  		PackageUpdates: []result.PackageUpdate{
   398  			{
   399  				Name:        "org.dep:plugin-dep",
   400  				VersionFrom: "2.3.3",
   401  				VersionTo:   "2.3.4",
   402  				Type:        depPlugin,
   403  			},
   404  			{
   405  				Name:        "org.example:abc",
   406  				VersionFrom: "1.0.1",
   407  				VersionTo:   "1.0.2",
   408  			},
   409  			{
   410  				Name:        "org.example:another-property",
   411  				VersionFrom: "1.0.0",
   412  				VersionTo:   "1.1.0",
   413  			},
   414  			{
   415  				Name:        "org.example:property",
   416  				VersionFrom: "1.0.0",
   417  				VersionTo:   "1.0.1",
   418  			},
   419  			{
   420  				Name:        "org.example:property-no-update",
   421  				VersionFrom: "1.9",
   422  				VersionTo:   "2.0.0",
   423  			},
   424  			{
   425  				Name:        "org.example:same-property",
   426  				VersionFrom: "1.0.0",
   427  				VersionTo:   "1.0.1",
   428  			},
   429  			{
   430  				Name:        "org.example:xyz",
   431  				VersionFrom: "2.0.0",
   432  				VersionTo:   "2.0.1",
   433  				Type:        depMgmt,
   434  			},
   435  			{
   436  				Name:        "org.import:xyz",
   437  				VersionFrom: "6.6.6",
   438  				VersionTo:   "6.7.0",
   439  				Type:        depProfileTwoMgmt,
   440  			},
   441  			{
   442  				Name:        "org.profile:abc",
   443  				VersionFrom: "1.2.3",
   444  				VersionTo:   "1.2.4",
   445  				Type:        depProfileOne,
   446  			},
   447  			{
   448  				Name:        "org.profile:def",
   449  				VersionFrom: "2.3.4",
   450  				VersionTo:   "2.3.5",
   451  				Type:        depProfileOne,
   452  			},
   453  		},
   454  	}
   455  	sort.Slice(got.PackageUpdates, func(i, j int) bool {
   456  		return got.PackageUpdates[i].Name < got.PackageUpdates[j].Name
   457  	})
   458  	if diff := cmp.Diff(want, got); diff != "" {
   459  		t.Fatalf("Patch suggested does not match expected (-want +got): %s\n", diff)
   460  	}
   461  }
   462  
   463  func Test_suggestMavenVersion(t *testing.T) {
   464  	ctx := t.Context()
   465  	lc := resolve.NewLocalClient()
   466  
   467  	pk := resolve.PackageKey{
   468  		System: resolve.Maven,
   469  		Name:   "abc:xyz",
   470  	}
   471  	// Version 3.0.0-beta1 will be skipped as it is a prerelease version.
   472  	for _, version := range []string{"1.0.0", "1.0.1", "1.1.0", "1.2.3", "2.0.0", "2.2.2", "2.3.4", "3.0.0-beta1"} {
   473  		lc.AddVersion(resolve.Version{
   474  			VersionKey: resolve.VersionKey{
   475  				PackageKey:  pk,
   476  				VersionType: resolve.Concrete,
   477  				Version:     version,
   478  			}}, nil)
   479  	}
   480  
   481  	tests := []struct {
   482  		requirement string
   483  		level       upgrade.Level
   484  		want        string
   485  	}{
   486  		{"1.0.0", upgrade.Major, "2.3.4"},
   487  		// No major updates allowed
   488  		{"1.0.0", upgrade.Minor, "1.2.3"},
   489  		// Only allow patch updates
   490  		{"1.0.0", upgrade.Patch, "1.0.1"},
   491  		// Version range requirement is not outdated
   492  		{"[1.0.0,)", upgrade.Major, "[1.0.0,)"},
   493  		{"[2.0.0,2.3.4]", upgrade.Major, "[2.0.0,2.3.4]"},
   494  		// Version range requirement is outdated
   495  		{"[2.0.0,2.3.4)", upgrade.Major, "2.3.4"},
   496  		{"[2.0.0,2.2.2]", upgrade.Major, "2.3.4"},
   497  		// Version range requirement is outdated but latest version is a major update
   498  		{"[1.0.0,2.0.0)", upgrade.Major, "2.3.4"},
   499  		{"[1.0.0,2.0.0)", upgrade.Minor, "[1.0.0,2.0.0)"},
   500  	}
   501  	for _, tt := range tests {
   502  		vk := resolve.VersionKey{
   503  			PackageKey:  pk,
   504  			VersionType: resolve.Requirement,
   505  			Version:     tt.requirement,
   506  		}
   507  		want := resolve.RequirementVersion{
   508  			VersionKey: resolve.VersionKey{
   509  				PackageKey:  pk,
   510  				VersionType: resolve.Requirement,
   511  				Version:     tt.want,
   512  			},
   513  		}
   514  		got, err := suggestMavenVersion(ctx, lc, resolve.RequirementVersion{VersionKey: vk}, tt.level)
   515  		if err != nil {
   516  			t.Fatalf("fail to suggest a new version for %v: %v", vk, err)
   517  		}
   518  		if !reflect.DeepEqual(got, want) {
   519  			t.Errorf("suggestMavenVersion(%v, %v): got %s want %s", vk, tt.level, got, want)
   520  		}
   521  	}
   522  }
   523  
   524  func TestSuggestVersion_Guava(t *testing.T) {
   525  	ctx := t.Context()
   526  	lc := resolve.NewLocalClient()
   527  
   528  	pk := resolve.PackageKey{
   529  		System: resolve.Maven,
   530  		Name:   "com.google.guava:guava",
   531  	}
   532  	for _, version := range []string{"1.0.0", "1.0.1-android", "1.0.1-jre", "1.1.0-android", "1.1.0-jre", "2.0.0-android", "2.0.0-jre"} {
   533  		lc.AddVersion(resolve.Version{
   534  			VersionKey: resolve.VersionKey{
   535  				PackageKey:  pk,
   536  				VersionType: resolve.Concrete,
   537  				Version:     version,
   538  			}}, nil)
   539  	}
   540  
   541  	tests := []struct {
   542  		requirement string
   543  		level       upgrade.Level
   544  		want        string
   545  	}{
   546  		{"1.0.0", upgrade.Major, "2.0.0-jre"},
   547  		// Update to the version with the same flavour
   548  		{"1.0.1-jre", upgrade.Major, "2.0.0-jre"},
   549  		{"1.0.1-android", upgrade.Major, "2.0.0-android"},
   550  		{"1.0.1-jre", upgrade.Minor, "1.1.0-jre"},
   551  		{"1.0.1-android", upgrade.Minor, "1.1.0-android"},
   552  		// Version range requirement is not outdated
   553  		{"[1.0.0,)", upgrade.Major, "[1.0.0,)"},
   554  		// Version range requirement is outdated and the latest version is a major update
   555  		{"[1.0.0,2.0.0)", upgrade.Major, "2.0.0-jre"},
   556  		{"[1.0.0,2.0.0)", upgrade.Minor, "[1.0.0,2.0.0)"},
   557  	}
   558  	for _, tt := range tests {
   559  		vk := resolve.VersionKey{
   560  			PackageKey:  pk,
   561  			VersionType: resolve.Requirement,
   562  			Version:     tt.requirement,
   563  		}
   564  		want := resolve.RequirementVersion{
   565  			VersionKey: resolve.VersionKey{
   566  				PackageKey:  pk,
   567  				VersionType: resolve.Requirement,
   568  				Version:     tt.want,
   569  			},
   570  		}
   571  		got, err := suggestMavenVersion(ctx, lc, resolve.RequirementVersion{VersionKey: vk}, tt.level)
   572  		if err != nil {
   573  			t.Fatalf("fail to suggest a new version for %v: %v", vk, err)
   574  		}
   575  		if !reflect.DeepEqual(got, want) {
   576  			t.Errorf("suggestMavenVersion(%v, %v): got %s want %s", vk, tt.level, got, want)
   577  		}
   578  	}
   579  }
   580  
   581  func TestSuggestVersion_Commons(t *testing.T) {
   582  	ctx := t.Context()
   583  	lc := resolve.NewLocalClient()
   584  
   585  	pk := resolve.PackageKey{
   586  		System: resolve.Maven,
   587  		Name:   "commons-io:commons-io",
   588  	}
   589  	for _, version := range []string{"1.0.0", "1.0.1", "1.1.0", "2.0.0", "20010101.000000"} {
   590  		lc.AddVersion(resolve.Version{
   591  			VersionKey: resolve.VersionKey{
   592  				PackageKey:  pk,
   593  				VersionType: resolve.Concrete,
   594  				Version:     version,
   595  			}}, nil)
   596  	}
   597  
   598  	tests := []struct {
   599  		requirement string
   600  		level       upgrade.Level
   601  		want        string
   602  	}{
   603  		{"1.0.0", upgrade.Major, "2.0.0"},
   604  		// No major updates allowed
   605  		{"1.0.0", upgrade.Minor, "1.1.0"},
   606  		// Only allow patch updates
   607  		{"1.0.0", upgrade.Patch, "1.0.1"},
   608  		// Version range requirement is not outdated
   609  		{"[1.0.0,)", upgrade.Major, "[1.0.0,)"},
   610  		// Version range requirement is outdated and the latest version is a major update
   611  		{"[1.0.0,2.0.0)", upgrade.Major, "2.0.0"},
   612  		{"[1.0.0,2.0.0)", upgrade.Minor, "[1.0.0,2.0.0)"},
   613  	}
   614  	for _, tt := range tests {
   615  		vk := resolve.VersionKey{
   616  			PackageKey:  pk,
   617  			VersionType: resolve.Requirement,
   618  			Version:     tt.requirement,
   619  		}
   620  		want := resolve.RequirementVersion{
   621  			VersionKey: resolve.VersionKey{
   622  				PackageKey:  pk,
   623  				VersionType: resolve.Requirement,
   624  				Version:     tt.want,
   625  			},
   626  		}
   627  		got, err := suggestMavenVersion(ctx, lc, resolve.RequirementVersion{VersionKey: vk}, tt.level)
   628  		if err != nil {
   629  			t.Fatalf("fail to suggest a new version for %v: %v", vk, err)
   630  		}
   631  		if !reflect.DeepEqual(got, want) {
   632  			t.Errorf("suggestMavenVersion(%v, %v): got %s want %s", vk, tt.level, got, want)
   633  		}
   634  	}
   635  }