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