package edge import ( "net/http" "net/http/httptest" "sync" "testing" "github.com/carlosframework/platform/internal/analytics" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) type memAnalytics struct { mu sync.Mutex obs []analytics.Obs } func (m *memAnalytics) Observe(o analytics.Obs) { m.mu.Lock(); m.obs = append(m.obs, o); m.mu.Unlock() } func (m *memAnalytics) snap() []analytics.Obs { m.mu.Lock() defer m.mu.Unlock() return append([]analytics.Obs(nil), m.obs...) } func TestClientIP(t *testing.T) { r := httptest.NewRequest("GET", "http://h/", nil) r.RemoteAddr = "203.0.113.5:4444" r.Header.Set("X-Forwarded-For", "198.51.100.1") if ip := clientIP(r); ip != "203.0.113.5" { t.Fatalf("unverified: %q", ip) } r2 := httptest.NewRequest("GET", "http://h/", nil) r2 = r2.WithContext(withHopVerified(r2.Context())) r2.RemoteAddr = "203.0.113.5:4444" r2.Header.Set("X-Forwarded-For", "10.0.0.1, 198.51.100.7") if ip := clientIP(r2); ip != "198.51.100.7" { t.Fatalf("verified hop: %q", ip) } } func TestEdgeObservesOnlyWhenSwitchedOn(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte("

hi

")) })) t.Cleanup(backend.Close) reg := openReg(t) rt := registry.Route{Host: "app.example.com", Kind: "service", Addr: backend.Listener.Addr().String(), Status: "ready", App: "web"} if err := reg.Upsert(rt); err != nil { t.Fatal(err) } e, err := New(reg, nil) if err != nil { t.Fatal(err) } m := &memAnalytics{} e.EnableAnalytics(m) srv := httptest.NewServer(e) t.Cleanup(srv.Close) get := func(path string, hdr map[string]string) { req, _ := http.NewRequest("GET", srv.URL+path, nil) req.Host = "app.example.com" for k, v := range hdr { req.Header.Set(k, v) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() } get("/", nil) if len(m.snap()) != 0 { t.Fatal("nothing is observed while the app is off") } on := map[string]bool{release.OpsAccount + "/web": true} e.SetAnalyticsOn(on) // The switch map is copied, so a caller may keep writing to its own: // the edge reads its copy from every serving goroutine without a lock, // and a shared map would be a fatal concurrent map read-and-write. delete(on, release.OpsAccount+"/web") on["scribble"] = true get("/pricing?x=1", map[string]string{"Referer": "https://news.ycombinator.com/item?id=1", "User-Agent": "Mozilla/5.0 Safari", "Sec-Fetch-Dest": "document"}) obs := m.snap() if len(obs) != 1 { t.Fatalf("observed %d", len(obs)) } o := obs[0] if o.Acct != release.OpsAccount || o.App != "web" || o.Host != "app.example.com" || o.Method != "GET" || o.Status != 200 || o.ContentType != "text/html; charset=utf-8" || o.Path != "/pricing" || o.RefererHost != "news.ycombinator.com" || o.FetchDest != "document" || o.Bytes == 0 || o.ClientIP == "" || o.When.IsZero() { t.Fatalf("obs = %+v", o) } get("/x", map[string]string{"Purpose": "prefetch"}) if !m.snap()[1].Prefetch { t.Fatal("prefetch header must be carried") } // A speculation-rules prerender arrives as Sec-Purpose: // prefetch;prerender with Sec-Fetch-Dest: document — a pageview // nobody looked at unless the first token is matched. get("/y", map[string]string{"Sec-Purpose": "prefetch;prerender", "Sec-Fetch-Dest": "document"}) if !m.snap()[2].Prefetch { t.Fatal("prefetch;prerender is a prefetch") } e.SetAnalyticsOn(nil) get("/", nil) if len(m.snap()) != 3 { t.Fatal("switching off stops observation") } }