carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "net"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/geo"
17 "github.com/carlosframework/platform/internal/geoip"
18 "github.com/carlosframework/platform/internal/metrics"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22func geoFixture(t *testing.T, name string) []byte {
23 b, err := os.ReadFile(filepath.Join("..", "geo", "testdata", name))
24 if err != nil {
25 t.Fatal(err)
26 }
27 return b
28}
29
30// publishFixture writes blob + pointer the way the console does.
31func publishFixture(t *testing.T, s release.Store, body []byte, dbType string, build time.Time) geoip.Pointer {
32 t.Helper()
33 sum := sha256.Sum256(body)
34 sha := hex.EncodeToString(sum[:])
35 ctx := context.Background()
36 if _, err := s.Put(ctx, geoip.BlobKey(sha), body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) {
37 t.Fatal(err) // an existing blob is the same bytes: content-addressed
38 }
39 p := geoip.Pointer{V: 1, SHA256: sha, Key: geoip.BlobKey(sha), Size: int64(len(body)),
40 DatabaseType: dbType, BuildEpoch: build.Unix(), Origin: "manual"}
41 raw, _ := json.Marshal(p)
42 _, etag, _ := s.Get(ctx, geoip.PointerKey)
43 if _, err := s.Put(ctx, geoip.PointerKey, raw, etag); err != nil {
44 t.Fatal(err)
45 }
46 return p
47}
48
49var testMirrorStoreDir = map[release.Store]string{}
50
51func newTestMirror(t *testing.T) (*geoMirror, release.Store, string) {
52 root := t.TempDir()
53 s := release.NewDirStore(root)
54 testMirrorStoreDir[s] = root
55 dir := filepath.Join(t.TempDir(), "carlos", "geoip")
56 return newGeoMirror(s, dir, geo.NewSwappable()), s, dir
57}
58
59func lookupGB(m *geoMirror) bool {
60 snap := m.sw.Lookup(net.ParseIP("81.2.69.142"))
61 return snap.Found && snap.Result.Country == "GB"
62}
63
64func TestMirrorInstallsAPublishedDatabase(t *testing.T) {
65 m, s, dir := newTestMirror(t)
66 p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
67 if err := m.Poll(context.Background()); err != nil {
68 t.Fatal(err)
69 }
70 if !lookupGB(m) {
71 t.Fatal("not serving after poll")
72 }
73 if _, err := os.Stat(filepath.Join(dir, p.SHA256+".mmdb")); err != nil {
74 t.Fatal(err)
75 }
76 if st := m.State(); st.SHA256 != p.SHA256 || st.Type != "GeoIP2-City" {
77 t.Fatalf("state = %+v", st)
78 }
79 fi, _ := os.Stat(dir)
80 if fi.Mode().Perm() != 0o755 {
81 t.Fatalf("dir mode = %v, want 0755 (the console reads it)", fi.Mode().Perm())
82 }
83}
84
85func TestMirrorBootsFromTheLocalGenerationWithoutTheBucket(t *testing.T) {
86 m, s, dir := newTestMirror(t)
87 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
88 if err := m.Poll(context.Background()); err != nil {
89 t.Fatal(err)
90 }
91 // A fresh process, a dead bucket.
92 m2 := newGeoMirror(release.NewDirStore(filepath.Join(t.TempDir(), "empty")), dir, geo.NewSwappable())
93 m2.Boot()
94 if !lookupGB(m2) {
95 t.Fatal("boot did not serve the local generation")
96 }
97}
98
99func TestMirrorBootWithCurrentNamingAMissingFile(t *testing.T) {
100 m, _, dir := newTestMirror(t)
101 os.MkdirAll(dir, 0o755)
102 os.WriteFile(filepath.Join(dir, "current"), []byte(`{"sha256":"`+strings.Repeat("d", 64)+`","database_type":"X","build_epoch":1}`), 0o644)
103 m.Boot()
104 if snap := m.sw.Lookup(net.ParseIP("81.2.69.142")); snap.Loaded {
105 t.Fatal("loaded something from a dangling current")
106 }
107 if _, err := os.Stat(filepath.Join(dir, "current")); !os.IsNotExist(err) {
108 t.Fatal("dangling current was not removed")
109 }
110}
111
112func TestMirrorRejectsADigestMismatchOnce(t *testing.T) {
113 m, s, _ := newTestMirror(t)
114 p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
115 // Corrupt the blob after publishing.
116 os.WriteFile(filepath.Join(testMirrorStoreDir[s], p.Key), append(geoFixture(t, "GeoIP2-City-Test.mmdb")[:p.Size-1], 'x'), 0o644)
117 if err := m.Poll(context.Background()); err == nil {
118 t.Fatal("a corrupt blob installed")
119 }
120 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
121 t.Fatal("serving a corrupt blob")
122 }
123 if err := m.Poll(context.Background()); err != nil {
124 t.Fatalf("second poll of the same rejected sha should be a quiet no-op, got %v", err)
125 }
126}
127
128func TestMirrorOffClearsEverything(t *testing.T) {
129 m, s, dir := newTestMirror(t)
130 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
131 m.Poll(context.Background())
132 s.Delete(context.Background(), geoip.PointerKey)
133 if err := m.Poll(context.Background()); err != nil {
134 t.Fatal(err)
135 }
136 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
137 t.Fatal("still serving after off")
138 }
139 if matches, _ := filepath.Glob(filepath.Join(dir, "*.mmdb")); len(matches) != 0 {
140 t.Fatalf("generations left: %v", matches)
141 }
142}
143
144func TestMirrorNeverInstallsAnExpiredGeoLite(t *testing.T) {
145 m, s, _ := newTestMirror(t)
146 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
147 m.now = func() time.Time { return now }
148 // The fixture's own metadata says GeoIP2-City (no expiry), so make the
149 // POINTER claim GeoLite2 with an old build: expired before download.
150 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour))
151 if err := m.Poll(context.Background()); err != nil {
152 t.Fatal(err)
153 }
154 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
155 t.Fatal("installed an expired pointer")
156 }
157 // Repeated polls of the same unmarked expired pointer stay quiet.
158 if err := m.Poll(context.Background()); err != nil {
159 t.Fatal(err)
160 }
161}
162
163func TestMirrorExpiryTickRemovesOnlyTheCurrentExpiredGeneration(t *testing.T) {
164 m, s, _ := newTestMirror(t)
165 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
166 m.now = func() time.Time { return now }
167 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
168 m.Poll(context.Background())
169 // Pretend what is loaded is a GeoLite2 built 31 days ago.
170 m.genMu.Lock()
171 m.loadedType, m.loadedBuild = "GeoLite2-City", now.Add(-31*24*time.Hour)
172 m.genMu.Unlock()
173 m.ExpireTick()
174 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
175 t.Fatal("expired generation still serving")
176 }
177 // A fresh, non-expiring install afterwards is untouched by later ticks.
178 publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", now)
179 m.Poll(context.Background())
180 m.ExpireTick()
181 if !lookupGB(m) {
182 t.Fatal("a later tick removed a fresh generation")
183 }
184}
185
186func TestMirrorRefusesAnOversizeBlob(t *testing.T) {
187 m, s, dir := newTestMirror(t)
188 p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
189 m.maxBytes = p.Size - 1
190 if err := m.Poll(context.Background()); err == nil {
191 t.Fatal("oversize installed")
192 }
193 if matches, _ := filepath.Glob(filepath.Join(dir, ".incoming-*")); len(matches) != 0 {
194 t.Fatalf("incoming left: %v", matches)
195 }
196}
197
198// Plan review R1-7: a pointer this binary cannot read turns GeoIP off.
199func TestMirrorUnknownPointerVersionTurnsOff(t *testing.T) {
200 m, s, _ := newTestMirror(t)
201 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
202 if err := m.Poll(context.Background()); err != nil || !lookupGB(m) {
203 t.Fatalf("setup: %v", err)
204 }
205 ctx := context.Background()
206 raw, etag, _ := s.Get(ctx, geoip.PointerKey)
207 var p map[string]any
208 json.Unmarshal(raw, &p)
209 p["v"] = 2
210 raw2, _ := json.Marshal(p)
211 if _, err := s.Put(ctx, geoip.PointerKey, raw2, etag); err != nil {
212 t.Fatal(err)
213 }
214 if err := m.Poll(ctx); err != nil {
215 t.Fatal(err)
216 }
217 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
218 t.Fatal("still serving under a pointer version this binary cannot read")
219 }
220}
221
222// Plan review R1-8: a rejection lasts only while the pointer names that sha.
223func TestMirrorRetriesARejectedShaAfterThePointerMoves(t *testing.T) {
224 m, s, _ := newTestMirror(t)
225 ctx := context.Background()
226 city := geoFixture(t, "GeoIP2-City-Test.mmdb")
227 b := publishFixture(t, s, city, "GeoIP2-City", time.Now())
228 good := append([]byte(nil), city...)
229 os.WriteFile(filepath.Join(testMirrorStoreDir[s], b.Key), append(city[:len(city)-1], 'x'), 0o644) // corrupt B
230 if err := m.Poll(ctx); err == nil {
231 t.Fatal("corrupt B installed")
232 }
233 publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", time.Now()) // A
234 if err := m.Poll(ctx); err != nil {
235 t.Fatal(err)
236 }
237 os.WriteFile(filepath.Join(testMirrorStoreDir[s], b.Key), good, 0o644) // B repaired
238 raw, _ := json.Marshal(b)
239 _, etag, _ := s.Get(ctx, geoip.PointerKey)
240 if _, err := s.Put(ctx, geoip.PointerKey, raw, etag); err != nil {
241 t.Fatal(err)
242 }
243 if err := m.Poll(ctx); err != nil {
244 t.Fatalf("repaired B after the pointer moved away and back: %v", err)
245 }
246 if st := m.State(); st.SHA256 != b.SHA256 {
247 t.Fatalf("loaded %s, want repaired B %s", st.SHA256, b.SHA256)
248 }
249}
250
251// Plan review R1-3: an expiry tick during a replacement download must not
252// unlink the download.
253func TestMirrorExpiryDuringADownloadKeepsTheDownload(t *testing.T) {
254 m, s, _ := newTestMirror(t)
255 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
256 m.now = func() time.Time { return now }
257 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
258 m.Poll(context.Background())
259 m.genMu.Lock()
260 m.loadedType, m.loadedBuild = "GeoLite2-City", now.Add(-31*24*time.Hour) // A is now expired
261 m.genMu.Unlock()
262 publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", now) // B
263 downloadHook = func() { m.ExpireTick() }
264 t.Cleanup(func() { downloadHook = nil })
265 if err := m.Poll(context.Background()); err != nil {
266 t.Fatalf("replacement broken by a concurrent expiry: %v", err)
267 }
268 if !lookupGB(m) {
269 t.Fatal("B not serving")
270 }
271}
272
273// Plan review R1-4: a directory fsync failing after `current` is renamed
274// must not delete the generation `current` names.
275func TestMirrorSyncFailureAfterRenameKeepsTheGeneration(t *testing.T) {
276 m, s, dir := newTestMirror(t)
277 syncDirFn = func(string) error { return errors.New("injected") }
278 t.Cleanup(func() { syncDirFn = syncDir })
279 p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
280 if err := m.Poll(context.Background()); err != nil {
281 t.Fatalf("poll: %v", err)
282 }
283 if _, err := os.Stat(filepath.Join(dir, p.SHA256+".mmdb")); err != nil {
284 t.Fatal("the generation current names was deleted")
285 }
286 // A second install while the first rename is still not durable keeps
287 // BOTH generations: a crash could bring back either `current` (code
288 // review R1-4).
289 q := publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", time.Now())
290 if err := m.Poll(context.Background()); err != nil {
291 t.Fatal(err)
292 }
293 for _, sha := range []string{p.SHA256, q.SHA256} {
294 if _, err := os.Stat(filepath.Join(dir, sha+".mmdb")); err != nil {
295 t.Fatalf("generation %s deleted while the selection was not durable", sha[:8])
296 }
297 }
298 syncDirFn = syncDir
299 m2 := newGeoMirror(release.NewDirStore(t.TempDir()), dir, geo.NewSwappable())
300 m2.Boot()
301 if !lookupGB(m2) {
302 t.Fatal("restart after the failed sync did not recover the generation")
303 }
304}
305
306// Codex R8-2: an off that lands during a download takes effect at the end
307// of that pass; the superseded download is never installed.
308func TestMirrorOffDuringADownloadNeverInstalls(t *testing.T) {
309 m, s, dir := newTestMirror(t)
310 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
311 downloadHook = func() { s.Delete(context.Background(), geoip.PointerKey) } // off, mid-download
312 t.Cleanup(func() { downloadHook = nil })
313 if err := m.Poll(context.Background()); err != nil {
314 t.Fatal(err)
315 }
316 if m.sw.Lookup(net.ParseIP("81.2.69.142")).Loaded {
317 t.Fatal("a download superseded by off was installed")
318 }
319 if matches, _ := filepath.Glob(filepath.Join(dir, "*.mmdb")); len(matches) != 0 {
320 t.Fatalf("generations left: %v", matches)
321 }
322}
323
324// ...and a newer publish during a download installs the newer one in the
325// same pass.
326func TestMirrorNewerPublishDuringADownloadInstallsTheNewer(t *testing.T) {
327 m, s, _ := newTestMirror(t)
328 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
329 var newer geoip.Pointer
330 downloadHook = func() {
331 downloadHook = nil // only the first download is interrupted
332 newer = publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", time.Now())
333 }
334 t.Cleanup(func() { downloadHook = nil })
335 if err := m.Poll(context.Background()); err != nil {
336 t.Fatal(err)
337 }
338 if st := m.State(); st.SHA256 != newer.SHA256 {
339 t.Fatalf("loaded %s, want the newer %s", st.SHA256, newer.SHA256)
340 }
341}
342
343// Plan 2 Task 4 / spec §10.4: the geoip rule's reason codes.
344func TestMirrorAlertCodes(t *testing.T) {
345 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
346 m, s, _ := newTestMirror(t)
347 m.now = func() time.Time { return now }
348 ctx := context.Background()
349
350 if c, _ := m.AlertCode(now, "box"); c != GeoAlertNone {
351 t.Fatalf("nothing published = %v", c)
352 }
353 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
354 m.Poll(ctx)
355 // The fixture was built 2026-02-04: more than 45 days before now.
356 if c, _ := m.AlertCode(now, "box"); c != GeoAlertStale {
357 t.Fatalf("an old database = %v, want stale", c)
358 }
359 if c, _ := m.AlertCode(time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC), "box"); c != GeoAlertNone {
360 t.Fatalf("a fresh database = %v", c)
361 }
362 // GeoLite2 at 22 days: expiring wins over stale's absence.
363 m.genMu.Lock()
364 m.loadedType, m.loadedBuild = "GeoLite2-City", now.Add(-22*24*time.Hour)
365 m.genMu.Unlock()
366 if c, _ := m.AlertCode(now, "box"); c != GeoAlertExpiring {
367 t.Fatalf("GeoLite2 at 22 days = %v, want expiring", c)
368 }
369}
370
371// Codex R11-1: an expired pointer keeps the alert firing after the reader
372// is dropped and before the console marks the bucket; off clears it.
373func TestMirrorAlertSurvivesExpiryUntilOff(t *testing.T) {
374 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
375 m, s, _ := newTestMirror(t)
376 m.now = func() time.Time { return now }
377 ctx := context.Background()
378 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour)) // unmarked, past D1
379 m.Poll(ctx)
380 if c, _ := m.AlertCode(now, "box"); c != GeoAlertExpired {
381 t.Fatalf("expired, unmarked = %v, want expired", c)
382 }
383 // A restart with the same unmarked pointer: still expired.
384 m2 := newGeoMirror(s, m.dir, geo.NewSwappable())
385 m2.now = m.now
386 m2.Boot()
387 m2.Poll(ctx)
388 if c, _ := m2.AlertCode(now, "box"); c != GeoAlertExpired {
389 t.Fatalf("after restart = %v, want expired", c)
390 }
391 // A fresh publish clears it.
392 publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", now)
393 m.Poll(ctx)
394 if c, _ := m.AlertCode(time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC), "box"); c == GeoAlertExpired {
395 t.Fatal("a fresh publish left the expired reason")
396 }
397 // Expired again, then off: off clears every pointer reason.
398 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour))
399 m.Poll(ctx)
400 s.Delete(ctx, geoip.PointerKey)
401 m.Poll(ctx)
402 if c, _ := m.AlertCode(now, "box"); c != GeoAlertNone {
403 t.Fatalf("after off = %v, want none", c)
404 }
405}
406
407func TestMirrorAlertNotLoadedAfterAnHour(t *testing.T) {
408 now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
409 m, s, _ := newTestMirror(t)
410 clock := now
411 m.now = func() time.Time { return clock }
412 p := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
413 os.WriteFile(filepath.Join(testMirrorStoreDir[s], p.Key), []byte("corrupt"), 0o644)
414 m.Poll(context.Background())
415 if c, _ := m.AlertCode(clock.Add(59*time.Minute), "box"); c != GeoAlertNone {
416 t.Fatalf("not loaded for 59 minutes = %v", c)
417 }
418 if c, _ := m.AlertCode(clock.Add(61*time.Minute), "box"); c != GeoAlertNotLoaded {
419 t.Fatalf("not loaded for 61 minutes = %v, want not loaded", c)
420 }
421}
422
423func TestMirrorAlertFeedReasonOnlyOnTheConsoleBox(t *testing.T) {
424 now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
425 m, s, _ := newTestMirror(t)
426 m.now = func() time.Time { return now }
427 raw, _ := json.Marshal(geoip.FeedStatus{V: 1, Feed: "dbip-city-lite", Box: "console-box", ConsecutiveNotFound: 7})
428 if _, err := s.Put(context.Background(), geoip.FeedKey, raw, ""); err != nil {
429 t.Fatal(err)
430 }
431 m.Poll(context.Background()) // no pointer at all: a fresh deployment
432 if c, _ := m.AlertCode(now, "console-box"); c != GeoAlertFeed {
433 t.Fatalf("console box = %v, want feed", c)
434 }
435 if c, _ := m.AlertCode(now, "other-box"); c != GeoAlertNone {
436 t.Fatalf("another box = %v, want none", c)
437 }
438}
439
440// Code review R2 on R1-4: while directory syncs keep failing, every
441// generation a crash could bring back is kept.
442func TestMirrorKeepsEveryUnsyncedGeneration(t *testing.T) {
443 m, s, dir := newTestMirror(t)
444 a := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now())
445 m.Poll(context.Background()) // A, durable
446 syncDirFn = func(string) error { return errors.New("injected") }
447 t.Cleanup(func() { syncDirFn = syncDir })
448 b := publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", time.Now())
449 m.Poll(context.Background())
450 c := publishFixture(t, s, geoFixture(t, "MaxMind-DB-test-ipv4-24.mmdb"), "Test", time.Now())
451 m.Poll(context.Background()) // C will be refused? the mirror does not probe; it installs whatever verifies
452 for _, sha := range []string{a.SHA256, b.SHA256} {
453 if _, err := os.Stat(filepath.Join(dir, sha+".mmdb")); err != nil {
454 t.Fatalf("generation %s deleted while syncs keep failing", sha[:8])
455 }
456 }
457 _ = c
458 syncDirFn = syncDir
459 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", time.Now().Add(time.Second))
460}
461
462// Code review R2-3: a read blip keeps the last known feed state.
463func TestMirrorFeedStateSurvivesAReadBlip(t *testing.T) {
464 now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
465 m, s, _ := newTestMirror(t)
466 m.now = func() time.Time { return now }
467 raw, _ := json.Marshal(geoip.FeedStatus{V: 1, Feed: "f", Box: "console-box", ConsecutiveFailures: 7})
468 s.Put(context.Background(), geoip.FeedKey, raw, "")
469 m.Poll(context.Background())
470 _, etag, _ := s.Get(context.Background(), geoip.FeedKey)
471 s.Put(context.Background(), geoip.FeedKey, []byte("garbled"), etag)
472 m.Poll(context.Background())
473 if c, _ := m.AlertCode(now, "console-box"); c != GeoAlertFeed {
474 t.Fatalf("a garbled read cleared the feed reason: %v", c)
475 }
476 s.Delete(context.Background(), geoip.FeedKey)
477 m.Poll(context.Background())
478 if c, _ := m.AlertCode(now, "console-box"); c != GeoAlertNone {
479 t.Fatalf("a deleted feed.json must clear it: %v", c)
480 }
481}
482
483// Code review R2-4: an unusable local directory still lets the alert see
484// the published pointer.
485func TestMirrorUnusableDirectoryStillTracksThePointer(t *testing.T) {
486 now := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
487 s := release.NewDirStore(t.TempDir())
488 blocker := filepath.Join(t.TempDir(), "file")
489 os.WriteFile(blocker, []byte("x"), 0o644)
490 m := newGeoMirror(s, filepath.Join(blocker, "geoip"), geo.NewSwappable()) // a path under a file: MkdirAll fails
491 m.now = func() time.Time { return now }
492 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
493 if err := m.Poll(context.Background()); err == nil {
494 t.Fatal("an unusable directory reported success")
495 }
496 if c, _ := m.AlertCode(now.Add(61*time.Minute), "box"); c != GeoAlertNotLoaded {
497 t.Fatalf("code = %v, want not loaded", c)
498 }
499}
500
501// Code review R3 on R1-4: an install error path never deletes a generation
502// a crash could bring back.
503func TestRemoveGenerationKeepsRetainedOnes(t *testing.T) {
504 m, _, dir := newTestMirror(t)
505 os.MkdirAll(dir, 0o755)
506 a, b, c := strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)
507 for _, sha := range []string{a, b, c} {
508 os.WriteFile(filepath.Join(dir, sha+".mmdb"), []byte("x"), 0o644)
509 }
510 m.loadedSHA, m.unsynced = a, map[string]geoCurrent{b: {SHA256: b}}
511 for _, sha := range []string{a, b, c} {
512 m.removeGenerationLocked(sha)
513 }
514 for sha, want := range map[string]bool{a: true, b: true, c: false} {
515 if _, err := os.Stat(filepath.Join(dir, sha+".mmdb")); (err == nil) != want {
516 t.Errorf("%s present=%v, want %v", sha[:1], err == nil, want)
517 }
518 }
519}
520
521// Code review R4-2: a cold mirror has no reading until it has read both
522// the pointer and the feed status.
523func TestMirrorAlertUnknownUntilStateIsRead(t *testing.T) {
524 m, s, _ := newTestMirror(t)
525 if _, known := m.AlertCode(time.Now(), "box"); known {
526 t.Fatal("a cold mirror claimed a reading")
527 }
528 s.Put(context.Background(), geoip.FeedKey, []byte("garbled"), "") // feed unreadable
529 m.Poll(context.Background())
530 if _, known := m.AlertCode(time.Now(), "box"); known {
531 t.Fatal("an unreadable feed status gave a reading")
532 }
533 _, etag, _ := s.Get(context.Background(), geoip.FeedKey)
534 _ = etag
535 s.Delete(context.Background(), geoip.FeedKey)
536 m.Poll(context.Background())
537 if c, known := m.AlertCode(time.Now(), "box"); !known || c != GeoAlertNone {
538 t.Fatalf("after both reads = (%v, %v)", c, known)
539 }
540}
541
542// Code review R5-1: a confirmed expiry is reported even when feed.json is
543// unreadable, through the evaluator.
544func TestMirrorExpiryReportedWithAnUnreadableFeed(t *testing.T) {
545 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
546 m, s, _ := newTestMirror(t)
547 m.now = func() time.Time { return now }
548 s.Put(context.Background(), geoip.FeedKey, []byte("garbled"), "")
549 publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoLite2-City", now.Add(-31*24*time.Hour))
550 m.Poll(context.Background())
551 c, known := m.AlertCode(now, "box")
552 if !known || c != GeoAlertExpired {
553 t.Fatalf("expired with an unreadable feed = (%v, %v), want (3, true)", c, known)
554 }
555 r := metrics.DefaultRules()
556 st := metrics.AlertState{V: 1, Box: "box"}
557 st, _ = metrics.Evaluate(r, st, metrics.Observation{At: now, GeoIP: &c})
558 _, ev := metrics.Evaluate(r, st, metrics.Observation{At: now.Add(30 * time.Minute), GeoIP: &c})
559 if len(ev) != 1 || ev[0].Value != GeoAlertExpired {
560 t.Fatalf("evaluator = %v", ev)
561 }
562}
563
564// Code review R10-2: one failed sync during a GeoLite2 -> DB-IP move; the
565// retained GeoLite2 copy goes once a later sync lands, or at its expiry.
566func TestMirrorRetainedCopyIsCleanedOrExpired(t *testing.T) {
567 for _, mode := range []string{"sync recovers", "expiry"} {
568 now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
569 m, s, dir := newTestMirror(t)
570 m.now = func() time.Time { return now }
571 a := publishFixture(t, s, geoFixture(t, "GeoIP2-City-Test.mmdb"), "GeoIP2-City", now)
572 m.Poll(context.Background())
573 m.genMu.Lock()
574 m.loadedType, m.loadedBuild = "GeoLite2-City", now.Add(-10*24*time.Hour) // A is a GeoLite2, 10 days old
575 m.genMu.Unlock()
576 syncDirFn = func(string) error { return errors.New("injected") }
577 publishFixture(t, s, geoFixture(t, "GeoIP2-Country-Test.mmdb"), "GeoIP2-Country", now)
578 m.Poll(context.Background())
579 if _, err := os.Stat(filepath.Join(dir, a.SHA256+".mmdb")); err != nil {
580 t.Fatalf("%s: A not retained", mode)
581 }
582 if mode == "sync recovers" {
583 syncDirFn = syncDir
584 m.Poll(context.Background()) // unchanged pointer
585 } else {
586 now = now.Add(21 * 24 * time.Hour) // A past its 30 days
587 m.ExpireTick()
588 syncDirFn = syncDir
589 }
590 if _, err := os.Stat(filepath.Join(dir, a.SHA256+".mmdb")); !os.IsNotExist(err) {
591 t.Fatalf("%s: A survived (%v)", mode, err)
592 }
593 }
594 syncDirFn = syncDir
595}
596