-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdsfree_service_test.go
More file actions
335 lines (284 loc) · 9.25 KB
/
dsfree_service_test.go
File metadata and controls
335 lines (284 loc) · 9.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package main
import (
"bytes"
"errors"
"log"
"os"
"strings"
"sync"
"syscall"
"testing"
"time"
"github.com/fsnotify/fsnotify"
)
type SpyHostsManager struct {
mu sync.Mutex
applyCallCount int
restoreCalled bool
failApplyOnCall int // Fails on the Nth call to Apply
appliedDomains []string
failApplyWithError error
failRestoreWithError error
}
func (s *SpyHostsManager) Apply(domains []string) error {
if s.failApplyWithError != nil {
return s.failApplyWithError
}
s.mu.Lock()
defer s.mu.Unlock()
s.applyCallCount++
s.appliedDomains = domains
if s.failApplyOnCall > 0 && s.applyCallCount == s.failApplyOnCall {
return errors.New("simulated apply failure on specific call")
}
return nil
}
func (s *SpyHostsManager) Restore() error {
s.mu.Lock()
defer s.mu.Unlock()
s.restoreCalled = true
if s.failRestoreWithError != nil {
return s.failRestoreWithError
}
return nil
}
// This mock gives the test full control over the file watcher's event and error channels.
type MockFileWatcher struct {
mu sync.Mutex
eventsChan chan fsnotify.Event
errorsChan chan error
isClosed bool
}
func NewMockFileWatcher() *MockFileWatcher {
return &MockFileWatcher{
eventsChan: make(chan fsnotify.Event),
errorsChan: make(chan error),
}
}
func (mfw *MockFileWatcher) Events() <-chan fsnotify.Event { return mfw.eventsChan }
func (mfw *MockFileWatcher) Errors() <-chan error { return mfw.errorsChan }
func (mfw *MockFileWatcher) Add(name string) error { return nil }
func (mfw *MockFileWatcher) Close() error {
mfw.mu.Lock()
defer mfw.mu.Unlock()
// Only close the channels if they haven't been closed already to prevent a panic.
if !mfw.isClosed {
close(mfw.eventsChan)
close(mfw.errorsChan)
mfw.isClosed = true
}
return nil
}
type MockScheduler struct {
mu sync.Mutex
callback func()
}
func (ms *MockScheduler) AfterFunc(d time.Duration, f func()) {
ms.mu.Lock()
defer ms.mu.Unlock()
// Instead of waiting, we save the callback to be invoked manually by the test.
ms.callback = f
}
func (ms *MockScheduler) Invoke() {
ms.mu.Lock()
cb := ms.callback
ms.mu.Unlock()
if cb != nil {
cb()
}
}
func TestDSFreeService_Run_CallsApplyOnceOnStart(t *testing.T) {
spyManager := &SpyHostsManager{}
mockWatcher := NewMockFileWatcher()
domains := []string{"test.com"}
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
mockWatcher.Close() // Force Run() à se terminer.
wg.Wait() // Attend que la goroutine soit VRAIMENT terminée.
spyManager.mu.Lock()
defer spyManager.mu.Unlock()
if spyManager.applyCallCount != 1 {
t.Errorf("Expected Apply() to be called exactly once, but it was called %d time(s)", spyManager.applyCallCount)
}
if len(spyManager.appliedDomains) != 1 || spyManager.appliedDomains[0] != "test.com" {
t.Errorf("Run() passed wrong domains to Apply(). Expected ['test.com'], got %v", spyManager.appliedDomains)
}
}
func TestDSFreeService_Run_DoesNotCallApplyWhenInitialApplyFails(t *testing.T) {
expectedError := errors.New("simulated apply failure")
spyManager := &SpyHostsManager{
failApplyWithError: expectedError,
}
mockWatcher := NewMockFileWatcher()
domains := []string{"test.com"}
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
// For this test no need to call service.Run in it's own gorotine, the for select loop
// is not reach, the error is returned before
service.Run()
spyManager.mu.Lock()
defer spyManager.mu.Unlock()
if spyManager.applyCallCount != 0 {
t.Errorf("Expected Apply() not to be called when it returns an error, but it was called %d time(s)", spyManager.applyCallCount)
}
}
func TestDSFreeService_Run_ReappliesOnTampering(t *testing.T) {
spyManager := &SpyHostsManager{}
mockWatcher := NewMockFileWatcher()
domains := []string{"test.com"}
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
mockWatcher.eventsChan <- fsnotify.Event{Name: hostsFilePath, Op: fsnotify.Write}
time.Sleep(10 * time.Millisecond)
mockWatcher.Close()
wg.Wait() // Attend que Run() soit terminée avant de vérifier.
spyManager.mu.Lock()
defer spyManager.mu.Unlock()
if spyManager.applyCallCount < 2 {
t.Errorf("Expected Apply() to be called at least twice, but it was called %d time(s)", spyManager.applyCallCount)
}
}
func TestDSFreeService_Run_HandlesWatcherError(t *testing.T) {
spyManager := &SpyHostsManager{}
mockWatcher := NewMockFileWatcher()
domains := []string{"test.com"}
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
var logBuffer bytes.Buffer
log.SetOutput(&logBuffer)
defer log.SetOutput(os.Stderr)
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
mockWatcher.errorsChan <- errors.New("simulated watcher error")
time.Sleep(10 * time.Millisecond)
mockWatcher.Close()
wg.Wait() // Déjà correct ici.
if !strings.Contains(logBuffer.String(), "ERROR: File watcher error: simulated watcher error") {
t.Errorf("Expected to see watcher error in logs, but got: %s", logBuffer.String())
}
}
func TestDSFreeService_Run_CallsRestoreOnShutdown(t *testing.T) {
spyManager := &SpyHostsManager{}
mockWatcher := NewMockFileWatcher()
shutdownChan := make(chan os.Signal, 1)
domains := []string{"test.com"}
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
shutdownChan <- syscall.SIGINT
wg.Wait() // Attend que Run() soit terminée après avoir traité le signal.
spyManager.mu.Lock()
defer spyManager.mu.Unlock()
if !spyManager.restoreCalled {
t.Error("Expected Restore() to be called on shutdown, but it wasn't")
}
}
func TestDSFreeService_Run_HandlesFailuresInLoop(t *testing.T) {
var logBuffer bytes.Buffer
log.SetOutput(&logBuffer)
defer log.SetOutput(os.Stderr)
spyManager := &SpyHostsManager{
failApplyOnCall: 2,
failRestoreWithError: errors.New("simulated restore failure"),
}
mockWatcher := NewMockFileWatcher()
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, []string{"test.com"})
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
mockWatcher.eventsChan <- fsnotify.Event{Op: fsnotify.Write}
time.Sleep(10 * time.Millisecond)
mockWatcher.eventsChan <- fsnotify.Event{Op: fsnotify.Write}
time.Sleep(10 * time.Millisecond)
// The shutdown signal must be sent while Run() is active.
shutdownChan <- syscall.SIGTERM
wg.Wait() // wait Run terminate
spyManager.mu.Lock()
if spyManager.applyCallCount != 2 {
t.Errorf("Expected Apply to be called twice, got %d", spyManager.applyCallCount)
}
if !spyManager.restoreCalled {
t.Error("Expected Restore to be called, but it wasn't")
}
spyManager.mu.Unlock()
logs := logBuffer.String()
if !strings.Contains(logs, "Failed to re-apply sabotage") {
t.Error("Expected to see log for failed re-apply")
}
if !strings.Contains(logs, "Could not restore hosts file on shutdown") {
t.Error("Expected to see log for failed restore")
}
}
func TestDSFreeService_Run_ResetsCooldownAfterTampering(t *testing.T) {
spyManager := &SpyHostsManager{}
mockWatcher := NewMockFileWatcher()
shutdownChan := make(chan os.Signal, 1)
mockScheduler := &MockScheduler{}
domains := []string{"test.com"}
service := NewDSFreeService(spyManager, mockWatcher, shutdownChan, mockScheduler, domains)
// Run the service in a background goroutine so the test can interact with it.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
service.Run()
}()
time.Sleep(10 * time.Millisecond)
mockWatcher.eventsChan <- fsnotify.Event{Op: fsnotify.Write}
time.Sleep(10 * time.Millisecond)
service.mu.Lock()
if !service.isCoolingDown {
service.mu.Unlock()
t.Fatal("Expected cooldown to be active immediately after tampering event")
}
service.mu.Unlock()
mockScheduler.Invoke()
service.mu.Lock()
if service.isCoolingDown {
service.mu.Unlock()
t.Fatal("Expected cooldown to be false after scheduler callback")
}
service.mu.Unlock()
mockWatcher.Close()
wg.Wait()
}