package chainsource import ( "context" "net" "testing" "git.smesh.lol/nostrino/pkg/chain" "git.smesh.lol/nostrino/pkg/chaindata" "git.smesh.lol/nostrino/pkg/neutrino" ) // The adapter must be the thing a chaindata domain serves, or the wiring is a comment // rather than a fact. var _ chaindata.Source = (*Nostrino)(nil) // storeOnly builds a client over a memory store, with a dialer that is never used. // // The tests here exercise what the adapter reads from the store and how it handles // cancellation. Connecting needs a peer, and a peer is what the end-to-end test has. func storeOnly(t *testing.T, store neutrino.Store) *Nostrino { t.Helper() profile := chain.Regtest client, err := neutrino.New(neutrino.Config{ Profile: &profile, Store: store, Dial: func(context.Context) (net.Conn, error) { return nil, errNoPeer }, }) if err != nil { t.Fatal(err) } adapter, err := New(client) if err != nil { t.Fatal(err) } return adapter } // errNoPeer is what the unused dialer returns. var errNoPeer = errorString("chainsource test: no peer") // open is a context that is not cancelled. type openContext struct{} // Done implements chaindata.Context. func (openContext) Done() <-chan struct{} { return nil } // Err implements chaindata.Context. func (openContext) Err() string { return "" } // cancelled is a context that is already cancelled. type cancelled struct{} // Done implements chaindata.Context. func (cancelled) Done() <-chan struct{} { closed := make(chan struct{}) close(closed) return closed } // Err implements chaindata.Context. func (cancelled) Err() string { return "cancelled by the test" } // TestPositionOfAnEmptyStore: a store with no verified chain is at zero, not at a made-up // height. func TestPositionOfAnEmptyStore(t *testing.T) { adapter := storeOnly(t, neutrino.NewMemory()) position, err := adapter.Position(openContext{}) if err != nil { t.Fatal(err) } if position.Tip != 0 || position.Filters != 0 || position.Running { t.Fatalf("position = %+v", position) } } // TestMissingFilterIsAnError: a gap in the batch is an error, because a wallet that silently // skipped a block would miss its own money. func TestMissingFilterIsAnError(t *testing.T) { adapter := storeOnly(t, neutrino.NewMemory()) if _, err := adapter.Filters(openContext{}, 5, 1); err == nil { t.Fatal("a missing filter was returned as an empty batch") } if _, err := adapter.Filters(openContext{}, 0, 0); err == nil { t.Fatal("an empty range was accepted") } if _, err := adapter.Filters(openContext{}, 0, chaindata.MaxFilters+1); err == nil { t.Fatal("an oversized range was accepted") } if _, err := adapter.Match(openContext{}, nil); err == nil { t.Fatal("a match with no scripts was accepted") } } // TestCancellationIsRefusedBeforeTheWork: a cancelled request does not start a sync or a // scan, which is the point of carrying a channel rather than a flag. func TestCancellationIsRefusedBeforeTheWork(t *testing.T) { adapter := storeOnly(t, neutrino.NewMemory()) if _, err := adapter.Sync(cancelled{}, 0); err == nil { t.Fatal("a cancelled sync began") } if _, err := adapter.Match(cancelled{}, [][]byte{{0x51}}); err == nil { t.Fatal("a cancelled match began") } if _, err := adapter.Filters(cancelled{}, 0, 1); err == nil { t.Fatal("a cancelled filter read began") } // Filter reads need no peer, so a cancelled one must be refused rather than served. if _, err := adapter.Filters(openContext{}, 0, 1); err == nil { t.Fatal("an empty store served a filter") } } // TestNoClientIsRefused: the adapter is built from a client, and a nil one is a wiring bug // this constructor names rather than a panic later. func TestNoClientIsRefused(t *testing.T) { if _, err := New(nil); err == nil { t.Fatal("a nil client was wrapped") } }