package main import ( "bytes" "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" ) func TestSystemVerbKnowsGeoip(t *testing.T) { if verb, rest, _ := systemVerb([]string{"geoip", "publish", "x.mmdb"}); verb != "geoip" || len(rest) != 2 { t.Fatalf("systemVerb = %q %v", verb, rest) } } func TestPublishGeoipStreamsARawBodyAndFollowsTheJob(t *testing.T) { geoipPollInterval = time.Millisecond t.Cleanup(func() { geoipPollInterval = 2 * time.Second }) db := bytes.Repeat([]byte("m"), 4096) path := filepath.Join(t.TempDir(), "db.mmdb") if err := os.WriteFile(path, db, 0o600); err != nil { t.Fatal(err) } var polls atomic.Int32 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodPut && r.URL.Path == "/api/cli/system/geoip": if r.Header.Get("Content-Type") != "application/octet-stream" || r.ContentLength != int64(len(db)) { http.Error(w, "bad upload headers", http.StatusBadRequest) return } if r.URL.Query().Get("force") != "1" { http.Error(w, "force not passed", http.StatusBadRequest) return } b, _ := io.ReadAll(r.Body) if !bytes.Equal(b, db) { http.Error(w, "body mangled", http.StatusBadRequest) return } w.WriteHeader(http.StatusAccepted) w.Write([]byte(`{"job":"j1"}`)) case r.URL.Path == "/api/cli/system/jobs/j1": if polls.Add(1) < 2 { w.Write([]byte(`{"steps":["received"],"done":false,"error":""}`)) return } w.Write([]byte(`{"steps":["received","verifying"],"done":true,"error":"","result":{"sha256":"9f2c0000aaaa","database_type":"DBIP-City-Lite","build_epoch":1790812800}}`)) default: http.NotFound(w, r) } })) t.Cleanup(ts.Close) sess := &apiSession{c: newConsoleClient(ts.URL, "tok"), console: ts.URL} var out bytes.Buffer ptr, err := sess.publishGeoip(context.Background(), path, true, &out) if err != nil { t.Fatal(err) } if ptr.DatabaseType != "DBIP-City-Lite" || ptr.SHA256 != "9f2c0000aaaa" { t.Fatalf("pointer = %+v", ptr) } if !strings.Contains(out.String(), "verifying") { t.Fatalf("steps not printed: %q", out.String()) } } func TestPublishGeoipReportsTheJobsRefusal(t *testing.T) { geoipPollInterval = time.Millisecond t.Cleanup(func() { geoipPollInterval = 2 * time.Second }) path := filepath.Join(t.TempDir(), "db.mmdb") os.WriteFile(path, []byte("junk"), 0o600) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPut { io.Copy(io.Discard, r.Body) w.WriteHeader(http.StatusAccepted) w.Write([]byte(`{"job":"j2"}`)) return } json.NewEncoder(w).Encode(map[string]any{"steps": []string{}, "done": true, "error": "geoip database refused: not a valid MaxMind-format database"}) })) t.Cleanup(ts.Close) sess := &apiSession{c: newConsoleClient(ts.URL, "tok"), console: ts.URL} if _, err := sess.publishGeoip(context.Background(), path, false, io.Discard); err == nil || !strings.Contains(err.Error(), "not a valid MaxMind-format database") { t.Fatalf("err = %v", err) } } func TestPrintGeoipStatusVerdicts(t *testing.T) { var resp geoipStatusResponse json.Unmarshal([]byte(`{"pointer":{"sha256":"9f2c0000aaaa","database_type":"GeoLite2-City","build":"2026-10-01T00:00:00Z","origin":"manual","expires_at":"2026-10-31T00:00:00Z","attribution":"This product includes GeoLite2 data"}, "boxes":[{"label":"ie-1","state":"ok"},{"label":"node-1","fleet":"tito","state":"old binary"}], "ready":false,"blocking":["tito/node-1: runs a binary that does not strip forged geo headers"]}`), &resp) var out bytes.Buffer printGeoipStatus(&out, resp, time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)) s := out.String() for _, want := range []string{"GeoLite2-City", "5 days ago", "expires 2026-10-31", "tito/node-1", "not ready", "1 in the way"} { if !strings.Contains(s, want) { t.Errorf("status lacks %q:\n%s", want, s) } } out.Reset() printGeoipStatus(&out, geoipStatusResponse{Ready: true}, time.Now()) if !strings.Contains(out.String(), "none published") || !strings.Contains(out.String(), "ready") { t.Errorf("empty status = %q", out.String()) } } func TestPrintGeoipOffWarnsAboutTheFeed(t *testing.T) { var out bytes.Buffer printGeoipOff(&out, true) if !strings.Contains(out.String(), "unset CONSOLE_GEOIP_FEED") { t.Fatalf("off with the feed set = %q", out.String()) } out.Reset() printGeoipOff(&out, false) if strings.Contains(out.String(), "CONSOLE_GEOIP_FEED") { t.Fatalf("off without a feed mentioned it: %q", out.String()) } } func TestPrintGeoipStatusFeedLine(t *testing.T) { var resp geoipStatusResponse json.Unmarshal([]byte(`{"feed":{"name":"dbip-city-lite","failures":3,"last_failure":"download"},"ready":true}`), &resp) var out bytes.Buffer printGeoipStatus(&out, resp, time.Now()) if !strings.Contains(out.String(), "feed: dbip-city-lite, failing (3 in a row, last: download)") { t.Fatalf("feed line = %q", out.String()) } json.Unmarshal([]byte(`{"feed":{"name":"dbip-city-lite","last_success":"2026-10-01T00:00:00Z"},"ready":true}`), &resp) resp.Feed.Failures = 0 out.Reset() printGeoipStatus(&out, resp, time.Now()) if !strings.Contains(out.String(), "feed: dbip-city-lite, last success 2026-10-01") { t.Fatalf("feed line = %q", out.String()) } }