github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/dsref/spec/resolve.go (about) 1 package spec 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/url" 8 "testing" 9 "time" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/multiformats/go-multiaddr" 13 "github.com/qri-io/dataset" 14 "github.com/qri-io/qfs" 15 testkeys "github.com/qri-io/qri/auth/key/test" 16 "github.com/qri-io/qri/dsref" 17 "github.com/qri-io/qri/event" 18 "github.com/qri-io/qri/logbook" 19 "github.com/qri-io/qri/logbook/oplog" 20 "github.com/qri-io/qri/profile" 21 ) 22 23 // PutRefFunc adds a reference to a system that retains references 24 // PutRefFunc is required to run the ResolverSpec test, when called the Resolver 25 // should retain the reference for later retrieval by the spec test. PutRefFunc 26 // also passes the author & oplog that back the reference 27 type PutRefFunc func(ref dsref.Ref, author *profile.Profile, log *oplog.Log) error 28 29 // AssertResolverSpec confirms the expected behaviour of a dsref.Resolver 30 // Interface implementation. In addition to this test passing, implementations 31 // MUST be nil-callable. Please add a nil-callable test for each implementation 32 func AssertResolverSpec(t *testing.T, r dsref.Resolver, putFunc PutRefFunc) { 33 var ( 34 ctx = context.Background() 35 username, dsname = "resolve_spec_test_peer", "stored_ref_dataset" 36 headPath = "/ipfs/QmeXaMpLe" 37 journal = ForeignLogbook(t, username) 38 ) 39 40 profileID := journal.Owner().ID.Encode() 41 initID, log, err := GenerateExampleOplog(ctx, journal, dsname, headPath) 42 if err != nil { 43 t.Fatal(err) 44 } 45 46 expectRef := dsref.Ref{ 47 InitID: initID, 48 ProfileID: profileID, 49 Username: username, 50 Name: dsname, 51 Path: headPath, 52 } 53 54 t.Run("dsrefResolverSpec", func(t *testing.T) { 55 if err := putFunc(expectRef, journal.Owner(), log); err != nil { 56 t.Fatalf("put ref failed: %s", err) 57 } 58 59 _, err := r.ResolveRef(ctx, &dsref.Ref{Username: "username", Name: "does_not_exist"}) 60 if err == nil { 61 t.Errorf("expected error resolving nonexistent reference, got none") 62 } else if !errors.Is(err, dsref.ErrRefNotFound) { 63 t.Errorf("expected standard error resolving nonexistent ref: %q, got: %q", dsref.ErrRefNotFound, err) 64 } 65 66 resolveMe := dsref.Ref{ 67 Username: username, 68 Name: dsname, 69 } 70 71 addr, err := r.ResolveRef(ctx, &resolveMe) 72 if err != nil { 73 t.Error(err) 74 } 75 76 if addr != "" { 77 if _, err := multiaddr.NewMultiaddr(addr); err != nil { 78 if _, urlParseErr := url.Parse(addr); urlParseErr == nil { 79 t.Logf("warning: non-empty source must be a valid multiaddr, but returned a url: %s\nURLS will not be permitted in the future", addr) 80 } else { 81 t.Errorf("non-empty source must be a valid multiaddr.\nmultiaddr parse error: %s", err) 82 } 83 } 84 } 85 86 if diff := cmp.Diff(expectRef, resolveMe); diff != "" { 87 t.Errorf("result mismatch. (-want +got):\n%s", diff) 88 } 89 90 resolveMe = dsref.Ref{ 91 Username: username, 92 Name: dsname, 93 Path: "/ill_provide_the_path_thank_you_very_much", 94 } 95 96 expectRef = dsref.Ref{ 97 Username: username, 98 Name: dsname, 99 ProfileID: profileID, 100 Path: "/ill_provide_the_path_thank_you_very_much", 101 InitID: expectRef.InitID, 102 } 103 104 addr, err = r.ResolveRef(ctx, &resolveMe) 105 if err != nil { 106 t.Error(err) 107 } 108 109 if addr != "" { 110 if _, err := multiaddr.NewMultiaddr(addr); err != nil { 111 if _, urlParseErr := url.Parse(addr); urlParseErr == nil { 112 t.Logf("warning: non-empty source must be a valid multiaddr, but returned a url: %s\nURLS will not be permitted in the future", addr) 113 } else { 114 t.Errorf("non-empty source must be a valid multiaddr.\nmultiaddr parse error: %s", err) 115 } 116 } 117 } 118 119 if diff := cmp.Diff(expectRef, resolveMe); diff != "" { 120 t.Errorf("provided path result mismatch. (-want +got):\n%s", diff) 121 } 122 123 // resolveMe = dsref.Ref{ 124 // Username: username, 125 // Name: dsname, 126 // InitID: initID, 127 // } 128 129 // expectRef = dsref.Ref{ 130 // Username: username, 131 // Name: dsname, 132 // ProfileID: profileID, 133 // Path: headPath, 134 // InitID: initID, 135 // } 136 137 // _, err = r.ResolveRef(ctx, &resolveMe) 138 // if err != nil { 139 // t.Error(err) 140 // } 141 // if resolveMe.InitID != expectRef.InitID { 142 // t.Errorf("providing an InitID result mismatch. want: %q\ngot: %q", expectRef.InitID, resolveMe.InitID) 143 // } 144 // if diff := cmp.Diff(expectRef, resolveMe); diff != "" { 145 // t.Errorf("provided InitID result mismatch. (-want +got):\n%s", diff) 146 // } 147 148 // providing just an initID MUST populate the alias (human side) of a 149 // reference. 150 resolveMe = dsref.Ref{ 151 InitID: initID, 152 153 // erroneous fields need to be be overwritten 154 Username: "no not good", 155 Name: "incorrect", 156 Path: "nope_not_right", 157 } 158 159 expectRef = dsref.Ref{ 160 Username: username, 161 Name: dsname, 162 ProfileID: profileID, 163 Path: headPath, 164 InitID: initID, 165 } 166 167 _, err = r.ResolveRef(ctx, &resolveMe) 168 if err != nil { 169 t.Error(err) 170 } 171 if resolveMe.InitID != expectRef.InitID { 172 t.Errorf("providing InitID-only result mismatch. want: %q\ngot: %q", expectRef.InitID, resolveMe.InitID) 173 } 174 if diff := cmp.Diff(expectRef, resolveMe); diff != "" { 175 t.Errorf("provided InitID-only result mismatch. (-want +got):\n%s", diff) 176 } 177 178 // providing a missing initID MUST return ErrRefNotFound or a wrap thereof 179 resolveMe = dsref.Ref{ 180 InitID: "nope_not_here", 181 } 182 183 if _, err = r.ResolveRef(ctx, &resolveMe); !errors.Is(err, dsref.ErrRefNotFound) { 184 t.Errorf("resolving a missing initID must return ErrRefNotFound or a wrap thereof.\ngot: %s", err) 185 } 186 187 // TODO(b5): need to add a test that confirms ResolveRef CANNOT return 188 // paths outside of logbook HEAD. Subsystems that store references to 189 // mutable paths (eg: FSI links) cannot be set as reference resolution 190 }) 191 } 192 193 // ErrResolversInconsistent indicates two resolvers honored a 194 // resolution request, but gave differing responses 195 var ErrResolversInconsistent = fmt.Errorf("inconsistent resolvers") 196 197 // InconsistentResolvers confirms two resolvers have different responses for 198 // the same reference 199 // this function will not fail the test on error, only write warnings via t.Log 200 func InconsistentResolvers(t *testing.T, ref dsref.Ref, a, b dsref.Resolver) error { 201 err := ConsistentResolvers(t, ref, a, b) 202 if err == nil { 203 return fmt.Errorf("resolvers are consistent, expected inconsitency") 204 } 205 if errors.Is(err, ErrResolversInconsistent) { 206 return nil 207 } 208 209 return err 210 } 211 212 // ConsistentResolvers checks that a set of resolvers return equivalent values 213 // for a given reference 214 // this function will not fail the test on error, only write warnings via t.Log 215 func ConsistentResolvers(t *testing.T, ref dsref.Ref, resolvers ...dsref.Resolver) error { 216 var ( 217 ctx = context.Background() 218 err error 219 resolved *dsref.Ref 220 ) 221 222 for i, r := range resolvers { 223 got := ref.Copy() 224 if _, resolveErr := r.ResolveRef(ctx, &got); resolveErr != nil { 225 // only legal error return value is dsref.ErrRefNotFound 226 if resolveErr != dsref.ErrRefNotFound { 227 return fmt.Errorf("unexpected error checking consistency with resolver %d (%v): %w", i, r, resolveErr) 228 } 229 230 if err == nil && resolved == nil { 231 err = resolveErr 232 continue 233 } else if resolved != nil { 234 return fmt.Errorf("%w: index %d (%v) doesn't have reference that was found elsewhere", ErrResolversInconsistent, i, r) 235 } 236 // err and resolveErr are both ErrNotFound 237 continue 238 } 239 240 if resolved == nil { 241 resolved = &got 242 continue 243 } else if resolved.Equals(got) { 244 continue 245 } 246 247 return fmt.Errorf("%w: index %d (%v): %s != %s", ErrResolversInconsistent, i, r, resolved, got) 248 } 249 250 return nil 251 } 252 253 // ForeignLogbook creates a logbook to use as an external source of oplog data 254 func ForeignLogbook(t *testing.T, username string) *logbook.Book { 255 t.Helper() 256 257 ms := qfs.NewMemFS() 258 pk := testkeys.GetKeyData(9).PrivKey 259 pro, err := profile.NewSparsePKProfile(username, pk) 260 if err != nil { 261 t.Fatal(err) 262 } 263 journal, err := logbook.NewJournal(*pro, event.NilBus, ms, "/mem/logbook.qfb") 264 if err != nil { 265 t.Fatal(err) 266 } 267 268 return journal 269 } 270 271 // GenerateExampleOplog makes an example dataset history on a given journal, 272 // returning the initID and a signed log 273 func GenerateExampleOplog(ctx context.Context, journal *logbook.Book, dsname, headPath string) (string, *oplog.Log, error) { 274 author := journal.Owner() 275 initID, err := journal.WriteDatasetInit(ctx, author, dsname) 276 if err != nil { 277 return "", nil, err 278 } 279 280 err = journal.WriteVersionSave(ctx, author, &dataset.Dataset{ 281 ID: initID, 282 Peername: author.Peername, 283 Name: dsname, 284 Commit: &dataset.Commit{ 285 Timestamp: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), 286 Title: "initial commit", 287 }, 288 Path: headPath, 289 PreviousPath: "", 290 }, nil) 291 if err != nil { 292 return "", nil, err 293 } 294 295 lg, err := journal.UserDatasetBranchesLog(ctx, initID) 296 if err != nil { 297 return "", nil, err 298 } 299 if err := lg.Sign(author.PrivKey); err != nil { 300 return "", nil, err 301 } 302 303 return initID, lg, err 304 }