github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/lib_test.go (about)

     1  package lib
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"runtime"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	crypto "github.com/libp2p/go-libp2p-core/crypto"
    17  	"github.com/qri-io/dataset"
    18  	"github.com/qri-io/dataset/dstest"
    19  	"github.com/qri-io/qfs"
    20  	"github.com/qri-io/qri/auth/key"
    21  	"github.com/qri-io/qri/base"
    22  	"github.com/qri-io/qri/base/params"
    23  	"github.com/qri-io/qri/collection"
    24  	"github.com/qri-io/qri/config"
    25  	testcfg "github.com/qri-io/qri/config/test"
    26  	"github.com/qri-io/qri/dsref"
    27  	"github.com/qri-io/qri/event"
    28  	"github.com/qri-io/qri/logbook"
    29  	"github.com/qri-io/qri/p2p"
    30  	"github.com/qri-io/qri/profile"
    31  	profiletest "github.com/qri-io/qri/profile/test"
    32  	"github.com/qri-io/qri/remote"
    33  	"github.com/qri-io/qri/remote/access"
    34  	"github.com/qri-io/qri/repo"
    35  	repotest "github.com/qri-io/qri/repo/test"
    36  )
    37  
    38  // base64-encoded Test Private Key, decoded in init
    39  // peerId: QmZePf5LeXow3RW5U1AgEiNbW46YnRGhZ7HPvm1UmPFPwt
    40  var (
    41  	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==`
    42  	privKey crypto.PrivKey
    43  
    44  	testPeerProfile = &profile.Profile{
    45  		Peername: "peer",
    46  		ID:       profile.IDB58DecodeOrEmpty("QmZePf5LeXow3RW5U1AgEiNbW46YnRGhZ7HPvm1UmPFPwt"),
    47  	}
    48  )
    49  
    50  func init() {
    51  	var err error
    52  	testPeerProfile.PrivKey, err = key.DecodeB64PrivKey(testPk)
    53  	if err != nil {
    54  		panic(fmt.Errorf("error unmarshaling private key: %s", err.Error()))
    55  	}
    56  }
    57  
    58  func TestNewInstance(t *testing.T) {
    59  	if _, err := NewInstance(context.Background(), ""); err == nil {
    60  		t.Error("expected NewInstance to error when provided no repo path")
    61  	}
    62  
    63  	tr, err := repotest.NewTempRepo("foo", "new_instance_test", repotest.NewTestCrypto())
    64  	if err != nil {
    65  		t.Fatal(err)
    66  	}
    67  	defer tr.Delete()
    68  
    69  	cfg := testcfg.DefaultConfigForTesting()
    70  	cfg.Filesystems = []qfs.Config{
    71  		{Type: "mem"},
    72  		{Type: "local"},
    73  	}
    74  	cfg.Repo.Type = "mem"
    75  
    76  	var firedEventWg sync.WaitGroup
    77  	firedEventWg.Add(1)
    78  	handler := func(_ context.Context, e event.Event) error {
    79  		if e.Type == event.ETInstanceConstructed {
    80  			firedEventWg.Done()
    81  		}
    82  		return nil
    83  	}
    84  
    85  	ctx, cancel := context.WithCancel(context.Background())
    86  	defer cancel()
    87  
    88  	got, err := NewInstance(ctx, tr.QriPath, OptConfig(cfg), OptEventHandler(handler, event.ETInstanceConstructed))
    89  	if err != nil {
    90  		t.Fatal(err)
    91  	}
    92  
    93  	expect := &Instance{
    94  		cfg: cfg,
    95  	}
    96  
    97  	if err = CompareInstances(got, expect); err != nil {
    98  		t.Error(err)
    99  	}
   100  
   101  	firedEventWg.Wait()
   102  
   103  	finished := make(chan struct{})
   104  	go func() {
   105  		select {
   106  		case <-time.NewTimer(time.Millisecond * 100).C:
   107  			t.Errorf("done didn't fire within 100ms of canceling instance context")
   108  		case <-got.Shutdown():
   109  		}
   110  		finished <- struct{}{}
   111  	}()
   112  	cancel()
   113  	<-finished
   114  }
   115  
   116  func TestNewDefaultInstance(t *testing.T) {
   117  	ctx, cancel := context.WithCancel(context.Background())
   118  	defer cancel()
   119  
   120  	tempDir, err := ioutil.TempDir("", "TestNewDefaultInstance")
   121  	if err != nil {
   122  		t.Fatal(err)
   123  	}
   124  	defer os.RemoveAll(tempDir)
   125  
   126  	qriPath := filepath.Join(tempDir, "qri")
   127  	ipfsPath := filepath.Join(qriPath, "ipfs")
   128  	t.Logf(tempDir)
   129  
   130  	if err = repotest.InitIPFSRepo(ipfsPath, ""); err != nil {
   131  		t.Fatal(err)
   132  	}
   133  
   134  	cfg := testcfg.DefaultConfigForTesting()
   135  	cfg.Filesystems = []qfs.Config{
   136  		{Type: "ipfs", Config: map[string]interface{}{"path": ipfsPath}},
   137  		{Type: "local"},
   138  		{Type: "http"},
   139  	}
   140  	cfg.WriteToFile(filepath.Join(qriPath, "config.yaml"))
   141  
   142  	_, err = NewInstance(ctx, qriPath)
   143  	if err != nil {
   144  		t.Fatal(err)
   145  	}
   146  	if _, err = os.Stat(filepath.Join(qriPath, "stats")); os.IsNotExist(err) {
   147  		t.Errorf("NewInstance error: stats cache never created")
   148  	}
   149  }
   150  
   151  func CompareInstances(a, b *Instance) error {
   152  	if !reflect.DeepEqual(a.cfg, b.cfg) {
   153  		return fmt.Errorf("config mismatch")
   154  	}
   155  
   156  	// TODO (b5): compare all instance fields
   157  	return nil
   158  }
   159  
   160  // pulled from base packages
   161  // TODO - we should probably get a test package going at github.com/qri-io/qri/test
   162  func addCitiesDataset(t *testing.T, node *p2p.QriNode) dsref.Ref {
   163  	t.Helper()
   164  	ctx := context.Background()
   165  	tc, err := dstest.NewTestCaseFromDir(repotest.TestdataPath("cities"))
   166  	if err != nil {
   167  		t.Fatal(err.Error())
   168  	}
   169  	ds := tc.Input
   170  	ds.Name = tc.Name
   171  	ds.BodyBytes = tc.Body
   172  
   173  	ref, err := saveDataset(ctx, node.Repo, ds, base.SaveSwitches{Pin: true})
   174  	if err != nil {
   175  		t.Fatal(err.Error())
   176  	}
   177  	return ref
   178  }
   179  
   180  func saveDataset(ctx context.Context, r repo.Repo, ds *dataset.Dataset, sw base.SaveSwitches) (dsref.Ref, error) {
   181  	author := r.Profiles().Owner(ctx)
   182  	ref, _, err := base.PrepareSaveRef(ctx, author, r.Logbook(), r.Logbook(), fmt.Sprintf("%s/%s", author.Peername, ds.Name), "", false)
   183  	if err != nil {
   184  		return dsref.Ref{}, err
   185  	}
   186  	res, err := base.SaveDataset(ctx, r, r.Filesystem().DefaultWriteFS(), author, ref.InitID, ref.Path, ds, nil, sw)
   187  	if err != nil {
   188  		return dsref.Ref{}, err
   189  	}
   190  	return dsref.ConvertDatasetToVersionInfo(res).SimpleRef(), nil
   191  }
   192  
   193  func TestInstanceEventSubscription(t *testing.T) {
   194  	timeoutMs := time.Millisecond * 250
   195  	if runtime.GOOS == "windows" {
   196  		// TODO(dustmop): Why is windows slow? Perhaps its due to the IPFS lock.
   197  		timeoutMs = time.Millisecond * 2500
   198  	}
   199  	// TODO (b5) - can't use testrunner for this test because event busses aren't
   200  	// wired up correctly in the test runner constructor. The proper fix is to have
   201  	// testrunner build it's instance using NewInstance
   202  	ctx, done := context.WithTimeout(context.Background(), timeoutMs)
   203  	defer done()
   204  
   205  	tmpDir, err := ioutil.TempDir("", "event_sub_test")
   206  	if err != nil {
   207  		t.Fatal(err)
   208  	}
   209  	defer os.RemoveAll(tmpDir)
   210  
   211  	cfg := testcfg.DefaultConfigForTesting()
   212  	cfg.Repo.Type = "mem"
   213  	// remove default ipfs fs, not needed for this test
   214  	cfg.Filesystems = []qfs.Config{{Type: "mem"}}
   215  
   216  	eventCh := make(chan event.Type, 1)
   217  	expect := event.ETP2PGoneOnline
   218  	eventHandler := func(_ context.Context, e event.Event) error {
   219  		eventCh <- e.Type
   220  		return nil
   221  	}
   222  
   223  	inst, err := NewInstance(ctx, tmpDir,
   224  		OptConfig(cfg),
   225  		// remove logbook, not needed for this test
   226  		OptLogbook(&logbook.Book{}),
   227  		OptEventHandler(eventHandler, expect),
   228  	)
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  
   233  	go inst.Node().GoOnline(ctx)
   234  	select {
   235  	case evt := <-eventCh:
   236  		if evt != expect {
   237  			t.Errorf("received event mismatch. expected: %q, got: %q", expect, evt)
   238  		}
   239  	case <-ctx.Done():
   240  		t.Error("expected event before context deadline, got none")
   241  	}
   242  }
   243  
   244  func TestNewInstanceWithAccessControlPolicy(t *testing.T) {
   245  	ctx, cancel := context.WithCancel(context.Background())
   246  	defer cancel()
   247  
   248  	tempDir, err := ioutil.TempDir("", "TestNewInstanceWithAccessControlPolicy")
   249  	if err != nil {
   250  		t.Fatal(err)
   251  	}
   252  	defer os.RemoveAll(tempDir)
   253  
   254  	qriPath := filepath.Join(tempDir, "qri")
   255  	ipfsPath := filepath.Join(qriPath, "ipfs")
   256  	t.Logf(tempDir)
   257  
   258  	if err = repotest.InitIPFSRepo(ipfsPath, ""); err != nil {
   259  		t.Fatal(err)
   260  	}
   261  
   262  	cfg := testcfg.DefaultConfigForTesting()
   263  	cfg.Filesystems = []qfs.Config{
   264  		{Type: "ipfs", Config: map[string]interface{}{"path": ipfsPath}},
   265  		{Type: "local"},
   266  		{Type: "http"},
   267  	}
   268  
   269  	cfg.RemoteServer = &config.RemoteServer{
   270  		Enabled: true,
   271  	}
   272  
   273  	proID := profile.ID("foo")
   274  	acpPath := filepath.Join(qriPath, access.DefaultAccessControlPolicyFilename)
   275  	acp := []byte(`
   276  	[
   277  		{
   278  			"title": "pull foo:bar dataset",
   279  			"effect": "allow",
   280  			"subject": "` + proID.Encode() + `",
   281  			"resources": [
   282  				"dataset:foo:bar"
   283  			],
   284  			"actions": [
   285  				"remote:pull"
   286  			]
   287  		}
   288  	]
   289  `)
   290  
   291  	if err := ioutil.WriteFile(acpPath, acp, 0644); err != nil {
   292  		t.Fatalf("error writing access control policy: %s", err)
   293  	}
   294  
   295  	got, err := NewInstance(
   296  		ctx,
   297  		qriPath,
   298  		OptConfig(cfg),
   299  		OptRemoteServerOptions(
   300  			[]remote.OptionsFunc{
   301  				remote.OptLoadPolicyFileIfExists(acpPath),
   302  			}),
   303  	)
   304  	if err != nil {
   305  		t.Fatal(err)
   306  	}
   307  
   308  	p := got.remoteServer.Policy()
   309  	if p == nil {
   310  		t.Fatal("expected remote policy to exist, got nil")
   311  	}
   312  
   313  	if err != nil {
   314  		t.Fatal("error creating profileID:", err)
   315  	}
   316  	if err := p.Enforce(&profile.Profile{ID: proID, Peername: "foo"}, "dataset:foo:bar", "remote:pull"); err != nil {
   317  		t.Errorf("expected no policy enforce error, got: %s", err)
   318  	}
   319  }
   320  
   321  func TestNewInstanceWithCollectionOption(t *testing.T) {
   322  	ctx, cancel := context.WithCancel(context.Background())
   323  	defer cancel()
   324  
   325  	tr, err := repotest.NewTempRepo("test_collections", "TestNewInstanceWithCollectionOption", repotest.NewTestCrypto())
   326  	if err != nil {
   327  		t.Fatal(err)
   328  	}
   329  	defer tr.Delete()
   330  
   331  	s, err := collection.NewLocalSet(ctx, tr.RootPath)
   332  	if err != nil {
   333  		t.Fatal(err)
   334  	}
   335  
   336  	kermit := profiletest.GetProfile("kermit")
   337  
   338  	addInfo := dsref.VersionInfo{
   339  		InitID:    "init_id",
   340  		Username:  kermit.Peername,
   341  		Name:      "dataset",
   342  		ProfileID: kermit.ID.Encode(),
   343  	}
   344  
   345  	err = s.Add(ctx, kermit.ID, addInfo)
   346  	if err != nil {
   347  		t.Fatal(err)
   348  	}
   349  
   350  	inst, err := NewInstance(ctx, tr.QriPath,
   351  		OptCollectionSet(s),
   352  	)
   353  	if err != nil {
   354  		t.Fatal(err)
   355  	}
   356  
   357  	got, _, err := inst.Collection().List(ctx, &CollectionListParams{List: params.List{Limit: -1}})
   358  	if err != nil {
   359  		t.Error(err)
   360  	}
   361  	expect := []dsref.VersionInfo{addInfo}
   362  
   363  	if diff := cmp.Diff(expect, got); diff != "" {
   364  		t.Errorf("result mismatch (-want +got):\n%s", diff)
   365  	}
   366  }
   367  
   368  // NewMemTestInstance creates an in-memory instance
   369  // TODO(b5): currently "NewInstance" hard-requires a repo-path, even if we can
   370  // provide a configuration that specifies entirely in-memory stores. We should
   371  // make it possible to create fully in-memory Instances using NewInstance,
   372  // but for now I'm working around it with a temp directory & cleanup function
   373  func NewMemTestInstance(ctx context.Context, t *testing.T) (inst *Instance, cleanup func()) {
   374  	t.Helper()
   375  	tmpPath, err := ioutil.TempDir("", "qri_test_mem_instance")
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  
   380  	cfg := testcfg.DefaultConfigForTesting()
   381  	cfg.Filesystems = []qfs.Config{
   382  		{Type: "mem"},
   383  		{Type: "local"},
   384  	}
   385  	cfg.Repo.Type = "mem"
   386  	if err := cfg.WriteToFile(filepath.Join(tmpPath, "config.yaml")); err != nil {
   387  		t.Fatal(err)
   388  	}
   389  
   390  	// TODO(b5): I'd like to be able to do this:
   391  	// if inst, err = NewInstance(ctx, "", OptConfig(cfg)); err != nil {
   392  	if inst, err = NewInstance(ctx, tmpPath); err != nil {
   393  		t.Fatal(err)
   394  	}
   395  
   396  	return inst, func() { os.RemoveAll(tmpPath) }
   397  }