package server import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "galaxy/backend/internal/geo" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap/zaptest" ) type fakeAdminGeoLister struct { entries []geo.CountryCounter err error } func (f *fakeAdminGeoLister) ListUserCounters(_ context.Context, _ uuid.UUID) ([]geo.CountryCounter, error) { if f.err != nil { return nil, f.err } return f.entries, nil } func newAdminGeoEngine(t *testing.T, fake AdminGeoLister) *gin.Engine { t.Helper() gin.SetMode(gin.TestMode) r := gin.New() h := NewAdminGeoHandlers(fake, zaptest.NewLogger(t)) r.GET("/api/v1/admin/geo/users/:user_id/countries", h.ListUserCountries()) return r } func TestAdminGeoListUserCountriesSuccess(t *testing.T) { t.Parallel() now := time.Date(2026, 5, 4, 12, 0, 0, 0, time.UTC) fake := &fakeAdminGeoLister{entries: []geo.CountryCounter{ {Country: "AU", Count: 3, LastSeenAt: &now}, {Country: "DE", Count: 1, LastSeenAt: nil}, }} r := newAdminGeoEngine(t, fake) userID := uuid.New() req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/geo/users/"+userID.String()+"/countries", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: want 200, got %d (%s)", rec.Code, rec.Body.String()) } var body adminGeoListWire if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal: %v", err) } if body.UserID != userID.String() { t.Errorf("user_id: want %s, got %s", userID, body.UserID) } if len(body.Items) != 2 { t.Fatalf("items: want 2, got %d (%+v)", len(body.Items), body.Items) } if body.Items[0].Country != "AU" || body.Items[0].Count != 3 || body.Items[0].LastSeenAt == nil { t.Errorf("items[0] mismatch: %+v", body.Items[0]) } if body.Items[1].Country != "DE" || body.Items[1].Count != 1 || body.Items[1].LastSeenAt != nil { t.Errorf("items[1] mismatch: %+v", body.Items[1]) } } func TestAdminGeoListUserCountriesEmpty(t *testing.T) { t.Parallel() r := newAdminGeoEngine(t, &fakeAdminGeoLister{}) userID := uuid.New() req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/geo/users/"+userID.String()+"/countries", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: want 200, got %d", rec.Code) } var body adminGeoListWire if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal: %v", err) } if body.Items == nil { t.Fatal("items: want non-nil empty slice, got nil") } if len(body.Items) != 0 { t.Fatalf("items: want empty, got %+v", body.Items) } } func TestAdminGeoListUserCountriesInvalidUserID(t *testing.T) { t.Parallel() r := newAdminGeoEngine(t, &fakeAdminGeoLister{}) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/geo/users/not-a-uuid/countries", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status: want 400, got %d", rec.Code) } } func TestAdminGeoListUserCountriesStoreError(t *testing.T) { t.Parallel() r := newAdminGeoEngine(t, &fakeAdminGeoLister{err: errors.New("boom")}) userID := uuid.New() req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/geo/users/"+userID.String()+"/countries", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("status: want 500, got %d", rec.Code) } } func TestAdminGeoListUserCountriesNilServiceReturns501(t *testing.T) { t.Parallel() gin.SetMode(gin.TestMode) r := gin.New() h := NewAdminGeoHandlers(nil, zaptest.NewLogger(t)) r.GET("/api/v1/admin/geo/users/:user_id/countries", h.ListUserCountries()) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/geo/users/"+uuid.New().String()+"/countries", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != http.StatusNotImplemented { t.Fatalf("status: want 501, got %d", rec.Code) } }