mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package webapp
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// phSpy stands in for PostHog's ingestion host.
|
||
|
|
type phSpy struct {
|
||
|
|
*httptest.Server
|
||
|
|
mu sync.Mutex
|
||
|
|
got []map[string]any
|
||
|
|
seen chan struct{}
|
||
|
|
}
|
||
|
|
|
||
|
|
func newPHSpy(t *testing.T) *phSpy {
|
||
|
|
t.Helper()
|
||
|
|
spy := &phSpy{seen: make(chan struct{}, 64)}
|
||
|
|
spy.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
body, _ := io.ReadAll(r.Body)
|
||
|
|
var ev map[string]any
|
||
|
|
if err := json.Unmarshal(body, &ev); err != nil {
|
||
|
|
t.Errorf("posthog got unparseable body %q: %v", body, err)
|
||
|
|
}
|
||
|
|
if r.URL.Path != "/i/v0/e/" {
|
||
|
|
t.Errorf("capture posted to %q, want /i/v0/e/", r.URL.Path)
|
||
|
|
}
|
||
|
|
spy.mu.Lock()
|
||
|
|
spy.got = append(spy.got, ev)
|
||
|
|
spy.mu.Unlock()
|
||
|
|
w.WriteHeader(200)
|
||
|
|
spy.seen <- struct{}{}
|
||
|
|
}))
|
||
|
|
t.Cleanup(spy.Close)
|
||
|
|
return spy
|
||
|
|
}
|
||
|
|
|
||
|
|
// events waits for n deliveries. capture is fire-and-forget, so the send
|
||
|
|
// outlives the request that triggered it.
|
||
|
|
func (spy *phSpy) events(t *testing.T, n int) []map[string]any {
|
||
|
|
t.Helper()
|
||
|
|
for i := 0; i < n; i++ {
|
||
|
|
select {
|
||
|
|
case <-spy.seen:
|
||
|
|
case <-time.After(3 * time.Second):
|
||
|
|
spy.mu.Lock()
|
||
|
|
defer spy.mu.Unlock()
|
||
|
|
t.Fatalf("waited for %d events, got %d: %v", n, len(spy.got), spy.got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
spy.mu.Lock()
|
||
|
|
defer spy.mu.Unlock()
|
||
|
|
return append([]map[string]any(nil), spy.got...)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (spy *phSpy) count(t *testing.T) int {
|
||
|
|
t.Helper()
|
||
|
|
// Nothing more should arrive; give a stray goroutine a moment to land.
|
||
|
|
time.Sleep(200 * time.Millisecond)
|
||
|
|
spy.mu.Lock()
|
||
|
|
defer spy.mu.Unlock()
|
||
|
|
return len(spy.got)
|
||
|
|
}
|
||
|
|
|
||
|
|
func evProp(t *testing.T, ev map[string]any, key string) any {
|
||
|
|
t.Helper()
|
||
|
|
props, ok := ev["properties"].(map[string]any)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("event has no properties: %v", ev)
|
||
|
|
}
|
||
|
|
return props[key]
|
||
|
|
}
|
||
|
|
|
||
|
|
// A device PUTs its WHOLE journal every cycle, so the count has to come from
|
||
|
|
// the ops the hub has not already stored. Counting the body would report the
|
||
|
|
// device's entire history again every ten seconds, and "number of file
|
||
|
|
// changes" would climb on its own while nobody edited anything.
|
||
|
|
func TestAnalytics_SyncCountsOnlyNewOps(t *testing.T) {
|
||
|
|
spy := newPHSpy(t)
|
||
|
|
h, srv, c, p := permHub(t)
|
||
|
|
srv.Analytics = AnalyticsConfig{Key: "phc_test", Host: spy.URL}
|
||
|
|
|
||
|
|
const dev = "alice-laptop-6f2a"
|
||
|
|
// Alice syncs once so the device id is hers.
|
||
|
|
if rec := secfx4Store(t, h, "GET", "/api/p/"+p.ID+"/store/list", "", c["alice"], dev); rec.Code != 200 {
|
||
|
|
t.Fatalf("control: alice's own sync: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
|
||
|
|
first := secaudOpLine(1, dev, "put", "plan.md", strings.Repeat("a", 64)) +
|
||
|
|
secaudOpLine(2, dev, "put", "notes.md", strings.Repeat("b", 64))
|
||
|
|
if rec := secfx4PushJournal(t, h, p.ID, dev, first, c["alice"]); rec.Code != 200 {
|
||
|
|
t.Fatalf("first push: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
ev := spy.events(t, 1)[0]
|
||
|
|
if got := ev["event"]; got != "files_changed" {
|
||
|
|
t.Errorf("event = %v, want files_changed", got)
|
||
|
|
}
|
||
|
|
if got := ev["distinct_id"]; got != "alice@x.io" {
|
||
|
|
t.Errorf("distinct_id = %v, want alice@x.io — it must match the id the frontend "+
|
||
|
|
"identifies with (analytics.ts), or one person counts as two users", got)
|
||
|
|
}
|
||
|
|
if got := evProp(t, ev, "puts"); got != float64(2) {
|
||
|
|
t.Errorf("puts = %v, want 2", got)
|
||
|
|
}
|
||
|
|
if got := evProp(t, ev, "source"); got != "sync" {
|
||
|
|
t.Errorf("source = %v, want sync", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
// The second cycle repeats both ops and appends one delete, exactly as a
|
||
|
|
// real client does. Only the delete is new.
|
||
|
|
second := first + secaudOpLine(3, dev, "delete", "plan.md", "")
|
||
|
|
if rec := secfx4PushJournal(t, h, p.ID, dev, second, c["alice"]); rec.Code != 200 {
|
||
|
|
t.Fatalf("second push: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
ev = spy.events(t, 1)[1]
|
||
|
|
if got, want := evProp(t, ev, "deletes"), float64(1); got != want {
|
||
|
|
t.Errorf("deletes = %v, want %v", got, want)
|
||
|
|
}
|
||
|
|
if got := evProp(t, ev, "puts"); got != float64(0) {
|
||
|
|
t.Errorf("puts = %v on a re-push of the same journal, want 0 — the whole history "+
|
||
|
|
"is being counted again every cycle", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A cycle that adds nothing (the daemon re-pushing an unchanged journal)
|
||
|
|
// is not a file change and must not land as one.
|
||
|
|
if rec := secfx4PushJournal(t, h, p.ID, dev, second, c["alice"]); rec.Code != 200 {
|
||
|
|
t.Fatalf("idempotent push: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
if n := spy.count(t); n != 2 {
|
||
|
|
t.Errorf("%d events after a no-op re-push, want 2", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The OSS default: no key, no third-party request. A self-hosted hub must not
|
||
|
|
// phone home, which is the same rule the frontend follows.
|
||
|
|
func TestAnalytics_UnconfiguredHubSendsNothing(t *testing.T) {
|
||
|
|
spy := newPHSpy(t)
|
||
|
|
h, srv, c, p := permHub(t)
|
||
|
|
srv.Analytics = AnalyticsConfig{Host: spy.URL} // host set, key empty
|
||
|
|
|
||
|
|
const dev = "alice-laptop-6f2a"
|
||
|
|
if rec := secfx4Store(t, h, "GET", "/api/p/"+p.ID+"/store/list", "", c["alice"], dev); rec.Code != 200 {
|
||
|
|
t.Fatalf("control: alice's own sync: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
body := secaudOpLine(1, dev, "put", "plan.md", strings.Repeat("a", 64))
|
||
|
|
if rec := secfx4PushJournal(t, h, p.ID, dev, body, c["alice"]); rec.Code != 200 {
|
||
|
|
t.Fatalf("push: %d %s", rec.Code, rec.Body)
|
||
|
|
}
|
||
|
|
if n := spy.count(t); n != 0 {
|
||
|
|
t.Errorf("a hub with no analytics key sent %d events", n)
|
||
|
|
}
|
||
|
|
}
|