294 lines
7.7 KiB
Go
294 lines
7.7 KiB
Go
package sbom_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/forgeo/forgebucket/internal/domain/sbom"
|
|
)
|
|
|
|
// ─── go.mod ──────────────────────────────────────────────────────────────────
|
|
|
|
func TestParseGoMod_Block(t *testing.T) {
|
|
content := []byte(`module github.com/example/app
|
|
|
|
go 1.21
|
|
|
|
require (
|
|
github.com/go-chi/chi/v5 v5.2.5
|
|
golang.org/x/crypto v0.50.0 // indirect
|
|
)
|
|
|
|
require github.com/lib/pq v1.12.3
|
|
`)
|
|
comps := sbom.ParseGoMod(content)
|
|
if len(comps) != 3 {
|
|
t.Fatalf("expected 3 components, got %d", len(comps))
|
|
}
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["github.com/go-chi/chi/v5"]; !ok {
|
|
t.Error("missing github.com/go-chi/chi/v5")
|
|
} else {
|
|
if c.Version != "v5.2.5" {
|
|
t.Errorf("wrong version: %s", c.Version)
|
|
}
|
|
if c.PURL != "pkg:golang/github.com/go-chi/chi/v5@v5.2.5" {
|
|
t.Errorf("wrong PURL: %s", c.PURL)
|
|
}
|
|
}
|
|
|
|
if _, ok := byName["golang.org/x/crypto"]; !ok {
|
|
t.Error("missing golang.org/x/crypto (indirect deps must be included)")
|
|
}
|
|
if _, ok := byName["github.com/lib/pq"]; !ok {
|
|
t.Error("missing github.com/lib/pq (single-line require)")
|
|
}
|
|
}
|
|
|
|
func TestParseGoMod_Empty(t *testing.T) {
|
|
comps := sbom.ParseGoMod([]byte("module foo\n\ngo 1.21\n"))
|
|
if len(comps) != 0 {
|
|
t.Errorf("expected 0 components, got %d", len(comps))
|
|
}
|
|
}
|
|
|
|
// ─── package.json ─────────────────────────────────────────────────────────────
|
|
|
|
func TestParsePackageJSON(t *testing.T) {
|
|
content := []byte(`{
|
|
"name": "my-app",
|
|
"dependencies": {
|
|
"react": "^18.2.0",
|
|
"axios": "1.4.0"
|
|
},
|
|
"devDependencies": {
|
|
"vitest": "~1.0.0"
|
|
}
|
|
}`)
|
|
|
|
comps := sbom.ParsePackageJSON(content)
|
|
if len(comps) != 3 {
|
|
t.Fatalf("expected 3 components, got %d", len(comps))
|
|
}
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["react"]; !ok {
|
|
t.Error("missing react")
|
|
} else {
|
|
if c.Version != "18.2.0" {
|
|
t.Errorf("expected version stripped of ^, got %s", c.Version)
|
|
}
|
|
if c.PURL != "pkg:npm/react@18.2.0" {
|
|
t.Errorf("wrong PURL: %s", c.PURL)
|
|
}
|
|
if c.Scope != "required" {
|
|
t.Errorf("expected scope required, got %s", c.Scope)
|
|
}
|
|
}
|
|
|
|
if c, ok := byName["vitest"]; !ok {
|
|
t.Error("missing vitest")
|
|
} else if c.Scope != "optional" {
|
|
t.Errorf("devDependency should be optional, got %s", c.Scope)
|
|
}
|
|
}
|
|
|
|
func TestParsePackageJSON_Invalid(t *testing.T) {
|
|
comps := sbom.ParsePackageJSON([]byte("not json"))
|
|
if len(comps) != 0 {
|
|
t.Errorf("expected 0 on invalid JSON, got %d", len(comps))
|
|
}
|
|
}
|
|
|
|
// ─── requirements.txt ────────────────────────────────────────────────────────
|
|
|
|
func TestParseRequirementsTxt(t *testing.T) {
|
|
content := []byte(`# comment
|
|
requests==2.31.0
|
|
flask>=2.3.0
|
|
numpy~=1.24.0
|
|
boto3[s3]==1.28.0 # with extras
|
|
no-version-package
|
|
-r other-requirements.txt
|
|
`)
|
|
comps := sbom.ParseRequirementsTxt(content)
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["requests"]; !ok {
|
|
t.Error("missing requests")
|
|
} else if c.Version != "2.31.0" {
|
|
t.Errorf("requests version: %s", c.Version)
|
|
}
|
|
|
|
if c, ok := byName["flask"]; !ok {
|
|
t.Error("missing flask")
|
|
} else if c.Version != "2.3.0" {
|
|
t.Errorf("flask version: %s", c.Version)
|
|
}
|
|
|
|
if _, ok := byName["boto3"]; !ok {
|
|
t.Error("missing boto3 (extras should be stripped from name)")
|
|
}
|
|
|
|
if _, ok := byName["no-version-package"]; !ok {
|
|
t.Error("missing no-version-package")
|
|
}
|
|
}
|
|
|
|
// ─── Cargo.toml ──────────────────────────────────────────────────────────────
|
|
|
|
func TestParseCargoToml(t *testing.T) {
|
|
content := []byte(`[package]
|
|
name = "my-crate"
|
|
version = "0.1.0"
|
|
|
|
[dependencies]
|
|
serde = "1.0"
|
|
tokio = { version = "1.28", features = ["full"] }
|
|
clap = "4.3"
|
|
|
|
[dev-dependencies]
|
|
criterion = "0.5"
|
|
`)
|
|
comps := sbom.ParseCargoToml(content)
|
|
if len(comps) != 4 {
|
|
t.Fatalf("expected 4 components, got %d: %v", len(comps), comps)
|
|
}
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["serde"]; !ok {
|
|
t.Error("missing serde")
|
|
} else if c.Version != "1.0" {
|
|
t.Errorf("serde version: %s", c.Version)
|
|
}
|
|
|
|
if c, ok := byName["tokio"]; !ok {
|
|
t.Error("missing tokio")
|
|
} else if c.Version != "1.28" {
|
|
t.Errorf("tokio version: %s", c.Version)
|
|
}
|
|
|
|
if _, ok := byName["criterion"]; !ok {
|
|
t.Error("missing criterion (dev-dependency)")
|
|
}
|
|
}
|
|
|
|
// ─── Gemfile.lock ─────────────────────────────────────────────────────────────
|
|
|
|
func TestParseGemfileLock(t *testing.T) {
|
|
content := []byte(`GEM
|
|
remote: https://rubygems.org/
|
|
specs:
|
|
rails (7.1.0)
|
|
actionpack (= 7.1.0)
|
|
railties (= 7.1.0)
|
|
actionpack (7.1.0)
|
|
rake (13.0.6)
|
|
|
|
PLATFORMS
|
|
ruby
|
|
|
|
DEPENDENCIES
|
|
rails (~> 7.1.0)
|
|
`)
|
|
comps := sbom.ParseGemfileLock(content)
|
|
if len(comps) != 3 {
|
|
t.Fatalf("expected 3 components, got %d: %v", len(comps), comps)
|
|
}
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["rails"]; !ok {
|
|
t.Error("missing rails")
|
|
} else if c.Version != "7.1.0" {
|
|
t.Errorf("rails version: %s", c.Version)
|
|
}
|
|
|
|
if c, ok := byName["rake"]; !ok {
|
|
t.Error("missing rake")
|
|
} else if c.Version != "13.0.6" {
|
|
t.Errorf("rake version: %s", c.Version)
|
|
}
|
|
}
|
|
|
|
// ─── pom.xml ─────────────────────────────────────────────────────────────────
|
|
|
|
func TestParsePomXML(t *testing.T) {
|
|
content := []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<project>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-web</artifactId>
|
|
<version>3.1.0</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.postgresql</groupId>
|
|
<artifactId>postgresql</artifactId>
|
|
<version>42.6.0</version>
|
|
</dependency>
|
|
</dependencies>
|
|
</project>`)
|
|
|
|
comps := sbom.ParsePomXML(content)
|
|
if len(comps) != 2 {
|
|
t.Fatalf("expected 2 components, got %d", len(comps))
|
|
}
|
|
|
|
byName := make(map[string]sbom.Component)
|
|
for _, c := range comps {
|
|
byName[c.Name] = c
|
|
}
|
|
|
|
if c, ok := byName["org.springframework.boot:spring-boot-starter-web"]; !ok {
|
|
t.Error("missing spring-boot-starter-web")
|
|
} else {
|
|
if c.Version != "3.1.0" {
|
|
t.Errorf("spring-boot version: %s", c.Version)
|
|
}
|
|
if c.PURL != "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.1.0" {
|
|
t.Errorf("wrong PURL: %s", c.PURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Document builder ─────────────────────────────────────────────────────────
|
|
|
|
func TestNewDocument(t *testing.T) {
|
|
doc := sbom.NewDocument("my-repo", "abc1234567890")
|
|
if doc.BOMFormat != "CycloneDX" {
|
|
t.Errorf("BOMFormat: %s", doc.BOMFormat)
|
|
}
|
|
if doc.SpecVersion != "1.4" {
|
|
t.Errorf("SpecVersion: %s", doc.SpecVersion)
|
|
}
|
|
if doc.Metadata.Component.Name != "my-repo" {
|
|
t.Errorf("metadata component name: %s", doc.Metadata.Component.Name)
|
|
}
|
|
if len(doc.Metadata.Tools) == 0 {
|
|
t.Error("expected at least one tool in metadata")
|
|
}
|
|
if doc.Components == nil {
|
|
t.Error("expected non-nil Components slice")
|
|
}
|
|
}
|