1package usecase_test
2
3import (
4 "errors"
5 "testing"
6 "time"
7
8 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/domain/entity"
11 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/mock"
14 "github.com/stretchr/testify/require"
15)
16
17func TestCleanupSessionsUseCase_ReconcileActiveBrowserSessions_EndsOnlyProvenDeadSessions(t *testing.T) {
18 ctx := testContext()
19
20 sessionRepo := repomocks.NewMockSessionRepository(t)
21 processProbe := portmocks.NewMockSessionProcessProbe(t)
22
23 now := time.Date(2026, 6, 18, 20, 0, 0, 0, time.UTC)
24 livePID := 111
25 deadPID := 222
26 current := &entity.Session{ID: "current", Type: entity.SessionTypeBrowser, StartedAt: now}
27 live := &entity.Session{ID: "live", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-time.Minute), ProcessID: &livePID}
28 dead := &entity.Session{ID: "dead", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-2 * time.Minute), ProcessID: &deadPID}
29 unknown := &entity.Session{ID: "unknown", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-3 * time.Minute)}
30
31 sessionRepo.EXPECT().
32 GetRecent(ctx, 25).
33 Return([]*entity.Session{current, live, dead, unknown}, nil)
34 processProbe.EXPECT().
35 IsProcessAlive(ctx, livePID).
36 Return(true, nil)
37 processProbe.EXPECT().
38 IsProcessAlive(ctx, deadPID).
39 Return(false, nil)
40 sessionRepo.EXPECT().
41 MarkEnded(ctx, dead.ID, now).
42 Return(nil)
43
44 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, processProbe)
45
46 output, err := uc.ReconcileActiveBrowserSessions(ctx, usecase.ReconcileActiveBrowserSessionsInput{
47 CurrentSessionID: current.ID,
48 RecentLimit: 25,
49 EndedAt: now,
50 })
51
52 require.NoError(t, err)
53 assert.Equal(t, int64(1), output.EndedDeadSessions)
54}
55
56func TestCleanupSessionsUseCase_ReconcileActiveBrowserSessions_SkipsWhenProbeErrors(t *testing.T) {
57 ctx := testContext()
58
59 sessionRepo := repomocks.NewMockSessionRepository(t)
60 processProbe := portmocks.NewMockSessionProcessProbe(t)
61
62 now := time.Date(2026, 6, 18, 20, 30, 0, 0, time.UTC)
63 pid := 333
64 active := &entity.Session{ID: "active", Type: entity.SessionTypeBrowser, StartedAt: now.Add(-time.Minute), ProcessID: &pid}
65
66 sessionRepo.EXPECT().
67 GetRecent(ctx, 20).
68 Return([]*entity.Session{active}, nil)
69 processProbe.EXPECT().
70 IsProcessAlive(ctx, pid).
71 Return(false, errors.New("probe failed"))
72
73 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, processProbe)
74
75 output, err := uc.ReconcileActiveBrowserSessions(ctx, usecase.ReconcileActiveBrowserSessionsInput{
76 RecentLimit: 20,
77 EndedAt: now,
78 })
79
80 require.NoError(t, err)
81 assert.Equal(t, int64(0), output.EndedDeadSessions)
82}
83
84func TestCleanupSessionsUseCase_Execute_AgeAndCountCleanup(t *testing.T) {
85 ctx := testContext()
86
87 sessionRepo := repomocks.NewMockSessionRepository(t)
88
89 // Expect age-based deletion
90 sessionRepo.EXPECT().
91 DeleteExitedBefore(ctx, mock.MatchedBy(func(cutoff time.Time) bool {
92 // Should be approximately 7 days ago
93 expected := time.Now().AddDate(0, 0, -7)
94 diff := expected.Sub(cutoff)
95 return diff > -time.Minute && diff < time.Minute
96 })).
97 Return(int64(3), nil)
98
99 // Expect count-based deletion
100 sessionRepo.EXPECT().
101 DeleteOldestExited(ctx, 50).
102 Return(int64(2), nil)
103
104 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
105
106 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
107 MaxExitedSessions: 50,
108 MaxExitedSessionAgeDays: 7,
109 })
110
111 require.NoError(t, err)
112 assert.Equal(t, int64(3), output.DeletedByAge)
113 assert.Equal(t, int64(2), output.DeletedByCount)
114 assert.Equal(t, int64(5), output.TotalDeleted)
115}
116
117func TestCleanupSessionsUseCase_Execute_AgeOnlyCleanup(t *testing.T) {
118 ctx := testContext()
119
120 sessionRepo := repomocks.NewMockSessionRepository(t)
121
122 // Expect age-based deletion
123 sessionRepo.EXPECT().
124 DeleteExitedBefore(ctx, mock.Anything).
125 Return(int64(5), nil)
126
127 // Count-based with -1 should be skipped (disabled)
128 // MaxExitedSessions >= 0 triggers count cleanup, so use -1 to disable
129 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
130
131 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
132 MaxExitedSessions: -1, // Disabled
133 MaxExitedSessionAgeDays: 30,
134 })
135
136 require.NoError(t, err)
137 assert.Equal(t, int64(5), output.DeletedByAge)
138 assert.Equal(t, int64(0), output.DeletedByCount)
139 assert.Equal(t, int64(5), output.TotalDeleted)
140}
141
142func TestCleanupSessionsUseCase_Execute_CountOnlyCleanup(t *testing.T) {
143 ctx := testContext()
144
145 sessionRepo := repomocks.NewMockSessionRepository(t)
146
147 // Count-based deletion only (age disabled with 0)
148 sessionRepo.EXPECT().
149 DeleteOldestExited(ctx, 10).
150 Return(int64(4), nil)
151
152 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
153
154 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
155 MaxExitedSessions: 10,
156 MaxExitedSessionAgeDays: 0, // Disabled
157 })
158
159 require.NoError(t, err)
160 assert.Equal(t, int64(0), output.DeletedByAge)
161 assert.Equal(t, int64(4), output.DeletedByCount)
162 assert.Equal(t, int64(4), output.TotalDeleted)
163}
164
165func TestCleanupSessionsUseCase_Execute_NoCleanupNeeded(t *testing.T) {
166 ctx := testContext()
167
168 sessionRepo := repomocks.NewMockSessionRepository(t)
169
170 // Nothing to delete
171 sessionRepo.EXPECT().
172 DeleteExitedBefore(ctx, mock.Anything).
173 Return(int64(0), nil)
174
175 sessionRepo.EXPECT().
176 DeleteOldestExited(ctx, 50).
177 Return(int64(0), nil)
178
179 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
180
181 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
182 MaxExitedSessions: 50,
183 MaxExitedSessionAgeDays: 7,
184 })
185
186 require.NoError(t, err)
187 assert.Equal(t, int64(0), output.DeletedByAge)
188 assert.Equal(t, int64(0), output.DeletedByCount)
189 assert.Equal(t, int64(0), output.TotalDeleted)
190}
191
192func TestCleanupSessionsUseCase_Execute_AgeDeleteError_ContinuesToCount(t *testing.T) {
193 ctx := testContext()
194
195 sessionRepo := repomocks.NewMockSessionRepository(t)
196
197 // Age-based deletion fails
198 sessionRepo.EXPECT().
199 DeleteExitedBefore(ctx, mock.Anything).
200 Return(int64(0), errors.New("db error"))
201
202 // Should still attempt count-based deletion
203 sessionRepo.EXPECT().
204 DeleteOldestExited(ctx, 50).
205 Return(int64(3), nil)
206
207 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
208
209 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
210 MaxExitedSessions: 50,
211 MaxExitedSessionAgeDays: 7,
212 })
213
214 // Should not return error, just log warning
215 require.NoError(t, err)
216 assert.Equal(t, int64(0), output.DeletedByAge)
217 assert.Equal(t, int64(3), output.DeletedByCount)
218 assert.Equal(t, int64(3), output.TotalDeleted)
219}
220
221func TestCleanupSessionsUseCase_Execute_CountDeleteError(t *testing.T) {
222 ctx := testContext()
223
224 sessionRepo := repomocks.NewMockSessionRepository(t)
225
226 // Age-based deletion succeeds
227 sessionRepo.EXPECT().
228 DeleteExitedBefore(ctx, mock.Anything).
229 Return(int64(2), nil)
230
231 // Count-based deletion fails
232 sessionRepo.EXPECT().
233 DeleteOldestExited(ctx, 50).
234 Return(int64(0), errors.New("db error"))
235
236 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
237
238 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
239 MaxExitedSessions: 50,
240 MaxExitedSessionAgeDays: 7,
241 })
242
243 // Should not return error, just log warning
244 require.NoError(t, err)
245 assert.Equal(t, int64(2), output.DeletedByAge)
246 assert.Equal(t, int64(0), output.DeletedByCount)
247 assert.Equal(t, int64(2), output.TotalDeleted)
248}
249
250func TestCleanupSessionsUseCase_Execute_BothDisabled(t *testing.T) {
251 ctx := testContext()
252
253 sessionRepo := repomocks.NewMockSessionRepository(t)
254
255 // No expectations - nothing should be called
256 // Actually, count with 0 means "keep 0" which will trigger cleanup
257 // So we need -1 for count and 0 for age to fully disable
258
259 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
260
261 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
262 MaxExitedSessions: -1, // Disabled
263 MaxExitedSessionAgeDays: 0, // Disabled
264 })
265
266 require.NoError(t, err)
267 assert.Equal(t, int64(0), output.DeletedByAge)
268 assert.Equal(t, int64(0), output.DeletedByCount)
269 assert.Equal(t, int64(0), output.TotalDeleted)
270}
271
272func TestCleanupSessionsUseCase_Execute_ZeroMaxSessions(t *testing.T) {
273 ctx := testContext()
274
275 sessionRepo := repomocks.NewMockSessionRepository(t)
276
277 // MaxExitedSessions = 0 means keep 0 sessions (delete all)
278 sessionRepo.EXPECT().
279 DeleteOldestExited(ctx, 0).
280 Return(int64(10), nil)
281
282 uc := usecase.NewCleanupSessionsUseCase(sessionRepo, nil)
283
284 output, err := uc.Execute(ctx, usecase.CleanupSessionsInput{
285 MaxExitedSessions: 0, // Keep 0 = delete all
286 MaxExitedSessionAgeDays: 0, // Disabled
287 })
288
289 require.NoError(t, err)
290 assert.Equal(t, int64(0), output.DeletedByAge)
291 assert.Equal(t, int64(10), output.DeletedByCount)
292 assert.Equal(t, int64(10), output.TotalDeleted)
293}