From 6a9dc5a10e35d0bea9a6b1df91b3149869b5550a Mon Sep 17 00:00:00 2001 From: Tushar Saxena <019saxenatushar@gmail.com> Date: Sat, 25 Apr 2026 19:41:45 +0530 Subject: [PATCH] added tests for extractor Signed-off-by: Tushar Saxena <019saxenatushar@gmail.com> --- pkg/attestation/extractor_test.go | 180 ++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 pkg/attestation/extractor_test.go diff --git a/pkg/attestation/extractor_test.go b/pkg/attestation/extractor_test.go new file mode 100644 index 0000000..18385a5 --- /dev/null +++ b/pkg/attestation/extractor_test.go @@ -0,0 +1,180 @@ +package attestation + +import ( + "reflect" + "testing" +) + +type fakeExtractor struct { + name string + out []FileInfo + called int + lastData map[string]interface{} +} + +func (f *fakeExtractor) Name() string { + return f.name +} + +func (f *fakeExtractor) Extract(data map[string]interface{}) []FileInfo { + f.called++ + f.lastData = data + + copied := make([]FileInfo, len(f.out)) + copy(copied, f.out) + return copied +} + +func TestNewExtractorChain_RegistersDefaultExtractors(t *testing.T) { + chain := NewExtractorChain() + + for _, typ := range []string{"material", "command-run", "product"} { + extractor, ok := chain.GetExtractor(typ) + if !ok { + t.Fatalf("expected extractor for type %q to be registered", typ) + } + if extractor == nil { + t.Fatalf("expected non-nil extractor for type %q", typ) + } + if extractor.Name() != typ { + t.Fatalf("expected extractor name %q, got %q", typ, extractor.Name()) + } + } + + if _, ok := chain.GetExtractor("unknown"); ok { + t.Fatalf("did not expect extractor for unknown type") + } +} + +func TestExtractorChain_SupportedTypes(t *testing.T) { + chain := NewExtractorChain() + got := chain.SupportedTypes() + + wantSet := map[string]struct{}{ + "material": {}, + "command-run": {}, + "product": {}, + } + + if len(got) != len(wantSet) { + t.Fatalf("expected %d supported types, got %d (%v)", len(wantSet), len(got), got) + } + + for _, typ := range got { + if _, ok := wantSet[typ]; !ok { + t.Fatalf("unexpected supported type %q", typ) + } + delete(wantSet, typ) + } + + if len(wantSet) != 0 { + t.Fatalf("missing supported types: %v", wantSet) + } +} + +func TestExtractorChain_RegisterExtractor(t *testing.T) { + chain := &ExtractorChain{extractors: make(map[string]Extractor)} + f := &fakeExtractor{name: "custom"} + + chain.RegisterExtractor(f) + + got, ok := chain.GetExtractor("custom") + if !ok { + t.Fatalf("expected custom extractor to be registered") + } + if got != f { + t.Fatalf("expected the registered extractor instance to be returned") + } +} + +func TestExtractorChain_ExtractAll(t *testing.T) { + tests := []struct { + name string + attest []TypedAttestation + typeFilter []string + wantFiles []FileInfo + wantMatCall int + wantProCall int + }{ + { + name: "deduplicates paths and preserves first seen file", + attest: []TypedAttestation{ + {Type: "material", Data: map[string]interface{}{"source": "materials"}}, + {Type: "product", Data: map[string]interface{}{"source": "products"}}, + {Type: "unknown", Data: map[string]interface{}{"source": "ignored"}}, + }, + wantFiles: []FileInfo{ + {Path: "/a.txt", Hashes: map[string]string{"sha256": "a"}}, + {Path: "/shared.txt", Hashes: map[string]string{"sha256": "from-material"}}, + {Path: "/b.txt", Hashes: map[string]string{"sha256": "b"}}, + }, + wantMatCall: 1, + wantProCall: 1, + }, + { + name: "applies type filter", + attest: []TypedAttestation{ + {Type: "material", Data: map[string]interface{}{"source": "materials"}}, + {Type: "product", Data: map[string]interface{}{"source": "products"}}, + }, + typeFilter: []string{"product"}, + wantFiles: []FileInfo{ + {Path: "/shared.txt", Hashes: map[string]string{"sha256": "from-product"}}, + {Path: "/b.txt", Hashes: map[string]string{"sha256": "b"}}, + }, + wantMatCall: 0, + wantProCall: 1, + }, + { + name: "no matching type filter returns no files", + attest: []TypedAttestation{ + {Type: "material", Data: map[string]interface{}{"source": "materials"}}, + {Type: "product", Data: map[string]interface{}{"source": "products"}}, + }, + typeFilter: []string{"command-run"}, + wantFiles: nil, + wantMatCall: 0, + wantProCall: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + material := &fakeExtractor{ + name: "material", + out: []FileInfo{ + {Path: "/a.txt", Hashes: map[string]string{"sha256": "a"}}, + {Path: "/shared.txt", Hashes: map[string]string{"sha256": "from-material"}}, + }, + } + product := &fakeExtractor{ + name: "product", + out: []FileInfo{ + {Path: "/shared.txt", Hashes: map[string]string{"sha256": "from-product"}}, + {Path: "/b.txt", Hashes: map[string]string{"sha256": "b"}}, + }, + } + + chain := &ExtractorChain{ + extractors: map[string]Extractor{ + "material": material, + "product": product, + }, + } + + got := chain.ExtractAll(tt.attest, tt.typeFilter) + + if !reflect.DeepEqual(got, tt.wantFiles) { + t.Fatalf("ExtractAll() mismatch\nwant: %#v\ngot: %#v", tt.wantFiles, got) + } + + if material.called != tt.wantMatCall { + t.Fatalf("material extractor called %d times, want %d", material.called, tt.wantMatCall) + } + + if product.called != tt.wantProCall { + t.Fatalf("product extractor called %d times, want %d", product.called, tt.wantProCall) + } + }) + } +}