Files
trivy/pkg/cache/redis_test.go
2025-07-29 07:13:54 +00:00

575 lines
15 KiB
Go

package cache_test
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/pkg/cache"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
const correctHash = "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7"
func TestRedisCache_PutArtifact(t *testing.T) {
type args struct {
artifactID string
artifactConfig types.ArtifactInfo
}
tests := []struct {
name string
setupRedis bool
args args
wantKey string
wantErr string
}{
{
name: "happy path",
setupRedis: true,
args: args{
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
artifactConfig: types.ArtifactInfo{
SchemaVersion: 2,
Architecture: "amd64",
Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC),
DockerVersion: "19.03.12",
OS: "linux",
},
},
wantKey: "fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
},
{
name: "no such host",
setupRedis: false,
args: args{
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
artifactConfig: types.ArtifactInfo{},
},
wantErr: "unable to store artifact information in Redis cache",
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:16379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
err = c.PutArtifact(tt.args.artifactID, tt.args.artifactConfig)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
got, err := s.Get(tt.wantKey)
require.NoError(t, err)
want, err := json.Marshal(tt.args.artifactConfig)
require.NoError(t, err)
assert.JSONEq(t, string(want), got)
})
}
}
func TestRedisCache_PutBlob(t *testing.T) {
type args struct {
blobID string
blobConfig types.BlobInfo
}
tests := []struct {
name string
setupRedis bool
args args
wantKey string
wantErr string
}{
{
name: "happy path",
setupRedis: true,
args: args{
blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
blobConfig: types.BlobInfo{
SchemaVersion: 2,
Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609",
DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
OS: types.OS{
Family: "alpine",
Name: "3.10.2",
},
PackageInfos: []types.PackageInfo{
{
FilePath: "lib/apk/db/installed",
Packages: []types.Package{
{
Name: "musl",
Version: "1.1.22-r3",
SrcName: "musl",
SrcVersion: "1.1.22-r3",
},
},
},
},
},
},
wantKey: "fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
},
{
name: "no such host",
setupRedis: false,
args: args{
blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
blobConfig: types.BlobInfo{},
},
wantErr: "unable to store blob information in Redis cache",
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:16379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
err = c.PutBlob(tt.args.blobID, tt.args.blobConfig)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
got, err := s.Get(tt.wantKey)
require.NoError(t, err)
want, err := json.Marshal(tt.args.blobConfig)
require.NoError(t, err)
assert.JSONEq(t, string(want), got)
})
}
}
func TestRedisCache_GetArtifact(t *testing.T) {
info := types.ArtifactInfo{
SchemaVersion: 2,
Architecture: "amd64",
Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC),
DockerVersion: "19.03.12",
OS: "linux",
}
tests := []struct {
name string
setupRedis bool
artifactID string
want types.ArtifactInfo
wantErr string
}{
{
name: "happy path",
setupRedis: true,
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf",
want: info,
},
{
name: "malformed JSON",
setupRedis: true,
artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
wantErr: "failed to unmarshal artifact",
},
{
name: "no such host",
setupRedis: false,
artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
wantErr: "failed to get artifact from the Redis cache",
},
{
name: "nonexistent key",
setupRedis: true,
artifactID: "sha256:foo",
wantErr: "artifact (sha256:foo) is missing in Redis cache",
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
// Set key/value pairs
b, err := json.Marshal(info)
require.NoError(t, err)
s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", string(b))
s.Set("fanal::artifact::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:16379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
got, err := c.GetArtifact(tt.artifactID)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestRedisCache_GetBlob(t *testing.T) {
blobInfo := types.BlobInfo{
SchemaVersion: 2,
Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609",
DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
OS: types.OS{
Family: "alpine",
Name: "3.10.2",
},
PackageInfos: []types.PackageInfo{
{
FilePath: "lib/apk/db/installed",
Packages: []types.Package{
{
Name: "musl",
Version: "1.1.22-r3",
SrcName: "musl",
SrcVersion: "1.1.22-r3",
},
},
},
},
}
tests := []struct {
name string
setupRedis bool
blobID string
want types.BlobInfo
wantErr string
}{
{
name: "happy path",
setupRedis: true,
blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
want: blobInfo,
},
{
name: "malformed JSON",
setupRedis: true,
blobID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
wantErr: "failed to unmarshal blob",
},
{
name: "no such host",
setupRedis: false,
blobID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
wantErr: "failed to get blob from the Redis cache",
},
{
name: "nonexistent key",
setupRedis: true,
blobID: "sha256:foo",
wantErr: "blob (sha256:foo) is missing in Redis cache",
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
// Set key/value pairs
b, err := json.Marshal(blobInfo)
require.NoError(t, err)
s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", string(b))
s.Set("fanal::blob::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:16379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
got, err := c.GetBlob(tt.blobID)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestRedisCache_MissingBlobs(t *testing.T) {
type args struct {
artifactID string
blobIDs []string
}
tests := []struct {
name string
setupRedis bool
args args
wantMissingArtifact bool
wantMissingBlobIDs []string
wantErr string
}{
{
name: "missing both",
setupRedis: true,
args: args{
artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1",
blobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
},
wantMissingArtifact: true,
wantMissingBlobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
},
{
name: "missing artifact",
setupRedis: true,
args: args{
artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1",
blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"},
},
wantMissingArtifact: true,
},
{
name: "missing blobs",
setupRedis: true,
args: args{
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
blobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
},
wantMissingArtifact: false,
wantMissingBlobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"},
},
{
name: "missing artifact with different schema version",
setupRedis: true,
args: args{
artifactID: "sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1",
blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"},
},
wantMissingArtifact: true,
},
{
name: "missing blobs with different schema version",
setupRedis: true,
args: args{
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
blobIDs: []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"},
},
wantMissingArtifact: false,
wantMissingBlobIDs: []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"},
},
{
name: "different analyzer versions",
setupRedis: true,
args: args{
artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/0",
blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"},
},
wantMissingArtifact: true,
wantMissingBlobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"},
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1",
fmt.Sprintf("{\"SchemaVersion\": %d}", types.ArtifactJSONSchemaVersion))
s.Set("fanal::artifact::sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1",
`{"SchemaVersion": 999999}`) // This version should not match the current version
s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111",
fmt.Sprintf("{\"SchemaVersion\": %d}", types.BlobJSONSchemaVersion))
s.Set("fanal::blob::sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111",
`{"SchemaVersion": 999999}`) // This version should not match the current version
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:6379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
missingArtifact, missingBlobIDs, err := c.MissingBlobs(tt.args.artifactID, tt.args.blobIDs)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantMissingArtifact, missingArtifact)
assert.Equal(t, tt.wantMissingBlobIDs, missingBlobIDs)
})
}
}
func TestRedisCache_Close(t *testing.T) {
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
t.Run("close", func(t *testing.T) {
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", s.Addr()), "", "", "", false, 0)
require.NoError(t, err)
closeErr := c.Close()
require.NoError(t, closeErr)
time.Sleep(3 * time.Second) // give it some time
assert.Equal(t, 0, s.CurrentConnectionCount(), "The client is disconnected")
})
}
func TestRedisCache_Clear(t *testing.T) {
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
for i := range 200 {
s.Set(fmt.Sprintf("fanal::key%d", i), "value")
}
s.Set("foo", "bar")
t.Run("clear", func(t *testing.T) {
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", s.Addr()), "", "", "", false, 0)
require.NoError(t, err)
require.NoError(t, c.Clear())
for i := range 200 {
assert.False(t, s.Exists(fmt.Sprintf("fanal::key%d", i)))
}
assert.True(t, s.Exists("foo"))
})
}
func TestRedisCache_DeleteBlobs(t *testing.T) {
type args struct {
blobIDs []string
}
tests := []struct {
name string
setupRedis bool
args args
wantKey string
wantErr string
}{
{
name: "happy path",
setupRedis: true,
args: args{
blobIDs: []string{correctHash},
},
wantKey: "fanal::blob::" + correctHash,
},
{
name: "no such host",
setupRedis: false,
args: args{
blobIDs: []string{"sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae800"},
},
wantErr: "unable to delete blob",
},
}
// Set up Redis test server
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
addr := s.Addr()
if !tt.setupRedis {
addr = "dummy:16379"
}
c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0)
require.NoError(t, err)
s.Set(tt.wantKey, "any string")
err = c.DeleteBlobs(tt.args.blobIDs)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
// Verify that the blobs are deleted
got := s.Keys()
assert.NotContains(t, got, tt.wantKey)
})
}
}
func TestRedisOptions_BackendMasked(t *testing.T) {
tests := []struct {
name string
fields cache.RedisOptions
want string
}{
{
name: "redis cache backend masked",
fields: cache.RedisOptions{Backend: "redis://root:password@localhost:6379"},
want: "redis://****@localhost:6379",
},
{
name: "redis cache backend masked does nothing",
fields: cache.RedisOptions{Backend: "redis://localhost:6379"},
want: "redis://localhost:6379",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.fields.BackendMasked())
})
}
}