@@ -2,12 +2,16 @@ package controller
22
33import (
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
1317func 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
5256func 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
7276func 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
98102func 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