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(` org.springframework.boot spring-boot-starter-web 3.1.0 org.postgresql postgresql 42.6.0 `) 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") } }