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  }