github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/remote/mock/client.go (about)

     1  package mock
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/qri-io/dataset"
     8  	"github.com/qri-io/qfs"
     9  	testkeys "github.com/qri-io/qri/auth/key/test"
    10  	"github.com/qri-io/qri/base/dsfs"
    11  	"github.com/qri-io/qri/dsref"
    12  	"github.com/qri-io/qri/event"
    13  	"github.com/qri-io/qri/logbook"
    14  	"github.com/qri-io/qri/logbook/oplog"
    15  	"github.com/qri-io/qri/p2p"
    16  	"github.com/qri-io/qri/profile"
    17  	"github.com/qri-io/qri/remote"
    18  	"github.com/qri-io/qri/repo"
    19  	repotest "github.com/qri-io/qri/repo/test"
    20  )
    21  
    22  // ErrNotImplemented is returned for methods that are not implemented
    23  var ErrNotImplemented = fmt.Errorf("not implemented")
    24  
    25  // OtherPeer represents another peer which the Client connects to
    26  type OtherPeer struct {
    27  	keyData  *testkeys.KeyData
    28  	repoRoot *repotest.TempRepo
    29  	book     *logbook.Book
    30  	resolver map[string]string
    31  	dscache  map[string]string
    32  }
    33  
    34  // Client is a remote client suitable for tests
    35  type Client struct {
    36  	node *p2p.QriNode
    37  	pub  event.Publisher
    38  
    39  	otherPeers map[string]*OtherPeer
    40  
    41  	doneCh   chan struct{}
    42  	doneErr  error
    43  	shutdown context.CancelFunc
    44  }
    45  
    46  var _ remote.Client = (*Client)(nil)
    47  
    48  // NewClient returns a mock remote client. context passed to NewClient
    49  // MUST use the `Shutdown` method or cancel externally for proper cleanup
    50  func NewClient(ctx context.Context, node *p2p.QriNode, pub event.Publisher) (c remote.Client, err error) {
    51  	ctx, cancel := context.WithCancel(ctx)
    52  
    53  	cli := &Client{
    54  		pub:        pub,
    55  		node:       node,
    56  		otherPeers: map[string]*OtherPeer{},
    57  		doneCh:     make(chan struct{}),
    58  		shutdown:   cancel,
    59  	}
    60  
    61  	go func() {
    62  		<-ctx.Done()
    63  		// TODO (b5) - return an error here if client is in the process of pulling anything
    64  		cli.doneErr = ctx.Err()
    65  		close(cli.doneCh)
    66  	}()
    67  
    68  	return cli, nil
    69  }
    70  
    71  // Feeds is not implemented
    72  func (c *Client) Feeds(ctx context.Context, remoteAddr string) (map[string][]dsref.VersionInfo, error) {
    73  	return nil, ErrNotImplemented
    74  }
    75  
    76  // Feed is not implemented
    77  func (c *Client) Feed(ctx context.Context, remoteAddr, feedName string, page, pageSize int) ([]dsref.VersionInfo, error) {
    78  	return nil, ErrNotImplemented
    79  }
    80  
    81  // PreviewDatasetVersion is not implemented
    82  func (c *Client) PreviewDatasetVersion(ctx context.Context, ref dsref.Ref, remoteAddr string) (*dataset.Dataset, error) {
    83  	return nil, ErrNotImplemented
    84  }
    85  
    86  // FetchLogs is not implemented
    87  func (c *Client) FetchLogs(ctx context.Context, ref dsref.Ref, remoteAddr string) (*oplog.Log, error) {
    88  	return nil, ErrNotImplemented
    89  }
    90  
    91  // NewRemoteRefResolver mocks a ref resolver off a foreign logbook
    92  func (c *Client) NewRemoteRefResolver(addr string) dsref.Resolver {
    93  	// TODO(b5) - switch based on address input? it would make for a better mock
    94  	return &writeOnResolver{c: c}
    95  }
    96  
    97  // writeOnResolver creates dataset histories on the fly when
    98  // ResolveRef is called, storing them for future
    99  type writeOnResolver struct {
   100  	c *Client
   101  }
   102  
   103  func (r *writeOnResolver) ResolveRef(ctx context.Context, ref *dsref.Ref) (string, error) {
   104  	return "", r.c.createTheirDataset(ctx, ref)
   105  }
   106  
   107  // PushDataset is not implemented
   108  func (c *Client) PushDataset(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   109  	return ErrNotImplemented
   110  }
   111  
   112  // RemoveDataset is not implemented
   113  func (c *Client) RemoveDataset(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   114  	return ErrNotImplemented
   115  }
   116  
   117  // RemoveDatasetVersion is not implemented
   118  func (c *Client) RemoveDatasetVersion(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   119  	return ErrNotImplemented
   120  }
   121  
   122  // PullDataset adds a reference to a dataset using test peer info
   123  func (c *Client) PullDataset(ctx context.Context, ref *dsref.Ref, remoteAddr string) (*dataset.Dataset, error) {
   124  	// Create the dataset on the foreign side.
   125  	if err := c.createTheirDataset(ctx, ref); err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	// Get the logs for that dataset, merge them into our own.
   130  	if err := c.pullLogs(ctx, *ref, remoteAddr); err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	// Put the dataset into our repo as well
   135  	vi, err := c.mockDagSync(ctx, *ref)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	ds, err := dsfs.LoadDataset(ctx, c.node.Repo.Filesystem(), vi.Path)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	info := dsref.ConvertDatasetToVersionInfo(ds)
   146  	info.InitID = ref.InitID
   147  	info.Username = ref.Username
   148  	info.Name = ref.Name
   149  	info.ProfileID = ref.ProfileID
   150  	if err := c.pub.Publish(ctx, event.ETDatasetPulled, info); err != nil {
   151  		return nil, err
   152  	}
   153  	return ds, err
   154  }
   155  
   156  func (c *Client) createTheirDataset(ctx context.Context, ref *dsref.Ref) error {
   157  	other := c.otherPeer(ref.Username)
   158  
   159  	// Check if the dataset already exists
   160  	if initID, exists := other.resolver[ref.Human()]; exists {
   161  		if dsPath, ok := other.dscache[initID]; ok {
   162  			ref.InitID = initID
   163  			ref.ProfileID = other.keyData.EncodedPeerID
   164  			ref.Path = dsPath
   165  			return nil
   166  		}
   167  	}
   168  
   169  	// TODO(dlong): HACK: This mockClient adds dataset to the *local* IPFS repo, instead
   170  	// of the *foreign* IPFS repo. This is because there's no easy way to copy blocks
   171  	// from one repo to another in tests. For now, this behavior works okay for our
   172  	// existing tests, but will break if we need a test the expects different blocks to
   173  	// exist on our repo versus theirs. The pull command is still doing useful work,
   174  	// since the mockClient is producing different logbook and refstore info on each peer.
   175  	// To fix this, create the dataset in the other.repoRoot.Repo.store instead, and
   176  	// then down in mockDagSync copy the IPFS blocks from that store to the local store.
   177  	fs := c.node.Repo.Filesystem()
   178  
   179  	// Construct a simple dataset
   180  	ds := dataset.Dataset{
   181  		Commit: &dataset.Commit{},
   182  		Structure: &dataset.Structure{
   183  			Format: "json",
   184  			Schema: dataset.BaseSchemaObject,
   185  		},
   186  		BodyBytes: []byte("{}"),
   187  	}
   188  	err := ds.OpenBodyFile(ctx, fs)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	// Allocate an initID for this dataset
   194  	ref.InitID, err = other.book.WriteDatasetInit(ctx, other.book.Owner(), ref.Name)
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	// Store with dsfs
   200  	sw := dsfs.SaveSwitches{}
   201  	path, err := dsfs.CreateDataset(ctx, fs, fs.DefaultWriteFS(), event.NilBus, &ds, nil, other.keyData.PrivKey, sw)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	// Save the IPFS path with our fake refstore
   207  	other.resolver[ref.Human()] = ref.InitID
   208  	other.dscache[ref.InitID] = path
   209  	ref.ProfileID = other.keyData.EncodedPeerID
   210  	ref.Path = path
   211  
   212  	// Add a save operation to logbook
   213  	ds.ID = ref.InitID
   214  	err = other.book.WriteVersionSave(ctx, other.book.Owner(), &ds, nil)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  func (c *Client) otherPeer(username string) *OtherPeer {
   223  	other, ok := c.otherPeers[username]
   224  	if !ok {
   225  		// Get test peer info, skipping 0th peer because many tests already use that one
   226  		i := len(c.otherPeers) + 1
   227  		kd := testkeys.GetKeyData(i)
   228  		// Construct a tempRepo to hold IPFS data (not used, see HACK note above).
   229  		tempRepo, err := repotest.NewTempRepoFixedProfileID(username, "")
   230  		if err != nil {
   231  			panic(err)
   232  		}
   233  		// Construct logbook
   234  		fs, err := qfs.NewMemFilesystem(context.Background(), nil)
   235  		if err != nil {
   236  			panic(err)
   237  		}
   238  		pro, err := profile.NewSparsePKProfile(username, kd.PrivKey)
   239  		if err != nil {
   240  			panic(err)
   241  		}
   242  		book, err := logbook.NewJournal(*pro, event.NilBus, fs, "logbook.qfb")
   243  		if err != nil {
   244  			panic(err)
   245  		}
   246  		// Other peer represents a peer with the given username
   247  		other = &OtherPeer{
   248  			resolver: map[string]string{},
   249  			dscache:  map[string]string{},
   250  			keyData:  kd,
   251  			repoRoot: &tempRepo,
   252  			book:     book,
   253  		}
   254  		c.otherPeers[username] = other
   255  	}
   256  	return other
   257  }
   258  
   259  // pullLogs creates a log from a temp logbook, and merges those into the
   260  // client's logbook
   261  func (c *Client) pullLogs(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   262  	// Get other peer to retrieve its logbook
   263  	other := c.otherPeer(ref.Username)
   264  	theirBook := other.book
   265  	theirLog, err := theirBook.UserDatasetBranchesLog(ctx, ref.InitID)
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	// Merge their logbook into ours
   271  	if err = theirLog.Sign(theirBook.Owner().PrivKey); err != nil {
   272  		return err
   273  	}
   274  	return c.node.Repo.Logbook().MergeLog(ctx, theirBook.Owner().PrivKey.GetPublic(), theirLog)
   275  }
   276  
   277  // mockDagSync immitates a dagsync, pulling a dataset from a peer, and saving it with our refs
   278  func (c *Client) mockDagSync(ctx context.Context, ref dsref.Ref) (*dsref.VersionInfo, error) {
   279  	other := c.otherPeer(ref.Username)
   280  
   281  	// Resolve the ref using the other peer's information
   282  	initID := other.resolver[ref.Human()]
   283  	dsPath := other.dscache[initID]
   284  
   285  	// TODO(dustmop): HACK: Because we created the dataset to our own IPFS repo, there's no
   286  	// need to copy the blocks. We should instead have added them to other.repoRoot, and here
   287  	// copy the blocks to our own repo.
   288  
   289  	// Add to our repository
   290  	vi := dsref.VersionInfo{
   291  		InitID:    initID,
   292  		Path:      dsPath,
   293  		ProfileID: ref.ProfileID,
   294  		Username:  ref.Username,
   295  		Name:      ref.Name,
   296  	}
   297  	if err := repo.PutVersionInfoShim(ctx, c.node.Repo, &vi); err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	return &vi, nil
   302  }
   303  
   304  // PushLogs is not implemented
   305  func (c *Client) PushLogs(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   306  	return ErrNotImplemented
   307  }
   308  
   309  // PullDatasetVersion is not implemented
   310  func (c *Client) PullDatasetVersion(ctx context.Context, ref *dsref.Ref, remoteAddr string) error {
   311  	return ErrNotImplemented
   312  }
   313  
   314  // RemoveLogs is not implemented
   315  func (c *Client) RemoveLogs(ctx context.Context, ref dsref.Ref, remoteAddr string) error {
   316  	return ErrNotImplemented
   317  }
   318  
   319  // Done returns a channel that the client will send on when finished closing
   320  func (c *Client) Done() <-chan struct{} {
   321  	return c.doneCh
   322  }
   323  
   324  // DoneErr gives an error that occurred during the shutdown process
   325  func (c *Client) DoneErr() error {
   326  	return c.doneErr
   327  }
   328  
   329  // Shutdown allows you to close the client before the parent context closes
   330  func (c *Client) Shutdown() <-chan struct{} {
   331  	c.shutdown()
   332  	return c.Done()
   333  }