Files

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")
}
}