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 }