github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/manifest/maven/pomxml_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 maven
    16  
    17  import (
    18  	"bytes"
    19  	"io"
    20  	"os"
    21  	"path/filepath"
    22  	"reflect"
    23  	"runtime"
    24  	"testing"
    25  
    26  	"deps.dev/util/maven"
    27  	"deps.dev/util/resolve"
    28  	"deps.dev/util/resolve/dep"
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/osv-scalibr/clients/clienttest"
    31  	"github.com/google/osv-scalibr/clients/datasource"
    32  	scalibrfs "github.com/google/osv-scalibr/fs"
    33  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    34  	"github.com/google/osv-scalibr/guidedremediation/result"
    35  )
    36  
    37  var (
    38  	depMgmt           = depTypeWithOrigin("management")
    39  	depParent         = depTypeWithOrigin("parent")
    40  	depPlugin         = depTypeWithOrigin("plugin@org.plugin:plugin")
    41  	depProfileOne     = depTypeWithOrigin("profile@profile-one")
    42  	depProfileTwoMgmt = depTypeWithOrigin("profile@profile-two@management")
    43  )
    44  
    45  func depTypeWithOrigin(origin string) dep.Type {
    46  	var result dep.Type
    47  	result.AddAttr(dep.MavenDependencyOrigin, origin)
    48  
    49  	return result
    50  }
    51  
    52  func mavenReqKey(t *testing.T, name, artifactType, classifier string) manifest.RequirementKey {
    53  	t.Helper()
    54  	var typ dep.Type
    55  	if artifactType != "" {
    56  		typ.AddAttr(dep.MavenArtifactType, artifactType)
    57  	}
    58  	if classifier != "" {
    59  		typ.AddAttr(dep.MavenClassifier, classifier)
    60  	}
    61  
    62  	return MakeRequirementKey(resolve.RequirementVersion{
    63  		VersionKey: resolve.VersionKey{
    64  			PackageKey: resolve.PackageKey{
    65  				Name:   name,
    66  				System: resolve.Maven,
    67  			},
    68  		},
    69  		Type: typ,
    70  	})
    71  }
    72  
    73  type testManifest struct {
    74  	FilePath          string
    75  	Root              resolve.Version
    76  	System            resolve.System
    77  	Requirements      []resolve.RequirementVersion
    78  	Groups            map[manifest.RequirementKey][]string
    79  	EcosystemSpecific ManifestSpecific
    80  }
    81  
    82  func checkManifest(t *testing.T, name string, got manifest.Manifest, want testManifest) {
    83  	t.Helper()
    84  	if want.FilePath != got.FilePath() {
    85  		t.Errorf("%s.FilePath() = %q, want %q", name, got.FilePath(), want.FilePath)
    86  	}
    87  	if diff := cmp.Diff(want.Root, got.Root()); diff != "" {
    88  		t.Errorf("%s.Root() (-want +got):\n%s", name, diff)
    89  	}
    90  	if want.System != got.System() {
    91  		t.Errorf("%s.System() = %v, want %v", name, got.System(), want.System)
    92  	}
    93  	if diff := cmp.Diff(want.Requirements, got.Requirements()); diff != "" {
    94  		t.Errorf("%s.Requirements() (-want +got):\n%s", name, diff)
    95  	}
    96  	if diff := cmp.Diff(want.Groups, got.Groups()); diff != "" {
    97  		t.Errorf("%s.Groups() (-want +got):\n%s", name, diff)
    98  	}
    99  	if diff := cmp.Diff(want.EcosystemSpecific, got.EcosystemSpecific()); diff != "" {
   100  		t.Errorf("%s.EcosystemSpecific() (-want +got):\n%s", name, diff)
   101  	}
   102  }
   103  
   104  func compareToFile(t *testing.T, got io.Reader, wantFile string) {
   105  	t.Helper()
   106  	wantBytes, err := os.ReadFile(wantFile)
   107  	if err != nil {
   108  		t.Fatalf("error reading %s: %v", wantFile, err)
   109  	}
   110  	gotBytes, err := io.ReadAll(got)
   111  	if err != nil {
   112  		t.Fatalf("error reading manifest: %v", err)
   113  	}
   114  
   115  	if runtime.GOOS == "windows" {
   116  		// Go doesn't write CRLF in xml on Windows, trying to fix this is difficult.
   117  		// Just ignore it in the tests.
   118  		wantBytes = bytes.ReplaceAll(wantBytes, []byte("\r\n"), []byte("\n"))
   119  		gotBytes = bytes.ReplaceAll(gotBytes, []byte("\r\n"), []byte("\n"))
   120  	}
   121  
   122  	if diff := cmp.Diff(wantBytes, gotBytes); diff != "" {
   123  		t.Errorf("%s (-want +got):\n%s", wantFile, diff)
   124  	}
   125  }
   126  
   127  func TestReadWrite(t *testing.T) {
   128  	srv := clienttest.NewMockHTTPServer(t)
   129  	srv.SetResponse(t, "org/upstream/parent-pom/1.2.3/parent-pom-1.2.3.pom", []byte(`
   130  <project>
   131  	<groupId>org.upstream</groupId>
   132  	<artifactId>parent-pom</artifactId>
   133  	<version>1.2.3</version>
   134  	<packaging>pom</packaging>
   135  	<properties>
   136  		<bbb.artifact>bbb</bbb.artifact>
   137  		<bbb.version>2.2.2</bbb.version>
   138  	</properties>
   139  	<dependencyManagement>
   140  	<dependencies>
   141  		<dependency>
   142  		<groupId>org.example</groupId>
   143  		<artifactId>${bbb.artifact}</artifactId>
   144  		<version>${bbb.version}</version>
   145  		</dependency>
   146  	</dependencies>
   147  	</dependencyManagement>
   148  </project>
   149  `))
   150  	srv.SetResponse(t, "org/import/import/1.0.0/import-1.0.0.pom", []byte(`
   151  <project>
   152  	<groupId>org.import</groupId>
   153  	<artifactId>import</artifactId>
   154  	<version>1.0.0</version>
   155  	<packaging>pom</packaging>
   156  	<properties>
   157  		<ccc.version>3.3.3</ccc.version>
   158  	</properties>
   159  	<dependencyManagement>
   160  		<dependencies>
   161  			<dependency>
   162  				<groupId>org.example</groupId>
   163  				<artifactId>ccc</artifactId>
   164  				<version>${ccc.version}</version>
   165  			</dependency>
   166  		</dependencies>
   167  	</dependencyManagement>
   168  </project>
   169  `))
   170  
   171  	client, _ := datasource.NewDefaultMavenRegistryAPIClient(t.Context(), srv.URL)
   172  	mavenRW, err := GetReadWriter(client)
   173  	if err != nil {
   174  		t.Fatalf("error creating ReadWriter: %v", err)
   175  	}
   176  
   177  	fsys := scalibrfs.DirFS("./testdata")
   178  	got, err := mavenRW.Read("my-app/pom.xml", fsys)
   179  	if err != nil {
   180  		t.Fatalf("error reading manifest: %v", err)
   181  	}
   182  
   183  	depType := depMgmt.Clone()
   184  	depType.AddAttr(dep.MavenArtifactType, "pom")
   185  	depType.AddAttr(dep.Scope, "import")
   186  
   187  	depParent.AddAttr(dep.MavenArtifactType, "pom")
   188  
   189  	var depExclusions dep.Type
   190  	depExclusions.AddAttr(dep.MavenExclusions, "org.exclude:exclude")
   191  
   192  	want := testManifest{
   193  		FilePath: "my-app/pom.xml",
   194  		Root: resolve.Version{
   195  			VersionKey: resolve.VersionKey{
   196  				PackageKey: resolve.PackageKey{
   197  					System: resolve.Maven,
   198  					Name:   "com.mycompany.app:my-app",
   199  				},
   200  				VersionType: resolve.Concrete,
   201  				Version:     "1.0",
   202  			},
   203  		},
   204  		System: resolve.Maven,
   205  		Requirements: []resolve.RequirementVersion{
   206  			{
   207  				VersionKey: resolve.VersionKey{
   208  					PackageKey: resolve.PackageKey{
   209  						System: resolve.Maven,
   210  						Name:   "junit:junit",
   211  					},
   212  					VersionType: resolve.Requirement,
   213  					Version:     "4.12",
   214  				},
   215  				// Type: dep.NewType(dep.Test), test scope is ignored to make resolution work.
   216  			},
   217  			{
   218  				VersionKey: resolve.VersionKey{
   219  					PackageKey: resolve.PackageKey{
   220  						System: resolve.Maven,
   221  						Name:   "org.example:abc",
   222  					},
   223  					VersionType: resolve.Requirement,
   224  					Version:     "1.0.1",
   225  				},
   226  			},
   227  			{
   228  				VersionKey: resolve.VersionKey{
   229  					PackageKey: resolve.PackageKey{
   230  						System: resolve.Maven,
   231  						Name:   "org.example:no-version",
   232  					},
   233  					VersionType: resolve.Requirement,
   234  					Version:     "2.0.0",
   235  				},
   236  			},
   237  			{
   238  				VersionKey: resolve.VersionKey{
   239  					PackageKey: resolve.PackageKey{
   240  						System: resolve.Maven,
   241  						Name:   "org.example:exclusions",
   242  					},
   243  					VersionType: resolve.Requirement,
   244  					Version:     "1.0.0",
   245  				},
   246  				Type: depExclusions,
   247  			},
   248  			{
   249  				VersionKey: resolve.VersionKey{
   250  					PackageKey: resolve.PackageKey{
   251  						System: resolve.Maven,
   252  						Name:   "org.profile:abc",
   253  					},
   254  					VersionType: resolve.Requirement,
   255  					Version:     "1.2.3",
   256  				},
   257  			},
   258  			{
   259  				VersionKey: resolve.VersionKey{
   260  					PackageKey: resolve.PackageKey{
   261  						System: resolve.Maven,
   262  						Name:   "org.profile:def",
   263  					},
   264  					VersionType: resolve.Requirement,
   265  					Version:     "2.3.4",
   266  				},
   267  			},
   268  			{
   269  				VersionKey: resolve.VersionKey{
   270  					PackageKey: resolve.PackageKey{
   271  						System: resolve.Maven,
   272  						Name:   "org.example:ddd",
   273  					},
   274  					VersionType: resolve.Requirement,
   275  					Version:     "1.2.3",
   276  				},
   277  			},
   278  			{
   279  				VersionKey: resolve.VersionKey{
   280  					PackageKey: resolve.PackageKey{
   281  						System: resolve.Maven,
   282  						Name:   "org.example:xyz",
   283  					},
   284  					VersionType: resolve.Requirement,
   285  					Version:     "2.0.0",
   286  				},
   287  				Type: depMgmt,
   288  			},
   289  			{
   290  				VersionKey: resolve.VersionKey{
   291  					PackageKey: resolve.PackageKey{
   292  						System: resolve.Maven,
   293  						Name:   "org.example:no-version",
   294  					},
   295  					VersionType: resolve.Requirement,
   296  					Version:     "2.0.0",
   297  				},
   298  				Type: depMgmt,
   299  			},
   300  			{
   301  				VersionKey: resolve.VersionKey{
   302  					PackageKey: resolve.PackageKey{
   303  						System: resolve.Maven,
   304  						Name:   "org.example:aaa",
   305  					},
   306  					VersionType: resolve.Requirement,
   307  					Version:     "1.1.1",
   308  				},
   309  				Type: depMgmt,
   310  			},
   311  			{
   312  				VersionKey: resolve.VersionKey{
   313  					PackageKey: resolve.PackageKey{
   314  						System: resolve.Maven,
   315  						Name:   "org.example:bbb",
   316  					},
   317  					VersionType: resolve.Requirement,
   318  					Version:     "2.2.2",
   319  				},
   320  				Type: depMgmt,
   321  			},
   322  			{
   323  				VersionKey: resolve.VersionKey{
   324  					PackageKey: resolve.PackageKey{
   325  						System: resolve.Maven,
   326  						Name:   "org.example:ccc",
   327  					},
   328  					VersionType: resolve.Requirement,
   329  					Version:     "3.3.3",
   330  				},
   331  				Type: depMgmt,
   332  			},
   333  		},
   334  		Groups: map[manifest.RequirementKey][]string{
   335  			mavenReqKey(t, "junit:junit", "", ""):       {"test"},
   336  			mavenReqKey(t, "org.import:xyz", "pom", ""): {"import"},
   337  		},
   338  		EcosystemSpecific: ManifestSpecific{
   339  			Parent: maven.Parent{
   340  				ProjectKey: maven.ProjectKey{
   341  					GroupID:    "org.parent",
   342  					ArtifactID: "parent-pom",
   343  					Version:    "1.1.1",
   344  				},
   345  				RelativePath: "../parent/pom.xml",
   346  			},
   347  			ParentPaths: []string{"my-app/pom.xml", "parent/pom.xml", "parent/grandparent/pom.xml"},
   348  			Properties: []PropertyWithOrigin{
   349  				{Property: maven.Property{Name: "project.build.sourceEncoding", Value: "UTF-8"}},
   350  				{Property: maven.Property{Name: "maven.compiler.source", Value: "1.7"}},
   351  				{Property: maven.Property{Name: "maven.compiler.target", Value: "1.7"}},
   352  				{Property: maven.Property{Name: "junit.version", Value: "4.12"}},
   353  				{Property: maven.Property{Name: "zeppelin.daemon.package.base", Value: "../bin"}},
   354  				{Property: maven.Property{Name: "def.version", Value: "2.3.4"}, Origin: "profile@profile-one"},
   355  				{Property: maven.Property{Name: "aaa.version", Value: "1.1.1"}},
   356  			},
   357  			OriginalRequirements: []DependencyWithOrigin{
   358  				{
   359  					Dependency: maven.Dependency{GroupID: "org.parent", ArtifactID: "parent-pom", Version: "1.1.1", Type: "pom"},
   360  					Origin:     "parent",
   361  				},
   362  				{
   363  					Dependency: maven.Dependency{GroupID: "junit", ArtifactID: "junit", Version: "${junit.version}", Scope: "test"},
   364  				},
   365  				{
   366  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "abc", Version: "1.0.1"},
   367  				},
   368  				{
   369  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version"},
   370  				},
   371  				{
   372  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "exclusions", Version: "1.0.0",
   373  						Exclusions: []maven.Exclusion{
   374  							{GroupID: "org.exclude", ArtifactID: "exclude"},
   375  						}},
   376  				},
   377  				{
   378  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "xyz", Version: "2.0.0"},
   379  					Origin:     "management",
   380  				},
   381  				{
   382  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version", Version: "2.0.0"},
   383  					Origin:     "management",
   384  				},
   385  				{
   386  					Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "import", Version: "1.0.0", Scope: "import", Type: "pom"},
   387  					Origin:     "management",
   388  				},
   389  				{
   390  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "abc", Version: "1.2.3"},
   391  					Origin:     "profile@profile-one",
   392  				},
   393  				{
   394  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "def", Version: "${def.version}"},
   395  					Origin:     "profile@profile-one",
   396  				},
   397  				{
   398  					Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "xyz", Version: "6.6.6", Scope: "import", Type: "pom"},
   399  					Origin:     "profile@profile-two@management",
   400  				},
   401  				{
   402  					Dependency: maven.Dependency{GroupID: "org.dep", ArtifactID: "plugin-dep", Version: "2.3.3"},
   403  					Origin:     "plugin@org.plugin:plugin",
   404  				},
   405  			},
   406  			LocalRequirements: []DependencyWithOrigin{
   407  				{
   408  					Dependency: maven.Dependency{GroupID: "org.parent", ArtifactID: "parent-pom", Version: "1.1.1", Type: "pom"},
   409  					Origin:     "parent",
   410  				},
   411  				{
   412  					Dependency: maven.Dependency{GroupID: "junit", ArtifactID: "junit", Version: "${junit.version}", Scope: "test"},
   413  				},
   414  				{
   415  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "abc", Version: "1.0.1"},
   416  				},
   417  				{
   418  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version"},
   419  				},
   420  				{
   421  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "exclusions", Version: "1.0.0",
   422  						Exclusions: []maven.Exclusion{
   423  							{GroupID: "org.exclude", ArtifactID: "exclude"},
   424  						}},
   425  				},
   426  				{
   427  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "xyz", Version: "2.0.0"},
   428  					Origin:     "management",
   429  				},
   430  				{
   431  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version", Version: "2.0.0"},
   432  					Origin:     "management",
   433  				},
   434  				{
   435  					Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "import", Version: "1.0.0", Scope: "import", Type: "pom"},
   436  					Origin:     "management",
   437  				},
   438  				{
   439  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "abc", Version: "1.2.3"},
   440  					Origin:     "profile@profile-one",
   441  				},
   442  				{
   443  					Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "def", Version: "${def.version}"},
   444  					Origin:     "profile@profile-one",
   445  				},
   446  				{
   447  					Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "xyz", Version: "6.6.6", Scope: "import", Type: "pom"},
   448  					Origin:     "profile@profile-two@management",
   449  				},
   450  				{
   451  					Dependency: maven.Dependency{GroupID: "org.dep", ArtifactID: "plugin-dep", Version: "2.3.3"},
   452  					Origin:     "plugin@org.plugin:plugin",
   453  				},
   454  				{
   455  					Dependency: maven.Dependency{GroupID: "org.grandparent", ArtifactID: "grandparent-pom", Version: "1.1.1", Type: "pom"},
   456  					Origin:     "parent@parent/pom.xml@parent",
   457  				},
   458  				{
   459  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "ddd", Version: "1.2.3"},
   460  					Origin:     "parent@parent/pom.xml",
   461  				},
   462  				{
   463  					Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "aaa", Version: "${aaa.version}"},
   464  					Origin:     "parent@parent/pom.xml@management",
   465  				},
   466  				{
   467  					Dependency: maven.Dependency{GroupID: "org.upstream", ArtifactID: "parent-pom", Version: "1.2.3", Type: "pom"},
   468  					Origin:     "parent@parent/grandparent/pom.xml@parent",
   469  				},
   470  			},
   471  			RequirementsForUpdates: []resolve.RequirementVersion{
   472  				{
   473  					VersionKey: resolve.VersionKey{
   474  						PackageKey: resolve.PackageKey{
   475  							System: resolve.Maven,
   476  							Name:   "org.parent:parent-pom",
   477  						},
   478  						VersionType: resolve.Requirement,
   479  						Version:     "1.1.1",
   480  					},
   481  					Type: depParent,
   482  				},
   483  				{
   484  					VersionKey: resolve.VersionKey{
   485  						PackageKey: resolve.PackageKey{
   486  							System: resolve.Maven,
   487  							Name:   "org.import:import",
   488  						},
   489  						VersionType: resolve.Requirement,
   490  						Version:     "1.0.0",
   491  					},
   492  					Type: depType,
   493  				},
   494  				{
   495  					VersionKey: resolve.VersionKey{
   496  						PackageKey: resolve.PackageKey{
   497  							System: resolve.Maven,
   498  							Name:   "org.profile:abc",
   499  						},
   500  						VersionType: resolve.Requirement,
   501  						Version:     "1.2.3",
   502  					},
   503  				},
   504  				{
   505  					VersionKey: resolve.VersionKey{
   506  						PackageKey: resolve.PackageKey{
   507  							System: resolve.Maven,
   508  							Name:   "org.profile:def",
   509  						},
   510  						VersionType: resolve.Requirement,
   511  						Version:     "${def.version}",
   512  					},
   513  				},
   514  				{
   515  					VersionKey: resolve.VersionKey{
   516  						PackageKey: resolve.PackageKey{
   517  							System: resolve.Maven,
   518  							Name:   "org.import:xyz",
   519  						},
   520  						VersionType: resolve.Requirement,
   521  						Version:     "6.6.6",
   522  					},
   523  					Type: depType,
   524  				},
   525  				{
   526  					VersionKey: resolve.VersionKey{
   527  						PackageKey: resolve.PackageKey{
   528  							System: resolve.Maven,
   529  							Name:   "org.dep:plugin-dep",
   530  						},
   531  						VersionType: resolve.Requirement,
   532  						Version:     "2.3.3",
   533  					},
   534  				},
   535  			},
   536  		},
   537  	}
   538  
   539  	checkManifest(t, "Manifest", got, want)
   540  
   541  	// Test writing the files produces the same pom.xml files.
   542  	dir := t.TempDir()
   543  	if err := mavenRW.Write(got, fsys, nil, filepath.Join(dir, "my-app", "pom.xml")); err != nil {
   544  		t.Fatalf("error writing manifest: %v", err)
   545  	}
   546  
   547  	gotFile, err := os.Open(filepath.Join(dir, "my-app", "pom.xml"))
   548  	if err != nil {
   549  		t.Fatalf("error opening pom.xml: %v", err)
   550  	}
   551  	defer gotFile.Close()
   552  	compareToFile(t, gotFile, "testdata/my-app/pom.xml")
   553  
   554  	gotFile, err = os.Open(filepath.Join(dir, "parent", "pom.xml"))
   555  	if err != nil {
   556  		t.Fatalf("error opening pom.xml: %v", err)
   557  	}
   558  	defer gotFile.Close()
   559  	compareToFile(t, gotFile, "testdata/parent/pom.xml")
   560  
   561  	gotFile, err = os.Open(filepath.Join(dir, "parent", "grandparent", "pom.xml"))
   562  	if err != nil {
   563  		t.Fatalf("error opening pom.xml: %v", err)
   564  	}
   565  	defer gotFile.Close()
   566  	compareToFile(t, gotFile, "testdata/parent/grandparent/pom.xml")
   567  }
   568  
   569  func TestMavenWrite(t *testing.T) {
   570  	dir, err := os.Getwd()
   571  	if err != nil {
   572  		t.Fatalf("failed to get current directory: %v", err)
   573  	}
   574  	in, err := os.ReadFile(filepath.Join(dir, "testdata", "my-app", "pom.xml"))
   575  	if err != nil {
   576  		t.Fatalf("fail to open file: %v", err)
   577  	}
   578  
   579  	patches := Patches{
   580  		DependencyPatches: DependencyPatches{
   581  			"": map[Patch]bool{
   582  				{
   583  					DependencyKey: maven.DependencyKey{
   584  						GroupID:    "org.example",
   585  						ArtifactID: "abc",
   586  						Type:       "jar",
   587  					},
   588  					NewRequire: "1.0.2",
   589  				}: true,
   590  				{
   591  					DependencyKey: maven.DependencyKey{
   592  						GroupID:    "org.example",
   593  						ArtifactID: "no-version",
   594  						Type:       "jar",
   595  					},
   596  					NewRequire: "2.0.1",
   597  				}: true,
   598  			},
   599  			"management": map[Patch]bool{
   600  				{
   601  					DependencyKey: maven.DependencyKey{
   602  						GroupID:    "org.example",
   603  						ArtifactID: "xyz",
   604  						Type:       "jar",
   605  					},
   606  					NewRequire: "2.0.1",
   607  				}: true,
   608  				{
   609  					DependencyKey: maven.DependencyKey{
   610  						GroupID:    "org.example",
   611  						ArtifactID: "extra-one",
   612  						Type:       "jar",
   613  					},
   614  					NewRequire: "6.6.6",
   615  				}: false,
   616  				{
   617  					DependencyKey: maven.DependencyKey{
   618  						GroupID:    "org.example",
   619  						ArtifactID: "extra-two",
   620  						Type:       "jar",
   621  					},
   622  					NewRequire: "9.9.9",
   623  				}: false,
   624  			},
   625  			"profile@profile-one": map[Patch]bool{
   626  				{
   627  					DependencyKey: maven.DependencyKey{
   628  						GroupID:    "org.profile",
   629  						ArtifactID: "abc",
   630  						Type:       "jar",
   631  					},
   632  					NewRequire: "1.2.4",
   633  				}: true,
   634  			},
   635  			"profile@profile-two@management": map[Patch]bool{
   636  				{
   637  					DependencyKey: maven.DependencyKey{
   638  						GroupID:    "org.import",
   639  						ArtifactID: "xyz",
   640  						Type:       "pom",
   641  					},
   642  					NewRequire: "7.0.0",
   643  				}: true,
   644  			},
   645  			"plugin@org.plugin:plugin": map[Patch]bool{
   646  				{
   647  					DependencyKey: maven.DependencyKey{
   648  						GroupID:    "org.dep",
   649  						ArtifactID: "plugin-dep",
   650  						Type:       "jar",
   651  					},
   652  					NewRequire: "2.3.4",
   653  				}: true,
   654  			},
   655  		},
   656  		PropertyPatches: PropertyPatches{
   657  			"": {
   658  				"junit.version": "4.13.2",
   659  			},
   660  			"profile@profile-one": {
   661  				"def.version": "2.3.5",
   662  			},
   663  		},
   664  	}
   665  
   666  	out := new(bytes.Buffer)
   667  	if err := write(string(in), out, patches); err != nil {
   668  		t.Fatalf("unable to update Maven pom.xml: %v", err)
   669  	}
   670  	compareToFile(t, out, filepath.Join(dir, "testdata", "my-app", "write_want.pom.xml"))
   671  }
   672  
   673  func TestMavenWriteDM(t *testing.T) {
   674  	dir, err := os.Getwd()
   675  	if err != nil {
   676  		t.Fatalf("failed to get current directory: %v", err)
   677  	}
   678  	in, err := os.ReadFile(filepath.Join(dir, "testdata", "no-dependency-management", "pom.xml"))
   679  	if err != nil {
   680  		t.Fatalf("fail to open file: %v", err)
   681  	}
   682  
   683  	patches := Patches{
   684  		DependencyPatches: DependencyPatches{
   685  			"": map[Patch]bool{
   686  				{
   687  					DependencyKey: maven.DependencyKey{
   688  						GroupID:    "junit",
   689  						ArtifactID: "junit",
   690  						Type:       "jar",
   691  					},
   692  					NewRequire: "4.13.2",
   693  				}: true,
   694  			},
   695  			"parent": map[Patch]bool{
   696  				{
   697  					DependencyKey: maven.DependencyKey{
   698  						GroupID:    "org.parent",
   699  						ArtifactID: "parent-pom",
   700  						Type:       "jar",
   701  					},
   702  					NewRequire: "1.2.0",
   703  				}: true,
   704  			},
   705  			"management": map[Patch]bool{
   706  				{
   707  					DependencyKey: maven.DependencyKey{
   708  						GroupID:    "org.management",
   709  						ArtifactID: "abc",
   710  						Type:       "jar",
   711  					},
   712  					NewRequire: "1.2.3",
   713  				}: false,
   714  				{
   715  					DependencyKey: maven.DependencyKey{
   716  						GroupID:    "org.management",
   717  						ArtifactID: "xyz",
   718  						Type:       "jar",
   719  					},
   720  					NewRequire: "2.3.4",
   721  				}: false,
   722  			},
   723  		},
   724  	}
   725  
   726  	out := new(bytes.Buffer)
   727  	if err := write(string(in), out, patches); err != nil {
   728  		t.Fatalf("unable to update Maven pom.xml: %v", err)
   729  	}
   730  	compareToFile(t, out, filepath.Join(dir, "testdata", "no-dependency-management", "want.pom.xml"))
   731  }
   732  
   733  func Test_buildPatches(t *testing.T) {
   734  	const parentPath = "testdata/parent/pom.xml"
   735  
   736  	depProfileTwoMgmt.AddAttr(dep.MavenArtifactType, "pom")
   737  	depProfileTwoMgmt.AddAttr(dep.Scope, "import")
   738  
   739  	depParent.AddAttr(dep.MavenArtifactType, "pom")
   740  
   741  	patches := []result.Patch{
   742  		{
   743  			PackageUpdates: []result.PackageUpdate{
   744  				{
   745  					Name:      "org.dep:plugin-dep",
   746  					VersionTo: "2.3.4",
   747  					Type:      depPlugin,
   748  				},
   749  				{
   750  					Name:      "org.example:abc",
   751  					VersionTo: "1.0.2",
   752  				},
   753  				{
   754  					Name:      "org.example:aaa",
   755  					VersionTo: "1.2.0",
   756  				},
   757  				{
   758  					Name:      "org.example:ddd",
   759  					VersionTo: "1.3.0",
   760  				},
   761  				{
   762  					Name:      "org.example:property",
   763  					VersionTo: "1.0.1",
   764  				},
   765  				{
   766  					Name:      "org.example:same-property",
   767  					VersionTo: "1.0.1",
   768  				},
   769  				{
   770  					Name:      "org.example:another-property",
   771  					VersionTo: "1.1.0",
   772  				},
   773  				{
   774  					Name:      "org.example:property-no-update",
   775  					VersionTo: "2.0.0",
   776  				},
   777  				{
   778  					Name:      "org.example:xyz",
   779  					VersionTo: "2.0.1",
   780  					Type:      depMgmt,
   781  				},
   782  				{
   783  					Name:      "org.import:xyz",
   784  					VersionTo: "6.7.0",
   785  					Type:      depProfileTwoMgmt,
   786  				},
   787  				{
   788  					Name:      "org.profile:abc",
   789  					VersionTo: "1.2.4",
   790  					Type:      depProfileOne,
   791  				},
   792  				{
   793  					Name:      "org.profile:def",
   794  					VersionTo: "2.3.5",
   795  					Type:      depProfileOne,
   796  				},
   797  				{
   798  					Name:      "org.parent:parent-pom",
   799  					VersionTo: "1.2.0",
   800  					Type:      depParent,
   801  				},
   802  				{
   803  					Name:        "org.example:suggest",
   804  					VersionFrom: "1.0.0",
   805  					VersionTo:   "2.0.0",
   806  					Type:        depMgmt,
   807  				},
   808  				{
   809  					Name:      "org.example:override",
   810  					VersionTo: "2.0.0",
   811  					Type:      depMgmt,
   812  				},
   813  				{
   814  					Name:      "org.example:no-version",
   815  					VersionTo: "2.0.1",
   816  					Type:      depMgmt,
   817  				},
   818  			},
   819  		},
   820  	}
   821  	specific := ManifestSpecific{
   822  		Parent: maven.Parent{
   823  			ProjectKey: maven.ProjectKey{
   824  				GroupID:    "org.parent",
   825  				ArtifactID: "parent-pom",
   826  				Version:    "1.1.1",
   827  			},
   828  			RelativePath: "../parent/pom.xml",
   829  		},
   830  		Properties: []PropertyWithOrigin{
   831  			{Property: maven.Property{Name: "property.version", Value: "1.0.0"}},
   832  			{Property: maven.Property{Name: "no.update.minor", Value: "9"}},
   833  			{Property: maven.Property{Name: "def.version", Value: "2.3.4"}, Origin: "profile@profile-one"},
   834  			{Property: maven.Property{Name: "aaa.version", Value: "1.1.1"}, Origin: "parent@" + parentPath},
   835  		},
   836  		LocalRequirements: []DependencyWithOrigin{
   837  			{
   838  				Dependency: maven.Dependency{GroupID: "org.parent", ArtifactID: "parent-pom", Version: "1.2.0", Type: "pom"},
   839  				Origin:     "parent",
   840  			},
   841  			{
   842  				Dependency: maven.Dependency{GroupID: "junit", ArtifactID: "junit", Version: "${junit.version}", Scope: "test"},
   843  			},
   844  			{
   845  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "abc", Version: "1.0.1"},
   846  			},
   847  			{
   848  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-updates", Version: "9.9.9"},
   849  			},
   850  			{
   851  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version"},
   852  			},
   853  			{
   854  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "property", Version: "${property.version}"},
   855  			},
   856  			{
   857  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "property-no-update", Version: "1.${no.update.minor}"},
   858  			},
   859  			{
   860  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "same-property", Version: "${property.version}"},
   861  			},
   862  			{
   863  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "another-property", Version: "${property.version}"},
   864  			},
   865  			{
   866  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "no-version", Version: "2.0.0"},
   867  				Origin:     "management",
   868  			},
   869  			{
   870  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "xyz", Version: "2.0.0"},
   871  				Origin:     "management",
   872  			},
   873  			{
   874  				Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "abc", Version: "1.2.3"},
   875  				Origin:     "profile@profile-one",
   876  			},
   877  			{
   878  				Dependency: maven.Dependency{GroupID: "org.profile", ArtifactID: "def", Version: "${def.version}"},
   879  				Origin:     "profile@profile-one",
   880  			},
   881  			{
   882  				Dependency: maven.Dependency{GroupID: "org.import", ArtifactID: "xyz", Version: "6.6.6", Scope: "import", Type: "pom"},
   883  				Origin:     "profile@profile-two@management",
   884  			},
   885  			{
   886  				Dependency: maven.Dependency{GroupID: "org.dep", ArtifactID: "plugin-dep", Version: "2.3.3"},
   887  				Origin:     "plugin@org.plugin:plugin",
   888  			},
   889  			{
   890  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "ddd", Version: "1.2.3"},
   891  				Origin:     "parent@" + parentPath,
   892  			},
   893  			{
   894  				Dependency: maven.Dependency{GroupID: "org.example", ArtifactID: "aaa", Version: "${aaa.version}"},
   895  				Origin:     "parent@" + parentPath + "@management",
   896  			},
   897  		},
   898  	}
   899  	want := map[string]Patches{
   900  		"": {
   901  			DependencyPatches: DependencyPatches{
   902  				"": map[Patch]bool{
   903  					{
   904  						DependencyKey: maven.DependencyKey{
   905  							GroupID:    "org.example",
   906  							ArtifactID: "abc",
   907  							Type:       "jar",
   908  						},
   909  						NewRequire: "1.0.2",
   910  					}: true,
   911  					{
   912  						DependencyKey: maven.DependencyKey{
   913  							GroupID:    "org.example",
   914  							ArtifactID: "another-property",
   915  							Type:       "jar",
   916  						},
   917  						NewRequire: "1.1.0",
   918  					}: true,
   919  					{
   920  						DependencyKey: maven.DependencyKey{
   921  							GroupID:    "org.example",
   922  							ArtifactID: "property-no-update",
   923  							Type:       "jar",
   924  						},
   925  						NewRequire: "2.0.0",
   926  					}: true,
   927  				},
   928  				"management": map[Patch]bool{
   929  					{
   930  						DependencyKey: maven.DependencyKey{
   931  							GroupID:    "org.example",
   932  							ArtifactID: "xyz",
   933  							Type:       "jar",
   934  						},
   935  						NewRequire: "2.0.1",
   936  					}: true,
   937  					{
   938  						DependencyKey: maven.DependencyKey{
   939  							GroupID:    "org.example",
   940  							ArtifactID: "no-version",
   941  							Type:       "jar",
   942  						},
   943  						NewRequire: "2.0.1",
   944  					}: true,
   945  					{
   946  						DependencyKey: maven.DependencyKey{
   947  							GroupID:    "org.example",
   948  							ArtifactID: "override",
   949  							Type:       "jar",
   950  						},
   951  						NewRequire: "2.0.0",
   952  					}: false,
   953  					{
   954  						DependencyKey: maven.DependencyKey{
   955  							GroupID:    "org.example",
   956  							ArtifactID: "suggest",
   957  							Type:       "jar",
   958  						},
   959  						NewRequire: "2.0.0",
   960  					}: false,
   961  				},
   962  				"profile@profile-one": map[Patch]bool{
   963  					{
   964  						DependencyKey: maven.DependencyKey{
   965  							GroupID:    "org.profile",
   966  							ArtifactID: "abc",
   967  							Type:       "jar",
   968  						},
   969  						NewRequire: "1.2.4",
   970  					}: true,
   971  				},
   972  				"profile@profile-two@management": map[Patch]bool{
   973  					{
   974  						DependencyKey: maven.DependencyKey{
   975  							GroupID:    "org.import",
   976  							ArtifactID: "xyz",
   977  							Type:       "pom",
   978  						},
   979  						NewRequire: "6.7.0",
   980  					}: true,
   981  				},
   982  				"plugin@org.plugin:plugin": map[Patch]bool{
   983  					{
   984  						DependencyKey: maven.DependencyKey{
   985  							GroupID:    "org.dep",
   986  							ArtifactID: "plugin-dep",
   987  							Type:       "jar",
   988  						},
   989  						NewRequire: "2.3.4",
   990  					}: true,
   991  				},
   992  				"parent": map[Patch]bool{
   993  					{
   994  						DependencyKey: maven.DependencyKey{
   995  							GroupID:    "org.parent",
   996  							ArtifactID: "parent-pom",
   997  							Type:       "pom",
   998  						},
   999  						NewRequire: "1.2.0",
  1000  					}: true,
  1001  				},
  1002  			},
  1003  			PropertyPatches: PropertyPatches{
  1004  				"": {
  1005  					"property.version": "1.0.1",
  1006  				},
  1007  				"profile@profile-one": {
  1008  					"def.version": "2.3.5",
  1009  				},
  1010  			},
  1011  		},
  1012  		parentPath: {
  1013  			DependencyPatches: DependencyPatches{
  1014  				"": map[Patch]bool{
  1015  					{
  1016  						DependencyKey: maven.DependencyKey{
  1017  							GroupID:    "org.example",
  1018  							ArtifactID: "ddd",
  1019  							Type:       "jar",
  1020  						},
  1021  						NewRequire: "1.3.0",
  1022  					}: true,
  1023  				},
  1024  			},
  1025  			PropertyPatches: PropertyPatches{
  1026  				"": {
  1027  					"aaa.version": "1.2.0",
  1028  				},
  1029  			},
  1030  		},
  1031  	}
  1032  
  1033  	allPatches, err := buildPatches(patches, specific)
  1034  	if err != nil {
  1035  		t.Fatalf("failed to build patches: %v", err)
  1036  	}
  1037  	if diff := cmp.Diff(want, allPatches); diff != "" {
  1038  		t.Errorf("result patches mismatch (-want +got):\n%s", diff)
  1039  	}
  1040  }
  1041  
  1042  func Test_generatePropertyPatches(t *testing.T) {
  1043  	tests := []struct {
  1044  		s1       string
  1045  		s2       string
  1046  		possible bool
  1047  		patches  map[string]string
  1048  	}{
  1049  		{"${version}", "1.2.3", true, map[string]string{"version": "1.2.3"}},
  1050  		{"${major}.2.3", "1.2.3", true, map[string]string{"major": "1"}},
  1051  		{"1.${minor}.3", "1.2.3", true, map[string]string{"minor": "2"}},
  1052  		{"1.2.${patch}", "1.2.3", true, map[string]string{"patch": "3"}},
  1053  		{"${major}.${minor}.${patch}", "1.2.3", true, map[string]string{"major": "1", "minor": "2", "patch": "3"}},
  1054  		{"${major}.2.3", "2.0.0", false, map[string]string{}},
  1055  		{"1.${minor}.3", "2.0.0", false, map[string]string{}},
  1056  	}
  1057  	for _, tt := range tests {
  1058  		patches, ok := generatePropertyPatches(tt.s1, tt.s2)
  1059  		if ok != tt.possible || !reflect.DeepEqual(patches, tt.patches) {
  1060  			t.Errorf("generatePropertyPatches(%s, %s): got %v %v, want %v %v", tt.s1, tt.s2, patches, ok, tt.patches, tt.possible)
  1061  		}
  1062  	}
  1063  }