This repository has no description
0

Configure Feed

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

core / repoident / knoturl_test.go
7.7 kB 193 lines
1package repoident 2 3import ( 4 "encoding/json" 5 "errors" 6 "strings" 7 "testing" 8 9 "github.com/bluesky-social/indigo/atproto/identity" 10) 11 12func knotService(url string) map[string]identity.ServiceEndpoint { 13 return map[string]identity.ServiceEndpoint{KnotServiceID: {Type: KnotServiceType, URL: url}} 14} 15 16func identWith(services map[string]identity.ServiceEndpoint) *identity.Identity { 17 return &identity.Identity{Services: services} 18} 19 20func mustParse(t *testing.T, raw string) KnotURL { 21 t.Helper() 22 u, err := ParseKnotURL(raw, AllowHTTP) 23 if err != nil { 24 t.Fatalf("ParseKnotURL(%q): %v", raw, err) 25 } 26 return u 27} 28 29func TestKnotURL_ParsedAsBaseAndAsServiceEndpoint(t *testing.T) { 30 const canonical = "https://knot.oyster.cafe" 31 const rejected = "" 32 cases := map[string]struct{ wantAsBase, wantAsEndpoint string }{ 33 canonical: {canonical, canonical}, 34 canonical + "/": {canonical, canonical}, 35 "HTTPS://Knot.Oyster.Cafe/": {canonical, canonical}, 36 "https://KNOT.OYSTER.CAFE:443": {canonical, canonical}, 37 "https://knot.oyster.cafe:": {canonical, canonical}, 38 "https://knot.oyster.cafe:80": {canonical + ":80", canonical + ":80"}, 39 "https://[2001:DB8::1]:443": {"https://[2001:db8::1]", "https://[2001:db8::1]"}, 40 "https://[2001:db8::1]:8443": {"https://[2001:db8::1]:8443", "https://[2001:db8::1]:8443"}, 41 "https://[fe80::1%25eth0]": {"https://[fe80::1%25eth0]", "https://[fe80::1%25eth0]"}, 42 "https://☃.oyster.cafe": {"https://%E2%98%83.oyster.cafe", "https://%E2%98%83.oyster.cafe"}, 43 canonical + "/repo/m5326fp3qemiriiqy": {rejected, canonical}, 44 canonical + "/base": {rejected, canonical}, 45 canonical + "?utm=knot": {rejected, rejected}, 46 canonical + "#pulls": {rejected, rejected}, 47 "https://nel@knot.oyster.cafe": {rejected, rejected}, 48 "https://nel:hunter2@knot.oyster.cafe": {rejected, rejected}, 49 "ftp://knot.oyster.cafe": {rejected, rejected}, 50 "http://knot.oyster.cafe": {rejected, rejected}, 51 "knot.oyster.cafe": {rejected, rejected}, 52 "https://": {rejected, rejected}, 53 "https://:443": {rejected, rejected}, 54 "not a url at all": {rejected, rejected}, 55 "": {rejected, rejected}, 56 } 57 for raw, want := range cases { 58 t.Run(raw, func(t *testing.T) { 59 check := func(door string, got KnotURL, err error, want string) { 60 switch { 61 case want == rejected: 62 if err == nil { 63 t.Errorf("%s accepted %q as %q, want an error", door, raw, got) 64 } 65 case err != nil: 66 t.Errorf("%s(%q): %v", door, raw, err) 67 case got.String() != want: 68 t.Errorf("%s(%q) = %q, want %q", door, raw, got, want) 69 case got != mustParse(t, want): 70 t.Errorf("%s(%q) doesn't compare equal to its canonical spelling %q", door, raw, want) 71 } 72 } 73 base, baseErr := ParseKnotURL(raw, RequireHTTPS) 74 check("ParseKnotURL", base, baseErr, want.wantAsBase) 75 endpoint, endpointErr := KnotURLFromIdentity(identWith(knotService(raw)), RequireHTTPS) 76 check("KnotURLFromIdentity", endpoint, endpointErr, want.wantAsEndpoint) 77 }) 78 } 79} 80 81func TestKnotURLFromIdentity_PicksTheKnotService(t *testing.T) { 82 const legacyURL = "https://knot.oyster.cafe" 83 const tangledURL = "https://nel.pet/repo/fcicrjbr6oh3" 84 cases := map[string]struct{ tangledType, want string }{ 85 "legacy atproto_pds is the fallback": {"", legacyURL}, 86 "tangled_knot wins over legacy": {KnotServiceType, "https://nel.pet"}, 87 "tangled_knot with the wrong type is ignored": {"AtprotoLabeler", legacyURL}, 88 } 89 for name, tc := range cases { 90 t.Run(name, func(t *testing.T) { 91 services := map[string]identity.ServiceEndpoint{ 92 LegacyKnotServiceID: {Type: LegacyKnotServiceType, URL: legacyURL}, 93 } 94 if tc.tangledType != "" { 95 services[KnotServiceID] = identity.ServiceEndpoint{Type: tc.tangledType, URL: tangledURL} 96 } 97 u, err := KnotURLFromIdentity(identWith(services), RequireHTTPS) 98 if err != nil { 99 t.Fatalf("KnotURLFromIdentity: %v", err) 100 } 101 if u.String() != tc.want { 102 t.Errorf("KnotURLFromIdentity = %q, want %q", u, tc.want) 103 } 104 }) 105 } 106} 107 108func TestKnotURLFromIdentity_ErrNoKnotService(t *testing.T) { 109 cases := map[string]*identity.Identity{ 110 "nothing declared": identWith(nil), 111 "another service only": identWith(map[string]identity.ServiceEndpoint{ 112 "atproto_labeler": {Type: "AtprotoLabeler", URL: "https://nel.pet"}, 113 }), 114 "tangled_knot with an empty url": identWith(knotService("")), 115 "legacy service with the wrong type": identWith(map[string]identity.ServiceEndpoint{ 116 LegacyKnotServiceID: {Type: "AtprotoLabeler", URL: "https://knot.oyster.cafe"}, 117 }), 118 } 119 for name, ident := range cases { 120 t.Run(name, func(t *testing.T) { 121 if _, err := KnotURLFromIdentity(ident, RequireHTTPS); !errors.Is(err, ErrNoKnotService) { 122 t.Errorf("error = %v, want ErrNoKnotService", err) 123 } 124 }) 125 } 126 if _, err := KnotURLFromIdentity(nil, RequireHTTPS); !errors.Is(err, ErrNilIdentity) { 127 t.Errorf("nil identity error = %v, want ErrNilIdentity", err) 128 } 129} 130 131func TestKnotURL_ErrorQuotesTheDeclaredEndpoint(t *testing.T) { 132 const declared = "https://knot.oyster.cafe/repo/limpet?utm=knot" 133 _, err := KnotURLFromIdentity(identWith(knotService(declared)), RequireHTTPS) 134 if err == nil { 135 t.Fatal("KnotURLFromIdentity accepted a query") 136 } 137 if !strings.Contains(err.Error(), declared) { 138 t.Errorf("error %q doesn't quote the declared endpoint %q", err, declared) 139 } 140} 141 142func TestSchemePolicy_OnlyAllowHTTPPermitsPlaintext(t *testing.T) { 143 if RequireHTTPS != 0 || SchemeFor(true) != AllowHTTP || SchemeFor(false) != RequireHTTPS { 144 t.Fatalf("RequireHTTPS=%d SchemeFor(true)=%d: the zero value must stay RequireHTTPS", RequireHTTPS, SchemeFor(true)) 145 } 146 if u := mustParse(t, "http://knot.oyster.cafe:80"); u.String() != "http://knot.oyster.cafe" { 147 t.Errorf("AllowHTTP parse = %q, want http://knot.oyster.cafe", u) 148 } 149 for _, policy := range []SchemePolicy{RequireHTTPS, SchemePolicy(42), SchemePolicy(-1)} { 150 if _, err := ParseKnotURL("http://knot.oyster.cafe", policy); err == nil { 151 t.Errorf("policy %d permitted http", policy) 152 } 153 } 154} 155 156func TestKnotURL_JoinPathKeepsTheDidColons(t *testing.T) { 157 const want = "http://localhost:5555/did:plc:limpet" 158 if got := mustParse(t, "http://localhost:5555").JoinPath("did:plc:limpet"); got != want { 159 t.Errorf("JoinPath = %q, want %q", got, want) 160 } 161} 162 163func TestKnotURL_ZeroValueIsInert(t *testing.T) { 164 var zero KnotURL 165 if !zero.IsZero() || zero.String() != "" || zero.Host() != "" { 166 t.Errorf("zero KnotURL isn't inert: IsZero=%v String=%q Host=%q", zero.IsZero(), zero.String(), zero.Host()) 167 } 168 if _, err := zero.MarshalText(); !errors.Is(err, ErrZeroKnotURL) { 169 t.Errorf("zero KnotURL MarshalText error = %v, want ErrZeroKnotURL", err) 170 } 171} 172 173func TestKnotURL_JSONCanonicalizesAndValidates(t *testing.T) { 174 var decoded struct { 175 Knot KnotURL `json:"knot"` 176 } 177 if err := json.Unmarshal([]byte(`{"knot":"HTTP://Localhost:80/"}`), &decoded); err != nil { 178 t.Fatalf("Unmarshal: %v", err) 179 } 180 if decoded.Knot.Host() != "localhost" { 181 t.Errorf("decoded host = %q, want localhost", decoded.Knot.Host()) 182 } 183 out, err := json.Marshal(decoded) 184 if err != nil { 185 t.Fatalf("Marshal: %v", err) 186 } 187 if string(out) != `{"knot":"http://localhost"}` { 188 t.Errorf("re-encoded = %s, want {\"knot\":\"http://localhost\"}", out) 189 } 190 if err := json.Unmarshal([]byte(`{"knot":"https://knot.oyster.cafe/repo/limpet"}`), &decoded); err == nil { 191 t.Error("Unmarshal accepted a URL with a path") 192 } 193}