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 }