Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
6.0 kB · 207 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208package main
import ( "context" "database/sql" "encoding/json" "io" "net/http" "net/http/httptest" "sort" "strings" "sync" "testing")
// fakeKV is a minimal in-memory stand-in for the Cloudflare KV REST API:// PUT /values/<key> stores, GET /keys lists (with cursor pagination).type fakeKV struct { mu sync.Mutex values map[string]string fail429 int // number of PUTs to fail with 429 before succeeding puts int // total PUTs seen}
func newFakeKV(fail429 int) *fakeKV { return &fakeKV{values: map[string]string{}, fail429: fail429}}
func (f *fakeKV) handler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPut && strings.HasPrefix(r.URL.Path, "/values/") { f.mu.Lock() f.puts++ if f.puts <= f.fail429 { f.mu.Unlock() http.Error(w, `{"success":false,"errors":[{"code":8000013,"message":"rate limited"}]}`, http.StatusTooManyRequests) return } key := strings.TrimPrefix(r.URL.Path, "/values/") body, _ := io.ReadAll(r.Body) f.values[key] = string(body) f.mu.Unlock() io.WriteString(w, `{"success":true}`) return } if r.Method == http.MethodGet && r.URL.Path == "/keys" { f.mu.Lock() keys := make([]string, 0, len(f.values)) for k := range f.values { keys = append(keys, k) } sort.Strings(keys) f.mu.Unlock()
page := keys cursor := "" switch r.URL.Query().Get("cursor") { case "": if len(keys) > 2 { page, cursor = keys[:2], "next-page" } case "next-page": page = keys[2:] } names := make([]map[string]string, 0, len(page)) for _, k := range page { names = append(names, map[string]string{"name": k}) } encoded, err := json.Marshal(names) if err != nil { panic(err) } cursorField := "null" if cursor != "" { cursorField = `"` + cursor + `"` } io.WriteString(w, `{"success":true,"result":`+string(encoded)+`,"result_info":{"cursor":`+cursorField+`}}`) return } http.NotFound(w, r) })}
// openTestDB creates an appview-shaped emails table and returns the handle.func openTestDB(t *testing.T, rows [][4]any) *sql.DB { t.Helper() db, err := sql.Open("sqlite3", t.TempDir()+"/appview.db") if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { db.Close() }) if _, err := db.Exec(` create table emails ( did text not null, email text not null, verified integer not null default 0, is_primary integer not null default 0, unique(did, email) )`); err != nil { t.Fatalf("create table: %v", err) } stmt, err := db.Prepare(`insert into emails (did, email, verified, is_primary) values (?, ?, ?, ?)`) if err != nil { t.Fatalf("prepare: %v", err) } defer stmt.Close() for _, row := range rows { if _, err := stmt.Exec(row[0], row[1], row[2], row[3]); err != nil { t.Fatalf("insert %v: %v", row, err) } } return db}
func runAgainst(t *testing.T, kv *fakeKV, db *sql.DB) { t.Helper() srv := httptest.NewServer(kv.handler()) t.Cleanup(srv.Close) if err := run(context.Background(), db, "test-token", srv.URL+"/"); err != nil { t.Fatalf("run: %v", err) }}
func TestMigratePhasesAndVerification(t *testing.T) { db := openTestDB(t, [][4]any{ {"did:plc:alice", "alice@example.com", 1, 1}, // uppercase on the way in — keys are stored lowercased {"did:plc:bob", "Bob@Example.com", 1, 0}, // unverified signups are never written {"did:plc:carol", "carol@example.com", 0, 0}, {"did:plc:dave", "dave@example.com", 1, 0}, }) kv := newFakeKV(0) runAgainst(t, kv, db)
want := map[string]string{ "alice@example.com": "did:plc:alice", "bob@example.com": "did:plc:bob", // lowercased key "dave@example.com": "did:plc:dave", "__primary:did:plc:alice": "alice@example.com", } kv.mu.Lock() defer kv.mu.Unlock() if len(kv.values) != len(want) { t.Fatalf("stored %d keys, want %d: %v", len(kv.values), len(want), kv.values) } for key, value := range want { if got := kv.values[key]; got != value { t.Errorf("key %q = %q, want %q", key, got, value) } } if _, ok := kv.values["carol@example.com"]; ok { t.Error("unverified carol@example.com was written") }}
func TestCollidingEmailKeysPreferPrimary(t *testing.T) { // the appview schema allows the same address (case variants included) to // be verified for several dids; they collapse onto one lowercased kv key. // the primary account must win and the verification count stays exact. db := openTestDB(t, [][4]any{ {"did:plc:nonprimary", "A@Example.com", 1, 0}, {"did:plc:primary", "a@example.com", 1, 1}, {"did:plc:solo", "solo@example.com", 1, 0}, }) kv := newFakeKV(0) runAgainst(t, kv, db)
want := map[string]string{ "a@example.com": "did:plc:primary", // primary wins the shared key "solo@example.com": "did:plc:solo", "__primary:did:plc:primary": "a@example.com", } kv.mu.Lock() defer kv.mu.Unlock() if len(kv.values) != len(want) { t.Fatalf("stored %d keys, want %d: %v", len(kv.values), len(want), kv.values) } for key, value := range want { if got := kv.values[key]; got != value { t.Errorf("key %q = %q, want %q", key, got, value) } } if _, ok := kv.values["A@example.com"]; ok { t.Error("non-normalized duplicate key was written") } if _, ok := kv.values["__primary:did:plc:nonprimary"]; ok { t.Error("non-primary did must not get a __primary key") }}
func TestPutRetriesTransientFailure(t *testing.T) { db := openTestDB(t, [][4]any{ {"did:plc:alice", "alice@example.com", 1, 1}, }) kv := newFakeKV(1) // first PUT 429s, then succeeds runAgainst(t, kv, db)
kv.mu.Lock() defer kv.mu.Unlock() if got := kv.values["alice@example.com"]; got != "did:plc:alice" { t.Errorf("key after retry = %q, want did:plc:alice", got) } if got := kv.values["__primary:did:plc:alice"]; got != "alice@example.com" { t.Errorf("primary after retry = %q, want alice@example.com", got) }}