From 57603f4e67b2aa2721df2e7a1d7c39c9205601fb Mon Sep 17 00:00:00 2001 From: Otavio Salvador Date: Sun, 18 Oct 2026 18:46:36 -0300 Subject: [PATCH] fix(server): decrement device counts only by the devices a cleanup deleted The removed-device and ephemeral cleanups listed the devices to purge, deleted them, and then lowered each namespace's device count by the number they had listed. When two runs overlap, the second run's DELETE waits on the first one's row locks and then deletes nothing, yet it still subtracted its whole list, so the counts dropped twice and stayed wrong until the 03:00 count sync. Scheduled runs do not overlap: both jobs are registered unique, which holds a Redis lock across replicas. A run queued outside the scheduler does, such as the e2e suite's RunCron or an asynq CLI enqueue, and so does any run when SHELLHUB_ASYNQ_UNIQUENESS_TIMEOUT is 0. DeviceDeleteMany now returns the devices it actually deleted, each with its namespace and status, and both cleanups count from that. DeviceDelete keeps its contract on top of it. --- server/api/services/task.go | 30 +++-- server/api/services/task_test.go | 142 ++++++++++++++++++++- server/api/store/device.go | 15 ++- server/api/store/mocks/mock_store.go | 18 +-- server/api/store/pg/device.go | 43 ++++--- server/api/store/storetest/device_tests.go | 60 +++++++-- 6 files changed, 253 insertions(+), 55 deletions(-) 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)