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 }