This repository has no description
0

Configure Feed

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

repoverify,idresolver/mock: extract Describe, resolve knots via KnotURL

Lewis: May this revision serve well! <did:plc:3fwecdnvtcscjnrx2p4n7alz>

author did:plc:3fwecdnvtcscjnrx2p4n7a… committer
Tangled
date (Jul 30, 2026, 3:00 PM UTC) commit d78be590 parent 189fdd42 change-id pxswzwrl
+194 -261
+2 -2
appview/ingester_repo.go
··· 443 443 ) 444 444 return false, nil 445 445 } 446 - if !strings.EqualFold(recordKnot, result.KnotURL.Host) { 446 + if !strings.EqualFold(recordKnot, result.KnotURL.Host()) { 447 447 l.Warn("rejecting repo event: record knot does not match DID-doc endpoint", 448 448 "repoDid", repoDid, 449 449 "recordKnot", recordKnot, 450 - "canonicalKnot", result.KnotURL.Host, 450 + "canonicalKnot", result.KnotURL.Host(), 451 451 ) 452 452 return false, nil 453 453 }
+3 -4
appview/ingester_repo_test.go
··· 6 6 "encoding/json" 7 7 "errors" 8 8 "log/slog" 9 - "net/url" 10 9 "path/filepath" 11 10 "testing" 12 11 ··· 22 21 "tangled.org/core/repoverify" 23 22 ) 24 23 25 - func mustKnotURL(t *testing.T, raw string) *url.URL { 24 + func mustKnotURL(t *testing.T, raw string) repoident.KnotURL { 26 25 t.Helper() 27 - u, err := repoverify.ParseKnotEndpoint(raw, true) 26 + u, err := repoident.ParseKnotURL(raw, repoident.AllowHTTP) 28 27 if err != nil { 29 - t.Fatalf("ParseKnotEndpoint(%q): %v", raw, err) 28 + t.Fatalf("ParseKnotURL(%q): %v", raw, err) 30 29 } 31 30 return u 32 31 }
+26 -1
idresolver/mock.go
··· 1 1 package idresolver 2 2 3 - import "github.com/bluesky-social/indigo/atproto/identity" 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/identity" 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 4 9 5 10 func NewMockResolver(dir identity.Directory) *Resolver { 6 11 return &Resolver{ ··· 8 13 base: &identity.BaseDirectory{}, 9 14 } 10 15 } 16 + 17 + type MockDirectory struct { 18 + Ident *identity.Identity 19 + } 20 + 21 + func (m MockDirectory) LookupDID(context.Context, syntax.DID) (*identity.Identity, error) { 22 + return m.Ident, nil 23 + } 24 + 25 + func (m MockDirectory) LookupHandle(context.Context, syntax.Handle) (*identity.Identity, error) { 26 + return m.Ident, nil 27 + } 28 + 29 + func (m MockDirectory) Lookup(context.Context, syntax.AtIdentifier) (*identity.Identity, error) { 30 + return m.Ident, nil 31 + } 32 + 33 + func (m MockDirectory) Purge(context.Context, syntax.AtIdentifier) error { 34 + return nil 35 + }
+17 -48
knotmirror/xrpc/proxy.go
··· 16 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 17 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 18 "github.com/go-git/go-git/v5/plumbing/filemode" 19 + "github.com/samber/lo" 19 20 "tangled.org/core/api/tangled" 20 21 "tangled.org/core/knotmirror/db" 21 22 "tangled.org/core/knotmirror/models" 23 + "tangled.org/core/repoident" 24 + "tangled.org/core/repoverify" 22 25 ) 23 26 24 27 var mirrorToKnotNSID = map[string]string{ ··· 49 52 repoIdentifier string 50 53 } 51 54 52 - // validateKnotURL ensures a knot base URL is safe to proxy to. 53 - // It rejects URLs with path components, query strings, or fragments 54 - // that could be used for path injection. 55 - func validateKnotURL(raw string) (string, error) { 56 - u, err := url.Parse(raw) 57 - if err != nil { 58 - return "", fmt.Errorf("invalid knot URL: %w", err) 59 - } 60 - if u.Scheme != "http" && u.Scheme != "https" { 61 - return "", errors.New("knot URL must use http or https scheme") 62 - } 63 - if u.Path != "" && u.Path != "/" { 64 - return "", fmt.Errorf("knot URL must not contain a path: %q", raw) 65 - } 66 - if u.RawQuery != "" || u.Fragment != "" { 67 - return "", fmt.Errorf("knot URL must not contain query or fragment: %q", raw) 68 - } 69 - if u.User != nil { 70 - return "", fmt.Errorf("knot URL must not contain userinfo: %q", raw) 71 - } 72 - // Strip trailing slash for consistent formatting 73 - return strings.TrimRight(u.String(), "/"), nil 74 - } 55 + func (x *Xrpc) resolveKnot(ctx context.Context, repoDid syntax.DID) (*knotInfo, error) { 56 + policy := repoident.SchemeFor(!x.cfg.KnotSSRF) 75 57 76 - func (x *Xrpc) resolveKnot(ctx context.Context, repoDid syntax.DID) (*knotInfo, error) { 77 58 if repo, err := db.GetRepoByRepoDid(ctx, x.db, repoDid); err == nil && repo != nil { 78 59 knotURL := repo.KnotDomain 79 60 if !strings.Contains(repo.KnotDomain, "://") { ··· 81 62 knotURL = host.URL() 82 63 } else { 83 64 x.logger.Warn("repo is from unknown knot") 84 - if x.cfg.KnotUseSSL { 85 - knotURL = "https://" + knotURL 86 - } else { 87 - knotURL = "http://" + knotURL 88 - } 65 + knotURL = lo.Ternary(x.cfg.KnotUseSSL, "https://", "http://") + knotURL 89 66 } 90 67 } 91 - knotURL, err = validateKnotURL(knotURL) 68 + base, err := repoident.ParseKnotURL(knotURL, policy) 92 69 if err != nil { 93 70 return nil, err 94 71 } 95 - return &knotInfo{baseURL: knotURL, repoIdentifier: repo.RepoIdentifier()}, nil 72 + return &knotInfo{baseURL: base.String(), repoIdentifier: repo.RepoIdentifier()}, nil 96 73 } 97 74 98 75 ident, err := x.resolver.ResolveIdent(ctx, repoDid.String()) 99 76 if err != nil { 100 77 return nil, fmt.Errorf("resolving repoDid %s: %w", repoDid, err) 101 78 } 102 - knotURL, err := validateKnotURL(ident.GetServiceEndpoint("atproto_pds")) 79 + base, err := repoident.KnotURLFromIdentity(ident, policy) 103 80 if err != nil { 104 81 return nil, fmt.Errorf("repoDid %s: %w", repoDid, err) 105 82 } 83 + knotURL := base.String() 106 84 107 - xrpcc := &indigoxrpc.Client{Host: knotURL, Client: x.httpClient} 108 - out, err := tangled.RepoDescribeRepo(ctx, xrpcc, repoDid.String()) 85 + described, err := repoverify.Describe(ctx, x.httpClient, base, repoident.RepoDid(repoDid)) 86 + if errors.Is(err, repoverify.ErrKnotAnswer) { 87 + return nil, err 88 + } 109 89 if err != nil { 110 90 x.logger.Warn("describeRepo failed; serving without metadata upsert", "knot", knotURL, "repo", repoDid, "err", err) 111 91 return &knotInfo{baseURL: knotURL, repoIdentifier: repoDid.String()}, nil 112 92 } 113 - if out.RepoDid != repoDid.String() { 114 - return nil, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knotURL, out.RepoDid, repoDid) 115 - } 116 - ownerDid, err := syntax.ParseDID(out.OwnerDid) 117 - if err != nil { 118 - return nil, fmt.Errorf("describeRepo on %s returned invalid ownerDid %q: %w", knotURL, out.OwnerDid, err) 119 - } 120 - rkey, err := syntax.ParseRecordKey(out.Rkey) 121 - if err != nil { 122 - return nil, fmt.Errorf("describeRepo on %s returned invalid rkey %q: %w", knotURL, out.Rkey, err) 123 - } 124 93 125 94 go func() { 126 95 pending := &models.Repo{ 127 - Did: ownerDid, 128 - Rkey: rkey, 129 - Name: string(rkey), 96 + Did: syntax.DID(described.OwnerDid), 97 + Rkey: described.Rkey, 98 + Name: string(described.Rkey), 130 99 KnotDomain: knotURL, 131 100 RepoDid: repoDid, 132 101 State: models.RepoStatePending,
+8 -25
knotserver/keys/keys_test.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "errors" 7 6 "net/http" 8 7 "net/http/httptest" 9 8 "path/filepath" ··· 14 13 "github.com/bluesky-social/indigo/atproto/syntax" 15 14 lexutil "github.com/bluesky-social/indigo/lex/util" 16 15 "tangled.org/core/api/tangled" 16 + "tangled.org/core/idresolver" 17 17 "tangled.org/core/knotserver/db" 18 18 ) 19 19 ··· 32 32 }) 33 33 defer srv.Close() 34 34 35 - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 35 + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { 36 36 t.Fatalf("FetchAndStore: %v", err) 37 37 } 38 38 ··· 54 54 }) 55 55 defer srv.Close() 56 56 57 - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 57 + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { 58 58 t.Fatalf("FetchAndStore: %v", err) 59 59 } 60 60 ··· 82 82 }) 83 83 defer srv.Close() 84 84 85 - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { 85 + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { 86 86 t.Fatalf("FetchAndStore: %v", err) 87 87 } 88 88 ··· 173 173 })) 174 174 } 175 175 176 - type fakeDirectory struct { 177 - pdsURL string 178 - } 179 - 180 - func (f fakeDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 181 - return &identity.Identity{ 182 - DID: did, 176 + func pdsDirectory(url string) idresolver.MockDirectory { 177 + return idresolver.MockDirectory{Ident: &identity.Identity{ 183 178 Services: map[string]identity.ServiceEndpoint{ 184 - "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: f.pdsURL}, 179 + "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: url}, 185 180 }, 186 - }, nil 187 - } 188 - 189 - func (f fakeDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { 190 - return nil, errors.New("LookupHandle unused in tests") 191 - } 192 - 193 - func (f fakeDirectory) Lookup(ctx context.Context, atid syntax.AtIdentifier) (*identity.Identity, error) { 194 - return nil, errors.New("Lookup unused in tests") 195 - } 196 - 197 - func (f fakeDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { 198 - return nil 181 + }} 199 182 }
+14 -6
knotserver/repodid/repodid.go
··· 9 9 atcrypto "github.com/bluesky-social/indigo/atproto/atcrypto" 10 10 "github.com/did-method-plc/go-didplc/didplc" 11 11 "tangled.org/core/idresolver" 12 + "tangled.org/core/repoident" 12 13 ) 13 14 14 15 type PreparedDID struct { ··· 47 48 }, 48 49 AlsoKnownAs: []string{}, 49 50 Services: map[string]didplc.OpService{ 50 - "atproto_pds": { 51 - Type: "AtprotoPersonalDataServer", 51 + repoident.LegacyKnotServiceID: { 52 + Type: repoident.LegacyKnotServiceType, 52 53 Endpoint: knotServiceUrl, 53 54 }, 54 55 }, ··· 107 108 return fmt.Errorf("resolving did:web document: %w", err) 108 109 } 109 110 110 - knotEndpoint := ident.GetServiceEndpoint("atproto_pds") 111 - if strings.TrimRight(knotEndpoint, "/") != strings.TrimRight(knotServiceUrl, "/") { 111 + knotEndpoint, err := repoident.KnotURLFromIdentity(ident, repoident.AllowHTTP) 112 + if err != nil { 113 + return fmt.Errorf("did:web document: %w", err) 114 + } 115 + expected, err := repoident.ParseKnotURL(knotServiceUrl, repoident.AllowHTTP) 116 + if err != nil { 117 + return fmt.Errorf("knot service URL %q: %w", knotServiceUrl, err) 118 + } 119 + if knotEndpoint != expected { 112 120 return fmt.Errorf( 113 - "did:web atproto_pds service endpoint %q does not match this knot %q", 114 - knotEndpoint, knotServiceUrl, 121 + "did:web knot service endpoint %q doesn't match this knot %q", 122 + knotEndpoint, expected, 115 123 ) 116 124 } 117 125
+37 -98
repoverify/verify.go
··· 4 4 "context" 5 5 "errors" 6 6 "fmt" 7 - "net" 8 7 "net/http" 9 - "net/url" 10 - "syscall" 11 8 "time" 12 9 10 + "github.com/bluesky-social/indigo/atproto/syntax" 13 11 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 14 12 "tangled.org/core/api/tangled" 13 + "tangled.org/core/hostutil" 15 14 "tangled.org/core/idresolver" 16 15 "tangled.org/core/repoident" 17 16 "tangled.org/core/xrpc/xrpcclient" 18 17 ) 19 18 20 - func ParseKnotEndpoint(raw string, dev bool) (*url.URL, error) { 21 - if raw == "" { 22 - return nil, fmt.Errorf("empty knot URL") 23 - } 24 - u, err := url.Parse(raw) 25 - if err != nil { 26 - return nil, fmt.Errorf("invalid knot URL %q: %w", raw, err) 27 - } 28 - if u.Host == "" { 29 - return nil, fmt.Errorf("knot URL %q has no host", raw) 30 - } 31 - switch u.Scheme { 32 - case "https": 33 - case "http": 34 - if !dev { 35 - return nil, fmt.Errorf("knot URL %q must use https outside dev mode", raw) 36 - } 37 - default: 38 - return nil, fmt.Errorf("knot URL %q has unsupported scheme %q", raw, u.Scheme) 39 - } 40 - return u, nil 41 - } 42 - 43 19 type Result struct { 44 20 RepoDid repoident.RepoDid 45 21 OwnerDid repoident.OwnerDid 46 - KnotURL *url.URL 22 + KnotURL repoident.KnotURL 47 23 // Rkey of the sh.tangled.repo record tracked by the knot; empty when the 48 24 // knot does not support describeRepo. 49 - Rkey string 25 + Rkey syntax.RecordKey 50 26 } 51 27 52 - type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) 53 - 54 - const verifyTimeout = 10 * time.Second 28 + var ErrKnotAnswer = errors.New("knot returned an invalid describeRepo answer") 55 29 56 - func New(resolver *idresolver.Resolver, dev bool) Verifier { 57 - transport := &http.Transport{ 58 - DialContext: safeDialer(dev).DialContext, 30 + func Describe(ctx context.Context, httpClient *http.Client, knot repoident.KnotURL, repoDid repoident.RepoDid) (Result, error) { 31 + client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} 32 + out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) 33 + if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { 34 + return Result{}, fmt.Errorf("describeRepo on %s: %w (%v)", knot, xrpcErr, err) 59 35 } 60 - httpClient := &http.Client{ 61 - Timeout: verifyTimeout, 62 - Transport: transport, 63 - } 64 - 65 - return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { 66 - ctx, cancel := context.WithTimeout(ctx, verifyTimeout) 67 - defer cancel() 68 - return resolveAndDescribe(ctx, resolver, httpClient, repoDid, dev) 36 + if out.RepoDid != repoDid.String() { 37 + return Result{}, fmt.Errorf("%w: knot %s returned repoDid %q, want %q", ErrKnotAnswer, knot, out.RepoDid, repoDid) 69 38 } 70 - } 71 - 72 - func resolveAndDescribe( 73 - ctx context.Context, 74 - resolver *idresolver.Resolver, 75 - httpClient *http.Client, 76 - repoDid repoident.RepoDid, 77 - dev bool, 78 - ) (Result, error) { 79 - ident, err := resolver.ResolveIdent(ctx, repoDid.String()) 39 + ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) 80 40 if err != nil { 81 - return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) 41 + return Result{}, fmt.Errorf("%w from knot %s: %w", ErrKnotAnswer, knot, err) 82 42 } 83 - 84 - knot, err := ParseKnotEndpoint(ident.GetServiceEndpoint("atproto_pds"), dev) 43 + rkey, err := syntax.ParseRecordKey(out.Rkey) 85 44 if err != nil { 86 - return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) 45 + return Result{}, fmt.Errorf("%w: knot %s returned rkey %q: %w", ErrKnotAnswer, knot, out.Rkey, err) 87 46 } 47 + return Result{RepoDid: repoDid, OwnerDid: ownerDid, KnotURL: knot, Rkey: rkey}, nil 48 + } 88 49 89 - client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} 90 - out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) 91 - if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { 92 - if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) { 93 - return Result{RepoDid: repoDid, KnotURL: knot}, nil 94 - } 95 - return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr) 96 - } 50 + type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) 97 51 98 - if out.RepoDid != repoDid.String() { 99 - return Result{}, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knot, out.RepoDid, repoDid) 100 - } 52 + const verifyTimeout = 10 * time.Second 101 53 102 - ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) 103 - if err != nil { 104 - return Result{}, fmt.Errorf("describeRepo on %s returned invalid ownerDid: %w", knot, err) 105 - } 54 + func New(resolver *idresolver.Resolver, dev bool) Verifier { 55 + httpClient := hostutil.SafeClient(dev, verifyTimeout) 56 + policy := repoident.SchemeFor(dev) 106 57 107 - return Result{ 108 - RepoDid: repoDid, 109 - OwnerDid: ownerDid, 110 - KnotURL: knot, 111 - Rkey: out.Rkey, 112 - }, nil 113 - } 58 + return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { 59 + ctx, cancel := context.WithTimeout(ctx, verifyTimeout) 60 + defer cancel() 114 61 115 - func safeDialer(dev bool) *net.Dialer { 116 - d := &net.Dialer{ 117 - Timeout: 5 * time.Second, 118 - KeepAlive: 30 * time.Second, 119 - } 120 - if dev { 121 - return d 122 - } 123 - d.Control = func(network, address string, _ syscall.RawConn) error { 124 - host, _, err := net.SplitHostPort(address) 62 + ident, err := resolver.ResolveIdent(ctx, repoDid.String()) 125 63 if err != nil { 126 - return fmt.Errorf("invalid dial address %q: %w", address, err) 64 + return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) 127 65 } 128 - ip := net.ParseIP(host) 129 - if ip == nil { 130 - return fmt.Errorf("dial address %q did not resolve to IP", address) 66 + 67 + knot, err := repoident.KnotURLFromIdentity(ident, policy) 68 + if err != nil { 69 + return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) 131 70 } 132 - if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || 133 - ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { 134 - return fmt.Errorf("refusing to dial %s: reserved or private address", ip) 71 + 72 + result, err := Describe(ctx, httpClient, knot, repoDid) 73 + if errors.Is(err, xrpcclient.ErrXrpcUnsupported) { 74 + return Result{RepoDid: repoDid, KnotURL: knot}, nil 135 75 } 136 - return nil 76 + return result, err 137 77 } 138 - return d 139 78 }
+84 -34
repoverify/verify_test.go
··· 1 1 package repoverify 2 2 3 - import "testing" 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "errors" 7 + "net/http" 8 + "net/http/httptest" 9 + "strings" 10 + "testing" 11 + 12 + "github.com/bluesky-social/indigo/atproto/identity" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/idresolver" 15 + "tangled.org/core/repoident" 16 + ) 4 17 5 - func TestParseKnotEndpoint_RejectsHttpInProd(t *testing.T) { 6 - if _, err := ParseKnotEndpoint("http://knot.example", false); err == nil { 7 - t.Error("http:// knot URL accepted in prod") 8 - } 18 + const ( 19 + testRepoDid = repoident.RepoDid("did:plc:limpet") 20 + testOwnerDid = "did:plc:akshay" 21 + ) 22 + 23 + func describeRepoServer(t *testing.T, describedRepoDid repoident.RepoDid) *httptest.Server { 24 + t.Helper() 25 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 + if r.URL.Path != "/xrpc/"+tangled.RepoDescribeRepoNSID { 27 + http.NotFound(w, r) 28 + return 29 + } 30 + w.Header().Set("Content-Type", "application/json") 31 + _ = json.NewEncoder(w).Encode(tangled.RepoDescribeRepo_Output{ 32 + RepoDid: describedRepoDid.String(), 33 + OwnerDid: testOwnerDid, 34 + Rkey: "3kkkkkkkkkkkk", 35 + }) 36 + })) 37 + t.Cleanup(srv.Close) 38 + return srv 9 39 } 10 40 11 - func TestParseKnotEndpoint_AllowsHttpInDev(t *testing.T) { 12 - u, err := ParseKnotEndpoint("http://knot.example", true) 13 - if err != nil { 14 - t.Fatalf("dev mode should allow http: %v", err) 15 - } 16 - if u.Host != "knot.example" { 17 - t.Errorf("Host = %q, want knot.example", u.Host) 18 - } 41 + func verifyKnot(t *testing.T, knotURL string, dev bool) (Result, error) { 42 + t.Helper() 43 + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: &identity.Identity{ 44 + Services: map[string]identity.ServiceEndpoint{ 45 + repoident.KnotServiceID: {Type: repoident.KnotServiceType, URL: knotURL}, 46 + }, 47 + }}) 48 + return New(resolver, dev)(context.Background(), testRepoDid) 19 49 } 20 50 21 - func TestParseKnotEndpoint_RejectsUnsupportedScheme(t *testing.T) { 22 - if _, err := ParseKnotEndpoint("ftp://knot.example", true); err == nil { 23 - t.Error("ParseKnotEndpoint accepted ftp:// in dev") 51 + func TestNew_DevModeAcceptsHttpKnotEndpointAndStripsThePath(t *testing.T) { 52 + srv := describeRepoServer(t, testRepoDid) 53 + 54 + result, err := verifyKnot(t, srv.URL+"/repo/m5326fp3qemiriiqypxv6rrhai", true) 55 + if err != nil { 56 + t.Fatalf("dev mode should accept an http knot endpoint: %v", err) 24 57 } 25 - if _, err := ParseKnotEndpoint("ftp://knot.example", false); err == nil { 26 - t.Error("ParseKnotEndpoint accepted ftp:// in prod") 58 + if result.OwnerDid.String() != testOwnerDid { 59 + t.Errorf("OwnerDid = %q, want %q", result.OwnerDid, testOwnerDid) 60 + } 61 + if result.KnotURL.String() != srv.URL { 62 + t.Errorf("KnotURL = %q, want %q", result.KnotURL, srv.URL) 27 63 } 28 64 } 29 65 30 - func TestParseKnotEndpoint_RejectsEmptyOrHostless(t *testing.T) { 31 - cases := []string{"", "https://", "not a url at all"} 32 - for _, raw := range cases { 33 - t.Run(raw, func(t *testing.T) { 34 - if _, err := ParseKnotEndpoint(raw, false); err == nil { 35 - t.Errorf("ParseKnotEndpoint(%q) accepted bogus URL", raw) 66 + func TestNew_Rejections(t *testing.T) { 67 + target := describeRepoServer(t, testRepoDid) 68 + otherRepo := describeRepoServer(t, "did:plc:anemone") 69 + redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 + http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound) 71 + })) 72 + t.Cleanup(redirector.Close) 73 + 74 + cases := map[string]struct { 75 + knotURL string 76 + dev bool 77 + want string 78 + sentinel error 79 + }{ 80 + "http endpoint outside dev mode": {target.URL, false, "must use https", nil}, 81 + "knot redirects elsewhere": {redirector.URL, true, "describeRepo on", nil}, 82 + "identity declares no knot": {"", true, "", repoident.ErrNoKnotService}, 83 + "describeRepo answers for another repo": {otherRepo.URL, true, `repoDid "did:plc:anemone"`, ErrKnotAnswer}, 84 + } 85 + for name, tc := range cases { 86 + t.Run(name, func(t *testing.T) { 87 + _, err := verifyKnot(t, tc.knotURL, tc.dev) 88 + if err == nil { 89 + t.Fatalf("verify accepted a knot it should reject") 90 + } 91 + if !strings.Contains(err.Error(), tc.want) { 92 + t.Errorf("error = %v, want one mentioning %q", err, tc.want) 93 + } 94 + if tc.sentinel != nil && !errors.Is(err, tc.sentinel) { 95 + t.Errorf("error = %v, want one matching %v", err, tc.sentinel) 36 96 } 37 97 }) 38 98 } 39 99 } 40 - 41 - func TestParseKnotEndpoint_HostPreservesPort(t *testing.T) { 42 - u, err := ParseKnotEndpoint("http://localhost:3000", true) 43 - if err != nil { 44 - t.Fatalf("ParseKnotEndpoint: %v", err) 45 - } 46 - if u.Host != "localhost:3000" { 47 - t.Errorf("Host = %q, want localhost:3000", u.Host) 48 - } 49 - }
+1 -1
spindle/server.go
··· 579 579 if err != nil { 580 580 return nil, fmt.Errorf("verify sourceRepo %s: %w", repoDid, err) 581 581 } 582 - return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host, res.OwnerDid.String(), res.Rkey, repoDid.String()), nil 582 + return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host(), res.OwnerDid.String(), res.Rkey.String(), repoDid.String()), nil 583 583 } 584 584 585 585 // runPipeline compiles and enqueues the pipeline for the given revision.
+1 -21
spindle/tapclient_test.go
··· 21 21 "tangled.org/core/tapc" 22 22 ) 23 23 24 - type mockDirectory struct { 25 - ident *identity.Identity 26 - } 27 - 28 - func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 29 - return m.ident, nil 30 - } 31 - 32 - func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { 33 - return m.ident, nil 34 - } 35 - 36 - func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { 37 - return m.ident, nil 38 - } 39 - 40 - func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { 41 - return nil 42 - } 43 - 44 24 func TestProcessRepo_MembershipCheck(t *testing.T) { 45 25 d, e := newTestSpindleDB(t) 46 26 ··· 323 303 DID: subjectDid, 324 304 Handle: h, 325 305 } 326 - resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) 306 + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: mockIdent}) 327 307 328 308 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 329 309 if jcerr != nil {
+1 -21
spindle/xrpc/xrpc_test.go
··· 232 232 } 233 233 } 234 234 235 - type mockDirectory struct { 236 - ident *identity.Identity 237 - } 238 - 239 - func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 240 - return m.ident, nil 241 - } 242 - 243 - func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { 244 - return m.ident, nil 245 - } 246 - 247 - func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { 248 - return m.ident, nil 249 - } 250 - 251 - func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { 252 - return nil 253 - } 254 - 255 235 func TestSecrets_RBAC(t *testing.T) { 256 236 d, e := newTestXrpcDB(t) 257 237 ··· 322 302 }, 323 303 } 324 304 325 - resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) 305 + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: mockIdent}) 326 306 327 307 x := &Xrpc{ 328 308 Logger: slog.Default(),