internal/worker/store: storage layout for worker
Add types and code that will be used to store worker data in a DB.
The DB contains two kinds of data:
- CVERecords, holding CVEs and related information.
- UpdateRecords, metadata that describe the worker's update runs.
The main interface to the storage layer is a Store.
Provide a memory-based implementation of Store for testing.
Change-Id: Ie9e6c88c354a4946383b2f72897a28633ddc98f7
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/366154
Trust: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/go.mod b/go.mod
index 92f7316..d4194cb 100644
--- a/go.mod
+++ b/go.mod
@@ -18,10 +18,10 @@
github.com/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
- golang.org/x/mod v0.4.1
- golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
- golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
- golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
+ golang.org/x/mod v0.4.2
+ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 // indirect
+ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0
)
diff --git a/go.sum b/go.sum
index ea8f4a0..cd3bec2 100644
--- a/go.sum
+++ b/go.sum
@@ -73,13 +73,14 @@
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -90,20 +91,24 @@
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/worker/store/mem_store.go b/internal/worker/store/mem_store.go
new file mode 100644
index 0000000..d0378e7
--- /dev/null
+++ b/internal/worker/store/mem_store.go
@@ -0,0 +1,125 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "math/rand"
+ "sort"
+ "sync"
+ "time"
+)
+
+// MemStore is an in-memory implementation of Store, for testing.
+type MemStore struct {
+ mu sync.Mutex
+ cveRecords map[string]*CVERecord
+ updateRecords map[string]*UpdateRecord
+}
+
+// NewMemStore creates a new, empty MemStore.
+func NewMemStore() *MemStore {
+ m := &MemStore{}
+ _ = m.Clear(nil)
+ return m
+}
+
+// Clear removes all data from the MemStore.
+func (ms *MemStore) Clear(context.Context) error {
+ ms.cveRecords = map[string]*CVERecord{}
+ ms.updateRecords = map[string]*UpdateRecord{}
+ return nil
+}
+
+// CVERecords return all the CVERecords of the store.
+func (ms *MemStore) CVERecords() map[string]*CVERecord {
+ return ms.cveRecords
+}
+
+// CreateUpdateRecord implements Store.CreateUpdateRecord.
+func (ms *MemStore) CreateUpdateRecord(ctx context.Context, r *UpdateRecord) error {
+ r.ID = fmt.Sprint(rand.Uint32())
+ if ms.updateRecords[r.ID] != nil {
+ panic("duplicate ID")
+ }
+ r.UpdatedAt = time.Now()
+ return ms.SetUpdateRecord(ctx, r)
+}
+
+// SetUpdateRecord implements Store.SetUpdateRecord.
+func (ms *MemStore) SetUpdateRecord(_ context.Context, r *UpdateRecord) error {
+ if r.ID == "" {
+ return errors.New("SetUpdateRecord: need ID")
+ }
+ c := *r
+ c.UpdatedAt = time.Now()
+ ms.updateRecords[c.ID] = &c
+ return nil
+}
+
+// ListUpdateRecords implements Store.ListUpdateRecords.
+func (ms *MemStore) ListUpdateRecords(context.Context) ([]*UpdateRecord, error) {
+ var urs []*UpdateRecord
+ for _, ur := range ms.updateRecords {
+ urs = append(urs, ur)
+ }
+ sort.Slice(urs, func(i, j int) bool {
+ return urs[i].StartedAt.After(urs[j].StartedAt)
+ })
+ return urs, nil
+}
+
+// RunTransaction implements Store.RunTransaction.
+// A transaction runs with a single lock on the entire DB.
+func (ms *MemStore) RunTransaction(ctx context.Context, f func(context.Context, Transaction) error) error {
+ tx := &memTransaction{ms}
+ ms.mu.Lock()
+ defer ms.mu.Unlock()
+ return f(ctx, tx)
+}
+
+// memTransaction implements Store.Transaction.
+type memTransaction struct {
+ ms *MemStore
+}
+
+// CreateCVERecord implements Transaction.CreateCVERecord.
+func (tx *memTransaction) CreateCVERecord(r *CVERecord) error {
+ if err := r.Validate(); err != nil {
+ return err
+ }
+ tx.ms.cveRecords[r.ID] = r
+ return nil
+}
+
+// SetCVERecord implements Transaction.SetCVERecord.
+func (tx *memTransaction) SetCVERecord(r *CVERecord) error {
+ if err := r.Validate(); err != nil {
+ return err
+ }
+ if tx.ms.cveRecords[r.ID] == nil {
+ return fmt.Errorf("CVERecord with ID %q not found", r.ID)
+ }
+ tx.ms.cveRecords[r.ID] = r
+ return nil
+}
+
+// GetCVERecords implements Transaction.GetCVERecords.
+func (tx *memTransaction) GetCVERecords(startID, endID string) ([]*CVERecord, error) {
+ var crs []*CVERecord
+ for id, r := range tx.ms.cveRecords {
+ if id >= startID && id <= endID {
+ c := *r
+ crs = append(crs, &c)
+ }
+ }
+ // Sort for testing.
+ sort.Slice(crs, func(i, j int) bool {
+ return crs[i].ID < crs[j].ID
+ })
+ return crs, nil
+}
diff --git a/internal/worker/store/mem_store_test.go b/internal/worker/store/mem_store_test.go
new file mode 100644
index 0000000..2779041
--- /dev/null
+++ b/internal/worker/store/mem_store_test.go
@@ -0,0 +1,11 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import "testing"
+
+func TestMemStore(t *testing.T) {
+ testStore(t, NewMemStore())
+}
diff --git a/internal/worker/store/store.go b/internal/worker/store/store.go
new file mode 100644
index 0000000..776643d
--- /dev/null
+++ b/internal/worker/store/store.go
@@ -0,0 +1,153 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package store supports permanent data storage for the vuln worker.
+package store
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "golang.org/x/vuln/internal/cveschema"
+)
+
+// A CVERecord contains information about a CVE.
+type CVERecord struct {
+ // ID is the CVE ID, which is the same as the filename base. E.g. "CVE-2020-0034".
+ ID string
+ // Path is the path to the CVE file in the repo.
+ Path string
+ // Blobhash is the hash of the CVE's blob in repo, for quick change detection.
+ BlobHash string
+ // CommitHash is the commit of the cvelist repo from which this information came.
+ CommitHash string
+ // CVEState is the value of the metadata.STATE field.
+ CVEState string
+ // TriageState is the state of our triage processing on the CVE.
+ TriageState TriageState
+ // TriageStateReason is an explanation of TriageState.
+ TriageStateReason string
+
+ // IssueReference is a reference to the GitHub issue that was filed.
+ // E.g. golang/vulndb#12345.
+ // Set only after a GitHub issue has been successfully created.
+ IssueReference string
+
+ // IssueCreatedAt is the time when the issue was created.
+ // Set only after a GitHub issue has been successfully created.
+ IssueCreatedAt time.Time
+}
+
+// Validate returns an error if the CVERecord is not valid.
+func (r *CVERecord) Validate() error {
+ if r.ID == "" {
+ return errors.New("need ID")
+ }
+ if r.Path == "" {
+ return errors.New("need Path")
+ }
+ if r.BlobHash == "" {
+ return errors.New("need BlobHash")
+ }
+ if r.CommitHash == "" {
+ return errors.New("need CommitHash")
+ }
+ return r.TriageState.Validate()
+}
+
+// TriageState is the state of our work on the CVE.
+// It is implemented as a string rather than an int so that stored values are
+// immune to renumbering.
+type TriageState string
+
+const (
+ // No action is needed on the CVE (perhaps because it is rejected, reserved or invalid).
+ TriageStateNoActionNeeded TriageState = "NoActionNeeded"
+ // The CVE needs to have an issue created.
+ TriageStateNeedsIssue TriageState = "NeedsIssue"
+ // An issue has been created in the issue tracker.
+ // The IssueReference and IssueCreatedAt fields have more information.
+ TriageStateIssueCreated TriageState = "IssueCreated"
+ // The CVE state was changed after the CVE was created.
+ TriageStateUpdatedSinceIssueCreation TriageState = "UpdatedSinceIssueCreation"
+)
+
+// Validate returns an error if the TriageState is not one of the above values.
+func (s TriageState) Validate() error {
+ if s == TriageStateNoActionNeeded || s == TriageStateNeedsIssue || s == TriageStateIssueCreated || s == TriageStateUpdatedSinceIssueCreation {
+ return nil
+ }
+ return fmt.Errorf("bad TriageState %q", s)
+}
+
+// NewCVERecord creates a CVERecord from a CVE, its path and its blob hash.
+func NewCVERecord(cve *cveschema.CVE, path, blobHash string) *CVERecord {
+ return &CVERecord{
+ ID: cve.ID,
+ CVEState: cve.State,
+ Path: path,
+ BlobHash: blobHash,
+ }
+}
+
+// An UpdateRecord describes a single update operation, which reconciles
+// a commit in the CVE list repo with the DB state.
+type UpdateRecord struct {
+ // The ID of this record in the DB. Needed to modify the record.
+ ID string
+ // When the update started and completed. If EndedAt is zero,
+ // the update is in progress (or it crashed).
+ StartedAt, EndedAt time.Time
+ // The repo commit hash that this update is working on.
+ CommitHash string
+ // The total number of CVEs being processed in this update.
+ NumTotal int
+ // The number currently processed. When this equals NumTotal, the
+ // update is done.
+ NumProcessed int
+ // The number of CVEs added to the DB.
+ NumAdded int
+ // The number of CVEs modified.
+ NumModified int
+ // The error that stopped the update.
+ Error string
+ // The last time this record was updated.
+ UpdatedAt time.Time `firestore:",serverTimestamp"`
+}
+
+// A Store is a storage system for the CVE database.
+type Store interface {
+ // CreateUpdateRecord creates a new UpdateRecord. It should be called at the start
+ // of an update. On successful return, the UpdateRecord's ID field will be
+ // set to a new, unique ID.
+ CreateUpdateRecord(context.Context, *UpdateRecord) error
+
+ // SetUpdateRecord modifies the UpdateRecord. Use the same record passed to
+ // CreateUpdateRecord, because it will have the correct ID.
+ SetUpdateRecord(context.Context, *UpdateRecord) error
+
+ // ListUpdateRecords returns all the UpdateRecords in the store, from most to
+ // least recent.
+ ListUpdateRecords(context.Context) ([]*UpdateRecord, error)
+
+ // RunTransaction runs the function in a transaction.
+ RunTransaction(context.Context, func(context.Context, Transaction) error) error
+}
+
+// Transaction supports store operations that run inside a transaction.
+type Transaction interface {
+ // CreateCVERecord creates a new CVERecord. It is an error if one with the same ID
+ // already exists.
+ CreateCVERecord(*CVERecord) error
+
+ // SetCVERecord sets the CVE record in the database. It is
+ // an error if no such record exists.
+ SetCVERecord(r *CVERecord) error
+
+ // GetCVERecords retrieves CVERecords for all CVE IDs between startID and
+ // endID, inclusive.
+ GetCVERecords(startID, endID string) ([]*CVERecord, error)
+}
diff --git a/internal/worker/store/store_test.go b/internal/worker/store/store_test.go
new file mode 100644
index 0000000..8bf93cd
--- /dev/null
+++ b/internal/worker/store/store_test.go
@@ -0,0 +1,170 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package store
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "golang.org/x/vuln/internal/cveschema"
+)
+
+func must(t *testing.T, err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func testStore(t *testing.T, s Store) {
+ t.Run("Updates", func(t *testing.T) {
+ testUpdates(t, s)
+ })
+ t.Run("CVEs", func(t *testing.T) {
+ testCVEs(t, s)
+ })
+}
+
+func testUpdates(t *testing.T, s Store) {
+ ctx := context.Background()
+ start := time.Date(2021, time.September, 1, 0, 0, 0, 0, time.Local)
+
+ u1 := &UpdateRecord{
+ StartedAt: start,
+ CommitHash: "abc",
+ NumTotal: 100,
+ }
+ must(t, s.CreateUpdateRecord(ctx, u1))
+ u1.EndedAt = u1.StartedAt.Add(10 * time.Minute)
+ u1.NumAdded = 100
+ must(t, s.SetUpdateRecord(ctx, u1))
+ u2 := &UpdateRecord{
+ StartedAt: start.Add(time.Hour),
+ CommitHash: "def",
+ NumTotal: 80,
+ }
+ must(t, s.CreateUpdateRecord(ctx, u2))
+ u2.EndedAt = u2.StartedAt.Add(8 * time.Minute)
+ u2.NumAdded = 40
+ u2.NumModified = 40
+ must(t, s.SetUpdateRecord(ctx, u2))
+ got, err := s.ListUpdateRecords(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := []*UpdateRecord{u2, u1}
+ diff(t, want, got, cmpopts.IgnoreFields(UpdateRecord{}, "UpdatedAt"))
+ for _, g := range got {
+ if g.UpdatedAt.IsZero() {
+ t.Error("zero UpdatedAt field")
+ }
+ }
+}
+
+func testCVEs(t *testing.T, s Store) {
+ ctx := context.Background()
+ const (
+ id1 = "CVE-1905-0001"
+ id2 = "CVE-1905-0002"
+ id3 = "CVE-1905-0003"
+ )
+ crs := []*CVERecord{
+ {
+ ID: id1,
+ Path: "1905/" + id1 + ".json",
+ BlobHash: "123",
+ CommitHash: "456",
+ CVEState: "PUBLIC",
+ TriageState: TriageStateNeedsIssue,
+ },
+ {
+ ID: id2,
+ Path: "1906/" + id2 + ".json",
+ BlobHash: "abc",
+ CommitHash: "def",
+ CVEState: "RESERVED",
+ TriageState: TriageStateNoActionNeeded,
+ },
+ {
+ ID: id3,
+ Path: "1907/" + id3 + ".json",
+ BlobHash: "xyz",
+ CommitHash: "456",
+ CVEState: "REJECT",
+ TriageState: TriageStateNoActionNeeded,
+ },
+ }
+
+ getCVERecords := func(startID, endID string) []*CVERecord {
+ var got []*CVERecord
+ err := s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ var err error
+ got, err = tx.GetCVERecords(startID, endID)
+ return err
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return got
+ }
+
+ getCVERecord := func(id string) *CVERecord {
+ return getCVERecords(id, id)[0]
+ }
+
+ createCVERecords(t, ctx, s, crs)
+
+ diff(t, crs[:1], getCVERecords(id1, id1))
+ diff(t, crs[1:], getCVERecords(id2, id3))
+
+ // Test SetCVERecord.
+
+ set := func(r *CVERecord) *CVERecord {
+ err := s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ return tx.SetCVERecord(r)
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ return getCVERecord(r.ID)
+ }
+
+ // Make sure the first record is the same that we created.
+ got := getCVERecord(id1)
+ diff(t, crs[0], got)
+
+ // Change the state and the commit hash.
+ got.CVEState = cveschema.StateRejected
+ got.CommitHash = "999"
+ set(got)
+ want := *crs[0]
+ want.CVEState = cveschema.StateRejected
+ want.CommitHash = "999"
+ diff(t, &want, got)
+}
+
+func createCVERecords(t *testing.T, ctx context.Context, s Store, crs []*CVERecord) {
+ err := s.RunTransaction(ctx, func(ctx context.Context, tx Transaction) error {
+ for _, cr := range crs {
+ if err := tx.CreateCVERecord(cr); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func diff(t *testing.T, want, got interface{}, opts ...cmp.Option) {
+ t.Helper()
+ if diff := cmp.Diff(want, got, opts...); diff != "" {
+ t.Errorf("mismatch (-want, +got):\n%s", diff)
+ }
+}