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  }