1616package action
1717
1818import (
19+ "archive/zip"
20+ "context"
1921 "os"
2022 "path/filepath"
2123 "regexp"
24+ "strings"
2225 "testing"
2326
27+ schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
28+ "github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2429 api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2530 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
31+ "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners"
32+ "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/statemanager/filesystem"
33+ "github.com/chainloop-dev/chainloop/pkg/casclient"
2634 "github.com/chainloop-dev/chainloop/pkg/policies"
2735 "github.com/stretchr/testify/assert"
2836 "github.com/stretchr/testify/require"
@@ -32,6 +40,71 @@ import (
3240// names by the proto validation (name.dns-1123).
3341var materialNameRe = regexp .MustCompile (`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` )
3442
43+ // TestAddSourceArchiveEvidence exercises the Part B cross-link end to end: an
44+ // exploded archive is recorded once as an EVIDENCE material and linked with the
45+ // exploded materials in both directions.
46+ // TestExplodeRecordsSourceArchiveEvidence checks that AddMaterialsFromArchive
47+ // records the source archive once as an EVIDENCE material cross-linked with the
48+ // exploded materials in both directions, all in the one atomic add.
49+ func TestExplodeRecordsSourceArchiveEvidence (t * testing.T ) {
50+ ctx := context .Background ()
51+
52+ // A dry-run crafter backed by a local state file (no control plane).
53+ statePath := filepath .Join (t .TempDir (), "attestation.json" )
54+ sm , err := filesystem .New (statePath )
55+ require .NoError (t , err )
56+ c , err := crafter .NewCrafter (sm , nil )
57+ require .NoError (t , err )
58+ require .NoError (t , c .Init (ctx , & crafter.InitOpts {
59+ SchemaV1 : & schemaapi.CraftingSchema {SchemaVersion : "v1" },
60+ WfInfo : & api.WorkflowMetadata {},
61+ DryRun : true ,
62+ AttestationID : "" ,
63+ Runner : runners .NewGeneric (),
64+ }))
65+
66+ // A zip of two files exploded into "scan" / "scan-1".
67+ zipPath := filepath .Join (t .TempDir (), "bundle.zip" )
68+ writeZipWithFiles (t , zipPath , map [string ]string {"a.txt" : "a" , "b.txt" : "b" })
69+
70+ backend := & casclient.CASBackend {}
71+ mts , err := c .AddMaterialsFromArchive (ctx , "" , "ARTIFACT" , "scan" , zipPath , materials .ArchiveZip , backend , nil , materials .DefaultArchiveLimits (), crafter .WithSourceArchiveEvidence ())
72+ require .NoError (t , err )
73+ require .Len (t , mts , 2 )
74+
75+ state := c .CraftingState .GetAttestation ().GetMaterials ()
76+
77+ // The archive is recorded once as EVIDENCE under "scan-archive".
78+ ev , ok := state ["scan-archive" ]
79+ require .True (t , ok , "expected scan-archive evidence material" )
80+ assert .Equal (t , schemaapi .CraftingSchema_Material_EVIDENCE , ev .GetMaterialType ())
81+
82+ // Forward edge: the archive references exactly the exploded materials.
83+ fwd := ev .GetAnnotations ()[materials .AnnotationMaterialReferences ]
84+ assert .ElementsMatch (t , []string {"scan" , "scan-1" }, strings .Split (fwd , "," ))
85+
86+ // Reverse edge: every exploded material references the archive.
87+ for _ , name := range []string {"scan" , "scan-1" } {
88+ assert .Contains (t , state [name ].GetAnnotations ()[materials .AnnotationMaterialReferences ], "scan-archive" ,
89+ "exploded material %q must reference the archive" , name )
90+ }
91+ }
92+
93+ func writeZipWithFiles (t * testing.T , path string , files map [string ]string ) {
94+ t .Helper ()
95+ f , err := os .Create (path )
96+ require .NoError (t , err )
97+ defer f .Close ()
98+ zw := zip .NewWriter (f )
99+ for name , content := range files {
100+ w , err := zw .Create (name )
101+ require .NoError (t , err )
102+ _ , err = w .Write ([]byte (content ))
103+ require .NoError (t , err )
104+ }
105+ require .NoError (t , zw .Close ())
106+ }
107+
35108func TestPolicyInputEvidenceNames (t * testing.T ) {
36109 testCases := []struct {
37110 name string
0 commit comments