diff --git a/server/api/services/task.go b/server/api/services/task.go index 83627881cd7..d05ed2a9319 100644 --- a/server/api/services/task.go +++ b/server/api/services/task.go @@ -261,11 +261,10 @@ func (s *service) deviceCleanup() store.TransactionCb { uids := make([]string, len(devices)) for i, device := range devices { uids[i] = device.UID - totalDeleted++ - deletedPerTenant[device.TenantID]++ } - if _, err := s.store.DeviceDeleteMany(ctx, uids); err != nil { + deleted, err := s.store.DeviceDeleteMany(ctx, uids) + if err != nil { log.WithField("page", page+1). WithError(err). Info("Failed to delete devices") @@ -273,7 +272,12 @@ func (s *service) deviceCleanup() store.TransactionCb { return err } - log.WithFields(log.Fields{"page": page + 1, "total_pages": totalPages, "devices": len(devices), "total_deleted": totalDeleted}). + for _, device := range deleted { + totalDeleted++ + deletedPerTenant[device.TenantID]++ + } + + log.WithFields(log.Fields{"page": page + 1, "total_pages": totalPages, "listed": len(devices), "deleted": len(deleted), "total_deleted": totalDeleted}). Info("Processing page of removed devices") if page < totalPages-1 { @@ -318,21 +322,25 @@ func (s *service) ephemeralCleanup() store.TransactionCb { log.WithField("total_devices", len(devices)).Info("Found offline ephemeral devices to cleanup") uids := make([]string, len(devices)) - deletedPerTenant := make(map[string]map[models.DeviceStatus]int64) for i, device := range devices { uids[i] = device.UID - if deletedPerTenant[device.TenantID] == nil { - deletedPerTenant[device.TenantID] = make(map[models.DeviceStatus]int64) - } - deletedPerTenant[device.TenantID][device.Status]++ } - if _, err := s.store.DeviceDeleteMany(ctx, uids); err != nil { + deleted, err := s.store.DeviceDeleteMany(ctx, uids) + if err != nil { log.WithError(err).Error("Failed to delete offline ephemeral devices") return err } + deletedPerTenant := make(map[string]map[models.DeviceStatus]int64) + for _, device := range deleted { + if deletedPerTenant[device.TenantID] == nil { + deletedPerTenant[device.TenantID] = make(map[models.DeviceStatus]int64) + } + deletedPerTenant[device.TenantID][device.Status]++ + } + for _, tenantID := range slices.Sorted(maps.Keys(deletedPerTenant)) { for _, status := range slices.Sorted(maps.Keys(deletedPerTenant[tenantID])) { count := deletedPerTenant[tenantID][status] @@ -345,7 +353,7 @@ func (s *service) ephemeralCleanup() store.TransactionCb { } } - log.WithField("total_deleted", len(devices)).Info("Ephemeral device cleanup completed successfully") + log.WithField("total_deleted", len(deleted)).Info("Ephemeral device cleanup completed successfully") return nil } diff --git a/server/api/services/task_test.go b/server/api/services/task_test.go index 3893d6d71be..56b1152f52b 100644 --- a/server/api/services/task_test.go +++ b/server/api/services/task_test.go @@ -173,7 +173,7 @@ func TestService_DeviceCleanup(t *testing.T) { Once() storeMock. On("DeviceDeleteMany", ctx, []string{"device-1", "device-2"}). - Return(int64(0), errors.New("delete error")). + Return(nil, errors.New("delete error")). Once() }, expected: errors.New("delete error"), @@ -215,7 +215,7 @@ func TestService_DeviceCleanup(t *testing.T) { Once() storeMock. On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). - Return(int64(3), nil). + Return([]store.DeletedDevice{{UID: "device-1", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}, {UID: "device-2", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}, {UID: "device-3", TenantID: "tenant-2", Status: models.DeviceStatusRemoved}}, nil). Once() storeMock. On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-1"), models.DeviceStatusRemoved, int64(-2)). @@ -265,7 +265,7 @@ func TestService_DeviceCleanup(t *testing.T) { Once() storeMock. On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). - Return(int64(3), nil). + Return([]store.DeletedDevice{{UID: "device-1", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}, {UID: "device-2", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}, {UID: "device-3", TenantID: "tenant-2", Status: models.DeviceStatusRemoved}}, nil). Once() storeMock. On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-1"), models.DeviceStatusRemoved, int64(-2)). @@ -278,6 +278,52 @@ func TestService_DeviceCleanup(t *testing.T) { }, expected: nil, }, + { + description: "decrements the counters only by the devices it deleted", + requiredMocks: func() { + queryOptionsMock. + On("Match", mock.MatchedBy(matchFilter())). + Return(nil). + Once() + storeMock. + On("DeviceList", ctx, mock.Anything, store.DeviceAcceptableAsFalse, countOpts). + Return([]models.Device{}, int64(3), nil). + Once() + queryOptionsMock. + On("Match", mock.MatchedBy(matchFilter())). + Return(nil). + Once() + queryOptionsMock. + On("Sort", &sorter). + Return(nil). + Once() + queryOptionsMock. + On("Paginate", &query.Paginator{Page: 1, PerPage: 1000}). + Return(nil). + Once() + storeMock. + On("DeviceList", ctx, mock.Anything, store.DeviceAcceptableAsFalse, pageOpts). + Return( + []models.Device{ + {UID: "device-1", TenantID: "tenant-1", RemovedAt: &thirtyDaysAgo}, + {UID: "device-2", TenantID: "tenant-1", RemovedAt: &thirtyDaysAgo}, + {UID: "device-3", TenantID: "tenant-2", RemovedAt: &thirtyDaysAgo}, + }, + int64(3), + nil, + ). + Once() + storeMock. + On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). + Return([]store.DeletedDevice{{UID: "device-2", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}}, nil). + Once() + storeMock. + On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-1"), models.DeviceStatusRemoved, int64(-1)). + Return(nil). + Once() + }, + expected: nil, + }, { description: "succeeds with multiple pages and updates counters correctly", requiredMocks: func() { @@ -313,7 +359,7 @@ func TestService_DeviceCleanup(t *testing.T) { Once() storeMock. On("DeviceDeleteMany", ctx, []string{"device-1"}). - Return(int64(1), nil). + Return([]store.DeletedDevice{{UID: "device-1", TenantID: "tenant-1", Status: models.DeviceStatusRemoved}}, nil). Once() queryOptionsMock. On("Match", mock.MatchedBy(matchFilter())). @@ -339,7 +385,7 @@ func TestService_DeviceCleanup(t *testing.T) { Once() storeMock. On("DeviceDeleteMany", ctx, []string{"device-2"}). - Return(int64(1), nil). + Return([]store.DeletedDevice{{UID: "device-2", TenantID: "tenant-2", Status: models.DeviceStatusRemoved}}, nil). Once() queryOptionsMock. On("Match", mock.MatchedBy(matchFilter())). @@ -385,6 +431,92 @@ func TestService_DeviceCleanup(t *testing.T) { } } +func TestService_EphemeralCleanup(t *testing.T) { + ctx := context.Background() + + expired := []models.Device{ + {UID: "device-1", TenantID: "tenant-1", Status: models.DeviceStatusAccepted}, + {UID: "device-2", TenantID: "tenant-1", Status: models.DeviceStatusPending}, + {UID: "device-3", TenantID: "tenant-2", Status: models.DeviceStatusAccepted}, + } + + cases := []struct { + description string + requiredMocks func(storeMock *storemock.MockStore) + expected error + }{ + { + description: "fails when cannot delete the devices", + requiredMocks: func(storeMock *storemock.MockStore) { + storeMock.On("DeviceListExpiredEphemeral", ctx).Return(expired, nil).Once() + storeMock. + On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). + Return(nil, errors.New("delete error")). + Once() + }, + expected: errors.New("delete error"), + }, + { + description: "decrements each namespace's counter for the status of every device it deleted", + requiredMocks: func(storeMock *storemock.MockStore) { + storeMock.On("DeviceListExpiredEphemeral", ctx).Return(expired, nil).Once() + storeMock. + On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). + Return([]store.DeletedDevice{ + {UID: "device-1", TenantID: "tenant-1", Status: models.DeviceStatusAccepted}, + {UID: "device-2", TenantID: "tenant-1", Status: models.DeviceStatusPending}, + {UID: "device-3", TenantID: "tenant-2", Status: models.DeviceStatusAccepted}, + }, nil). + Once() + storeMock. + On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-1"), models.DeviceStatusAccepted, int64(-1)). + Return(nil). + Once() + storeMock. + On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-1"), models.DeviceStatusPending, int64(-1)). + Return(nil). + Once() + storeMock. + On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-2"), models.DeviceStatusAccepted, int64(-1)). + Return(nil). + Once() + }, + expected: nil, + }, + { + description: "decrements the counters only by the devices it deleted", + requiredMocks: func(storeMock *storemock.MockStore) { + storeMock.On("DeviceListExpiredEphemeral", ctx).Return(expired, nil).Once() + storeMock. + On("DeviceDeleteMany", ctx, []string{"device-1", "device-2", "device-3"}). + Return([]store.DeletedDevice{ + {UID: "device-3", TenantID: "tenant-2", Status: models.DeviceStatusAccepted}, + }, nil). + Once() + storeMock. + On("NamespaceIncrementDeviceCount", ctx, scope.MustBounded("tenant-2"), models.DeviceStatusAccepted, int64(-1)). + Return(nil). + Once() + }, + expected: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.description, func(tt *testing.T) { + storeMock := storemock.NewMockStore(tt) + storeMock. + On("WithTransaction", ctx, mock.AnythingOfType("store.TransactionCb")). + Return(func(ctx context.Context, cb store.TransactionCb) error { return cb(ctx) }). + Once() + tc.requiredMocks(storeMock) + + s := NewService(storeMock, privateKey, publicKey, cache.NewNullCache()) + require.Equal(tt, tc.expected, s.EphemeralCleanup()(ctx)) + }) + } +} + func TestService_NamespaceDeviceCountSync(t *testing.T) { storeMock := storemock.NewMockStore(t) diff --git a/server/api/store/device.go b/server/api/store/device.go index be46ea6ed7f..2ceeb127b01 100644 --- a/server/api/store/device.go +++ b/server/api/store/device.go @@ -8,6 +8,14 @@ import ( "github.com/shellhub-io/shellhub/pkg/models" ) +// DeletedDevice is a device [DeviceStore.DeviceDeleteMany] deleted, with the namespace and status +// it had, which are what a caller adjusts the namespace's device counts by. +type DeletedDevice struct { + UID string + TenantID string + Status models.DeviceStatus +} + // DeviceAcceptable selects how the acceptable flag is computed for the devices a query // returns. It depends on the namespace's limit, so it cannot be a stored column. type DeviceAcceptable uint @@ -108,8 +116,11 @@ type DeviceStore interface { DeviceLockMAC(ctx context.Context, sc scope.Scope, mac string) error DeviceDelete(ctx context.Context, device *models.Device) error - // DeviceDeleteMany deletes multiple devices by their UIDs. - DeviceDeleteMany(ctx context.Context, uids []string) (deletedCount int64, err error) + // DeviceDeleteMany deletes the devices uids names, with their sessions, and returns the ones it + // deleted. A uid that names no device, including one another call deleted first, is left out, + // so the result can be shorter than uids, and an empty uids deletes nothing. It returns ctx's + // error when ctx ends, and an error wrapping [ErrInternal] for any other database failure. + DeviceDeleteMany(ctx context.Context, uids []string) (deleted []DeletedDevice, err error) // DeviceListExpiredEphemeral lists ephemeral devices that have stayed offline longer than their // per-device ephemeral timeout (in minutes), so the cleanup can remove them. diff --git a/server/api/store/mocks/mock_store.go b/server/api/store/mocks/mock_store.go index 30643d17536..44cc14e889f 100644 --- a/server/api/store/mocks/mock_store.go +++ b/server/api/store/mocks/mock_store.go @@ -1597,22 +1597,24 @@ func (_c *MockStore_DeviceDeleteCustomField_Call) RunAndReturn(run func(ctx cont } // DeviceDeleteMany provides a mock function for the type MockStore -func (_mock *MockStore) DeviceDeleteMany(ctx context.Context, uids []string) (int64, error) { +func (_mock *MockStore) DeviceDeleteMany(ctx context.Context, uids []string) ([]store.DeletedDevice, error) { ret := _mock.Called(ctx, uids) if len(ret) == 0 { panic("no return value specified for DeviceDeleteMany") } - var r0 int64 + var r0 []store.DeletedDevice var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []string) (int64, error)); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, []string) ([]store.DeletedDevice, error)); ok { return returnFunc(ctx, uids) } - if returnFunc, ok := ret.Get(0).(func(context.Context, []string) int64); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, []string) []store.DeletedDevice); ok { r0 = returnFunc(ctx, uids) } else { - r0 = ret.Get(0).(int64) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]store.DeletedDevice) + } } if returnFunc, ok := ret.Get(1).(func(context.Context, []string) error); ok { r1 = returnFunc(ctx, uids) @@ -1652,12 +1654,12 @@ func (_c *MockStore_DeviceDeleteMany_Call) Run(run func(ctx context.Context, uid return _c } -func (_c *MockStore_DeviceDeleteMany_Call) Return(deletedCount int64, err error) *MockStore_DeviceDeleteMany_Call { - _c.Call.Return(deletedCount, err) +func (_c *MockStore_DeviceDeleteMany_Call) Return(deleted []store.DeletedDevice, err error) *MockStore_DeviceDeleteMany_Call { + _c.Call.Return(deleted, err) return _c } -func (_c *MockStore_DeviceDeleteMany_Call) RunAndReturn(run func(ctx context.Context, uids []string) (int64, error)) *MockStore_DeviceDeleteMany_Call { +func (_c *MockStore_DeviceDeleteMany_Call) RunAndReturn(run func(ctx context.Context, uids []string) ([]store.DeletedDevice, error)) *MockStore_DeviceDeleteMany_Call { _c.Call.Return(run) return _c } diff --git a/server/api/store/pg/device.go b/server/api/store/pg/device.go index 73fb796696e..cb35ae78ccb 100644 --- a/server/api/store/pg/device.go +++ b/server/api/store/pg/device.go @@ -346,11 +346,11 @@ func (pg *Pg) DeviceLockMAC(ctx context.Context, sc scope.Scope, mac string) err // DeviceDelete implements [store.DeviceStore]. func (pg *Pg) DeviceDelete(ctx context.Context, device *models.Device) error { - deletedCount, err := pg.DeviceDeleteMany(ctx, []string{device.UID}) + deleted, err := pg.DeviceDeleteMany(ctx, []string{device.UID}) switch { case err != nil: return err - case deletedCount < 1: + case len(deleted) < 1: return store.ErrNoDocuments default: return nil @@ -358,7 +358,7 @@ func (pg *Pg) DeviceDelete(ctx context.Context, device *models.Device) error { } // DeviceDeleteMany implements [store.DeviceStore]. -func (pg *Pg) DeviceDeleteMany(ctx context.Context, uids []string) (int64, error) { +func (pg *Pg) DeviceDeleteMany(ctx context.Context, uids []string) ([]store.DeletedDevice, error) { db := pg.GetConnection(ctx) fn := pg.deviceDeleteManyFn(ctx, uids) @@ -367,7 +367,7 @@ func (pg *Pg) DeviceDeleteMany(ctx context.Context, uids []string) (int64, error } else { tx, err := pg.driver.BeginTx(ctx, nil) if err != nil { - return 0, fromSQLError(err) + return nil, fromSQLError(err) } defer func() { @@ -377,38 +377,49 @@ func (pg *Pg) DeviceDeleteMany(ctx context.Context, uids []string) (int64, error } }() - count, err := fn(tx) + deleted, err := fn(tx) if err != nil { _ = tx.Rollback() - return 0, err + return nil, err } if err := tx.Commit(); err != nil { - return 0, fromSQLError(err) + return nil, fromSQLError(err) } - return count, nil + return deleted, nil } } -func (pg *Pg) deviceDeleteManyFn(ctx context.Context, uids []string) func(tx bun.Tx) (int64, error) { - return func(tx bun.Tx) (int64, error) { - r, err := tx.NewDelete().Model((*entity.Device)(nil)).Where("id IN (?)", bun.List(uids)).Exec(ctx) - if err != nil { - return 0, fromSQLError(err) +func (pg *Pg) deviceDeleteManyFn(ctx context.Context, uids []string) func(tx bun.Tx) ([]store.DeletedDevice, error) { + return func(tx bun.Tx) ([]store.DeletedDevice, error) { + deleted := []store.DeletedDevice{} + if len(uids) == 0 { + return deleted, nil } - count, _ := r.RowsAffected() + rows := []entity.Device{} + if err := tx.NewDelete(). + Model(&rows). + Where("id IN (?)", bun.List(uids)). + Returning("id, namespace_id, status"). + Scan(ctx); err != nil { + return nil, fromSQLError(err) + } if _, err := tx.NewDelete(). Model((*entity.Session)(nil)). Where("device_id IN (?)", bun.List(uids)). Exec(ctx); err != nil { - return 0, fromSQLError(err) + return nil, fromSQLError(err) + } + + for _, row := range rows { + deleted = append(deleted, store.DeletedDevice{UID: row.ID, TenantID: row.NamespaceID, Status: models.DeviceStatus(row.Status)}) } - return count, nil + return deleted, nil } } diff --git a/server/api/store/storetest/device_tests.go b/server/api/store/storetest/device_tests.go index 40c4816ad31..ff025751fc9 100644 --- a/server/api/store/storetest/device_tests.go +++ b/server/api/store/storetest/device_tests.go @@ -895,9 +895,9 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { s.CreateDevice(t, WithDeviceName("device-1")) s.CreateDevice(t, WithDeviceName("device-2")) - deletedCount, err := st.DeviceDeleteMany(ctx, []string{}) + deleted, err := st.DeviceDeleteMany(ctx, []string{}) require.NoError(t, err) - assert.Equal(t, int64(0), deletedCount) + assert.Empty(t, deleted) }) t.Run("succeeds when devices match", func(t *testing.T) { @@ -908,9 +908,9 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { uid3 := s.CreateDevice(t, WithDeviceName("device-3")) uids := []string{string(uid1), string(uid2)} - deletedCount, err := st.DeviceDeleteMany(ctx, uids) + deleted, err := st.DeviceDeleteMany(ctx, uids) require.NoError(t, err) - assert.Equal(t, int64(2), deletedCount) + assert.Len(t, deleted, 2) for _, uid := range uids { _, err := st.DeviceResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.DeviceUIDResolver, uid) @@ -922,6 +922,40 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { assert.Equal(t, string(uid3), device.UID) }) + t.Run("returns the namespace and status of each device it deleted", func(t *testing.T) { + require.NoError(t, s.provider.CleanDatabase(t)) + + accepted := s.CreateDevice(t, WithDeviceStatus(models.DeviceStatusAccepted)) + removed := s.CreateDevice(t, WithDeviceStatus(models.DeviceStatusRemoved)) + + acceptedDevice, err := st.DeviceResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.DeviceUIDResolver, string(accepted)) + require.NoError(t, err) + removedDevice, err := st.DeviceResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.DeviceUIDResolver, string(removed)) + require.NoError(t, err) + + deleted, err := st.DeviceDeleteMany(ctx, []string{string(accepted), string(removed)}) + require.NoError(t, err) + assert.ElementsMatch(t, []store.DeletedDevice{ + {UID: string(accepted), TenantID: acceptedDevice.TenantID, Status: models.DeviceStatusAccepted}, + {UID: string(removed), TenantID: removedDevice.TenantID, Status: models.DeviceStatusRemoved}, + }, deleted) + }) + + t.Run("leaves out a device an earlier call already deleted", func(t *testing.T) { + require.NoError(t, s.provider.CleanDatabase(t)) + + first := s.CreateDevice(t, WithDeviceName("deleted-first")) + second := s.CreateDevice(t, WithDeviceName("deleted-second")) + + _, err := st.DeviceDeleteMany(ctx, []string{string(first)}) + require.NoError(t, err) + + deleted, err := st.DeviceDeleteMany(ctx, []string{string(first), string(second)}) + require.NoError(t, err) + require.Len(t, deleted, 1) + assert.Equal(t, string(second), deleted[0].UID) + }) + t.Run("deletes related sessions in cascade", func(t *testing.T) { require.NoError(t, s.provider.CleanDatabase(t)) @@ -934,9 +968,9 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { session3UID := s.CreateSession(t, WithSessionDevice(uid3)) uids := []string{string(uid1), string(uid2)} - deletedCount, err := st.DeviceDeleteMany(ctx, uids) + deleted, err := st.DeviceDeleteMany(ctx, uids) require.NoError(t, err) - assert.Equal(t, int64(2), deletedCount) + assert.Len(t, deleted, 2) _, err = st.SessionResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.SessionUIDResolver, string(session1UID)) require.ErrorIs(t, err, store.ErrNoDocuments) @@ -954,15 +988,15 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { uid1 := s.CreateDevice(t, WithDeviceName("device-tx-1")) uid2 := s.CreateDevice(t, WithDeviceName("device-tx-2")) - var deletedCount int64 + var deleted []store.DeletedDevice err := st.WithTransaction(ctx, func(ctx context.Context) error { var err error - deletedCount, err = st.DeviceDeleteMany(ctx, []string{string(uid1), string(uid2)}) + deleted, err = st.DeviceDeleteMany(ctx, []string{string(uid1), string(uid2)}) return err }) require.NoError(t, err) - assert.Equal(t, int64(2), deletedCount) + assert.Len(t, deleted, 2) _, err = st.DeviceResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.DeviceUIDResolver, string(uid1)) require.ErrorIs(t, err, store.ErrNoDocuments) @@ -983,9 +1017,9 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { "non-existent-uid-2", } - deletedCount, err := st.DeviceDeleteMany(ctx, uids) + deleted, err := st.DeviceDeleteMany(ctx, uids) require.NoError(t, err) - assert.Equal(t, int64(2), deletedCount) + assert.Len(t, deleted, 2) _, err = st.DeviceResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.DeviceUIDResolver, string(uid1)) require.ErrorIs(t, err, store.ErrNoDocuments) @@ -1002,9 +1036,9 @@ func (s *Suite) TestDeviceDeleteMany(t *testing.T) { session2UID := s.CreateSession(t, WithSessionDevice(deviceUID), WithSessionUser("user2")) session3UID := s.CreateSession(t, WithSessionDevice(deviceUID), WithSessionUser("user3")) - deletedCount, err := st.DeviceDeleteMany(ctx, []string{string(deviceUID)}) + deleted, err := st.DeviceDeleteMany(ctx, []string{string(deviceUID)}) require.NoError(t, err) - assert.Equal(t, int64(1), deletedCount) + assert.Len(t, deleted, 1) _, err = st.SessionResolve(ctx, scope.NewUnbounded(reasonTestQueryMechanics), store.SessionUIDResolver, string(session1UID)) require.ErrorIs(t, err, store.ErrNoDocuments)