@@ -180,6 +180,30 @@ func TestKeychainSaveCollapsesDuplicatesInPlace(t *testing.T) {
180180 "the remaining duplicates must be collapsed, leaving only the first match" )
181181}
182182
183+ // The real-keychain dedup tests use their own service group/name so their items
184+ // are namespace-isolated from TestKeychain (which shares com.test.test/test).
185+ // GetAllMetadata/Filter search by {service:group, service:name}, so a leaked
186+ // dedup item can never show up in — and break — the shared suite.
187+ const (
188+ dedupServiceGroup = "com.test.dedup"
189+ dedupServiceName = "dedup"
190+ )
191+
192+ // ensureUnlocked unlocks the collection and waits until the daemon actually
193+ // reports it unlocked. The freedesktop Unlock call can return before the
194+ // collection is fully unlocked, so a CreateItem/DeleteItem issued immediately
195+ // after can still fail with "locked collection" — polling IsLocked closes that
196+ // race. (The store's own Save/Get/Delete avoid it only because the collection
197+ // stays unlocked once any earlier operation has unlocked it.)
198+ func ensureUnlocked (t * testing.T , svc * kc.SecretService , collection dbus.ObjectPath ) {
199+ t .Helper ()
200+ require .NoError (t , svc .Unlock ([]dbus.ObjectPath {collection }))
201+ require .Eventually (t , func () bool {
202+ locked , err := svc .IsLocked (collection )
203+ return err == nil && ! locked
204+ }, 5 * time .Second , 100 * time .Millisecond , "collection did not unlock" )
205+ }
206+
183207// searchRealItems queries a live Secret Service for every item sharing id's
184208// stable identity triple {service:group, service:name, id}. The store API keys
185209// results by ID and so would collapse duplicates into one logical entry —
@@ -251,15 +275,14 @@ func seedRealDuplicates(t *testing.T, serviceGroup, serviceName string, id store
251275 collection , err := getDefaultCollection (svc )
252276 require .NoError (t , err )
253277
254- // Unlock the collection first — talking to the daemon directly skips the
255- // unlock the store does internally, and gnome-keyring reports even the
256- // passwordless 'login' collection as locked until then. Unlock is a no-op on
257- // an already-unlocked collection.
258- require .NoError (t , svc .Unlock ([]dbus.ObjectPath {collection }))
278+ // Talking to the daemon directly skips the unlock the store does internally,
279+ // and gnome-keyring reports even the passwordless 'login' collection as
280+ // locked until then.
281+ ensureUnlocked (t , svc , collection )
259282
260283 label := serviceGroup + ":" + serviceName + ":" + id .String ()
261284 for i := range n {
262- sessSecret , err := session .NewSecret (fmt .Appendf (nil , "seed-secret -%d" , i ))
285+ sessSecret , err := session .NewSecret (fmt .Appendf (nil , "seed-user:seed-pass -%d" , i ))
263286 require .NoError (t , err )
264287
265288 attrs := map [string ]string {
@@ -276,19 +299,26 @@ func seedRealDuplicates(t *testing.T, serviceGroup, serviceName string, id store
276299}
277300
278301// purgeRealItems removes every item for id, draining any leftover duplicates so
279- // a failed assertion cannot leak state into another test. Best-effort: a delete
280- // that fails should not mask the test's own failure.
302+ // the test cannot leak state. It unlocks the collection first (DeleteItem fails
303+ // on a locked collection) and deletes all matches, asserting success so a silent
304+ // cleanup failure surfaces as a leak rather than corrupting a later test.
281305func purgeRealItems (t * testing.T , serviceGroup , serviceName string , id store.ID ) {
282306 t .Helper ()
283- items := findRealItems (t , serviceGroup , serviceName , id )
284- if len (items ) == 0 {
285- return
286- }
287307 svc , err := kc .NewService ()
288308 require .NoError (t , err )
289309 defer func () { _ = svc .Close () }()
310+
311+ collection , err := getDefaultCollection (svc )
312+ require .NoError (t , err )
313+ ensureUnlocked (t , svc , collection )
314+
315+ attrs := map [string ]string {}
316+ safelySetMetadata (serviceGroup , serviceName , attrs )
317+ safelySetID (id , attrs )
318+ items , err := svc .SearchCollection (collection , attrs )
319+ require .NoError (t , err )
290320 for _ , item := range items {
291- _ = svc .DeleteItem (item )
321+ require . NoError ( t , svc .DeleteItem (item ) )
292322 }
293323}
294324
@@ -297,18 +327,14 @@ func purgeRealItems(t *testing.T, serviceGroup, serviceName string, id store.ID)
297327// identity, a single Save must update one item in place and delete the rest,
298328// leaving exactly one item holding the latest secret.
299329func TestKeychainCollapsesExistingDuplicates (t * testing.T ) {
300- const (
301- serviceGroup = "com.test.test"
302- serviceName = "test"
303- )
304- id := store .MustParseID ("com.test.test/test/dup-backlog" )
305- t .Cleanup (func () { purgeRealItems (t , serviceGroup , serviceName , id ) })
330+ id := store .MustParseID (dedupServiceGroup + "/" + dedupServiceName + "/backlog" )
331+ t .Cleanup (func () { purgeRealItems (t , dedupServiceGroup , dedupServiceName , id ) })
306332
307333 // Pre-existing backlog: three duplicate items for one identity.
308- seedRealDuplicates (t , serviceGroup , serviceName , id , 3 )
309- require .Len (t , findRealItems (t , serviceGroup , serviceName , id ), 3 , "precondition: three duplicates seeded" )
334+ seedRealDuplicates (t , dedupServiceGroup , dedupServiceName , id , 3 )
335+ require .Len (t , findRealItems (t , dedupServiceGroup , dedupServiceName , id ), 3 , "precondition: three duplicates seeded" )
310336
311- ks , err := New (serviceGroup , serviceName , func (_ context.Context , _ store.ID ) store.Secret {
337+ ks , err := New (dedupServiceGroup , dedupServiceName , func (_ context.Context , _ store.ID ) store.Secret {
312338 return & mocks.MockCredential {}
313339 })
314340 require .NoError (t , err )
@@ -318,7 +344,7 @@ func TestKeychainCollapsesExistingDuplicates(t *testing.T) {
318344 Password : "final-password" ,
319345 }))
320346
321- requireItemCount (t , serviceGroup , serviceName , id , 1 ,
347+ requireItemCount (t , dedupServiceGroup , dedupServiceName , id , 1 ,
322348 "a single Save must collapse the duplicate backlog to one item" )
323349
324350 got , err := ks .Get (t .Context (), id )
@@ -332,14 +358,10 @@ func TestKeychainCollapsesExistingDuplicates(t *testing.T) {
332358// metadata that changes on every save (mimicking volatile JWT claims) must keep
333359// exactly one item, never minting duplicates.
334360func TestKeychainSaveDoesNotAccumulate (t * testing.T ) {
335- const (
336- serviceGroup = "com.test.test"
337- serviceName = "test"
338- )
339- id := store .MustParseID ("com.test.test/test/no-accumulate" )
340- t .Cleanup (func () { purgeRealItems (t , serviceGroup , serviceName , id ) })
341-
342- ks , err := New (serviceGroup , serviceName , func (_ context.Context , _ store.ID ) store.Secret {
361+ id := store .MustParseID (dedupServiceGroup + "/" + dedupServiceName + "/no-accumulate" )
362+ t .Cleanup (func () { purgeRealItems (t , dedupServiceGroup , dedupServiceName , id ) })
363+
364+ ks , err := New (dedupServiceGroup , dedupServiceName , func (_ context.Context , _ store.ID ) store.Secret {
343365 return & mocks.MockCredential {}
344366 })
345367 require .NoError (t , err )
@@ -355,7 +377,7 @@ func TestKeychainSaveDoesNotAccumulate(t *testing.T) {
355377 }))
356378 }
357379
358- requireItemCount (t , serviceGroup , serviceName , id , 1 ,
380+ requireItemCount (t , dedupServiceGroup , dedupServiceName , id , 1 ,
359381 "saving with changing metadata must not accumulate duplicate items" )
360382
361383 got , err := ks .Get (t .Context (), id )
0 commit comments