Skip to content

Commit f4ddc68

Browse files
committed
test: add a test for the report hook
1 parent ed39d14 commit f4ddc68

File tree

2 files changed

+321
-3
lines changed

2 files changed

+321
-3
lines changed

pkg/cloud/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ import (
2323
type ConfigType string
2424

2525
const (
26-
// Additional config types can be added here - in future custom checks etc
26+
// Additional config types can be added here - in future pipeline rego etc
2727
ConfigTypeSecret ConfigType = "secret"
2828
)
2929

3030
const (
3131
SecretConfigPath = "/configs/secrets/secret-config.yaml"
32-
configCacheTTL = -time.Hour
32+
configCacheTTL = time.Hour
3333
)
3434

3535
var configPaths = map[ConfigType]string{
@@ -73,7 +73,7 @@ func getConfigFromTrivyCloud(ctx context.Context, client *http.Client, opts *fla
7373

7474
configFilename := filepath.Join(configDir, "config.yaml")
7575
// Return cached config if it was updated within the last hour
76-
if stat, err := os.Stat(configFilename); err == nil && stat.ModTime().After(time.Now().Add(configCacheTTL)) {
76+
if stat, err := os.Stat(configFilename); err == nil && stat.ModTime().After(time.Now().Add(-configCacheTTL)) {
7777
logger.Debug("Config found in cache", log.String("configType", string(configType)), log.String("configPath", configFilename))
7878
return configFilename, nil
7979
}
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package hooks
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
14+
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
15+
"github.com/aquasecurity/trivy/pkg/flag"
16+
"github.com/aquasecurity/trivy/pkg/types"
17+
)
18+
19+
type mockReportServer struct {
20+
server *httptest.Server
21+
uploadURLRequested bool
22+
reportUploaded bool
23+
uploadedReport *types.Report
24+
returnUnauthorized bool
25+
returnInvalidJSON bool
26+
failUpload bool
27+
presignedUploadPath string
28+
}
29+
30+
func (m *mockReportServer) Start() {
31+
m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32+
if r.URL.Path == presignedUploadUrl {
33+
m.handlePresignedURLRequest(w, r)
34+
return
35+
}
36+
37+
if r.URL.Path == m.presignedUploadPath {
38+
m.handleReportUpload(w, r)
39+
return
40+
}
41+
42+
w.WriteHeader(http.StatusNotFound)
43+
}))
44+
m.presignedUploadPath = "/upload-report"
45+
}
46+
47+
func (m *mockReportServer) handlePresignedURLRequest(w http.ResponseWriter, r *http.Request) {
48+
if r.Method != http.MethodGet {
49+
w.WriteHeader(http.StatusMethodNotAllowed)
50+
return
51+
}
52+
53+
if r.Header.Get("Authorization") != "Bearer test-token" {
54+
w.WriteHeader(http.StatusUnauthorized)
55+
return
56+
}
57+
58+
if m.returnUnauthorized {
59+
w.WriteHeader(http.StatusUnauthorized)
60+
return
61+
}
62+
63+
m.uploadURLRequested = true
64+
65+
if m.returnInvalidJSON {
66+
w.WriteHeader(http.StatusOK)
67+
w.Write([]byte(`invalid json`))
68+
return
69+
}
70+
71+
uploadURL := m.server.URL + m.presignedUploadPath
72+
response := map[string]string{
73+
"uploadUrl": uploadURL,
74+
}
75+
76+
w.Header().Set("Content-Type", "application/json")
77+
w.WriteHeader(http.StatusOK)
78+
json.NewEncoder(w).Encode(response)
79+
}
80+
81+
func (m *mockReportServer) handleReportUpload(w http.ResponseWriter, r *http.Request) {
82+
if r.Method != http.MethodPut {
83+
w.WriteHeader(http.StatusMethodNotAllowed)
84+
return
85+
}
86+
87+
if r.Header.Get("Content-Type") != "application/json" {
88+
w.WriteHeader(http.StatusBadRequest)
89+
return
90+
}
91+
92+
if m.failUpload {
93+
w.WriteHeader(http.StatusInternalServerError)
94+
return
95+
}
96+
97+
body, err := io.ReadAll(r.Body)
98+
if err != nil {
99+
w.WriteHeader(http.StatusBadRequest)
100+
return
101+
}
102+
103+
var report types.Report
104+
if err := json.Unmarshal(body, &report); err != nil {
105+
w.WriteHeader(http.StatusBadRequest)
106+
return
107+
}
108+
109+
m.reportUploaded = true
110+
m.uploadedReport = &report
111+
112+
w.WriteHeader(http.StatusOK)
113+
}
114+
115+
func (m *mockReportServer) Close() {
116+
if m.server != nil {
117+
m.server.Close()
118+
}
119+
}
120+
121+
func TestReportHook_Name(t *testing.T) {
122+
hook := NewReportHook("http://api.example.com", "test-token")
123+
assert.Equal(t, "Trivy Cloud Results Hook", hook.Name())
124+
}
125+
126+
func TestReportHook_PreReport(t *testing.T) {
127+
hook := NewReportHook("http://api.example.com", "test-token")
128+
err := hook.PreReport(context.Background(), &types.Report{}, flag.Options{})
129+
assert.NoError(t, err)
130+
}
131+
132+
func TestReportHook_PostReport(t *testing.T) {
133+
tests := []struct {
134+
name string
135+
report *types.Report
136+
returnUnauthorized bool
137+
returnInvalidJSON bool
138+
failUpload bool
139+
errorContains string
140+
}{
141+
{
142+
name: "successful upload",
143+
report: &types.Report{
144+
ArtifactName: "test-artifact",
145+
ArtifactType: ftypes.TypeContainerImage,
146+
Results: types.Results{
147+
{
148+
Target: "test-target",
149+
Vulnerabilities: []types.DetectedVulnerability{
150+
{
151+
VulnerabilityID: "CVE-2021-1234",
152+
PkgName: "test-package",
153+
154+
},
155+
},
156+
},
157+
},
158+
},
159+
},
160+
{
161+
name: "empty report",
162+
report: &types.Report{
163+
ArtifactName: "empty-artifact",
164+
ArtifactType: ftypes.TypeContainerImage,
165+
},
166+
},
167+
{
168+
name: "invalid token 401 status code",
169+
report: &types.Report{
170+
ArtifactName: "test-artifact",
171+
},
172+
returnUnauthorized: true,
173+
errorContains: "failed to get presigned upload URL",
174+
},
175+
{
176+
name: "unauthorized access",
177+
report: &types.Report{
178+
ArtifactName: "test-artifact",
179+
},
180+
returnUnauthorized: true,
181+
errorContains: "failed to get presigned upload URL",
182+
},
183+
{
184+
name: "invalid json response",
185+
report: &types.Report{
186+
ArtifactName: "test-artifact",
187+
},
188+
returnInvalidJSON: true,
189+
errorContains: "failed to decode upload URL response",
190+
},
191+
{
192+
name: "upload failure",
193+
report: &types.Report{
194+
ArtifactName: "test-artifact",
195+
},
196+
failUpload: true,
197+
errorContains: "failed to upload results",
198+
},
199+
}
200+
201+
for _, tt := range tests {
202+
t.Run(tt.name, func(t *testing.T) {
203+
mockServer := &mockReportServer{
204+
returnUnauthorized: tt.returnUnauthorized,
205+
returnInvalidJSON: tt.returnInvalidJSON,
206+
failUpload: tt.failUpload,
207+
}
208+
mockServer.Start()
209+
defer mockServer.Close()
210+
211+
hook := NewReportHook(mockServer.server.URL, "test-token")
212+
err := hook.PostReport(context.Background(), tt.report, flag.Options{})
213+
214+
if tt.errorContains != "" {
215+
require.Error(t, err)
216+
assert.Contains(t, err.Error(), tt.errorContains)
217+
return
218+
}
219+
220+
require.NoError(t, err)
221+
assert.True(t, mockServer.uploadURLRequested)
222+
assert.True(t, mockServer.reportUploaded)
223+
assert.Equal(t, tt.report.ArtifactName, mockServer.uploadedReport.ArtifactName)
224+
})
225+
}
226+
}
227+
228+
func TestReportHook_uploadResults(t *testing.T) {
229+
tests := []struct {
230+
name string
231+
jsonReport []byte
232+
failUpload bool
233+
errorContains string
234+
}{
235+
{
236+
name: "successful upload",
237+
jsonReport: []byte(`{"artifactName": "test"}`),
238+
},
239+
{
240+
name: "upload failure",
241+
jsonReport: []byte(`{"artifactName": "test"}`),
242+
failUpload: true,
243+
errorContains: "failed to upload results",
244+
},
245+
}
246+
247+
for _, tt := range tests {
248+
t.Run(tt.name, func(t *testing.T) {
249+
mockServer := &mockReportServer{
250+
failUpload: tt.failUpload,
251+
}
252+
mockServer.Start()
253+
defer mockServer.Close()
254+
255+
hook := NewReportHook(mockServer.server.URL, "test-token")
256+
err := hook.uploadResults(context.Background(), tt.jsonReport)
257+
258+
if tt.errorContains != "" {
259+
require.Error(t, err)
260+
assert.Contains(t, err.Error(), tt.errorContains)
261+
return
262+
}
263+
264+
require.NoError(t, err)
265+
assert.True(t, mockServer.reportUploaded)
266+
})
267+
}
268+
}
269+
270+
func TestReportHook_getPresignedUploadUrl(t *testing.T) {
271+
tests := []struct {
272+
name string
273+
returnUnauthorized bool
274+
returnInvalidJSON bool
275+
errorContains string
276+
expectedURL string
277+
}{
278+
{
279+
name: "successful request",
280+
},
281+
{
282+
name: "unauthorized",
283+
returnUnauthorized: true,
284+
errorContains: "failed to get upload URL",
285+
},
286+
{
287+
name: "invalid json response",
288+
returnInvalidJSON: true,
289+
errorContains: "failed to decode upload URL response",
290+
},
291+
}
292+
293+
for _, tt := range tests {
294+
t.Run(tt.name, func(t *testing.T) {
295+
mockServer := &mockReportServer{
296+
returnUnauthorized: tt.returnUnauthorized,
297+
returnInvalidJSON: tt.returnInvalidJSON,
298+
}
299+
mockServer.Start()
300+
defer mockServer.Close()
301+
302+
hook := NewReportHook(mockServer.server.URL, "test-token")
303+
url, err := hook.getPresignedUploadUrl(context.Background())
304+
305+
if tt.errorContains != "" {
306+
require.Error(t, err)
307+
assert.Contains(t, err.Error(), tt.errorContains)
308+
assert.Empty(t, url)
309+
return
310+
}
311+
312+
require.NoError(t, err)
313+
assert.NotEmpty(t, url)
314+
assert.Contains(t, url, mockServer.server.URL)
315+
assert.Contains(t, url, "/upload-report")
316+
})
317+
}
318+
}

0 commit comments

Comments
 (0)