github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/pkg/cataloger/java/archive_parser_test.go (about)

     1  package java
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"syscall"
    13  	"testing"
    14  
    15  	"github.com/gookit/color"
    16  	"github.com/nextlinux/gosbom/gosbom/file"
    17  	"github.com/nextlinux/gosbom/gosbom/pkg"
    18  	"github.com/nextlinux/gosbom/gosbom/pkg/cataloger/internal/pkgtest"
    19  	"github.com/nextlinux/gosbom/internal"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  func generateJavaBuildFixture(t *testing.T, fixturePath string) {
    24  	if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
    25  		// fixture already exists...
    26  		return
    27  	}
    28  
    29  	makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/")
    30  	t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
    31  
    32  	cwd, err := os.Getwd()
    33  	if err != nil {
    34  		t.Errorf("unable to get cwd: %+v", err)
    35  	}
    36  
    37  	cmd := exec.Command("make", makeTask)
    38  	cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/")
    39  
    40  	stderr, err := cmd.StderrPipe()
    41  	if err != nil {
    42  		t.Fatalf("could not get stderr: %+v", err)
    43  	}
    44  	stdout, err := cmd.StdoutPipe()
    45  	if err != nil {
    46  		t.Fatalf("could not get stdout: %+v", err)
    47  	}
    48  
    49  	err = cmd.Start()
    50  	if err != nil {
    51  		t.Fatalf("failed to start cmd: %+v", err)
    52  	}
    53  
    54  	show := func(label string, reader io.ReadCloser) {
    55  		scanner := bufio.NewScanner(reader)
    56  		scanner.Split(bufio.ScanLines)
    57  		for scanner.Scan() {
    58  			t.Logf("%s: %s", label, scanner.Text())
    59  		}
    60  	}
    61  	go show("out", stdout)
    62  	go show("err", stderr)
    63  
    64  	if err := cmd.Wait(); err != nil {
    65  		if exiterr, ok := err.(*exec.ExitError); ok {
    66  			// The program has exited with an exit code != 0
    67  
    68  			// This works on both Unix and Windows. Although package
    69  			// syscall is generally platform dependent, WaitStatus is
    70  			// defined for both Unix and Windows and in both cases has
    71  			// an ExitStatus() method with the same signature.
    72  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
    73  				if status.ExitStatus() != 0 {
    74  					t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
    75  				}
    76  			}
    77  		} else {
    78  			t.Fatalf("unable to get generate fixture result: %+v", err)
    79  		}
    80  	}
    81  }
    82  
    83  func TestParseJar(t *testing.T) {
    84  	tests := []struct {
    85  		fixture      string
    86  		expected     map[string]pkg.Package
    87  		ignoreExtras []string
    88  	}{
    89  		{
    90  			fixture: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
    91  			ignoreExtras: []string{
    92  				"Plugin-Version", // has dynamic date
    93  				"Built-By",       // podman returns the real UID
    94  				"Build-Jdk",      // can't guarantee the JDK used at build time
    95  			},
    96  			expected: map[string]pkg.Package{
    97  				"example-jenkins-plugin": {
    98  					Name:    "example-jenkins-plugin",
    99  					Version: "1.0-SNAPSHOT",
   100  					PURL:    "pkg:maven/io.jenkins.plugins/example-jenkins-plugin@1.0-SNAPSHOT",
   101  					Licenses: pkg.NewLicenseSet(
   102  						pkg.NewLicenseFromLocations("MIT License", file.NewLocation("test-fixtures/java-builds/packages/example-jenkins-plugin.hpi")),
   103  					),
   104  					Language:     pkg.Java,
   105  					Type:         pkg.JenkinsPluginPkg,
   106  					MetadataType: pkg.JavaMetadataType,
   107  					Metadata: pkg.JavaMetadata{
   108  						VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
   109  						Manifest: &pkg.JavaManifest{
   110  							Main: map[string]string{
   111  								"Manifest-Version":       "1.0",
   112  								"Specification-Title":    "Example Jenkins Plugin",
   113  								"Specification-Version":  "1.0",
   114  								"Implementation-Title":   "Example Jenkins Plugin",
   115  								"Implementation-Version": "1.0-SNAPSHOT",
   116  								// extra fields...
   117  								//"Archiver-Version":    "Plexus Archiver",
   118  								"Plugin-License-Url":  "https://opensource.org/licenses/MIT",
   119  								"Plugin-License-Name": "MIT License",
   120  								"Created-By":          "Maven Archiver 3.6.0",
   121  								//"Built-By":            "?",
   122  								//"Build-Jdk":            "14.0.1",
   123  								"Build-Jdk-Spec":  "18",
   124  								"Jenkins-Version": "2.204",
   125  								//"Minimum-Java-Version": "1.8",
   126  								"Plugin-Developers": "",
   127  								"Plugin-ScmUrl":     "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
   128  								//"Extension-Name":      "example-jenkins-plugin",
   129  								"Short-Name":          "example-jenkins-plugin",
   130  								"Group-Id":            "io.jenkins.plugins",
   131  								"Plugin-Dependencies": "structs:1.20",
   132  								//"Plugin-Version": "1.0-SNAPSHOT (private-07/09/2020 13:30-?)",
   133  								"Hudson-Version": "2.204",
   134  								"Long-Name":      "Example Jenkins Plugin",
   135  							},
   136  						},
   137  						PomProperties: &pkg.PomProperties{
   138  							Path:       "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.properties",
   139  							GroupID:    "io.jenkins.plugins",
   140  							ArtifactID: "example-jenkins-plugin",
   141  							Version:    "1.0-SNAPSHOT",
   142  						},
   143  					},
   144  				},
   145  			},
   146  		},
   147  		{
   148  			fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
   149  			expected: map[string]pkg.Package{
   150  				"example-java-app-gradle": {
   151  					Name:         "example-java-app-gradle",
   152  					Version:      "0.1.0",
   153  					PURL:         "pkg:maven/example-java-app-gradle/example-java-app-gradle@0.1.0",
   154  					Language:     pkg.Java,
   155  					Type:         pkg.JavaPkg,
   156  					MetadataType: pkg.JavaMetadataType,
   157  					Metadata: pkg.JavaMetadata{
   158  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
   159  						Manifest: &pkg.JavaManifest{
   160  							Main: map[string]string{
   161  								"Manifest-Version": "1.0",
   162  								"Main-Class":       "hello.HelloWorld",
   163  							},
   164  						},
   165  					},
   166  				},
   167  				"joda-time": {
   168  					Name:         "joda-time",
   169  					Version:      "2.2",
   170  					PURL:         "pkg:maven/joda-time/joda-time@2.2",
   171  					Language:     pkg.Java,
   172  					Type:         pkg.JavaPkg,
   173  					MetadataType: pkg.JavaMetadataType,
   174  					Metadata: pkg.JavaMetadata{
   175  						// ensure that nested packages with different names than that of the parent are appended as
   176  						// a suffix on the virtual path
   177  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar:joda-time",
   178  						PomProperties: &pkg.PomProperties{
   179  							Path:       "META-INF/maven/joda-time/joda-time/pom.properties",
   180  							GroupID:    "joda-time",
   181  							ArtifactID: "joda-time",
   182  							Version:    "2.2",
   183  						},
   184  						PomProject: &pkg.PomProject{
   185  							Path:        "META-INF/maven/joda-time/joda-time/pom.xml",
   186  							GroupID:     "joda-time",
   187  							ArtifactID:  "joda-time",
   188  							Version:     "2.2",
   189  							Name:        "Joda time",
   190  							Description: "Date and time library to replace JDK date handling",
   191  							URL:         "http://joda-time.sourceforge.net",
   192  						},
   193  					},
   194  				},
   195  			},
   196  		},
   197  		{
   198  			fixture: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
   199  			ignoreExtras: []string{
   200  				"Build-Jdk", // can't guarantee the JDK used at build time
   201  				"Built-By",  // podman returns the real UID
   202  			},
   203  			expected: map[string]pkg.Package{
   204  				"example-java-app-maven": {
   205  					Name:         "example-java-app-maven",
   206  					Version:      "0.1.0",
   207  					PURL:         "pkg:maven/org.anchore/example-java-app-maven@0.1.0",
   208  					Language:     pkg.Java,
   209  					Type:         pkg.JavaPkg,
   210  					MetadataType: pkg.JavaMetadataType,
   211  					Metadata: pkg.JavaMetadata{
   212  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
   213  						Manifest: &pkg.JavaManifest{
   214  							Main: map[string]string{
   215  								"Manifest-Version": "1.0",
   216  								// extra fields...
   217  								"Archiver-Version": "Plexus Archiver",
   218  								"Created-By":       "Apache Maven 3.8.6",
   219  								//"Built-By":         "?",
   220  								//"Build-Jdk":        "14.0.1",
   221  								"Main-Class": "hello.HelloWorld",
   222  							},
   223  						},
   224  						PomProperties: &pkg.PomProperties{
   225  							Path:       "META-INF/maven/org.anchore/example-java-app-maven/pom.properties",
   226  							GroupID:    "org.anchore",
   227  							ArtifactID: "example-java-app-maven",
   228  							Version:    "0.1.0",
   229  						},
   230  					},
   231  				},
   232  				"joda-time": {
   233  					Name:         "joda-time",
   234  					Version:      "2.9.2",
   235  					PURL:         "pkg:maven/joda-time/joda-time@2.9.2",
   236  					Language:     pkg.Java,
   237  					Type:         pkg.JavaPkg,
   238  					MetadataType: pkg.JavaMetadataType,
   239  					Metadata: pkg.JavaMetadata{
   240  						// ensure that nested packages with different names than that of the parent are appended as
   241  						// a suffix on the virtual path
   242  						VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time",
   243  						PomProperties: &pkg.PomProperties{
   244  							Path:       "META-INF/maven/joda-time/joda-time/pom.properties",
   245  							GroupID:    "joda-time",
   246  							ArtifactID: "joda-time",
   247  							Version:    "2.9.2",
   248  						},
   249  						PomProject: &pkg.PomProject{
   250  							Path:        "META-INF/maven/joda-time/joda-time/pom.xml",
   251  							GroupID:     "joda-time",
   252  							ArtifactID:  "joda-time",
   253  							Version:     "2.9.2",
   254  							Name:        "Joda-Time",
   255  							Description: "Date and time library to replace JDK date handling",
   256  							URL:         "http://www.joda.org/joda-time/",
   257  						},
   258  					},
   259  				},
   260  			},
   261  		},
   262  	}
   263  
   264  	for _, test := range tests {
   265  		t.Run(path.Base(test.fixture), func(t *testing.T) {
   266  
   267  			generateJavaBuildFixture(t, test.fixture)
   268  
   269  			fixture, err := os.Open(test.fixture)
   270  			require.NoError(t, err)
   271  
   272  			for k := range test.expected {
   273  				p := test.expected[k]
   274  				p.Locations.Add(file.NewLocation(test.fixture))
   275  				test.expected[k] = p
   276  			}
   277  
   278  			parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{
   279  				Location:   file.NewLocation(fixture.Name()),
   280  				ReadCloser: fixture,
   281  			}, false)
   282  			defer cleanupFn()
   283  			require.NoError(t, err)
   284  
   285  			actual, _, err := parser.parse()
   286  			require.NoError(t, err)
   287  
   288  			if len(actual) != len(test.expected) {
   289  				for _, a := range actual {
   290  					t.Log("   ", a)
   291  				}
   292  				t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
   293  			}
   294  
   295  			var parent *pkg.Package
   296  			for _, a := range actual {
   297  				a := a
   298  				if strings.Contains(a.Name, "example-") {
   299  					parent = &a
   300  				}
   301  			}
   302  
   303  			if parent == nil {
   304  				t.Fatal("could not find the parent pkg")
   305  			}
   306  
   307  			for _, a := range actual {
   308  				if a.ID() == "" {
   309  					t.Fatalf("empty package ID: %+v", a)
   310  				}
   311  
   312  				e, ok := test.expected[a.Name]
   313  				if !ok {
   314  					t.Errorf("entry not found: %s", a.Name)
   315  					continue
   316  				}
   317  
   318  				if a.Name != parent.Name && a.Metadata.(pkg.JavaMetadata).Parent != nil && a.Metadata.(pkg.JavaMetadata).Parent.Name != parent.Name {
   319  					t.Errorf("mismatched parent: %+v", a.Metadata.(pkg.JavaMetadata).Parent)
   320  				}
   321  
   322  				// we need to compare the other fields without parent attached
   323  				metadata := a.Metadata.(pkg.JavaMetadata)
   324  				metadata.Parent = nil
   325  
   326  				// redact Digest which is computed differently between CI and local
   327  				if len(metadata.ArchiveDigests) > 0 {
   328  					metadata.ArchiveDigests = nil
   329  				}
   330  
   331  				// ignore select fields (only works for the main section)
   332  				for _, field := range test.ignoreExtras {
   333  					if metadata.Manifest != nil && metadata.Manifest.Main != nil {
   334  						delete(metadata.Manifest.Main, field)
   335  					}
   336  				}
   337  
   338  				// write censored data back
   339  				a.Metadata = metadata
   340  
   341  				pkgtest.AssertPackagesEqual(t, e, a)
   342  			}
   343  		})
   344  	}
   345  }
   346  
   347  func TestParseNestedJar(t *testing.T) {
   348  	tests := []struct {
   349  		fixture      string
   350  		expected     []pkg.Package
   351  		ignoreExtras []string
   352  	}{
   353  		{
   354  			fixture: "test-fixtures/java-builds/packages/spring-boot-0.0.1-SNAPSHOT.jar",
   355  			expected: []pkg.Package{
   356  				{
   357  					Name:    "spring-boot",
   358  					Version: "0.0.1-SNAPSHOT",
   359  				},
   360  				{
   361  					Name:    "spring-boot-starter",
   362  					Version: "2.2.2.RELEASE",
   363  				},
   364  				{
   365  					Name:    "jul-to-slf4j",
   366  					Version: "1.7.29",
   367  				},
   368  				{
   369  					Name:    "tomcat-embed-websocket",
   370  					Version: "9.0.29",
   371  				},
   372  				{
   373  					Name:    "spring-boot-starter-validation",
   374  					Version: "2.2.2.RELEASE",
   375  				},
   376  				{
   377  					Name:    "hibernate-validator",
   378  					Version: "6.0.18.Final",
   379  				},
   380  				{
   381  					Name:    "jboss-logging",
   382  					Version: "3.4.1.Final",
   383  				},
   384  				{
   385  					Name:    "spring-expression",
   386  					Version: "5.2.2.RELEASE",
   387  				},
   388  				{
   389  					Name:    "jakarta.validation-api",
   390  					Version: "2.0.1",
   391  				},
   392  				{
   393  					Name:    "spring-web",
   394  					Version: "5.2.2.RELEASE",
   395  				},
   396  				{
   397  					Name:    "spring-boot-starter-actuator",
   398  					Version: "2.2.2.RELEASE",
   399  				},
   400  				{
   401  					Name:    "log4j-api",
   402  					Version: "2.12.1",
   403  				},
   404  				{
   405  					Name:    "snakeyaml",
   406  					Version: "1.25",
   407  				},
   408  				{
   409  					Name:    "jackson-core",
   410  					Version: "2.10.1",
   411  				},
   412  				{
   413  					Name:    "jackson-datatype-jsr310",
   414  					Version: "2.10.1",
   415  				},
   416  				{
   417  					Name:    "spring-aop",
   418  					Version: "5.2.2.RELEASE",
   419  				},
   420  				{
   421  					Name:    "spring-boot-actuator-autoconfigure",
   422  					Version: "2.2.2.RELEASE",
   423  				},
   424  				{
   425  					Name:    "spring-jcl",
   426  					Version: "5.2.2.RELEASE",
   427  				},
   428  				{
   429  					Name:    "spring-boot",
   430  					Version: "2.2.2.RELEASE",
   431  				},
   432  				{
   433  					Name:    "spring-boot-starter-logging",
   434  					Version: "2.2.2.RELEASE",
   435  				},
   436  				{
   437  					Name:    "jakarta.annotation-api",
   438  					Version: "1.3.5",
   439  				},
   440  				{
   441  					Name:    "spring-webmvc",
   442  					Version: "5.2.2.RELEASE",
   443  				},
   444  				{
   445  					Name:    "HdrHistogram",
   446  					Version: "2.1.11",
   447  				},
   448  				{
   449  					Name:    "spring-boot-starter-web",
   450  					Version: "2.2.2.RELEASE",
   451  				},
   452  				{
   453  					Name:    "logback-classic",
   454  					Version: "1.2.3",
   455  				},
   456  				{
   457  					Name:    "log4j-to-slf4j",
   458  					Version: "2.12.1",
   459  				},
   460  				{
   461  					Name:    "spring-boot-starter-json",
   462  					Version: "2.2.2.RELEASE",
   463  				},
   464  				{
   465  					Name:    "jackson-databind",
   466  					Version: "2.10.1",
   467  				},
   468  				{
   469  					Name:    "jackson-module-parameter-names",
   470  					Version: "2.10.1",
   471  				},
   472  				{
   473  					Name:    "LatencyUtils",
   474  					Version: "2.0.3",
   475  				},
   476  				{
   477  					Name:    "spring-boot-autoconfigure",
   478  					Version: "2.2.2.RELEASE",
   479  				},
   480  				{
   481  					Name:    "jackson-datatype-jdk8",
   482  					Version: "2.10.1",
   483  				},
   484  				{
   485  					Name:    "tomcat-embed-core",
   486  					Version: "9.0.29",
   487  				},
   488  				{
   489  					Name:    "tomcat-embed-el",
   490  					Version: "9.0.29",
   491  				},
   492  				{
   493  					Name:    "spring-beans",
   494  					Version: "5.2.2.RELEASE",
   495  				},
   496  				{
   497  					Name:    "spring-boot-actuator",
   498  					Version: "2.2.2.RELEASE",
   499  				},
   500  				{
   501  					Name:    "slf4j-api",
   502  					Version: "1.7.29",
   503  				},
   504  				{
   505  					Name:    "spring-core",
   506  					Version: "5.2.2.RELEASE",
   507  				},
   508  				{
   509  					Name:    "logback-core",
   510  					Version: "1.2.3",
   511  				},
   512  				{
   513  					Name:    "micrometer-core",
   514  					Version: "1.3.1",
   515  				},
   516  				{
   517  					Name:    "pcollections",
   518  					Version: "3.1.0",
   519  				},
   520  				{
   521  					Name:    "jackson-annotations",
   522  					Version: "2.10.1",
   523  				},
   524  				{
   525  					Name:    "spring-boot-starter-tomcat",
   526  					Version: "2.2.2.RELEASE",
   527  				},
   528  				{
   529  					Name:    "classmate",
   530  					Version: "1.5.1",
   531  				},
   532  				{
   533  					Name:    "spring-context",
   534  					Version: "5.2.2.RELEASE",
   535  				},
   536  			},
   537  		},
   538  	}
   539  
   540  	for _, test := range tests {
   541  		t.Run(test.fixture, func(t *testing.T) {
   542  
   543  			generateJavaBuildFixture(t, test.fixture)
   544  
   545  			fixture, err := os.Open(test.fixture)
   546  			require.NoError(t, err)
   547  
   548  			actual, _, err := parseJavaArchive(nil, nil, file.LocationReadCloser{
   549  				Location:   file.NewLocation(fixture.Name()),
   550  				ReadCloser: fixture,
   551  			})
   552  			require.NoError(t, err)
   553  
   554  			expectedNameVersionPairSet := internal.NewStringSet()
   555  
   556  			makeKey := func(p *pkg.Package) string {
   557  				if p == nil {
   558  					t.Fatal("cannot make key for nil pkg")
   559  				}
   560  				return fmt.Sprintf("%s|%s", p.Name, p.Version)
   561  			}
   562  
   563  			for _, e := range test.expected {
   564  				expectedNameVersionPairSet.Add(makeKey(&e))
   565  			}
   566  
   567  			actualNameVersionPairSet := internal.NewStringSet()
   568  			for _, a := range actual {
   569  				a := a
   570  				key := makeKey(&a)
   571  				actualNameVersionPairSet.Add(key)
   572  				if !expectedNameVersionPairSet.Contains(key) {
   573  					t.Errorf("extra package: %s", a)
   574  				}
   575  			}
   576  
   577  			for _, key := range expectedNameVersionPairSet.ToSlice() {
   578  				if !actualNameVersionPairSet.Contains(key) {
   579  					t.Errorf("missing package: %s", key)
   580  				}
   581  			}
   582  
   583  			if len(actual) != len(expectedNameVersionPairSet) {
   584  				t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedNameVersionPairSet))
   585  			}
   586  
   587  			for _, a := range actual {
   588  				a := a
   589  				actualKey := makeKey(&a)
   590  
   591  				metadata := a.Metadata.(pkg.JavaMetadata)
   592  				if actualKey == "spring-boot|0.0.1-SNAPSHOT" {
   593  					if metadata.Parent != nil {
   594  						t.Errorf("expected no parent for root pkg, got %q", makeKey(metadata.Parent))
   595  					}
   596  				} else {
   597  					if metadata.Parent == nil {
   598  						t.Errorf("unassigned error for pkg=%q", actualKey)
   599  					} else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" {
   600  						// NB: this is a hard-coded condition to simplify the test harness to account for https://github.com/micrometer-metrics/micrometer/issues/1785
   601  						if a.Name == "pcollections" {
   602  							if metadata.Parent.Name != "micrometer-core" {
   603  								t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name)
   604  							}
   605  						} else {
   606  							t.Errorf("bad parent for pkg=%q parent=%q", actualKey, makeKey(metadata.Parent))
   607  						}
   608  					}
   609  				}
   610  			}
   611  		})
   612  	}
   613  }
   614  
   615  func Test_newPackageFromMavenData(t *testing.T) {
   616  	virtualPath := "given/virtual/path"
   617  	tests := []struct {
   618  		name            string
   619  		props           pkg.PomProperties
   620  		project         *pkg.PomProject
   621  		parent          *pkg.Package
   622  		expectedParent  pkg.Package
   623  		expectedPackage *pkg.Package
   624  	}{
   625  		{
   626  			name: "go case: get a single package from pom properties",
   627  			props: pkg.PomProperties{
   628  				Name:       "some-name",
   629  				GroupID:    "some-group-id",
   630  				ArtifactID: "some-artifact-id",
   631  				Version:    "1.0",
   632  			},
   633  			parent: &pkg.Package{
   634  				Name:    "some-parent-name",
   635  				Version: "2.0",
   636  				Metadata: pkg.JavaMetadata{
   637  					VirtualPath:   "some-parent-virtual-path",
   638  					Manifest:      nil,
   639  					PomProperties: nil,
   640  					Parent:        nil,
   641  				},
   642  			},
   643  			// note: the SAME as the original parent values
   644  			expectedParent: pkg.Package{
   645  				Name:    "some-parent-name",
   646  				Version: "2.0",
   647  				Metadata: pkg.JavaMetadata{
   648  					VirtualPath:   "some-parent-virtual-path",
   649  					Manifest:      nil,
   650  					PomProperties: nil,
   651  					Parent:        nil,
   652  				},
   653  			},
   654  			expectedPackage: &pkg.Package{
   655  				Name:         "some-artifact-id",
   656  				Version:      "1.0",
   657  				Language:     pkg.Java,
   658  				Type:         pkg.JavaPkg,
   659  				MetadataType: pkg.JavaMetadataType,
   660  				Metadata: pkg.JavaMetadata{
   661  					VirtualPath: virtualPath + ":" + "some-artifact-id",
   662  					PomProperties: &pkg.PomProperties{
   663  						Name:       "some-name",
   664  						GroupID:    "some-group-id",
   665  						ArtifactID: "some-artifact-id",
   666  						Version:    "1.0",
   667  					},
   668  					Parent: &pkg.Package{
   669  						Name:    "some-parent-name",
   670  						Version: "2.0",
   671  						Metadata: pkg.JavaMetadata{
   672  							VirtualPath:   "some-parent-virtual-path",
   673  							Manifest:      nil,
   674  							PomProperties: nil,
   675  							Parent:        nil,
   676  						},
   677  					},
   678  				},
   679  			},
   680  		},
   681  		{
   682  			name: "get a single package from pom properties + project",
   683  			props: pkg.PomProperties{
   684  				Name:       "some-name",
   685  				GroupID:    "some-group-id",
   686  				ArtifactID: "some-artifact-id",
   687  				Version:    "1.0",
   688  			},
   689  			project: &pkg.PomProject{
   690  				Parent: &pkg.PomParent{
   691  					GroupID:    "some-parent-group-id",
   692  					ArtifactID: "some-parent-artifact-id",
   693  					Version:    "1.0-parent",
   694  				},
   695  				Name:        "some-name",
   696  				GroupID:     "some-group-id",
   697  				ArtifactID:  "some-artifact-id",
   698  				Version:     "1.0",
   699  				Description: "desc",
   700  				URL:         "aweso.me",
   701  			},
   702  			parent: &pkg.Package{
   703  				Name:    "some-parent-name",
   704  				Version: "2.0",
   705  				Metadata: pkg.JavaMetadata{
   706  					VirtualPath:   "some-parent-virtual-path",
   707  					Manifest:      nil,
   708  					PomProperties: nil,
   709  					Parent:        nil,
   710  				},
   711  			},
   712  			// note: the SAME as the original parent values
   713  			expectedParent: pkg.Package{
   714  				Name:    "some-parent-name",
   715  				Version: "2.0",
   716  				Metadata: pkg.JavaMetadata{
   717  					VirtualPath:   "some-parent-virtual-path",
   718  					Manifest:      nil,
   719  					PomProperties: nil,
   720  					Parent:        nil,
   721  				},
   722  			},
   723  			expectedPackage: &pkg.Package{
   724  				Name:         "some-artifact-id",
   725  				Version:      "1.0",
   726  				Language:     pkg.Java,
   727  				Type:         pkg.JavaPkg,
   728  				MetadataType: pkg.JavaMetadataType,
   729  				Metadata: pkg.JavaMetadata{
   730  					VirtualPath: virtualPath + ":" + "some-artifact-id",
   731  					PomProperties: &pkg.PomProperties{
   732  						Name:       "some-name",
   733  						GroupID:    "some-group-id",
   734  						ArtifactID: "some-artifact-id",
   735  						Version:    "1.0",
   736  					},
   737  					PomProject: &pkg.PomProject{
   738  						Parent: &pkg.PomParent{
   739  							GroupID:    "some-parent-group-id",
   740  							ArtifactID: "some-parent-artifact-id",
   741  							Version:    "1.0-parent",
   742  						},
   743  						Name:        "some-name",
   744  						GroupID:     "some-group-id",
   745  						ArtifactID:  "some-artifact-id",
   746  						Version:     "1.0",
   747  						Description: "desc",
   748  						URL:         "aweso.me",
   749  					},
   750  					Parent: &pkg.Package{
   751  						Name:    "some-parent-name",
   752  						Version: "2.0",
   753  						Metadata: pkg.JavaMetadata{
   754  							VirtualPath:   "some-parent-virtual-path",
   755  							Manifest:      nil,
   756  							PomProperties: nil,
   757  							Parent:        nil,
   758  						},
   759  					},
   760  				},
   761  			},
   762  		},
   763  		{
   764  			name: "single package from pom properties that's a Jenkins plugin",
   765  			props: pkg.PomProperties{
   766  				Name:       "some-name",
   767  				GroupID:    "com.cloudbees.jenkins.plugins",
   768  				ArtifactID: "some-artifact-id",
   769  				Version:    "1.0",
   770  			},
   771  			parent: &pkg.Package{
   772  				Name:    "some-parent-name",
   773  				Version: "2.0",
   774  				Metadata: pkg.JavaMetadata{
   775  					VirtualPath:   "some-parent-virtual-path",
   776  					Manifest:      nil,
   777  					PomProperties: nil,
   778  					Parent:        nil,
   779  				},
   780  			},
   781  			// note: the SAME as the original parent values
   782  			expectedParent: pkg.Package{
   783  				Name:    "some-parent-name",
   784  				Version: "2.0",
   785  				Metadata: pkg.JavaMetadata{
   786  					VirtualPath:   "some-parent-virtual-path",
   787  					Manifest:      nil,
   788  					PomProperties: nil,
   789  					Parent:        nil,
   790  				},
   791  			},
   792  			expectedPackage: &pkg.Package{
   793  				Name:         "some-artifact-id",
   794  				Version:      "1.0",
   795  				Language:     pkg.Java,
   796  				Type:         pkg.JenkinsPluginPkg,
   797  				MetadataType: pkg.JavaMetadataType,
   798  				Metadata: pkg.JavaMetadata{
   799  					VirtualPath: virtualPath + ":" + "some-artifact-id",
   800  					PomProperties: &pkg.PomProperties{
   801  						Name:       "some-name",
   802  						GroupID:    "com.cloudbees.jenkins.plugins",
   803  						ArtifactID: "some-artifact-id",
   804  						Version:    "1.0",
   805  					},
   806  					Parent: &pkg.Package{
   807  						Name:    "some-parent-name",
   808  						Version: "2.0",
   809  						Metadata: pkg.JavaMetadata{
   810  							VirtualPath:   "some-parent-virtual-path",
   811  							Manifest:      nil,
   812  							PomProperties: nil,
   813  							Parent:        nil,
   814  						},
   815  					},
   816  				},
   817  			},
   818  		},
   819  		{
   820  			name: "child matches parent by key",
   821  			props: pkg.PomProperties{
   822  				Name:       "some-name",
   823  				GroupID:    "some-group-id",
   824  				ArtifactID: "some-parent-name", // note: matches parent package
   825  				Version:    "2.0",              // note: matches parent package
   826  			},
   827  			parent: &pkg.Package{
   828  				Name:    "some-parent-name",
   829  				Version: "2.0",
   830  				Type:    pkg.JavaPkg,
   831  				Metadata: pkg.JavaMetadata{
   832  					VirtualPath:   "some-parent-virtual-path",
   833  					Manifest:      nil,
   834  					PomProperties: nil,
   835  					Parent:        nil,
   836  				},
   837  			},
   838  			// note: the SAME as the original parent values
   839  			expectedParent: pkg.Package{
   840  				Name:    "some-parent-name",
   841  				Version: "2.0",
   842  				Type:    pkg.JavaPkg,
   843  				Metadata: pkg.JavaMetadata{
   844  					VirtualPath: "some-parent-virtual-path",
   845  					Manifest:    nil,
   846  					// note: we attach the discovered pom properties data
   847  					PomProperties: &pkg.PomProperties{
   848  						Name:       "some-name",
   849  						GroupID:    "some-group-id",
   850  						ArtifactID: "some-parent-name", // note: matches parent package
   851  						Version:    "2.0",              // note: matches parent package
   852  					},
   853  					Parent: nil,
   854  				},
   855  			},
   856  			expectedPackage: nil,
   857  		},
   858  		{
   859  			name: "child matches parent by key and is Jenkins plugin",
   860  			props: pkg.PomProperties{
   861  				Name:       "some-name",
   862  				GroupID:    "com.cloudbees.jenkins.plugins",
   863  				ArtifactID: "some-parent-name", // note: matches parent package
   864  				Version:    "2.0",              // note: matches parent package
   865  			},
   866  			parent: &pkg.Package{
   867  				Name:    "some-parent-name",
   868  				Version: "2.0",
   869  				Type:    pkg.JavaPkg,
   870  				Metadata: pkg.JavaMetadata{
   871  					VirtualPath:   "some-parent-virtual-path",
   872  					Manifest:      nil,
   873  					PomProperties: nil,
   874  					Parent:        nil,
   875  				},
   876  			},
   877  			expectedParent: pkg.Package{
   878  				Name:    "some-parent-name",
   879  				Version: "2.0",
   880  				Type:    pkg.JenkinsPluginPkg,
   881  				Metadata: pkg.JavaMetadata{
   882  					VirtualPath: "some-parent-virtual-path",
   883  					Manifest:    nil,
   884  					// note: we attach the discovered pom properties data
   885  					PomProperties: &pkg.PomProperties{
   886  						Name:       "some-name",
   887  						GroupID:    "com.cloudbees.jenkins.plugins",
   888  						ArtifactID: "some-parent-name", // note: matches parent package
   889  						Version:    "2.0",              // note: matches parent package
   890  					},
   891  					Parent: nil,
   892  				},
   893  			},
   894  			expectedPackage: nil,
   895  		},
   896  		{
   897  			name: "child matches parent by virtual path -- override name and version",
   898  			props: pkg.PomProperties{
   899  				Name:       "some-name",
   900  				GroupID:    "some-group-id",
   901  				ArtifactID: "some-parent-name", // note: DOES NOT match parent package
   902  				Version:    "3.0",              // note: DOES NOT match parent package
   903  			},
   904  			parent: &pkg.Package{
   905  				Name:    "", // note: empty, so should not be matched on
   906  				Version: "", // note: empty, so should not be matched on
   907  				Type:    pkg.JavaPkg,
   908  				Metadata: pkg.JavaMetadata{
   909  					VirtualPath:   virtualPath, // note: matching virtual path
   910  					Manifest:      nil,
   911  					PomProperties: nil,
   912  					Parent:        nil,
   913  				},
   914  			},
   915  			expectedParent: pkg.Package{
   916  				Name:    "some-parent-name",
   917  				Version: "3.0",
   918  				Type:    pkg.JavaPkg,
   919  				Metadata: pkg.JavaMetadata{
   920  					VirtualPath: virtualPath,
   921  					Manifest:    nil,
   922  					// note: we attach the discovered pom properties data
   923  					PomProperties: &pkg.PomProperties{
   924  						Name:       "some-name",
   925  						GroupID:    "some-group-id",
   926  						ArtifactID: "some-parent-name",
   927  						Version:    "3.0",
   928  					},
   929  					Parent: nil,
   930  				},
   931  			},
   932  			expectedPackage: nil,
   933  		},
   934  		{
   935  			name: "child matches parent by artifact id",
   936  			props: pkg.PomProperties{
   937  				Name:       "some-name",
   938  				GroupID:    "some-group-id",
   939  				ArtifactID: "some-parent-name",       // note: matches parent package
   940  				Version:    "NOT_THE_PARENT_VERSION", // note: DOES NOT match parent package
   941  			},
   942  			parent: &pkg.Package{
   943  				Name:    "some-parent-name",
   944  				Version: "2.0",
   945  				Type:    pkg.JavaPkg,
   946  				Metadata: pkg.JavaMetadata{
   947  					VirtualPath:   virtualPath + ":NEW_VIRTUAL_PATH", // note: DOES NOT match the existing virtual path
   948  					Manifest:      nil,
   949  					PomProperties: nil,
   950  					Parent:        nil,
   951  				},
   952  			},
   953  			// note: the SAME as the original parent values
   954  			expectedParent: pkg.Package{
   955  				Name:    "some-parent-name",
   956  				Version: "NOT_THE_PARENT_VERSION", // note: the version is updated from pom properties
   957  				Type:    pkg.JavaPkg,
   958  				Metadata: pkg.JavaMetadata{
   959  					VirtualPath: virtualPath + ":NEW_VIRTUAL_PATH",
   960  					Manifest:    nil,
   961  					// note: we attach the discovered pom properties data
   962  					PomProperties: &pkg.PomProperties{
   963  						Name:       "some-name",
   964  						GroupID:    "some-group-id",
   965  						ArtifactID: "some-parent-name",
   966  						Version:    "NOT_THE_PARENT_VERSION",
   967  					},
   968  					Parent: nil,
   969  				},
   970  			},
   971  			expectedPackage: nil,
   972  		},
   973  	}
   974  
   975  	for _, test := range tests {
   976  		t.Run(test.name, func(t *testing.T) {
   977  			locations := file.NewLocationSet(file.NewLocation(virtualPath))
   978  			if test.expectedPackage != nil {
   979  				test.expectedPackage.Locations = locations
   980  				if test.expectedPackage.Metadata.(pkg.JavaMetadata).Parent != nil {
   981  					test.expectedPackage.Metadata.(pkg.JavaMetadata).Parent.Locations = locations
   982  				}
   983  			}
   984  			if test.parent != nil {
   985  				test.parent.Locations = locations
   986  			}
   987  			test.expectedParent.Locations = locations
   988  
   989  			actualPackage := newPackageFromMavenData(test.props, test.project, test.parent, file.NewLocation(virtualPath))
   990  			if test.expectedPackage == nil {
   991  				require.Nil(t, actualPackage)
   992  			} else {
   993  				pkgtest.AssertPackagesEqual(t, *test.expectedPackage, *actualPackage)
   994  			}
   995  
   996  			pkgtest.AssertPackagesEqual(t, test.expectedParent, *test.parent)
   997  		})
   998  	}
   999  }