@@ -2,6 +2,7 @@ package storage
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "fmt"
78 "log/slog"
@@ -466,6 +467,115 @@ func (suite *storeTestSuite) TestWatchList() {
466467 suite .NotEmpty (bookmarkObj .ResourceVersion )
467468}
468469
470+ // When a non-Deleted event arrives for an object the store no longer holds, the watcher must still broadcast the payload.
471+ func (suite * storeTestSuite ) TestHandleMessageNotFoundStillBroadcasts () {
472+ ctx , cancel := context .WithCancel (suite .T ().Context ())
473+ defer cancel ()
474+
475+ w , err := suite .broadcaster .Watch ()
476+ suite .Require ().NoError (err )
477+ defer w .Stop ()
478+
479+ go suite .watcher .Start (ctx )
480+ suite .Require ().NoError (suite .nc .Flush ())
481+
482+ sbom := & storagev1alpha1.SBOM {
483+ ObjectMeta : metav1.ObjectMeta {
484+ Name : "ghost" ,
485+ Namespace : "default" ,
486+ },
487+ }
488+ sbomBytes , err := json .Marshal (sbom )
489+ suite .Require ().NoError (err )
490+
491+ payload , err := json .Marshal (event {
492+ EventType : watch .Added ,
493+ Object : runtime.RawExtension {Raw : sbomBytes },
494+ })
495+ suite .Require ().NoError (err )
496+
497+ suite .Require ().NoError (suite .nc .Publish ("watch.sboms" , payload ))
498+ suite .Require ().NoError (suite .nc .Flush ())
499+
500+ events := mustReadEvents (suite .T (), w , 1 )
501+ suite .Equal (watch .Added , events [0 ].Type )
502+
503+ got , ok := events [0 ].Object .(* storagev1alpha1.SBOM )
504+ suite .Require ().True (ok )
505+ suite .Equal ("ghost" , got .Name )
506+ suite .Equal ("default" , got .Namespace )
507+ }
508+
509+ // When the store holds a different object at the same namespace/name, the watcher must broadcast the payload, not the refetched object.
510+ func (suite * storeTestSuite ) TestHandleMessageUIDMismatchBroadcastsPayload () {
511+ ctx , cancel := context .WithCancel (suite .T ().Context ())
512+ defer cancel ()
513+
514+ w , err := suite .broadcaster .Watch ()
515+ suite .Require ().NoError (err )
516+ defer w .Stop ()
517+
518+ go suite .watcher .Start (ctx )
519+ suite .Require ().NoError (suite .nc .Flush ())
520+
521+ // Populate the store at default/collide. The stale event published next will carry a different UID for the same namespace/name.
522+ storedUID := types .UID ("stored-uid" )
523+ stored := & storagev1alpha1.SBOM {
524+ ObjectMeta : metav1.ObjectMeta {
525+ Name : "collide" ,
526+ Namespace : "default" ,
527+ UID : storedUID ,
528+ },
529+ SPDX : runtime.RawExtension {Raw : []byte (`{"stored": true}` )},
530+ }
531+ key := keyPrefix + "/default/collide"
532+ err = suite .store .Create (ctx , key , stored , & storagev1alpha1.SBOM {}, 0 )
533+ suite .Require ().NoError (err )
534+
535+ // Drain the ADDED event produced by the Create above
536+ created := mustReadEvents (suite .T (), w , 1 )
537+ suite .Equal (watch .Added , created [0 ].Type )
538+ createdSBOM , ok := created [0 ].Object .(* storagev1alpha1.SBOM )
539+ suite .Require ().True (ok )
540+ suite .Equal (storedUID , createdSBOM .UID )
541+
542+ // Publish a stale ADDED carrying a different UID at the same namespace/name
543+ staleUID := types .UID ("stale-uid" )
544+ suite .Require ().NotEqual (storedUID , staleUID )
545+
546+ stale := & storagev1alpha1.SBOM {
547+ ObjectMeta : metav1.ObjectMeta {
548+ Name : "collide" ,
549+ Namespace : "default" ,
550+ UID : staleUID ,
551+ },
552+ }
553+ staleBytes , err := json .Marshal (stale )
554+ suite .Require ().NoError (err )
555+
556+ payload , err := json .Marshal (event {
557+ EventType : watch .Added ,
558+ Object : runtime.RawExtension {Raw : staleBytes },
559+ })
560+ suite .Require ().NoError (err )
561+
562+ suite .Require ().NoError (suite .nc .Publish ("watch.sboms" , payload ))
563+ suite .Require ().NoError (suite .nc .Flush ())
564+
565+ events := mustReadEvents (suite .T (), w , 1 )
566+ suite .Equal (watch .Added , events [0 ].Type )
567+
568+ got , ok := events [0 ].Object .(* storagev1alpha1.SBOM )
569+ suite .Require ().True (ok )
570+ suite .Equal (staleUID , got .UID , "expected payload UID, not stored UID" )
571+
572+ // Stored object should still be intact and retrievable with its own UID
573+ fetched := & storagev1alpha1.SBOM {}
574+ err = suite .store .Get (ctx , key , k8sstorage.GetOptions {}, fetched )
575+ suite .Require ().NoError (err )
576+ suite .Equal (storedUID , fetched .UID )
577+ }
578+
469579func (suite * storeTestSuite ) TestGetList () {
470580 key := keyPrefix + "/default"
471581 sbom1 := storagev1alpha1.SBOM {
0 commit comments