Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

1package keys 2 3import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "net/http" 8 "net/http/httptest" 9 "path/filepath" 10 "testing" 11 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/knotserver/db" 18) 19 20const ( 21 didBoltless = "did:plc:boltless" 22 keyAlpha = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAlphaAlphaAlphaAlphaAlphaAlphaAlphaAlpha01" 23 keyBravo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIABravoBravoBravoBravoBravoBravoBravoBravo02" 24) 25 26func TestFetchAndStore_EmptyResponseDoesNotWipe(t *testing.T) { 27 store := newKeyStore(t) 28 seedKey(t, store, didBoltless, "seed", keyAlpha) 29 30 srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ 31 "": page(""), 32 }) 33 defer srv.Close() 34 35 if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 36 t.Fatalf("FetchAndStore: %v", err) 37 } 38 39 owners := ownersByKey(t, store) 40 if _, ok := owners[keyAlpha]; !ok { 41 t.Fatalf("seeded key was wiped by an empty fetch response, owners=%v", owners) 42 } 43 if got := len(owners); got != 1 { 44 t.Errorf("stored %d keys, want 1", got) 45 } 46} 47 48func TestFetchAndStore_ReplacesExistingKeys(t *testing.T) { 49 store := newKeyStore(t) 50 seedKey(t, store, didBoltless, "stale", keyAlpha) 51 52 srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ 53 "": page("", pubkeyRecord(didBoltless, "fresh", keyBravo)), 54 }) 55 defer srv.Close() 56 57 if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 58 t.Fatalf("FetchAndStore: %v", err) 59 } 60 61 owners := ownersByKey(t, store) 62 if _, ok := owners[keyAlpha]; ok { 63 t.Errorf("stale key %q survived a full replace", keyAlpha) 64 } 65 if _, ok := owners[keyBravo]; !ok { 66 t.Errorf("fresh key %q was not stored", keyBravo) 67 } 68 if got := len(owners); got != 1 { 69 t.Errorf("stored %d keys, want 1", got) 70 } 71} 72 73func TestFetchAndStore_PaginatesAcrossPages(t *testing.T) { 74 store := newKeyStore(t) 75 if err := db.AddDid(store, didBoltless); err != nil { 76 t.Fatalf("AddDid: %v", err) 77 } 78 79 srv := pdsServer(t, map[string]*comatproto.RepoListRecords_Output{ 80 "": page("next", pubkeyRecord(didBoltless, "r1", keyAlpha)), 81 "next": page("", pubkeyRecord(didBoltless, "r2", keyBravo)), 82 }) 83 defer srv.Close() 84 85 if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 86 t.Fatalf("FetchAndStore: %v", err) 87 } 88 89 owners := ownersByKey(t, store) 90 if _, ok := owners[keyAlpha]; !ok { 91 t.Errorf("first-page key %q missing", keyAlpha) 92 } 93 if _, ok := owners[keyBravo]; !ok { 94 t.Errorf("second-page key %q missing", keyBravo) 95 } 96 if got := len(owners); got != 2 { 97 t.Errorf("stored %d keys, want 2", got) 98 } 99} 100 101func newKeyStore(t *testing.T) *db.DB { 102 t.Helper() 103 store, err := db.Setup(context.Background(), filepath.Join(t.TempDir(), "test.db")) 104 if err != nil { 105 t.Fatalf("db.Setup: %v", err) 106 } 107 return store 108} 109 110func seedKey(t *testing.T, store *db.DB, did syntax.DID, rkey syntax.RecordKey, key string) { 111 t.Helper() 112 if err := db.AddDid(store, did.String()); err != nil { 113 t.Fatalf("AddDid: %v", err) 114 } 115 if err := store.UpsertPublicKey(db.PublicKey{ 116 Did: did, 117 Rkey: rkey, 118 PublicKey: tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"}, 119 }); err != nil { 120 t.Fatalf("UpsertPublicKey: %v", err) 121 } 122} 123 124func ownersByKey(t *testing.T, store *db.DB) map[string]string { 125 t.Helper() 126 rows, err := store.GetAllPublicKeys() 127 if err != nil { 128 t.Fatalf("GetAllPublicKeys: %v", err) 129 } 130 return foldOwners(rows, map[string]string{}) 131} 132 133func foldOwners(rows []db.PublicKey, acc map[string]string) map[string]string { 134 if len(rows) == 0 { 135 return acc 136 } 137 acc[rows[0].Key] = rows[0].Did.String() 138 return foldOwners(rows[1:], acc) 139} 140 141func page(cursor string, records ...*comatproto.RepoListRecords_Record) *comatproto.RepoListRecords_Output { 142 out := &comatproto.RepoListRecords_Output{Records: records} 143 if cursor != "" { 144 out.Cursor = &cursor 145 } 146 return out 147} 148 149func pubkeyRecord(did, rkey, key string) *comatproto.RepoListRecords_Record { 150 return &comatproto.RepoListRecords_Record{ 151 Uri: "at://" + did + "/" + tangled.PublicKeyNSID + "/" + rkey, 152 Cid: "bafyreib2rxk3rybk3aobmv5cjuql3bm2twh4jo5uxgr3rxbd3xrnp5z5cy", 153 Value: &lexutil.LexiconTypeDecoder{ 154 Val: &tangled.PublicKey{Key: key, CreatedAt: "2026-06-20T00:00:00Z"}, 155 }, 156 } 157} 158 159func pdsServer(t *testing.T, pages map[string]*comatproto.RepoListRecords_Output) *httptest.Server { 160 t.Helper() 161 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 162 cursor := r.URL.Query().Get("cursor") 163 out, ok := pages[cursor] 164 if !ok { 165 t.Errorf("listRecords requested with unexpected cursor %q", cursor) 166 http.Error(w, "no such page", http.StatusInternalServerError) 167 return 168 } 169 w.Header().Set("Content-Type", "application/json") 170 if err := json.NewEncoder(w).Encode(out); err != nil { 171 t.Errorf("encoding listRecords response: %v", err) 172 } 173 })) 174} 175 176type fakeDirectory struct { 177 pdsURL string 178} 179 180func (f fakeDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 181 return &identity.Identity{ 182 DID: did, 183 Services: map[string]identity.ServiceEndpoint{ 184 "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: f.pdsURL}, 185 }, 186 }, nil 187} 188 189func (f fakeDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { 190 return nil, errors.New("LookupHandle unused in tests") 191} 192 193func (f fakeDirectory) Lookup(ctx context.Context, atid syntax.AtIdentifier) (*identity.Identity, error) { 194 return nil, errors.New("Lookup unused in tests") 195} 196 197func (f fakeDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { 198 return nil 199}