github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/logbook/oplog/log_test.go (about) 1 package oplog 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "testing" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/google/go-cmp/cmp/cmpopts" 11 crypto "github.com/libp2p/go-libp2p-core/crypto" 12 "github.com/qri-io/qri/auth/key" 13 "github.com/qri-io/qri/logbook/oplog/logfb" 14 ) 15 16 var allowUnexported = cmp.AllowUnexported( 17 Journal{}, 18 Log{}, 19 ) 20 21 func TestJournalMerge(t *testing.T) { 22 tr, cleanup := newTestRunner(t) 23 defer cleanup() 24 ctx := tr.Ctx 25 26 if err := tr.Journal.MergeLog(ctx, &Log{}); err == nil { 27 t.Error("exceted adding an empty log to fail") 28 } 29 30 a := &Log{Ops: []Op{ 31 {Type: OpTypeInit, AuthorID: "a"}, 32 }} 33 if err := tr.Journal.MergeLog(ctx, a); err != nil { 34 t.Error(err) 35 } 36 37 expectLen := 1 38 gotLen := len(tr.Journal.logs) 39 if expectLen != gotLen { 40 t.Errorf("top level log length mismatch. expected: %d, got: %d", expectLen, gotLen) 41 } 42 43 a = &Log{ 44 Ops: []Op{ 45 {Type: OpTypeInit, AuthorID: "a"}, 46 }, 47 Logs: []*Log{ 48 { 49 Ops: []Op{ 50 {Type: OpTypeInit, Model: 1, AuthorID: "a"}, 51 }, 52 }, 53 }, 54 } 55 56 if err := tr.Journal.MergeLog(ctx, a); err != nil { 57 t.Error(err) 58 } 59 60 if expectLen != gotLen { 61 t.Errorf("top level log length shouldn't change after merging a child log. expected: %d, got: %d", expectLen, gotLen) 62 } 63 64 got, err := tr.Journal.Get(ctx, a.ID()) 65 if err != nil { 66 t.Error(err) 67 } 68 69 if !got.Logs[0].Ops[0].Equal(a.Logs[0].Ops[0]) { 70 t.Errorf("expected returned ops to be equal") 71 } 72 } 73 74 func TestJournalFlatbuffer(t *testing.T) { 75 log := InitLog(Op{ 76 Type: OpTypeInit, 77 Model: 0x1, 78 Ref: "QmRefHash", 79 Prev: "QmPrevHash", 80 Relations: []string{"a", "b", "c"}, 81 Name: "steve", 82 AuthorID: "QmSteveHash", 83 Timestamp: 1, 84 Size: 2, 85 Note: "note!", 86 }) 87 log.Signature = []byte{1, 2, 3} 88 89 log.AddChild(InitLog(Op{ 90 Type: OpTypeInit, 91 Model: 0x0002, 92 Ref: "QmRefHash", 93 Name: "steve", 94 AuthorID: "QmSteveHash", 95 Timestamp: 2, 96 Size: 2500000, 97 Note: "note?", 98 })) 99 100 j := &Journal{ 101 logs: []*Log{log}, 102 } 103 104 data := j.flatbufferBytes() 105 logsetfb := logfb.GetRootAsBook(data, 0) 106 107 got := &Journal{} 108 if err := got.unmarshalFlatbuffer(logsetfb); err != nil { 109 t.Fatalf("unmarshalling flatbuffer bytes: %s", err.Error()) 110 } 111 112 // TODO (b5) - need to ignore log.parent here. causes a stack overflow in cmp.Diff 113 // we should file an issue with a test that demonstrates the error 114 ignoreCircularPointers := cmpopts.IgnoreUnexported(Log{}) 115 116 if diff := cmp.Diff(j, got, allowUnexported, cmp.Comparer(comparePrivKeys), ignoreCircularPointers); diff != "" { 117 t.Errorf("result mismatch (-want +got):\n%s", diff) 118 } 119 } 120 121 func TestJournalCiphertext(t *testing.T) { 122 tr, cleanup := newTestRunner(t) 123 defer cleanup() 124 125 lg := tr.RandomLog(Op{ 126 Type: OpTypeInit, 127 Model: 0x1, 128 Name: "apples", 129 }, 10) 130 131 j := tr.Journal 132 if err := j.MergeLog(tr.Ctx, lg); err != nil { 133 t.Fatal(err) 134 } 135 136 gotcipher, err := j.FlatbufferCipher(tr.PrivKey) 137 if err != nil { 138 t.Fatalf("calculating flatbuffer cipher: %s", err.Error()) 139 } 140 141 plaintext := j.flatbufferBytes() 142 if bytes.Equal(gotcipher, plaintext) { 143 t.Errorf("plaintext bytes & ciphertext bytes can't be equal") 144 } 145 146 // TODO (b5) - we should confirm the ciphertext isn't readable, but 147 // this'll panic with out-of-bounds slice access... 148 // ciphertextAsBook := logfb.GetRootAsBook(gotcipher, 0) 149 // if err := book.unmarshalFlatbuffer(ciphertextAsBook); err == nil { 150 // t.Errorf("ciphertext as book should not have worked") 151 // } 152 153 if err = j.UnmarshalFlatbufferCipher(tr.Ctx, tr.PrivKey, gotcipher); err != nil { 154 t.Errorf("book.UnmarhsalFlatbufferCipher unexpected error: %s", err.Error()) 155 } 156 } 157 158 func TestJournalSignLog(t *testing.T) { 159 tr, cleanup := newTestRunner(t) 160 defer cleanup() 161 162 lg := tr.RandomLog(Op{ 163 Type: OpTypeInit, 164 Model: 0x1, 165 Name: "apples", 166 }, 400) 167 168 pk := tr.PrivKey 169 if err := lg.Sign(pk); err != nil { 170 t.Fatal(err) 171 } 172 data := lg.FlatbufferBytes() 173 174 received, err := FromFlatbufferBytes(data) 175 if err != nil { 176 t.Fatal(err) 177 } 178 179 if err := received.Verify(pk.GetPublic()); err != nil { 180 t.Fatal(err) 181 } 182 } 183 184 func TestLogHead(t *testing.T) { 185 l := &Log{} 186 if !l.Head().Equal(Op{}) { 187 t.Errorf("expected empty log head to equal empty Op") 188 } 189 190 l = &Log{Ops: []Op{Op{}, Op{Model: 4}, Op{Model: 5, AuthorID: "foo"}}} 191 if !l.Head().Equal(l.Ops[2]) { 192 t.Errorf("expected log head to equal last Op") 193 } 194 } 195 196 func TestLogGetID(t *testing.T) { 197 tr, cleanup := newTestRunner(t) 198 defer cleanup() 199 200 tr.AddAuthorLogTree(t) 201 ctx := tr.Ctx 202 203 got, err := tr.Journal.Get(ctx, "nonsense") 204 if !errors.Is(err, ErrNotFound) { 205 t.Errorf("expected not-found error for missing ID. got: %s", err) 206 } 207 208 root := tr.Journal.logs[0] 209 got, err = tr.Journal.Get(ctx, root.ID()) 210 if err != nil { 211 t.Errorf("unexpected error fetching root ID: %s", err) 212 } else if !got.Head().Equal(root.Head()) { 213 t.Errorf("returned log mismatch. Heads are different") 214 } 215 216 child := root.Logs[0] 217 got, err = tr.Journal.Get(ctx, child.ID()) 218 if err != nil { 219 t.Errorf("unexpected error fetching child ID: %s", err) 220 } 221 if !got.Head().Equal(child.Head()) { 222 t.Errorf("returned log mismatch. Heads are different") 223 } 224 } 225 226 func TestLogNameTracking(t *testing.T) { 227 lg := InitLog(Op{ 228 Type: OpTypeInit, 229 Model: 0x01, 230 Name: "apples", 231 AuthorID: "authorID", 232 }) 233 234 changeOp := Op{ 235 Type: OpTypeAmend, 236 Model: 0x01, 237 Name: "oranges", 238 AuthorID: "authorID2", 239 } 240 lg.Append(changeOp) 241 242 if lg.Name() != "oranges" { 243 t.Logf("name mismatch. expected 'oranges', got: '%s'", lg.Name()) 244 } 245 246 if lg.Author() != "authorID2" { 247 t.Logf("name mismatch. expected 'authorID2', got: '%s'", lg.Author()) 248 } 249 } 250 251 // NB: This test currently doesn't / can't confirm merging sets Log.parent. 252 // the cmp package can't deal with cyclic references 253 func TestLogMerge(t *testing.T) { 254 left := &Log{ 255 Signature: []byte{1, 2, 3}, 256 Ops: []Op{ 257 { 258 Type: OpTypeInit, 259 Model: 0x1, 260 AuthorID: "author", 261 Name: "root", 262 }, 263 }, 264 Logs: []*Log{ 265 { 266 Ops: []Op{ 267 { 268 Type: OpTypeInit, 269 Model: 0x0002, 270 AuthorID: "author", 271 Name: "child_a", 272 }, 273 { 274 Type: OpTypeInit, 275 Model: 0x0456, 276 }, 277 }, 278 }, 279 }, 280 } 281 282 right := &Log{ 283 Ops: []Op{ 284 { 285 Type: OpTypeInit, 286 Model: 0x1, 287 AuthorID: "author", 288 Name: "root", 289 }, 290 { 291 Type: OpTypeInit, 292 Model: 0x0011, 293 }, 294 }, 295 Logs: []*Log{ 296 { 297 Ops: []Op{ 298 { 299 Type: OpTypeInit, 300 Model: 0x0002, 301 AuthorID: "author", 302 Name: "child_a", 303 }, 304 }, 305 }, 306 { 307 Ops: []Op{ 308 { 309 Type: OpTypeInit, 310 Model: 0x0002, 311 AuthorID: "buthor", 312 Name: "child_b", 313 }, 314 }, 315 }, 316 }, 317 } 318 319 left.Merge(right) 320 321 expect := &Log{ 322 Ops: []Op{ 323 { 324 Type: OpTypeInit, 325 Model: 0x1, 326 AuthorID: "author", 327 Name: "root", 328 }, 329 { 330 Type: OpTypeInit, 331 Model: 0x0011, 332 }, 333 }, 334 Logs: []*Log{ 335 { 336 Ops: []Op{ 337 { 338 Type: OpTypeInit, 339 Model: 0x0002, 340 AuthorID: "author", 341 Name: "child_a", 342 }, 343 { 344 Type: OpTypeInit, 345 Model: 0x0456, 346 }, 347 }, 348 }, 349 { 350 ParentID: "adguqcqnrpc2rwxdykvsvengsccd5kew3x7jhs52rspg2f5nbina", 351 Ops: []Op{ 352 { 353 Type: OpTypeInit, 354 Model: 0x0002, 355 AuthorID: "buthor", 356 Name: "child_b", 357 }, 358 }, 359 }, 360 }, 361 } 362 363 if diff := cmp.Diff(expect, left, allowUnexported, cmpopts.IgnoreUnexported(Log{})); diff != "" { 364 t.Errorf("result mismatch (-want +got):\n%s", diff) 365 } 366 } 367 368 func TestHeadRefRemoveTracking(t *testing.T) { 369 tr, cleanup := newTestRunner(t) 370 defer cleanup() 371 372 ctx := tr.Ctx 373 374 l := &Log{ 375 Ops: []Op{ 376 {Type: OpTypeInit, Model: 1, Name: "a"}, 377 }, 378 Logs: []*Log{ 379 { 380 Ops: []Op{ 381 {Type: OpTypeInit, Model: 2, Name: "a"}, 382 }, 383 }, 384 { 385 Ops: []Op{ 386 {Type: OpTypeRemove, Model: 2, Name: "b"}, // "pre-deleted log" 387 }, 388 }, 389 }, 390 } 391 if err := tr.Journal.MergeLog(ctx, l); err != nil { 392 t.Fatal(err) 393 } 394 395 aLog, err := tr.Journal.HeadRef(ctx, "a") 396 if err != nil { 397 t.Errorf("expected no error fetching head ref for a. got: %v", err) 398 } 399 if _, err = tr.Journal.HeadRef(ctx, "a", "a"); err != nil { 400 t.Errorf("expected no error fetching head ref for a/a. got: %v", err) 401 } 402 if _, err = tr.Journal.HeadRef(ctx, "a", "b"); err != ErrNotFound { 403 t.Errorf("expected removed log to be not found. got: %v", err) 404 } 405 406 // add a remove operation to "a": 407 aLog.Ops = append(aLog.Ops, Op{Type: OpTypeRemove, Model: 1, Name: "a"}) 408 409 if _, err = tr.Journal.HeadRef(ctx, "a"); err != ErrNotFound { 410 t.Errorf("expected removed log to be not found. got: %v", err) 411 } 412 if _, err = tr.Journal.HeadRef(ctx, "a", "a"); err != ErrNotFound { 413 t.Errorf("expected child of removed log to be not found. got: %v", err) 414 } 415 416 expectLogs := []*Log{ 417 { 418 Ops: []Op{ 419 {Type: OpTypeInit, Model: 1, Name: "a"}, 420 {Type: OpTypeRemove, Model: 1, Name: "a"}, 421 }, 422 Logs: []*Log{ 423 { 424 Ops: []Op{ 425 {Type: OpTypeInit, Model: 2, Name: "a"}, 426 }, 427 }, 428 { 429 Ops: []Op{ 430 {Type: OpTypeRemove, Model: 2, Name: "b"}, 431 }, 432 }, 433 }, 434 }, 435 } 436 437 if diff := cmp.Diff(expectLogs, tr.Journal.logs, allowUnexported); diff != "" { 438 t.Errorf("result mismatch (-want +got):\n%s", diff) 439 } 440 } 441 442 func TestLogTraversal(t *testing.T) { 443 tr, cleanup := newTestRunner(t) 444 defer cleanup() 445 446 tr.AddAuthorLogTree(t) 447 ctx := tr.Ctx 448 449 if _, err := tr.Journal.HeadRef(ctx); err == nil { 450 t.Errorf("expected not providing a name to error") 451 } 452 453 if _, err := tr.Journal.HeadRef(ctx, "this", "isn't", "a", "thing"); err != ErrNotFound { 454 t.Errorf("expected asking for nonexistent log to return ErrNotFound. got: %v", err) 455 } 456 457 got, err := tr.Journal.HeadRef(ctx, "root", "b", "bazinga") 458 if err != nil { 459 t.Error(err) 460 } 461 462 // t.Logf("%#v", tr.Book.logs[0]) 463 464 expect := &Log{ 465 Ops: []Op{ 466 {Type: OpTypeInit, Model: 0x0002, AuthorID: "buthor", Name: "bazinga"}, 467 }, 468 } 469 470 if diff := cmp.Diff(expect, got, allowUnexported); diff != "" { 471 t.Errorf("result mismatch (-want +got):\n%s", diff) 472 } 473 } 474 475 func TestRemoveLog(t *testing.T) { 476 tr, cleanup := newTestRunner(t) 477 defer cleanup() 478 479 tr.AddAuthorLogTree(t) 480 ctx := tr.Ctx 481 482 if err := tr.Journal.RemoveLog(ctx); err == nil { 483 t.Errorf("expected no name remove to error") 484 } 485 486 if err := tr.Journal.RemoveLog(ctx, "root", "b", "bazinga"); err != nil { 487 t.Error(err) 488 } 489 490 if log, err := tr.Journal.HeadRef(ctx, "root", "b", "bazinga"); err != ErrNotFound { 491 t.Errorf("expected RemoveLog to remove log at path root/b/bazinga. got: %v. log: %v", err, log) 492 } 493 494 if err := tr.Journal.RemoveLog(ctx, "root", "b"); err != nil { 495 t.Error(err) 496 } 497 498 if _, err := tr.Journal.HeadRef(ctx, "root", "b"); err != ErrNotFound { 499 t.Error("expected RemoveLog to remove log at path root/b") 500 } 501 502 if err := tr.Journal.RemoveLog(ctx, "root"); err != nil { 503 t.Error(err) 504 } 505 506 if _, err := tr.Journal.HeadRef(ctx, "root"); err != ErrNotFound { 507 t.Error("expected RemoveLog to remove log at path root") 508 } 509 510 if err := tr.Journal.RemoveLog(ctx, "nonexistent"); err != ErrNotFound { 511 t.Error("expected RemoveLog for nonexistent path to error") 512 } 513 } 514 515 func TestLogID(t *testing.T) { 516 l := &Log{} 517 got := l.ID() 518 if "" != got { 519 t.Errorf("expected op hash of empty log to give the empty string, got: %s", got) 520 } 521 522 l = &Log{ 523 Ops: []Op{Op{Name: "hello"}}, 524 } 525 got = l.ID() 526 expect := "z7ghdteiybt7mopm5ysntbdr6ewiq5cfjlfev2v3ekbfbay6bp5q" 527 if expect != got { 528 t.Errorf("result mismatch, expect: %s, got: %s", expect, got) 529 } 530 531 // changing a feature like a timestamp should affect output hash 532 l = &Log{ 533 Ops: []Op{Op{Name: "hello", Timestamp: 2}}, 534 } 535 got = l.ID() 536 expect = "7ixp5z4h2dzjyljkjn7sbnsu6vg22gpgozmcl7wpg33pl5qfs3ra" 537 if expect != got { 538 t.Errorf("result mismatch, expect: %s, got: %s", expect, got) 539 } 540 } 541 542 type testRunner struct { 543 Ctx context.Context 544 Username string 545 PrivKey crypto.PrivKey 546 Journal *Journal 547 gen *opGenerator 548 } 549 550 type testFailer interface { 551 Fatal(args ...interface{}) 552 Fatalf(format string, args ...interface{}) 553 } 554 555 func newTestRunner(t testFailer) (tr testRunner, cleanup func()) { 556 ctx := context.Background() 557 authorName := "test_author" 558 pk := testPrivKey(t) 559 560 tr = testRunner{ 561 Ctx: ctx, 562 Username: authorName, 563 PrivKey: pk, 564 Journal: &Journal{}, 565 gen: &opGenerator{ctx: ctx, NoopProb: 60}, 566 } 567 cleanup = func() { 568 // noop 569 } 570 571 return tr, cleanup 572 } 573 574 func (tr testRunner) RandomLog(init Op, opCount int) *Log { 575 lg := InitLog(init) 576 for i := 0; i < opCount; i++ { 577 lg.Append(tr.gen.Gen()) 578 } 579 return lg 580 } 581 582 func testPrivKey(t testFailer) crypto.PrivKey { 583 // logbooks are encrypted at rest, we need a private key to interact with 584 // them, including to create a new logbook. This is a dummy Private Key 585 // you should never, ever use in real life. demo only folks. 586 testPk := `CAASpgkwggSiAgEAAoIBAQC/7Q7fILQ8hc9g07a4HAiDKE4FahzL2eO8OlB1K99Ad4L1zc2dCg+gDVuGwdbOC29IngMA7O3UXijycckOSChgFyW3PafXoBF8Zg9MRBDIBo0lXRhW4TrVytm4Etzp4pQMyTeRYyWR8e2hGXeHArXM1R/A/SjzZUbjJYHhgvEE4OZy7WpcYcW6K3qqBGOU5GDMPuCcJWac2NgXzw6JeNsZuTimfVCJHupqG/dLPMnBOypR22dO7yJIaQ3d0PFLxiDG84X9YupF914RzJlopfdcuipI+6gFAgBw3vi6gbECEzcohjKf/4nqBOEvCDD6SXfl5F/MxoHurbGBYB2CJp+FAgMBAAECggEAaVOxe6Y5A5XzrxHBDtzjlwcBels3nm/fWScvjH4dMQXlavwcwPgKhy2NczDhr4X69oEw6Msd4hQiqJrlWd8juUg6vIsrl1wS/JAOCS65fuyJfV3Pw64rWbTPMwO3FOvxj+rFghZFQgjg/i45uHA2UUkM+h504M5Nzs6Arr/rgV7uPGR5e5OBw3lfiS9ZaA7QZiOq7sMy1L0qD49YO1ojqWu3b7UaMaBQx1Dty7b5IVOSYG+Y3U/dLjhTj4Hg1VtCHWRm3nMOE9cVpMJRhRzKhkq6gnZmni8obz2BBDF02X34oQLcHC/Wn8F3E8RiBjZDI66g+iZeCCUXvYz0vxWAQQKBgQDEJu6flyHPvyBPAC4EOxZAw0zh6SF/r8VgjbKO3n/8d+kZJeVmYnbsLodIEEyXQnr35o2CLqhCvR2kstsRSfRz79nMIt6aPWuwYkXNHQGE8rnCxxyJmxV4S63GczLk7SIn4KmqPlCI08AU0TXJS3zwh7O6e6kBljjPt1mnMgvr3QKBgQD6fAkdI0FRZSXwzygx4uSg47Co6X6ESZ9FDf6ph63lvSK5/eue/ugX6p/olMYq5CHXbLpgM4EJYdRfrH6pwqtBwUJhlh1xI6C48nonnw+oh8YPlFCDLxNG4tq6JVo071qH6CFXCIank3ThZeW5a3ZSe5pBZ8h4bUZ9H8pJL4C7yQKBgFb8SN/+/qCJSoOeOcnohhLMSSD56MAeK7KIxAF1jF5isr1TP+rqiYBtldKQX9bIRY3/8QslM7r88NNj+aAuIrjzSausXvkZedMrkXbHgS/7EAPflrkzTA8fyH10AsLgoj/68mKr5bz34nuY13hgAJUOKNbvFeC9RI5g6eIqYH0FAoGAVqFTXZp12rrK1nAvDKHWRLa6wJCQyxvTU8S1UNi2EgDJ492oAgNTLgJdb8kUiH0CH0lhZCgr9py5IKW94OSM6l72oF2UrS6PRafHC7D9b2IV5Al9lwFO/3MyBrMocapeeyaTcVBnkclz4Qim3OwHrhtFjF1ifhP9DwVRpuIg+dECgYANwlHxLe//tr6BM31PUUrOxP5Y/cj+ydxqM/z6papZFkK6Mvi/vMQQNQkh95GH9zqyC5Z/yLxur4ry1eNYty/9FnuZRAkEmlUSZ/DobhU0Pmj8Hep6JsTuMutref6vCk2n02jc9qYmJuD7iXkdXDSawbEG6f5C4MUkJ38z1t1OjA==` 587 pk, err := key.DecodeB64PrivKey(testPk) 588 if err != nil { 589 t.Fatal(err) 590 } 591 return pk 592 } 593 594 func comparePrivKeys(a, b crypto.PrivKey) bool { 595 if a == nil && b != nil || a != nil && b == nil { 596 return false 597 } 598 599 abytes, err := a.Bytes() 600 if err != nil { 601 return false 602 } 603 604 bbytes, err := b.Bytes() 605 if err != nil { 606 return false 607 } 608 609 return string(abytes) == string(bbytes) 610 } 611 612 func (tr *testRunner) AddAuthorLogTree(t testFailer) *Log { 613 tree := &Log{ 614 Ops: []Op{ 615 Op{ 616 Type: OpTypeInit, 617 Model: 0x1, 618 AuthorID: "author", 619 Name: "root", 620 }, 621 Op{ 622 Type: OpTypeInit, 623 Model: 0x11, 624 }, 625 }, 626 Logs: []*Log{ 627 { 628 Ops: []Op{ 629 Op{ 630 Type: OpTypeInit, 631 Model: 0x2, 632 AuthorID: "author", 633 Name: "a", 634 }, 635 Op{ 636 Type: OpTypeInit, 637 Model: 0x456, 638 }, 639 }, 640 }, 641 { 642 Ops: []Op{ 643 Op{ 644 Type: OpTypeInit, 645 Model: 0x2, 646 AuthorID: "buthor", 647 Name: "b", 648 }, 649 }, 650 Logs: []*Log{ 651 { 652 Ops: []Op{ 653 {Type: OpTypeInit, Model: 0x0002, AuthorID: "buthor", Name: "bazinga"}, 654 }, 655 }, 656 }, 657 }, 658 }, 659 } 660 661 if err := tr.Journal.MergeLog(tr.Ctx, tree); err != nil { 662 t.Fatal(err) 663 } 664 665 return tree 666 }