github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/logsync/logsync_test.go (about) 1 package logsync 2 3 import ( 4 "context" 5 "fmt" 6 "net/http/httptest" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 golog "github.com/ipfs/go-log" 12 crypto "github.com/libp2p/go-libp2p-core/crypto" 13 "github.com/qri-io/dataset" 14 "github.com/qri-io/qfs" 15 "github.com/qri-io/qri/auth/key" 16 testkeys "github.com/qri-io/qri/auth/key/test" 17 "github.com/qri-io/qri/dsref" 18 "github.com/qri-io/qri/event" 19 "github.com/qri-io/qri/logbook" 20 "github.com/qri-io/qri/logbook/oplog" 21 "github.com/qri-io/qri/profile" 22 ) 23 24 func Example() { 25 ctx, done := context.WithCancel(context.Background()) 26 defer done() 27 28 // our example has two authors. Johnathon and Basit are going to sync logbooks 29 // let's start with two empty logbooks 30 johnathonsLogbook := makeJohnathonLogbook() 31 basitsLogbook := makeBasitLogbook() 32 33 wait := make(chan struct{}, 1) 34 35 // create a logsync from basit's logbook: 36 basitLogsync := New(basitsLogbook, func(o *Options) { 37 // we MUST override the PreCheck function. In this example we're only going 38 // to allow pushes from johnathon 39 o.PushPreCheck = func(ctx context.Context, author profile.Author, ref dsref.Ref, l *oplog.Log) error { 40 if author.AuthorID() != johnathonsLogbook.Owner().ID.Encode() { 41 return fmt.Errorf("rejected for secret reasons") 42 } 43 return nil 44 } 45 46 o.Pushed = func(ctx context.Context, author profile.Author, ref dsref.Ref, l *oplog.Log) error { 47 wait <- struct{}{} 48 return nil 49 } 50 }) 51 52 // for this example we're going to do sync over HTTP. 53 // create an HTTP handler for the remote & wire it up to an example server 54 handleFunc := HTTPHandler(basitLogsync) 55 server := httptest.NewServer(handleFunc) 56 defer server.Close() 57 58 // johnathon creates a dataset with a bunch of history: 59 worldBankDatasetRef := makeWorldBankLogs(ctx, johnathonsLogbook) 60 61 items, err := johnathonsLogbook.Items(ctx, worldBankDatasetRef, 0, 100, "") 62 if err != nil { 63 panic(err) 64 } 65 fmt.Printf("johnathon has %d references for %s\n", len(items), worldBankDatasetRef.Human()) 66 67 // johnathon creates a new push 68 johnathonLogsync := New(johnathonsLogbook) 69 push, err := johnathonLogsync.NewPush(worldBankDatasetRef, server.URL) 70 if err != nil { 71 panic(err) 72 } 73 74 // execute the push, sending jonathon's world bank reference to basit 75 if err = push.Do(ctx); err != nil { 76 panic(err) 77 } 78 79 // wait for sync to complete 80 <-wait 81 if items, err = basitsLogbook.Items(ctx, worldBankDatasetRef, 0, 100, ""); err != nil { 82 panic(err) 83 } 84 fmt.Printf("basit has %d references for %s\n", len(items), worldBankDatasetRef.Human()) 85 86 // this time basit creates a history 87 nasdaqDatasetRef := makeNasdaqLogs(ctx, basitsLogbook) 88 89 if items, err = basitsLogbook.Items(ctx, nasdaqDatasetRef, 0, 100, ""); err != nil { 90 panic(err) 91 } 92 fmt.Printf("basit has %d references for %s\n", len(items), nasdaqDatasetRef.Human()) 93 94 // prepare to pull nasdaq refs from basit 95 pull, err := johnathonLogsync.NewPull(nasdaqDatasetRef, server.URL) 96 if err != nil { 97 panic(err) 98 } 99 // setting merge=true will persist logs to the logbook if the pull succeeds 100 pull.Merge = true 101 102 if _, err = pull.Do(ctx); err != nil { 103 panic(err) 104 } 105 106 if items, err = johnathonsLogbook.Items(ctx, nasdaqDatasetRef, 0, 100, ""); err != nil { 107 panic(err) 108 } 109 fmt.Printf("johnathon has %d references for %s\n", len(items), nasdaqDatasetRef.Human()) 110 111 // Output: johnathon has 3 references for johnathon/world_bank_population 112 // basit has 3 references for johnathon/world_bank_population 113 // basit has 2 references for basit/nasdaq 114 // johnathon has 2 references for basit/nasdaq 115 } 116 117 func TestHookCalls(t *testing.T) { 118 tr, cleanup := newTestRunner(t) 119 defer cleanup() 120 121 hooksCalled := []string{} 122 callCheck := func(s string) Hook { 123 return func(ctx context.Context, a profile.Author, ref dsref.Ref, l *oplog.Log) error { 124 hooksCalled = append(hooksCalled, s) 125 return nil 126 } 127 } 128 129 nasdaqRef, err := writeNasdaqLogs(tr.Ctx, tr.A) 130 if err != nil { 131 t.Fatal(err) 132 } 133 134 lsA := New(tr.A, func(o *Options) { 135 o.PullPreCheck = callCheck("PullPreCheck") 136 o.Pulled = callCheck("Pulled") 137 o.PushPreCheck = callCheck("PushPreCheck") 138 o.PushFinalCheck = callCheck("PushFinalCheck") 139 o.Pushed = callCheck("Pushed") 140 o.RemovePreCheck = callCheck("RemovePreCheck") 141 o.Removed = callCheck("Removed") 142 }) 143 144 s := httptest.NewServer(HTTPHandler(lsA)) 145 defer s.Close() 146 147 lsB := New(tr.B) 148 149 pull, err := lsB.NewPull(nasdaqRef, s.URL) 150 if err != nil { 151 t.Fatal(err) 152 } 153 pull.Merge = true 154 155 if _, err := pull.Do(tr.Ctx); err != nil { 156 t.Fatal(err) 157 } 158 159 worldBankRef, err := writeWorldBankLogs(tr.Ctx, tr.B) 160 if err != nil { 161 t.Fatal(err) 162 } 163 push, err := lsB.NewPush(worldBankRef, s.URL) 164 if err != nil { 165 t.Fatal(err) 166 } 167 if err := push.Do(tr.Ctx); err != nil { 168 t.Fatal(err) 169 } 170 171 if err := lsB.DoRemove(tr.Ctx, worldBankRef, s.URL); err != nil { 172 t.Fatal(err) 173 } 174 175 expectHooksCallOrder := []string{ 176 "PullPreCheck", 177 "Pulled", 178 "PushPreCheck", 179 "PushFinalCheck", 180 "Pushed", 181 "RemovePreCheck", 182 "Removed", 183 } 184 185 if diff := cmp.Diff(expectHooksCallOrder, hooksCalled); diff != "" { 186 t.Errorf("result mismatch (-want +got):\n%s", diff) 187 } 188 } 189 190 func TestHookErrors(t *testing.T) { 191 tr, cleanup := newTestRunner(t) 192 defer cleanup() 193 194 worldBankRef, err := writeWorldBankLogs(tr.Ctx, tr.B) 195 if err != nil { 196 t.Fatal(err) 197 } 198 199 hooksCalled := []string{} 200 callCheck := func(s string) Hook { 201 return func(ctx context.Context, a profile.Author, ref dsref.Ref, l *oplog.Log) error { 202 hooksCalled = append(hooksCalled, s) 203 return fmt.Errorf("hook failed") 204 } 205 } 206 207 nasdaqRef, err := writeNasdaqLogs(tr.Ctx, tr.A) 208 if err != nil { 209 t.Fatal(err) 210 } 211 212 lsA := New(tr.A, func(o *Options) { 213 o.PullPreCheck = callCheck("PullPreCheck") 214 o.PushPreCheck = callCheck("PushPreCheck") 215 o.RemovePreCheck = callCheck("RemovePreCheck") 216 217 o.PushFinalCheck = callCheck("PushFinalCheck") 218 219 o.Pulled = callCheck("Pulled") 220 o.Pushed = callCheck("Pushed") 221 o.Removed = callCheck("Removed") 222 }) 223 224 s := httptest.NewServer(HTTPHandler(lsA)) 225 defer s.Close() 226 227 lsB := New(tr.B) 228 229 pull, err := lsB.NewPull(nasdaqRef, s.URL) 230 if err != nil { 231 t.Fatal(err) 232 } 233 pull.Merge = true 234 235 if _, err := pull.Do(tr.Ctx); err == nil { 236 t.Fatal(err) 237 } 238 push, err := lsB.NewPush(worldBankRef, s.URL) 239 if err != nil { 240 t.Fatal(err) 241 } 242 if err := push.Do(tr.Ctx); err == nil { 243 t.Fatal(err) 244 } 245 if err := lsB.DoRemove(tr.Ctx, worldBankRef, s.URL); err == nil { 246 t.Fatal(err) 247 } 248 249 lsA.pushPreCheck = nil 250 lsA.pullPreCheck = nil 251 lsA.removePreCheck = nil 252 253 push, err = lsB.NewPush(worldBankRef, s.URL) 254 if err != nil { 255 t.Fatal(err) 256 } 257 if err := push.Do(tr.Ctx); err == nil { 258 t.Fatal(err) 259 } 260 261 lsA.pushFinalCheck = nil 262 263 pull, err = lsB.NewPull(nasdaqRef, s.URL) 264 if err != nil { 265 t.Fatal(err) 266 } 267 pull.Merge = true 268 269 if _, err := pull.Do(tr.Ctx); err != nil { 270 t.Fatal(err) 271 } 272 push, err = lsB.NewPush(worldBankRef, s.URL) 273 if err != nil { 274 t.Fatal(err) 275 } 276 if err = push.Do(tr.Ctx); err != nil { 277 t.Fatal(err) 278 } 279 if err := lsB.DoRemove(tr.Ctx, worldBankRef, s.URL); err != nil { 280 t.Fatal(err) 281 } 282 283 expectHooksCallOrder := []string{ 284 "PullPreCheck", 285 "PushPreCheck", 286 "RemovePreCheck", 287 288 "PushFinalCheck", 289 290 "Pulled", 291 "Pushed", 292 "Removed", 293 } 294 295 if diff := cmp.Diff(expectHooksCallOrder, hooksCalled); diff != "" { 296 t.Errorf("result mismatch (-want +got):\n%s", diff) 297 } 298 } 299 300 func TestWrongProfileID(t *testing.T) { 301 tr, cleanup := newTestRunner(t) 302 defer cleanup() 303 304 worldBankRef, err := writeWorldBankLogs(tr.Ctx, tr.B) 305 if err != nil { 306 t.Fatal(err) 307 } 308 309 nasdaqRef, err := writeNasdaqLogs(tr.Ctx, tr.A) 310 if err != nil { 311 t.Fatal(err) 312 } 313 314 // Modify the profileID of this reference, which should cause it to fail to push 315 worldBankRef.ProfileID = testkeys.GetKeyData(1).EncodedPeerID 316 317 lsA := New(tr.A) 318 319 s := httptest.NewServer(HTTPHandler(lsA)) 320 defer s.Close() 321 322 lsB := New(tr.B) 323 pull, err := lsB.NewPull(nasdaqRef, s.URL) 324 if err != nil { 325 t.Fatal(err) 326 } 327 pull.Merge = true 328 if _, err := pull.Do(tr.Ctx); err != nil { 329 t.Fatal(err) 330 } 331 332 // B tries to push, but the profileID it uses has been modifed to something else 333 // Logsync will catch this error. 334 push, err := lsB.NewPush(worldBankRef, s.URL) 335 if err != nil { 336 t.Fatal(err) 337 } 338 err = push.Do(tr.Ctx) 339 if err == nil { 340 t.Errorf("expected error but did not get one") 341 } 342 expectErr := `ref contained in log data does not match` 343 if expectErr != err.Error() { 344 t.Errorf("error mismatch, expect: %s, got: %s", expectErr, err) 345 } 346 } 347 348 func TestNilCallable(t *testing.T) { 349 var logsync *Logsync 350 351 if a := logsync.Author(); a != nil { 352 t.Errorf("author mismatch. expected: '%v', got: '%v' ", nil, a) 353 } 354 355 if _, err := logsync.NewPush(dsref.Ref{}, ""); err != ErrNoLogsync { 356 t.Errorf("error mismatch. expected: '%v', got: '%v' ", ErrNoLogsync, err) 357 } 358 if _, err := logsync.NewPull(dsref.Ref{}, ""); err != ErrNoLogsync { 359 t.Errorf("error mismatch. expected: '%v', got: '%v' ", ErrNoLogsync, err) 360 } 361 if err := logsync.DoRemove(context.Background(), dsref.Ref{}, ""); err != ErrNoLogsync { 362 t.Errorf("error mismatch. expected: '%v', got: '%v' ", ErrNoLogsync, err) 363 } 364 } 365 366 func makeJohnathonLogbook() *logbook.Book { 367 pk := testkeys.GetKeyData(10).PrivKey 368 book, err := newTestbook("johnathon", pk) 369 if err != nil { 370 panic(err) 371 } 372 return book 373 } 374 375 func makeBasitLogbook() *logbook.Book { 376 pk := testkeys.GetKeyData(9).PrivKey 377 book, err := newTestbook("basit", pk) 378 if err != nil { 379 panic(err) 380 } 381 return book 382 } 383 384 func makeWorldBankLogs(ctx context.Context, book *logbook.Book) dsref.Ref { 385 ref, err := writeWorldBankLogs(ctx, book) 386 if err != nil { 387 panic(err) 388 } 389 return ref 390 } 391 392 func makeNasdaqLogs(ctx context.Context, book *logbook.Book) dsref.Ref { 393 ref, err := writeNasdaqLogs(ctx, book) 394 if err != nil { 395 panic(err) 396 } 397 return ref 398 } 399 400 type testRunner struct { 401 Ctx context.Context 402 A, B *logbook.Book 403 APrivKey, BPrivKey crypto.PrivKey 404 } 405 406 func (tr *testRunner) DefaultLogsyncs() (a, b *Logsync) { 407 return New(tr.A), New(tr.B) 408 } 409 410 func newTestRunner(t *testing.T) (tr *testRunner, cleanup func()) { 411 var aPk = testkeys.GetKeyData(10).EncodedPrivKey 412 var bPk = testkeys.GetKeyData(9).EncodedPrivKey 413 414 var err error 415 tr = &testRunner{ 416 Ctx: context.Background(), 417 } 418 419 tr.APrivKey, err = key.DecodeB64PrivKey(aPk) 420 if err != nil { 421 t.Fatal(err) 422 } 423 if tr.A, err = newTestbook("a", tr.APrivKey); err != nil { 424 t.Fatal(err) 425 } 426 427 tr.BPrivKey, err = key.DecodeB64PrivKey(bPk) 428 if err != nil { 429 t.Fatal(err) 430 } 431 if tr.B, err = newTestbook("b", tr.BPrivKey); err != nil { 432 t.Fatal(err) 433 } 434 435 golog.SetLogLevel("logsync", "CRITICAL") 436 cleanup = func() { 437 golog.SetLogLevel("logsync", "ERROR") 438 } 439 return tr, cleanup 440 } 441 442 func newTestbook(username string, pk crypto.PrivKey) (*logbook.Book, error) { 443 // logbook relies on a qfs.Filesystem for read & write. create an in-memory 444 // filesystem we can play with 445 fs := qfs.NewMemFS() 446 pro := mustProfileFromPrivKey(username, pk) 447 return logbook.NewJournal(*pro, event.NilBus, fs, "/mem/logbook.qfb") 448 } 449 450 func writeNasdaqLogs(ctx context.Context, book *logbook.Book) (ref dsref.Ref, err error) { 451 name := "nasdaq" 452 initID, err := book.WriteDatasetInit(ctx, book.Owner(), name) 453 if err != nil { 454 return ref, err 455 } 456 457 ds := &dataset.Dataset{ 458 ID: initID, 459 Peername: book.Owner().Peername, 460 Name: name, 461 Commit: &dataset.Commit{ 462 Timestamp: time.Date(2000, time.January, 3, 0, 0, 0, 0, time.UTC), 463 Title: "init dataset", 464 }, 465 Path: "v0", 466 PreviousPath: "", 467 } 468 469 if err = book.WriteVersionSave(ctx, book.Owner(), ds, nil); err != nil { 470 return ref, err 471 } 472 473 ds.Path = "v1" 474 ds.PreviousPath = "v0" 475 476 if err = book.WriteVersionSave(ctx, book.Owner(), ds, nil); err != nil { 477 return ref, err 478 } 479 480 return dsref.Ref{ 481 Username: book.Owner().Peername, 482 Name: name, 483 InitID: initID, 484 }, nil 485 } 486 487 func writeWorldBankLogs(ctx context.Context, book *logbook.Book) (ref dsref.Ref, err error) { 488 name := "world_bank_population" 489 author := book.Owner() 490 491 initID, err := book.WriteDatasetInit(ctx, author, name) 492 if err != nil { 493 return ref, err 494 } 495 496 ds := &dataset.Dataset{ 497 ID: initID, 498 Peername: author.Peername, 499 Name: name, 500 Commit: &dataset.Commit{ 501 Timestamp: time.Date(2000, time.January, 3, 0, 0, 0, 0, time.UTC), 502 Title: "init dataset", 503 }, 504 Path: "/ipfs/QmVersion0", 505 PreviousPath: "", 506 } 507 508 if err = book.WriteVersionSave(ctx, author, ds, nil); err != nil { 509 return ref, err 510 } 511 512 ds.Path = "/ipfs/QmVersion1" 513 ds.PreviousPath = "/ipfs/QmVesion0" 514 515 if err = book.WriteVersionSave(ctx, author, ds, nil); err != nil { 516 return ref, err 517 } 518 519 ds.Path = "/ipfs/QmVersion2" 520 ds.PreviousPath = "/ipfs/QmVersion1" 521 522 if err = book.WriteVersionSave(ctx, author, ds, nil); err != nil { 523 return ref, err 524 } 525 526 return dsref.Ref{ 527 Username: author.Peername, 528 Name: name, 529 ProfileID: author.ID.Encode(), 530 InitID: initID, 531 Path: ds.Path, 532 }, nil 533 } 534 535 func mustProfileFromPrivKey(username string, pk crypto.PrivKey) *profile.Profile { 536 p, err := profile.NewSparsePKProfile(username, pk) 537 if err != nil { 538 panic(err) 539 } 540 return p 541 }