package manager import ( "context" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/store" ) func TestCleanupOldAccessLogs(t *testing.T) { tests := []struct { name string retentionDays int setupMock func(*store.MockStore) expectedCount int64 expectedError bool }{ { name: "cleanup logs older than retention period", retentionDays: 30, setupMock: func(mockStore *store.MockStore) { mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, olderThan time.Time) (int64, error) { expectedCutoff := time.Now().AddDate(0, 0, -30) timeDiff := olderThan.Sub(expectedCutoff) if timeDiff.Abs() > time.Second { t.Errorf("cutoff time not as expected: got %v, want ~%v", olderThan, expectedCutoff) } return 5, nil }) }, expectedCount: 5, expectedError: false, }, { name: "no logs to cleanup", retentionDays: 30, setupMock: func(mockStore *store.MockStore) { mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(0), nil) }, expectedCount: 0, expectedError: false, }, { name: "zero retention days skips cleanup", retentionDays: 0, setupMock: func(mockStore *store.MockStore) { // No expectations - DeleteOldAccessLogs should not be called }, expectedCount: 0, expectedError: false, }, { name: "negative retention days skips cleanup", retentionDays: -10, setupMock: func(mockStore *store.MockStore) { // No expectations - DeleteOldAccessLogs should not be called }, expectedCount: 0, expectedError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) tt.setupMock(mockStore) manager := &managerImpl{ store: mockStore, } ctx := context.Background() deletedCount, err := manager.CleanupOldAccessLogs(ctx, tt.retentionDays) if tt.expectedError { require.Error(t, err) } else { require.NoError(t, err) } assert.Equal(t, tt.expectedCount, deletedCount, "unexpected number of deleted logs") }) } } func TestCleanupWithExactBoundary(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, olderThan time.Time) (int64, error) { expectedCutoff := time.Now().AddDate(0, 0, -30) timeDiff := olderThan.Sub(expectedCutoff) assert.Less(t, timeDiff.Abs(), time.Second, "cutoff time should be close to expected value") return 1, nil }) manager := &managerImpl{ store: mockStore, } ctx := context.Background() deletedCount, err := manager.CleanupOldAccessLogs(ctx, 30) require.NoError(t, err) assert.Equal(t, int64(1), deletedCount) } func TestStartPeriodicCleanup(t *testing.T) { t.Run("periodic cleanup disabled with negative retention", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) // No expectations - cleanup should not run manager := &managerImpl{ store: mockStore, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() manager.StartPeriodicCleanup(ctx, -1, 1) time.Sleep(100 * time.Millisecond) // If DeleteOldAccessLogs was called, the test will fail due to unexpected call }) t.Run("periodic cleanup runs immediately on start", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(2), nil). Times(1) manager := &managerImpl{ store: mockStore, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() manager.StartPeriodicCleanup(ctx, 30, 24) time.Sleep(200 * time.Millisecond) // Expectations verified by gomock on defer ctrl.Finish() }) t.Run("periodic cleanup stops on context cancel", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(1), nil). Times(1) manager := &managerImpl{ store: mockStore, } ctx, cancel := context.WithCancel(context.Background()) manager.StartPeriodicCleanup(ctx, 30, 24) time.Sleep(100 * time.Millisecond) cancel() time.Sleep(200 * time.Millisecond) }) t.Run("cleanup interval defaults to 24 hours when invalid", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(0), nil). Times(1) manager := &managerImpl{ store: mockStore, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() manager.StartPeriodicCleanup(ctx, 30, 0) time.Sleep(100 * time.Millisecond) manager.StopPeriodicCleanup() }) t.Run("cleanup interval uses configured hours", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(3), nil). Times(1) manager := &managerImpl{ store: mockStore, } ctx, cancel := context.WithCancel(context.Background()) defer cancel() manager.StartPeriodicCleanup(ctx, 30, 12) time.Sleep(100 * time.Millisecond) manager.StopPeriodicCleanup() }) } func TestStopPeriodicCleanup(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockStore := store.NewMockStore(ctrl) mockStore.EXPECT(). DeleteOldAccessLogs(gomock.Any(), gomock.Any()). Return(int64(1), nil). Times(1) manager := &managerImpl{ store: mockStore, } ctx := context.Background() manager.StartPeriodicCleanup(ctx, 30, 24) time.Sleep(100 * time.Millisecond) manager.StopPeriodicCleanup() time.Sleep(200 * time.Millisecond) // Expectations verified by gomock - would fail if more than 1 call happened } func TestStopPeriodicCleanup_NotStarted(t *testing.T) { manager := &managerImpl{} // Should not panic if cleanup was never started manager.StopPeriodicCleanup() }