Skip to content

Commit 1e60f34

Browse files
committed
add tests for processSyncEvents
Signed-off-by: Eric Pickard <piceri@github.com>
1 parent 85e4e6e commit 1e60f34

2 files changed

Lines changed: 195 additions & 17 deletions

File tree

internal/controller/controller_test.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/github/deployment-tracker/internal/metadata"
1112
"github.com/github/deployment-tracker/internal/workload"
1213
"github.com/github/deployment-tracker/pkg/deploymentrecord"
1314
"github.com/stretchr/testify/assert"
@@ -23,9 +24,13 @@ import (
2324

2425
// mockPoster records all PostOne calls and returns a configurable error.
2526
type mockPoster struct {
26-
mu sync.Mutex
27-
calls int
28-
lastErr error
27+
mu sync.Mutex
28+
calls int
29+
clusterCalls int
30+
clusterRecordCount int
31+
lastErr error
32+
clusterResp []byte
33+
clusterErr error
2934
}
3035

3136
func (m *mockPoster) PostOne(_ context.Context, _ *deploymentrecord.DeploymentRecord) error {
@@ -35,27 +40,46 @@ func (m *mockPoster) PostOne(_ context.Context, _ *deploymentrecord.DeploymentRe
3540
return m.lastErr
3641
}
3742

38-
func (m *mockPoster) PostCluster(_ context.Context, _ []*deploymentrecord.DeploymentRecord, _ string) ([]byte, error) {
39-
return nil, nil
43+
func (m *mockPoster) PostCluster(_ context.Context, records []*deploymentrecord.DeploymentRecord, _ string) ([]byte, error) {
44+
m.mu.Lock()
45+
defer m.mu.Unlock()
46+
m.clusterCalls++
47+
m.clusterRecordCount = len(records)
48+
return m.clusterResp, m.clusterErr
4049
}
4150

42-
func (m *mockPoster) getCalls() int {
51+
func (m *mockPoster) getPostOneCalls() int {
4352
m.mu.Lock()
4453
defer m.mu.Unlock()
4554
return m.calls
4655
}
4756

57+
func (m *mockPoster) getPostClusterCalls() int {
58+
m.mu.Lock()
59+
defer m.mu.Unlock()
60+
return m.clusterCalls
61+
}
62+
4863
// mockResolver is a test double for the workloadResolver interface.
49-
type mockResolver struct{}
64+
type mockResolver struct {
65+
name string
66+
}
5067

51-
func (*mockResolver) Resolve(_ *corev1.Pod) workload.Identity {
52-
return workload.Identity{}
68+
func (m *mockResolver) Resolve(_ *corev1.Pod) workload.Identity {
69+
return workload.Identity{Name: m.name}
5370
}
5471

5572
func (*mockResolver) IsActive(_ string, _ workload.Identity) bool {
5673
return false
5774
}
5875

76+
// mockMetadataAggregator is a test double for the podMetadataAggregator interface.
77+
type mockMetadataAggregator struct{}
78+
79+
func (*mockMetadataAggregator) BuildAggregatePodMetadata(_ context.Context, _ *metav1.PartialObjectMetadata) *metadata.AggregatePodMetadata {
80+
return nil
81+
}
82+
5983
// newTestController creates a minimal Controller suitable for unit-testing
6084
// recordContainer without a real Kubernetes cluster.
6185
func newTestController(poster *mockPoster) *Controller {
@@ -68,6 +92,7 @@ func newTestController(poster *mockPoster) *Controller {
6892
Cluster: "test",
6993
},
7094
workloadResolver: &mockResolver{},
95+
metadataAggregator: &mockMetadataAggregator{},
7196
observedDeployments: amcache.NewExpiring(),
7297
unknownArtifacts: amcache.NewExpiring(),
7398
}

internal/controller/reporting_test.go

Lines changed: 161 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package controller
22

33
import (
44
"context"
5+
"encoding/json"
6+
"fmt"
57
"testing"
68
"time"
79

810
"github.com/github/deployment-tracker/pkg/deploymentrecord"
911
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
13+
corev1 "k8s.io/api/core/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1115
)
1216

1317
func TestRecordContainer_UnknownArtifactCachePopulatedOn404(t *testing.T) {
@@ -22,7 +26,7 @@ func TestRecordContainer_UnknownArtifactCachePopulatedOn404(t *testing.T) {
2226
// First call should hit the API and get a 404
2327
err := ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
2428
require.NoError(t, err)
25-
assert.Equal(t, 1, poster.getCalls())
29+
assert.Equal(t, 1, poster.getPostOneCalls())
2630

2731
// Digest should now be in the unknown artifacts cache
2832
_, exists := ctrl.unknownArtifacts.Get(digest)
@@ -41,12 +45,12 @@ func TestRecordContainer_UnknownArtifactCacheSkipsAPICall(t *testing.T) {
4145
// First call — API returns 404, populates cache
4246
err := ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
4347
require.NoError(t, err)
44-
assert.Equal(t, 1, poster.getCalls())
48+
assert.Equal(t, 1, poster.getPostOneCalls())
4549

4650
// Second call — should be served from cache, no API call
4751
err = ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
4852
require.NoError(t, err)
49-
assert.Equal(t, 1, poster.getCalls(), "API should not be called for cached unknown artifact")
53+
assert.Equal(t, 1, poster.getPostOneCalls(), "API should not be called for cached unknown artifact")
5054
}
5155

5256
func TestRecordContainer_UnknownArtifactCacheAppliesToDecommission(t *testing.T) {
@@ -61,12 +65,12 @@ func TestRecordContainer_UnknownArtifactCacheAppliesToDecommission(t *testing.T)
6165
// Deploy call — 404, populates cache
6266
err := ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
6367
require.NoError(t, err)
64-
assert.Equal(t, 1, poster.getCalls())
68+
assert.Equal(t, 1, poster.getPostOneCalls())
6569

6670
// Decommission call for same digest — should skip API
6771
err = ctrl.recordContainer(context.Background(), pod, container, EventDeleted, "test-deployment", nil)
6872
require.NoError(t, err)
69-
assert.Equal(t, 1, poster.getCalls(), "decommission should also be skipped for cached unknown artifact")
73+
assert.Equal(t, 1, poster.getPostOneCalls(), "decommission should also be skipped for cached unknown artifact")
7074
}
7175

7276
func TestRecordContainer_UnknownArtifactCacheExpires(t *testing.T) {
@@ -84,15 +88,15 @@ func TestRecordContainer_UnknownArtifactCacheExpires(t *testing.T) {
8488
// Immediately — should be cached
8589
err := ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
8690
require.NoError(t, err)
87-
assert.Equal(t, 0, poster.getCalls(), "should skip API while cached")
91+
assert.Equal(t, 0, poster.getPostOneCalls(), "should skip API while cached")
8892

8993
// Wait for expiry
9094
time.Sleep(100 * time.Millisecond)
9195

9296
// After expiry — should call API again
9397
err = ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
9498
require.NoError(t, err)
95-
assert.Equal(t, 1, poster.getCalls(), "should call API after cache expiry")
99+
assert.Equal(t, 1, poster.getPostOneCalls(), "should call API after cache expiry")
96100
}
97101

98102
func TestRecordContainer_SuccessfulPostDoesNotPopulateUnknownCache(t *testing.T) {
@@ -104,9 +108,158 @@ func TestRecordContainer_SuccessfulPostDoesNotPopulateUnknownCache(t *testing.T)
104108

105109
err := ctrl.recordContainer(context.Background(), pod, container, EventCreated, "test-deployment", nil)
106110
require.NoError(t, err)
107-
assert.Equal(t, 1, poster.getCalls())
111+
assert.Equal(t, 1, poster.getPostOneCalls())
108112

109113
// Digest should NOT be in the unknown artifacts cache
110114
_, exists := ctrl.unknownArtifacts.Get(digest)
111115
assert.False(t, exists, "successful post should not cache digest as unknown")
112116
}
117+
118+
func TestProcessSyncEvents_EmptyPodList(t *testing.T) {
119+
t.Parallel()
120+
poster := &mockPoster{}
121+
ctrl := newTestController(poster)
122+
123+
err := ctrl.processSyncEvents(context.Background(), []interface{}{})
124+
require.NoError(t, err)
125+
assert.Equal(t, 0, poster.getPostClusterCalls(), "PostCluster should not be called for empty pod list")
126+
}
127+
128+
func TestProcessSyncEvents_HappyPath(t *testing.T) {
129+
t.Parallel()
130+
digest := "sha256:abc123"
131+
unknownDigest := "sha256:notfound999"
132+
unauthorizedDigest := "sha256:unauthorized999"
133+
clusterResp := deploymentrecord.DeploymentRecordsClusterResp{
134+
TotalCount: 1,
135+
DeploymentRecords: []*deploymentrecord.DeploymentRecordResp{{
136+
DeploymentRecord: deploymentrecord.DeploymentRecord{
137+
DeploymentRecordBase: deploymentrecord.DeploymentRecordBase{
138+
DeploymentName: "default/test-deploy/app",
139+
Digest: digest,
140+
},
141+
},
142+
}},
143+
Errors: []*deploymentrecord.DeploymentRecordErrorResp{
144+
{
145+
DeploymentRecord: deploymentrecord.DeploymentRecord{
146+
DeploymentRecordBase: deploymentrecord.DeploymentRecordBase{
147+
Digest: unknownDigest,
148+
},
149+
},
150+
Cause: "not_found",
151+
},
152+
{
153+
DeploymentRecord: deploymentrecord.DeploymentRecord{
154+
DeploymentRecordBase: deploymentrecord.DeploymentRecordBase{
155+
Digest: unauthorizedDigest,
156+
},
157+
},
158+
Cause: "unauthorized",
159+
},
160+
},
161+
}
162+
respJSON, err := json.Marshal(clusterResp)
163+
require.NoError(t, err)
164+
165+
poster := &mockPoster{clusterResp: respJSON}
166+
ctrl := newTestController(poster)
167+
ctrl.workloadResolver = &mockResolver{name: "test-deploy"}
168+
169+
err = ctrl.processSyncEvents(context.Background(), []interface{}{
170+
makePod("app", "test-deploy-abc123", digest, "ReplicaSet"),
171+
makePod("unknown", "test-deploy-abc123", unknownDigest, "ReplicaSet"),
172+
makePod("unauthorized", "test-deploy-abc123", unauthorizedDigest, "ReplicaSet"),
173+
})
174+
require.NoError(t, err)
175+
assert.Equal(t, 1, poster.getPostClusterCalls(), "PostCluster should be called once")
176+
assert.Equal(t, 3, poster.clusterRecordCount, "PostCluster should receive 3 records")
177+
178+
// Successful record should be in observedDeployments cache
179+
cacheKey := getCacheKey(EventCreated, "default/test-deploy/app", digest)
180+
_, exists := ctrl.observedDeployments.Get(cacheKey)
181+
assert.True(t, exists, "successful record should populate observedDeployments cache")
182+
183+
// not_found error should be in unknownArtifacts cache
184+
_, exists = ctrl.unknownArtifacts.Get("sha256:notfound999")
185+
assert.True(t, exists, "not_found error should populate unknownArtifacts cache")
186+
187+
// unauthorized error should not be in unknownArtifacts cache
188+
_, exists = ctrl.unknownArtifacts.Get("sha256:unauthorized999")
189+
assert.False(t, exists, "unauthorized error should not populate unknownArtifacts cache")
190+
}
191+
192+
func TestProcessSyncEvents_DedupeContainers(t *testing.T) {
193+
t.Parallel()
194+
digest := "sha256:abc123"
195+
poster := &mockPoster{}
196+
ctrl := newTestController(poster)
197+
ctrl.workloadResolver = &mockResolver{name: "test-deploy"}
198+
199+
pod := makePod("app", "test-deploy-abc123", digest, "ReplicaSet")
200+
201+
err := ctrl.processSyncEvents(context.Background(), []interface{}{pod, pod})
202+
require.NoError(t, err)
203+
assert.Equal(t, 1, poster.getPostClusterCalls(), "PostCluster should be called once")
204+
assert.Equal(t, 1, poster.clusterRecordCount, "PostCluster should receive only 1 record")
205+
}
206+
207+
func TestProcessSyncEvents_PostCluster404(t *testing.T) {
208+
t.Parallel()
209+
poster := &mockPoster{
210+
clusterErr: &deploymentrecord.ClusterNoRepositoriesError{},
211+
}
212+
ctrl := newTestController(poster)
213+
ctrl.workloadResolver = &mockResolver{name: "test-deploy"}
214+
pod := makePod("app", "test-deploy-abc123", "sha256:abc123", "ReplicaSet")
215+
216+
err := ctrl.processSyncEvents(context.Background(), []interface{}{pod})
217+
require.NoError(t, err, "ClusterNoRepositoriesError should not propagate")
218+
assert.Equal(t, 1, poster.getPostClusterCalls())
219+
220+
// Caches should remain empty since no response was processed
221+
cacheKey := getCacheKey(EventCreated, "default/test-deploy/app", "sha256:abc123")
222+
_, exists := ctrl.observedDeployments.Get(cacheKey)
223+
assert.False(t, exists, "observedDeployments should not be populated on 404")
224+
}
225+
226+
func TestProcessSyncEvents_PostCluster500(t *testing.T) {
227+
t.Parallel()
228+
poster := &mockPoster{
229+
clusterErr: fmt.Errorf("server error"),
230+
}
231+
ctrl := newTestController(poster)
232+
ctrl.workloadResolver = &mockResolver{name: "test-deploy"}
233+
pod := makePod("app", "test-deploy-abc123", "sha256:abc123", "ReplicaSet")
234+
235+
err := ctrl.processSyncEvents(context.Background(), []interface{}{pod})
236+
require.Error(t, err)
237+
assert.Contains(t, err.Error(), "failed to post sync cluster records")
238+
assert.Equal(t, 1, poster.getPostClusterCalls())
239+
}
240+
241+
func makePod(containerName string, parentName string, digest string, parentKind string) *corev1.Pod {
242+
return &corev1.Pod{
243+
ObjectMeta: metav1.ObjectMeta{
244+
Name: "test-pod",
245+
Namespace: "default",
246+
OwnerReferences: []metav1.OwnerReference{{
247+
Kind: parentKind,
248+
Name: parentName,
249+
}},
250+
},
251+
Spec: corev1.PodSpec{
252+
Containers: []corev1.Container{{
253+
Name: containerName,
254+
Image: "nginx:latest",
255+
}},
256+
},
257+
Status: corev1.PodStatus{
258+
Phase: corev1.PodRunning,
259+
ContainerStatuses: []corev1.ContainerStatus{{
260+
Name: containerName,
261+
ImageID: fmt.Sprintf("docker-pullable://nginx@%s", digest),
262+
}},
263+
},
264+
}
265+
}

0 commit comments

Comments
 (0)