Something went wrong. Try again.
Go boilerplate library for building atproto apps
atproto go
Something went wrong. Try again.
atp public_test.go
22 kB · 785 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786package atp
import ( "encoding/json" "fmt" "net" "net/http" "net/http/httptest" "testing"
"github.com/stretchr/testify/assert")
func TestIsPrivateIP(t *testing.T) { cases := []struct { ip string private bool }{ {"127.0.0.1", true}, {"::1", true}, {"169.254.1.1", true}, {"10.0.0.1", true}, {"172.16.0.1", true}, {"192.168.1.1", true}, {"0.0.0.0", true}, {"8.8.8.8", false}, {"93.184.216.34", false}, }
for _, tc := range cases { ip := net.ParseIP(tc.ip) got := isPrivateIP(ip) if got != tc.private { t.Errorf("isPrivateIP(%s) = %v, want %v", tc.ip, got, tc.private) } }}
func TestValidateDomain_Localhost(t *testing.T) { if err := validateDomain("localhost"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for localhost, got %v", err) }}
func TestValidateDomain_DotLocal(t *testing.T) { if err := validateDomain("internal.local"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for .local domain, got %v", err) }}
func TestValidateDomain_PrivateIP(t *testing.T) { if err := validateDomain("192.168.1.1"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for private IP, got %v", err) }}
func TestValidateDomain_PublicIP(t *testing.T) { if err := validateDomain("8.8.8.8"); err != nil { t.Fatalf("expected nil for public IP, got %v", err) }}
func TestValidateDomain_MetadataIP(t *testing.T) { if err := validateDomain("169.254.169.254"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for metadata IP, got %v", err) }}
func TestValidateDomain_LoopbackIP(t *testing.T) { if err := validateDomain("127.0.0.1"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for loopback, got %v", err) }}
func TestValidateDomain_LinkLocal(t *testing.T) { if err := validateDomain("169.254.0.1"); err != ErrSSRFBlocked { t.Fatalf("expected ErrSSRFBlocked for link-local, got %v", err) }}
func TestNewPublicClient(t *testing.T) { c := NewPublicClient() if c == nil { t.Fatal("expected non-nil client") }}
func TestNewPublicClientWithHTTP(t *testing.T) { hc := &http.Client{Timeout: 0} c := NewPublicClientWithHTTP(hc) if c == nil { t.Fatal("expected non-nil client") } // Should use the provided client}
func TestResolveHandle_PackageFunc(t *testing.T) { // Create a test server that mimics the public API's resolveHandle endpoint srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/xrpc/com.atproto.identity.resolveHandle" { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("handle") != "alice.bsky.social" { t.Fatalf("unexpected handle: %s", r.URL.Query().Get("handle")) }
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "did": "did:plc:alice", }) })) t.Cleanup(srv.Close)
// Override the PublicAPIBase for this test by using a client pointed at the test server oldBase := PublicAPIBase PublicAPIBase = srv.URL t.Cleanup(func() { PublicAPIBase = oldBase })
did, err := ResolveHandle(t.Context(), "alice.bsky.social") if err != nil { t.Fatal(err) } if did != "did:plc:alice" { t.Fatalf("expected did:plc:alice, got %s", did) }}
// testPublicClient creates a PublicClient backed by test servers that mock// the public API, PLC directory, and optionally a PDS.// Returns the client and a cleanup function.type testPublicServers struct { client *PublicClient api *httptest.Server // public.api.bsky.app plc *httptest.Server // plc.directory}
func newTestPublicClient(t *testing.T, apiHandler, plcHandler http.HandlerFunc) *testPublicServers { t.Helper() apiSrv := httptest.NewServer(apiHandler) plcSrv := httptest.NewServer(plcHandler) t.Cleanup(func() { apiSrv.Close() plcSrv.Close() })
oldAPI := PublicAPIBase oldPLC := PLCDirectory PublicAPIBase = apiSrv.URL PLCDirectory = plcSrv.URL t.Cleanup(func() { PublicAPIBase = oldAPI PLCDirectory = oldPLC })
return &testPublicServers{ client: NewPublicClient(), api: apiSrv, plc: plcSrv, }}
func TestPublicClient_ResolveHandle(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/xrpc/com.atproto.identity.resolveHandle", r.URL.Path) assert.Equal(t, "alice.bsky.social", r.URL.Query().Get("handle"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"did": "did:plc:alice"}) }, http.NotFound, )
did, err := s.client.ResolveHandle(t.Context(), "alice.bsky.social") if err != nil { t.Fatal(err) } if did != "did:plc:alice" { t.Fatalf("expected did:plc:alice, got %s", did) }
// Second call should use cache did, err = s.client.ResolveHandle(t.Context(), "alice.bsky.social") if err != nil { t.Fatal(err) } if did != "did:plc:alice" { t.Fatalf("expected did:plc:alice, got %s", did) }}
func TestPublicClient_ResolveHandle_Normalizes(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { // Should be normalized to punycode/ascii lower assert.Equal(t, "xn--caf-dma.example.com", r.URL.Query().Get("handle"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"did": "did:plc:cafe"}) }, http.NotFound, )
did, err := s.client.ResolveHandle(t.Context(), "Café.Example.com") if err != nil { t.Fatal(err) } if did != "did:plc:cafe" { t.Fatalf("expected did:plc:cafe, got %s", did) }}
func TestPublicClient_ResolveHandle_NotFound(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) }, http.NotFound, )
_, err := s.client.ResolveHandle(t.Context(), "nonexistent.bsky.social") if err == nil { t.Fatal("expected error for not found handle") }}
func TestPublicClient_ResolveHandle_ServerError(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, http.NotFound, )
_, err := s.client.ResolveHandle(t.Context(), "alice.bsky.social") if err == nil { t.Fatal("expected error for server error") }}
func TestPublicClient_GetPDSEndpoint_DidPLC(t *testing.T) { s := newTestPublicClient(t, http.NotFound, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/did:plc:alice", r.URL.Path)
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": "https://alice.pds.example.com", }, }, }) }, )
endpoint, err := s.client.GetPDSEndpoint(t.Context(), "did:plc:alice") if err != nil { t.Fatal(err) } if endpoint != "https://alice.pds.example.com" { t.Fatalf("expected https://alice.pds.example.com, got %s", endpoint) }
// Second call should use cache endpoint, err = s.client.GetPDSEndpoint(t.Context(), "did:plc:alice") if err != nil { t.Fatal(err) } if endpoint != "https://alice.pds.example.com" { t.Fatalf("expected cached endpoint, got %s", endpoint) }}
func TestPublicClient_GetPDSEndpoint_DidPLC_NoPDS(t *testing.T) { s := newTestPublicClient(t, http.NotFound, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#something_else", "type": "SomeOtherService", "serviceEndpoint": "https://other.example.com", }, }, }) }, )
_, err := s.client.GetPDSEndpoint(t.Context(), "did:plc:alice") if err == nil { t.Fatal("expected error when no PDS service found") }}
func TestPublicClient_GetPDSEndpoint_DidPLC_Error(t *testing.T) { s := newTestPublicClient(t, http.NotFound, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) }, )
_, err := s.client.GetPDSEndpoint(t.Context(), "did:plc:nonexistent") if err == nil { t.Fatal("expected error") }}
func TestPublicClient_GetPDSEndpoint_DidWeb(t *testing.T) { // did:web resolves without needing PLC directory — just validate domain s := newTestPublicClient(t, http.NotFound, http.NotFound)
endpoint, err := s.client.GetPDSEndpoint(t.Context(), "did:web:example.com") if err != nil { t.Fatal(err) } if endpoint != "https://example.com" { t.Fatalf("expected https://example.com, got %s", endpoint) }}
func TestPublicClient_GetPDSEndpoint_DidWeb_Port(t *testing.T) { s := newTestPublicClient(t, http.NotFound, http.NotFound)
endpoint, err := s.client.GetPDSEndpoint(t.Context(), "did:web:example.com%3A8080") if err != nil { t.Fatal(err) } if endpoint != "https://example.com:8080" { t.Fatalf("expected https://example.com:8080, got %s", endpoint) }}
func TestPublicClient_GetPDSEndpoint_DidWeb_Path(t *testing.T) { s := newTestPublicClient(t, http.NotFound, http.NotFound)
endpoint, err := s.client.GetPDSEndpoint(t.Context(), "did:web:example.com%3A443%2Fsubdomain") if err != nil { t.Fatal(err) } if endpoint != "https://example.com:443" { t.Fatalf("expected https://example.com:443, got %s", endpoint) }}
func TestPublicClient_GetPDSEndpoint_DidWeb_Localhost(t *testing.T) { s := newTestPublicClient(t, http.NotFound, http.NotFound)
_, err := s.client.GetPDSEndpoint(t.Context(), "did:web:localhost") if err == nil { t.Fatal("expected ErrSSRFBlocked for localhost") }}
func TestPublicClient_GetPDSEndpoint_Unknown(t *testing.T) { s := newTestPublicClient(t, http.NotFound, http.NotFound)
_, err := s.client.GetPDSEndpoint(t.Context(), "did:something:xyz") if err == nil { t.Fatal("expected error for unknown DID method") }}
func TestPublicClient_GetProfile(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/xrpc/app.bsky.actor.getProfile", r.URL.Path) assert.Equal(t, "did:plc:alice", r.URL.Query().Get("actor"))
w.Header().Set("Content-Type", "application/json") displayName := "Alice" avatar := "https://example.com/avatar.jpg" json.NewEncoder(w).Encode(map[string]any{ "did": "did:plc:alice", "handle": "alice.bsky.social", "displayName": &displayName, "avatar": &avatar, }) }, http.NotFound, )
profile, err := s.client.GetProfile(t.Context(), "did:plc:alice") if err != nil { t.Fatal(err) } if profile.DID != "did:plc:alice" { t.Fatalf("expected did:plc:alice, got %s", profile.DID) } if profile.Handle != "alice.bsky.social" { t.Fatalf("unexpected handle: %s", profile.Handle) } if profile.DisplayName == nil || *profile.DisplayName != "Alice" { t.Fatal("expected display name Alice") } if profile.Avatar == nil || *profile.Avatar != "https://example.com/avatar.jpg" { t.Fatal("expected avatar URL") }}
func TestPublicClient_GetProfile_Error(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) }, http.NotFound, )
_, err := s.client.GetProfile(t.Context(), "nonexistent") if err == nil { t.Fatal("expected error") }}
func TestPublicClient_GetProfile_WithHandle(t *testing.T) { s := newTestPublicClient(t, func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "alice.bsky.social", r.URL.Query().Get("actor"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "did": "did:plc:alice", "handle": "alice.bsky.social", }) }, http.NotFound, )
profile, err := s.client.GetProfile(t.Context(), "alice.bsky.social") if err != nil { t.Fatal(err) } if profile.Handle != "alice.bsky.social" { t.Fatalf("unexpected handle: %s", profile.Handle) }}
func TestPublicClient_ListPublicRecords(t *testing.T) { pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/xrpc/com.atproto.repo.listRecords", r.URL.Path) assert.Equal(t, "did:plc:alice", r.URL.Query().Get("repo")) assert.Equal(t, "app.bsky.feed.post", r.URL.Query().Get("collection"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ { "uri": "at://did:plc:alice/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "hello"}, }, }, }) })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldAPI := PublicAPIBase oldPLC := PLCDirectory PublicAPIBase = "http://unused.local" PLCDirectory = plcSrv.URL t.Cleanup(func() { PublicAPIBase = oldAPI PLCDirectory = oldPLC })
client := NewPublicClient()
records, cursor, err := client.ListPublicRecords(t.Context(), "did:plc:alice", "app.bsky.feed.post", ListPublicRecordsOpts{}) if err != nil { t.Fatal(err) } if len(records) != 1 { t.Fatalf("expected 1 record, got %d", len(records)) } if records[0].URI != "at://did:plc:alice/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected URI: %s", records[0].URI) } if cursor != "" { t.Fatalf("expected empty cursor, got %s", cursor) }}
func TestPublicClient_ListPublicRecords_WithOpts(t *testing.T) { pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "100", r.URL.Query().Get("limit")) assert.Equal(t, "next-cursor", r.URL.Query().Get("cursor")) assert.Equal(t, "true", r.URL.Query().Get("reverse"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{}, "cursor": "", }) })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldPLC := PLCDirectory PLCDirectory = plcSrv.URL t.Cleanup(func() { PLCDirectory = oldPLC })
client := NewPublicClient()
records, cursor, err := client.ListPublicRecords(t.Context(), "did:plc:alice", "app.bsky.feed.post", ListPublicRecordsOpts{ Limit: 100, Cursor: "next-cursor", Reverse: true, }) if err != nil { t.Fatal(err) } if len(records) != 0 { t.Fatalf("expected 0 records, got %d", len(records)) } if cursor != "" { t.Fatalf("expected empty cursor, got %s", cursor) }}
func TestPublicClient_ListPublicRecords_Error(t *testing.T) { pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldPLC := PLCDirectory PLCDirectory = plcSrv.URL t.Cleanup(func() { PLCDirectory = oldPLC })
client := NewPublicClient()
_, _, err := client.ListPublicRecords(t.Context(), "did:plc:alice", "app.bsky.feed.post", ListPublicRecordsOpts{}) if err == nil { t.Fatal("expected error") }}
func TestPublicClient_GetPublicRecord(t *testing.T) { pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/xrpc/com.atproto.repo.getRecord", r.URL.Path) assert.Equal(t, "did:plc:alice", r.URL.Query().Get("repo")) assert.Equal(t, "app.bsky.feed.post", r.URL.Query().Get("collection")) assert.Equal(t, "3jxy", r.URL.Query().Get("rkey"))
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "uri": "at://did:plc:alice/app.bsky.feed.post/3jxy", "cid": "cid1", "value": map[string]string{"text": "hello"}, }) })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldPLC := PLCDirectory PLCDirectory = plcSrv.URL t.Cleanup(func() { PLCDirectory = oldPLC })
client := NewPublicClient()
rec, err := client.GetPublicRecord(t.Context(), "did:plc:alice", "app.bsky.feed.post", "3jxy") if err != nil { t.Fatal(err) } if rec.URI != "at://did:plc:alice/app.bsky.feed.post/3jxy" { t.Fatalf("unexpected URI: %s", rec.URI) } if rec.Value["text"] != "hello" { t.Fatalf("unexpected value: %v", rec.Value) }}
func TestPublicClient_GetPublicRecord_Error(t *testing.T) { pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldPLC := PLCDirectory PLCDirectory = plcSrv.URL t.Cleanup(func() { PLCDirectory = oldPLC })
client := NewPublicClient()
_, err := client.GetPublicRecord(t.Context(), "did:plc:alice", "app.bsky.feed.post", "3jxy") if err == nil { t.Fatal("expected error") }}
func TestPublicClient_ListAllRecords(t *testing.T) { pageNum := 0 pdsSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { pageNum++ w.Header().Set("Content-Type", "application/json")
if pageNum < 3 { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": fmt.Sprintf("at://did:plc:alice/app.bsky.feed.post/rec%d", pageNum), "cid": fmt.Sprintf("cid%d", pageNum), "value": map[string]string{"text": fmt.Sprintf("page%d", pageNum)}}, }, "cursor": fmt.Sprintf("cursor-%d", pageNum), }) } else { json.NewEncoder(w).Encode(map[string]any{ "records": []map[string]any{ {"uri": "at://did:plc:alice/app.bsky.feed.post/last", "cid": "cid3", "value": map[string]string{"text": "last"}}, }, "cursor": "", }) } })) t.Cleanup(pdsSrv.Close)
plcSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "service": []map[string]any{ { "id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": pdsSrv.URL, }, }, }) })) t.Cleanup(plcSrv.Close)
oldPLC := PLCDirectory PLCDirectory = plcSrv.URL t.Cleanup(func() { PLCDirectory = oldPLC })
client := NewPublicClient()
records, err := client.ListAllRecords(t.Context(), "did:plc:alice", "app.bsky.feed.post") if err != nil { t.Fatal(err) } if len(records) != 3 { t.Fatalf("expected 3 records, got %d", len(records)) }}
func TestInvalidateHandle(t *testing.T) { c := NewPublicClient()
// Manually seed the cache to test invalidation c.handleMu.Lock() c.handleCache["alice.example.com"] = cachedValue{value: "did:plc:alice"} c.handleCache["bob.example.com"] = cachedValue{value: "did:plc:bob"} c.handleMu.Unlock()
c.InvalidateHandle("alice.example.com")
c.handleMu.RLock() _, exists := c.handleCache["alice.example.com"] assert.False(t, exists, "alice should be removed") _, exists = c.handleCache["bob.example.com"] assert.True(t, exists, "bob should remain") c.handleMu.RUnlock()
// InvalidateHandle normalizes input c.handleMu.Lock() c.handleCache["xn--caf-dma.example.com"] = cachedValue{value: "did:plc:cafe"} c.handleMu.Unlock() c.InvalidateHandle("café.example.com") c.handleMu.RLock() _, exists = c.handleCache["xn--caf-dma.example.com"] assert.False(t, exists, "unicode form should invalidate punycode cache entry") c.handleMu.RUnlock()
// Empty handle should not panic c.InvalidateHandle("") c.InvalidateHandle("@")}
func TestInvalidateDID(t *testing.T) { c := NewPublicClient()
// Seed caches c.pdsMu.Lock() c.pdsCache["did:plc:alice"] = cachedValue{value: "https://alice.pds.example"} c.pdsCache["did:plc:bob"] = cachedValue{value: "https://bob.pds.example"} c.pdsMu.Unlock()
c.handleMu.Lock() c.handleCache["alice.example.com"] = cachedValue{value: "did:plc:alice"} c.handleCache["bob.example.com"] = cachedValue{value: "did:plc:bob"} c.handleMu.Unlock()
c.InvalidateDID("did:plc:alice")
// PDS cache should be cleared c.pdsMu.RLock() _, exists := c.pdsCache["did:plc:alice"] assert.False(t, exists, "alice PDS entry should be removed") _, exists = c.pdsCache["did:plc:bob"] assert.True(t, exists, "bob PDS entry should remain") c.pdsMu.RUnlock()
// Handle cache should have alice's entry removed too c.handleMu.RLock() _, exists = c.handleCache["alice.example.com"] assert.False(t, exists, "alice handle entry should be removed") _, exists = c.handleCache["bob.example.com"] assert.True(t, exists, "bob handle entry should remain") c.handleMu.RUnlock()
// Empty DID should not panic c.InvalidateDID("")}