diff --git a/enrichment_test.go b/enrichment_test.go index 7a1ab6e..2498406 100644 --- a/enrichment_test.go +++ b/enrichment_test.go @@ -12,6 +12,7 @@ import ( "github.com/ecosyste-ms/ecosystems-go" "github.com/ecosyste-ms/ecosystems-go/packages" + "github.com/git-pkgs/registries" "github.com/oapi-codegen/nullable" ) @@ -399,22 +400,122 @@ func TestHasRepositoryURL(t *testing.T) { } } +func TestPURLType(t *testing.T) { + tests := []struct { + purl string + want string + }{ + {"pkg:npm/lodash", "npm"}, + {"not a purl", ""}, + } + + for _, tt := range tests { + t.Run(tt.purl, func(t *testing.T) { + got := purlType(tt.purl) + if got != tt.want { + t.Errorf("purlType(%q) = %q, want %q", tt.purl, got, tt.want) + } + }) + } +} + func TestFindLatestVersion(t *testing.T) { tests := []struct { + name string versions []VersionInfo + scheme string want string }{ - {nil, ""}, - {[]VersionInfo{{Number: "1.0.0"}}, "1.0.0"}, - {[]VersionInfo{{Number: "1.0.0"}, {Number: "2.0.0"}, {Number: "1.5.0"}}, "2.0.0"}, - {[]VersionInfo{{Number: "3.0.0"}, {Number: "1.0.0"}}, "3.0.0"}, + { + name: "empty", + }, + { + name: "single version", + versions: []VersionInfo{{Number: "1.0.0"}}, + scheme: "npm", + want: "1.0.0", + }, + { + name: "highest semver", + versions: []VersionInfo{ + {Number: "1.0.0"}, + {Number: "2.0.0"}, + {Number: "1.5.0"}, + }, + scheme: "npm", + want: "2.0.0", + }, + { + name: "keeps initial latest", + versions: []VersionInfo{ + {Number: "3.0.0"}, + {Number: "1.0.0"}, + }, + scheme: "npm", + want: "3.0.0", + }, + { + name: "uses scheme ordering", + versions: []VersionInfo{ + {Number: "1.0~rc1"}, + {Number: "1.0"}, + }, + scheme: "deb", + want: "1.0", + }, + { + name: "skips yanked status", + versions: []VersionInfo{ + {Number: "2.0.0", Status: string(registries.StatusYanked)}, + {Number: "1.0.0"}, + }, + scheme: "npm", + want: "1.0.0", + }, + { + name: "skips deprecated status", + versions: []VersionInfo{ + {Number: "2.0.0", Status: string(registries.StatusDeprecated)}, + {Number: "1.0.0"}, + }, + scheme: "npm", + want: "1.0.0", + }, + { + name: "skips retracted status", + versions: []VersionInfo{ + {Number: "2.0.0", Status: string(registries.StatusRetracted)}, + {Number: "1.0.0"}, + }, + scheme: "npm", + want: "1.0.0", + }, + { + name: "skips yanked boolean", + versions: []VersionInfo{ + {Number: "2.0.0", Yanked: true}, + {Number: "1.0.0"}, + }, + scheme: "npm", + want: "1.0.0", + }, + { + name: "returns empty when all unavailable", + versions: []VersionInfo{ + {Number: "2.0.0", Status: string(registries.StatusYanked)}, + {Number: "1.0.0", Status: string(registries.StatusDeprecated)}, + }, + scheme: "npm", + }, } for _, tt := range tests { - got := findLatestVersion(tt.versions) - if got != tt.want { - t.Errorf("findLatestVersion() = %q, want %q", got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + got := findLatestVersion(tt.versions, tt.scheme) + if got != tt.want { + t.Errorf("findLatestVersion() = %q, want %q", got, tt.want) + } + }) } } diff --git a/registries.go b/registries.go index ce11d60..c4f65d7 100644 --- a/registries.go +++ b/registries.go @@ -61,7 +61,7 @@ func (c *RegistriesClient) BulkLookup(ctx context.Context, purls []string) (map[ if err != nil || len(versions) == 0 { return } - latest := findLatestVersion(versions) + latest := findLatestVersion(versions, purlType(purlStr)) mu.Lock() latestVersions[purlStr] = latest mu.Unlock() @@ -76,11 +76,7 @@ func (c *RegistriesClient) BulkLookup(ctx context.Context, purls []string) (map[ continue } - p, _ := purl.Parse(purlStr) - ecosystem := "" - if p != nil { - ecosystem = p.Type - } + ecosystem := purlType(purlStr) info := &PackageInfo{ Ecosystem: ecosystem, @@ -112,20 +108,41 @@ func acquireSemaphore(ctx context.Context, sem chan<- struct{}) bool { } } -// findLatestVersion returns the highest version from a list using semver comparison. -func findLatestVersion(versions []VersionInfo) string { - if len(versions) == 0 { +func purlType(purlStr string) string { + p, err := purl.Parse(purlStr) + if err != nil || p == nil { return "" } - latest := versions[0].Number - for _, v := range versions[1:] { - if vers.Compare(v.Number, latest) > 0 { + return p.Type +} + +// findLatestVersion returns the highest available version using ecosystem ordering. +func findLatestVersion(versions []VersionInfo, scheme string) string { + var latest string + for _, v := range versions { + if unavailableVersion(v) { + continue + } + if latest == "" || vers.CompareWithScheme(v.Number, latest, scheme) > 0 { latest = v.Number } } return latest } +func unavailableVersion(v VersionInfo) bool { + if v.Yanked { + return true + } + + switch v.Status { + case string(registries.StatusYanked), string(registries.StatusDeprecated), string(registries.StatusRetracted): + return true + default: + return false + } +} + func (c *RegistriesClient) GetVersions(ctx context.Context, purlStr string) ([]VersionInfo, error) { reg, name, _, err := registries.NewFromPURL(purlStr, c.client) if err != nil {