github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/java/pomxmlnet/pomxmlnet_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 pomxmlnet_test
    16  
    17  import (
    18  	"testing"
    19  
    20  	"github.com/google/go-cmp/cmp"
    21  	"github.com/google/go-cmp/cmp/cmpopts"
    22  	cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
    23  	"github.com/google/osv-scalibr/clients/clienttest"
    24  	"github.com/google/osv-scalibr/clients/datasource"
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem/language/java/javalockfile"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/language/java/pomxmlnet"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    29  	"github.com/google/osv-scalibr/inventory"
    30  	"github.com/google/osv-scalibr/purl"
    31  	"github.com/google/osv-scalibr/testing/extracttest"
    32  )
    33  
    34  func TestMavenResolverExtractor_FileRequired(t *testing.T) {
    35  	tests := []struct {
    36  		path string
    37  		want bool
    38  	}{
    39  		{
    40  			path: "",
    41  			want: false,
    42  		},
    43  		{
    44  			path: "pom.xml",
    45  			want: true,
    46  		},
    47  		{
    48  			path: "path/to/my/pom.xml",
    49  			want: true,
    50  		},
    51  		{
    52  			path: "path/to/my/pom.xml/file",
    53  			want: false,
    54  		},
    55  		{
    56  			path: "path/to/my/pom.xml.file",
    57  			want: false,
    58  		},
    59  		{
    60  			path: "path.to.my.pom.xml",
    61  			want: false,
    62  		},
    63  	}
    64  	for _, tt := range tests {
    65  		t.Run(tt.path, func(t *testing.T) {
    66  			e := pomxmlnet.Extractor{}
    67  			got := e.FileRequired(simplefileapi.New(tt.path, nil))
    68  			if got != tt.want {
    69  				t.Errorf("Extract() got = %v, want %v", got, tt.want)
    70  			}
    71  		})
    72  	}
    73  }
    74  
    75  func TestExtractor_Extract(t *testing.T) {
    76  	tests := []extracttest.TestTableEntry{
    77  		{
    78  			Name: "Not a pom file",
    79  			InputConfig: extracttest.ScanInputMockConfig{
    80  				Path: "testdata/maven/not-pom.txt",
    81  			},
    82  			WantErr: extracttest.ContainsErrStr{Str: "could not extract"},
    83  		},
    84  		{
    85  			Name: "invalid xml syntax",
    86  			InputConfig: extracttest.ScanInputMockConfig{
    87  				Path: "testdata/maven/invalid-syntax.xml",
    88  			},
    89  			WantErr: extracttest.ContainsErrStr{Str: "XML syntax error"},
    90  		},
    91  		{
    92  			Name: "empty",
    93  			InputConfig: extracttest.ScanInputMockConfig{
    94  				Path: "testdata/maven/empty.xml",
    95  			},
    96  			WantPackages: nil,
    97  		},
    98  		{
    99  			Name: "one package",
   100  			InputConfig: extracttest.ScanInputMockConfig{
   101  				Path: "testdata/maven/one-package.xml",
   102  			},
   103  			WantPackages: []*extractor.Package{
   104  				{
   105  					Name:      "org.apache.maven:maven-artifact",
   106  					Version:   "1.0.0",
   107  					PURLType:  purl.TypeMaven,
   108  					Locations: []string{"testdata/maven/one-package.xml"},
   109  					Metadata: &javalockfile.Metadata{
   110  						ArtifactID:   "maven-artifact",
   111  						GroupID:      "org.apache.maven",
   112  						IsTransitive: false,
   113  						DepGroupVals: []string{},
   114  					},
   115  				},
   116  			},
   117  		},
   118  		{
   119  			Name: "two packages",
   120  			InputConfig: extracttest.ScanInputMockConfig{
   121  				Path: "testdata/maven/two-packages.xml",
   122  			},
   123  			WantPackages: []*extractor.Package{
   124  				{
   125  					Name:      "io.netty:netty-all",
   126  					Version:   "4.1.42.Final",
   127  					PURLType:  purl.TypeMaven,
   128  					Locations: []string{"testdata/maven/two-packages.xml"},
   129  					Metadata: &javalockfile.Metadata{
   130  						ArtifactID:   "netty-all",
   131  						GroupID:      "io.netty",
   132  						IsTransitive: false,
   133  						DepGroupVals: []string{},
   134  					},
   135  				},
   136  				{
   137  					Name:      "org.slf4j:slf4j-log4j12",
   138  					Version:   "1.7.25",
   139  					PURLType:  purl.TypeMaven,
   140  					Locations: []string{"testdata/maven/two-packages.xml"},
   141  					Metadata: &javalockfile.Metadata{
   142  						ArtifactID:   "slf4j-log4j12",
   143  						GroupID:      "org.slf4j",
   144  						IsTransitive: false,
   145  						DepGroupVals: []string{},
   146  					},
   147  				},
   148  			},
   149  		},
   150  		{
   151  			Name: "with dependency management",
   152  			InputConfig: extracttest.ScanInputMockConfig{
   153  				Path: "testdata/maven/with-dependency-management.xml",
   154  			},
   155  			WantPackages: []*extractor.Package{
   156  				{
   157  					Name:      "io.netty:netty-all",
   158  					Version:   "4.1.9",
   159  					PURLType:  purl.TypeMaven,
   160  					Locations: []string{"testdata/maven/with-dependency-management.xml"},
   161  					Metadata: &javalockfile.Metadata{
   162  						ArtifactID:   "netty-all",
   163  						GroupID:      "io.netty",
   164  						IsTransitive: false,
   165  						DepGroupVals: []string{},
   166  					},
   167  				},
   168  				{
   169  					Name:      "org.slf4j:slf4j-log4j12",
   170  					Version:   "1.7.25",
   171  					PURLType:  purl.TypeMaven,
   172  					Locations: []string{"testdata/maven/with-dependency-management.xml"},
   173  					Metadata: &javalockfile.Metadata{
   174  						ArtifactID:   "slf4j-log4j12",
   175  						GroupID:      "org.slf4j",
   176  						IsTransitive: false,
   177  						DepGroupVals: []string{},
   178  					},
   179  				},
   180  			},
   181  		},
   182  		{
   183  			Name: "interpolation",
   184  			InputConfig: extracttest.ScanInputMockConfig{
   185  				Path: "testdata/maven/interpolation.xml",
   186  			},
   187  			WantPackages: []*extractor.Package{
   188  				{
   189  					Name:      "org.mine:mypackage",
   190  					Version:   "1.0.0",
   191  					PURLType:  purl.TypeMaven,
   192  					Locations: []string{"testdata/maven/interpolation.xml"},
   193  					Metadata: &javalockfile.Metadata{
   194  						ArtifactID:   "mypackage",
   195  						GroupID:      "org.mine",
   196  						IsTransitive: false,
   197  						DepGroupVals: []string{},
   198  					},
   199  				},
   200  				{
   201  					Name:      "org.mine:my.package",
   202  					Version:   "2.3.4",
   203  					PURLType:  purl.TypeMaven,
   204  					Locations: []string{"testdata/maven/interpolation.xml"},
   205  					Metadata: &javalockfile.Metadata{
   206  						ArtifactID:   "my.package",
   207  						GroupID:      "org.mine",
   208  						IsTransitive: false,
   209  						DepGroupVals: []string{},
   210  					},
   211  				},
   212  				{
   213  					Name:      "org.mine:ranged-package",
   214  					Version:   "9.4.37",
   215  					PURLType:  purl.TypeMaven,
   216  					Locations: []string{"testdata/maven/interpolation.xml"},
   217  					Metadata: &javalockfile.Metadata{
   218  						ArtifactID:   "ranged-package",
   219  						GroupID:      "org.mine",
   220  						IsTransitive: false,
   221  						DepGroupVals: []string{},
   222  					},
   223  				},
   224  			},
   225  		},
   226  		{
   227  			Name: "with scope / dep groups",
   228  			InputConfig: extracttest.ScanInputMockConfig{
   229  				Path: "testdata/maven/with-scope.xml",
   230  			},
   231  			WantPackages: []*extractor.Package{
   232  				{
   233  					Name:      "junit:junit",
   234  					Version:   "4.12",
   235  					PURLType:  purl.TypeMaven,
   236  					Locations: []string{"testdata/maven/with-scope.xml"},
   237  					Metadata: &javalockfile.Metadata{
   238  						ArtifactID:   "junit",
   239  						GroupID:      "junit",
   240  						IsTransitive: false,
   241  						DepGroupVals: []string{"runtime"},
   242  					},
   243  				},
   244  			},
   245  		},
   246  		{
   247  			Name: "transitive dependencies",
   248  			InputConfig: extracttest.ScanInputMockConfig{
   249  				Path: "testdata/maven/transitive.xml",
   250  			},
   251  			WantPackages: []*extractor.Package{
   252  				{
   253  					Name:      "org.direct:alice",
   254  					Version:   "1.0.0",
   255  					PURLType:  purl.TypeMaven,
   256  					Locations: []string{"testdata/maven/transitive.xml"},
   257  					Metadata: &javalockfile.Metadata{
   258  						ArtifactID:   "alice",
   259  						GroupID:      "org.direct",
   260  						IsTransitive: false,
   261  						DepGroupVals: []string{},
   262  					},
   263  				},
   264  				{
   265  					Name:      "org.direct:bob",
   266  					Version:   "2.0.0",
   267  					PURLType:  purl.TypeMaven,
   268  					Locations: []string{"testdata/maven/transitive.xml"},
   269  					Metadata: &javalockfile.Metadata{
   270  						ArtifactID:   "bob",
   271  						GroupID:      "org.direct",
   272  						IsTransitive: false,
   273  						DepGroupVals: []string{},
   274  					},
   275  				},
   276  				{
   277  					Name:      "org.direct:chris",
   278  					Version:   "3.0.0",
   279  					PURLType:  purl.TypeMaven,
   280  					Locations: []string{"testdata/maven/transitive.xml"},
   281  					Metadata: &javalockfile.Metadata{
   282  						ArtifactID:   "chris",
   283  						GroupID:      "org.direct",
   284  						IsTransitive: false,
   285  						DepGroupVals: []string{},
   286  					},
   287  				},
   288  				{
   289  					Name:      "org.transitive:chuck",
   290  					Version:   "1.1.1",
   291  					PURLType:  purl.TypeMaven,
   292  					Locations: []string{"testdata/maven/transitive.xml"},
   293  					Metadata: &javalockfile.Metadata{
   294  						ArtifactID:   "chuck",
   295  						GroupID:      "org.transitive",
   296  						IsTransitive: true,
   297  						DepGroupVals: []string{},
   298  					},
   299  				},
   300  				{
   301  					Name:      "org.transitive:dave",
   302  					Version:   "2.2.2",
   303  					PURLType:  purl.TypeMaven,
   304  					Locations: []string{"testdata/maven/transitive.xml"},
   305  					Metadata: &javalockfile.Metadata{
   306  						ArtifactID:   "dave",
   307  						GroupID:      "org.transitive",
   308  						IsTransitive: true,
   309  						DepGroupVals: []string{},
   310  					},
   311  				},
   312  				{
   313  					Name:      "org.transitive:eve",
   314  					Version:   "3.3.3",
   315  					PURLType:  purl.TypeMaven,
   316  					Locations: []string{"testdata/maven/transitive.xml"},
   317  					Metadata: &javalockfile.Metadata{
   318  						ArtifactID:   "eve",
   319  						GroupID:      "org.transitive",
   320  						IsTransitive: true,
   321  						DepGroupVals: []string{},
   322  					},
   323  				},
   324  				{
   325  					Name:      "org.transitive:frank",
   326  					Version:   "4.4.4",
   327  					PURLType:  purl.TypeMaven,
   328  					Locations: []string{"testdata/maven/transitive.xml"},
   329  					Metadata: &javalockfile.Metadata{
   330  						ArtifactID:   "frank",
   331  						GroupID:      "org.transitive",
   332  						IsTransitive: true,
   333  						DepGroupVals: []string{},
   334  					},
   335  				},
   336  			},
   337  		},
   338  	}
   339  
   340  	for _, tt := range tests {
   341  		t.Run(tt.Name, func(t *testing.T) {
   342  			resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
   343  			extr := pomxmlnet.New(&cpb.PluginConfig{})
   344  			extr.(*pomxmlnet.Extractor).DepClient = resolutionClient
   345  
   346  			scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig)
   347  			defer extracttest.CloseTestScanInput(t, scanInput)
   348  
   349  			got, err := extr.Extract(t.Context(), &scanInput)
   350  
   351  			if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" {
   352  				t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   353  				return
   354  			}
   355  
   356  			wantInv := inventory.Inventory{Packages: tt.WantPackages}
   357  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" {
   358  				t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   359  			}
   360  		})
   361  	}
   362  }
   363  
   364  func TestExtractor_Extract_WithMockServer(t *testing.T) {
   365  	tt := extracttest.TestTableEntry{
   366  		// Name: "with parent",
   367  		InputConfig: extracttest.ScanInputMockConfig{
   368  			Path: "testdata/maven/with-parent.xml",
   369  		},
   370  		WantPackages: []*extractor.Package{
   371  			{
   372  				Name:      "org.alice:alice",
   373  				Version:   "1.0.0",
   374  				PURLType:  purl.TypeMaven,
   375  				Locations: []string{"testdata/maven/with-parent.xml"},
   376  				Metadata: &javalockfile.Metadata{
   377  					ArtifactID:   "alice",
   378  					GroupID:      "org.alice",
   379  					IsTransitive: false,
   380  					DepGroupVals: []string{},
   381  				},
   382  			},
   383  			{
   384  				Name:      "org.bob:bob",
   385  				Version:   "2.0.0",
   386  				PURLType:  purl.TypeMaven,
   387  				Locations: []string{"testdata/maven/with-parent.xml"},
   388  				Metadata: &javalockfile.Metadata{
   389  					ArtifactID:   "bob",
   390  					GroupID:      "org.bob",
   391  					IsTransitive: false,
   392  					DepGroupVals: []string{},
   393  				},
   394  			},
   395  			{
   396  				Name:      "org.chuck:chuck",
   397  				Version:   "3.0.0",
   398  				PURLType:  purl.TypeMaven,
   399  				Locations: []string{"testdata/maven/with-parent.xml"},
   400  				Metadata: &javalockfile.Metadata{
   401  					ArtifactID:   "chuck",
   402  					GroupID:      "org.chuck",
   403  					IsTransitive: false,
   404  					DepGroupVals: []string{},
   405  				},
   406  			},
   407  			{
   408  				Name:      "org.dave:dave",
   409  				Version:   "4.0.0",
   410  				PURLType:  purl.TypeMaven,
   411  				Locations: []string{"testdata/maven/with-parent.xml"},
   412  				Metadata: &javalockfile.Metadata{
   413  					ArtifactID:   "dave",
   414  					GroupID:      "org.dave",
   415  					IsTransitive: false,
   416  					DepGroupVals: []string{},
   417  				},
   418  			},
   419  			{
   420  				Name:      "org.eve:eve",
   421  				Version:   "5.0.0",
   422  				PURLType:  purl.TypeMaven,
   423  				Locations: []string{"testdata/maven/with-parent.xml"},
   424  				Metadata: &javalockfile.Metadata{
   425  					ArtifactID:   "eve",
   426  					GroupID:      "org.eve",
   427  					IsTransitive: false,
   428  					DepGroupVals: []string{},
   429  				},
   430  			},
   431  			{
   432  				Name:      "org.frank:frank",
   433  				Version:   "6.0.0",
   434  				PURLType:  purl.TypeMaven,
   435  				Locations: []string{"testdata/maven/with-parent.xml"},
   436  				Metadata: &javalockfile.Metadata{
   437  					ArtifactID:   "frank",
   438  					GroupID:      "org.frank",
   439  					IsTransitive: false,
   440  					DepGroupVals: []string{},
   441  				},
   442  			},
   443  		},
   444  	}
   445  
   446  	srv := clienttest.NewMockHTTPServer(t)
   447  	srv.SetResponse(t, "org/upstream/parent-pom/1.0/parent-pom-1.0.pom", []byte(`
   448  	<project>
   449  	  <groupId>org.upstream</groupId>
   450  	  <artifactId>parent-pom</artifactId>
   451  	  <version>1.0</version>
   452  	  <packaging>pom</packaging>
   453  		<dependencies>
   454        <dependency>
   455          <groupId>org.eve</groupId>
   456          <artifactId>eve</artifactId>
   457          <version>5.0.0</version>
   458        </dependency>
   459  		</dependencies>
   460  	</project>
   461  	`))
   462  	srv.SetResponse(t, "org/import/import/1.2.3/import-1.2.3.pom", []byte(`
   463  	<project>
   464  	  <groupId>org.import</groupId>
   465  	  <artifactId>import</artifactId>
   466  	  <version>1.2.3</version>
   467  	  <packaging>pom</packaging>
   468  	  <dependencyManagement>
   469        <dependencies>
   470          <dependency>
   471            <groupId>org.frank</groupId>
   472            <artifactId>frank</artifactId>
   473            <version>6.0.0</version>
   474          </dependency>
   475        </dependencies>
   476  	  </dependencyManagement>
   477  	</project>
   478  	`))
   479  
   480  	apiClient, err := datasource.NewDefaultMavenRegistryAPIClient(t.Context(), srv.URL)
   481  	if err != nil {
   482  		t.Fatalf("%v", err)
   483  	}
   484  
   485  	resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
   486  	extr := pomxmlnet.New(&cpb.PluginConfig{})
   487  	extr.(*pomxmlnet.Extractor).DepClient = resolutionClient
   488  	extr.(*pomxmlnet.Extractor).MavenClient = apiClient
   489  
   490  	scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig)
   491  	defer extracttest.CloseTestScanInput(t, scanInput)
   492  
   493  	got, err := extr.Extract(t.Context(), &scanInput)
   494  
   495  	if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" {
   496  		t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   497  		return
   498  	}
   499  
   500  	wantInv := inventory.Inventory{Packages: tt.WantPackages}
   501  	if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" {
   502  		t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   503  	}
   504  }