package ud import ( "fmt" "github.com/golang/protobuf/proto" "github.com/pkg/errors" "gitlab.com/elixxir/client/single" "gitlab.com/elixxir/client/stoppable" "gitlab.com/elixxir/client/storage" "gitlab.com/elixxir/comms/client" "gitlab.com/elixxir/crypto/contact" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/crypto/factID" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/crypto/large" "gitlab.com/xx_network/primitives/id" "math/rand" "reflect" "strings" "testing" "time" ) // Happy path. func TestManager_Search(t *testing.T) { // Set up manager isReg := uint32(1) comms, err := client.NewClientComms(nil, nil, nil, nil) if err != nil { t.Errorf("Failed to start client comms: %+v", err) } store := storage.InitTestingSession(t) m := &Manager{ comms: comms, storage: store, net: newTestNetworkManager(t), grp: store.E2e().GetGroup(), single: &mockSingleSearch{}, registered: &isReg, } // Generate callback function callbackChan := make(chan struct { c []contact.Contact err error }) callback := func(c []contact.Contact, err error) { callbackChan <- struct { c []contact.Contact err error }{c: c, err: err} } // Generate fact list var factList fact.FactList for i := 0; i < 10; i++ { factList = append(factList, fact.Fact{ Fact: fmt.Sprintf("fact %d", i), T: fact.FactType(rand.Intn(4)), }) } factHashes, _ := hashFactList(factList) var contacts []*Contact for i, hash := range factHashes { contacts = append(contacts, &Contact{ UserID: id.NewIdFromString("user", id.User, t).Marshal(), PubKey: []byte{byte(i + 1)}, TrigFacts: []*HashFact{hash}, }) } err = m.Search(factList, callback, 10*time.Millisecond) if err != nil { t.Errorf("Search() returned an error: %+v", err) } // Verify the callback is called select { case cb := <-callbackChan: if cb.err != nil { t.Errorf("Callback returned an error: %+v", cb.err) } c, err := m.getContact() if err != nil { t.Errorf("Failed to get UD contact: %+v", err) } expectedContacts := []contact.Contact{c} if !contact.Equal(expectedContacts[0], cb.c[0]) { t.Errorf("Failed to get expected Contacts."+ "\n\texpected: %+v\n\treceived: %+v", expectedContacts, cb.c) } case <-time.After(100 * time.Millisecond): t.Error("Callback not called.") } } // // // Error path: the callback returns an error. // func TestManager_Search_CallbackError(t *testing.T) { // isReg := uint32(1) // // Set up manager // m := &Manager{ // rng: fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG), // grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2)), // storage: storage.InitTestingSession(t), // udContact: contact.Contact{ID: &id.UDB}, // net: newTestNetworkManager(t), // registered: &isReg, // } // // // Generate callback function // callbackChan := make(chan struct { // c []contact.Contact // err error // }) // callback := func(c []contact.Contact, err error) { // callbackChan <- struct { // c []contact.Contact // err error // }{c: c, err: err} // } // // // Generate fact list // factList := fact.FactList{ // {Fact: "fact1", T: fact.Username}, // {Fact: "fact2", T: fact.Email}, // {Fact: "fact3", T: fact.Phone}, // } // // // Trigger lookup response chan // // go func() { // // time.Sleep(1 * time.Millisecond) // // m.inProgressSearch[0] <- &SearchResponse{ // // Contacts: nil, // // Error: "Error", // // } // // }() // // // Run the search // err := m.Search(factList, callback, 10*time.Millisecond) // if err != nil { // t.Errorf("Search() returned an error: %+v", err) // } // // // Verify the callback is called // select { // case cb := <-callbackChan: // if cb.err == nil { // t.Error("Callback did not return an expected error.") // } // // if cb.c != nil { // t.Errorf("Failed to get expected Contacts."+ // "\n\texpected: %v\n\treceived: %v", nil, cb.c) // } // case <-time.After(100 * time.Millisecond): // t.Error("Callback not called.") // } // // // if _, exists := m.inProgressSearch[m.commID-1]; exists { // // t.Error("Failed to delete SearchResponse from inProgressSearch.") // // } // } // // // Error path: the round event chan times out. // func TestManager_Search_EventChanTimeout(t *testing.T) { // isReg := uint32(1) // // Set up manager // m := &Manager{ // rng: fastRNG.NewStreamGenerator(12, 3, csprng.NewSystemRNG), // grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2)), // storage: storage.InitTestingSession(t), // udContact: contact.Contact{ID: &id.UDB}, // net: newTestNetworkManager(t), // registered: &isReg, // } // // // Generate callback function // callbackChan := make(chan struct { // c []contact.Contact // err error // }) // callback := func(c []contact.Contact, err error) { // callbackChan <- struct { // c []contact.Contact // err error // }{c: c, err: err} // } // // // Generate fact list // factList := fact.FactList{ // {Fact: "fact1", T: fact.Username}, // {Fact: "fact2", T: fact.Email}, // {Fact: "fact3", T: fact.Phone}, // } // // // Run the search // err := m.Search(factList, callback, 10*time.Millisecond) // if err != nil { // t.Errorf("Search() returned an error: %+v", err) // } // // // Verify the callback is called // select { // case cb := <-callbackChan: // if cb.err == nil { // t.Error("Callback did not return an expected error.") // } // // if cb.c != nil { // t.Errorf("Failed to get expected Contacts."+ // "\n\texpected: %v\n\treceived: %v", nil, cb.c) // } // case <-time.After(100 * time.Millisecond): // t.Error("Callback not called.") // } // // // if _, exists := m.inProgressSearch[m.commID-1]; exists { // // t.Error("Failed to delete SearchResponse from inProgressSearch.") // // } // } // Happy path. func TestManager_searchResponseHandler(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} callbackChan := make(chan struct { c []contact.Contact err error }) callback := func(c []contact.Contact, err error) { callbackChan <- struct { c []contact.Contact err error }{c: c, err: err} } // Generate fact list var factList fact.FactList for i := 0; i < 10; i++ { factList = append(factList, fact.Fact{ Fact: fmt.Sprintf("fact %d", i), T: fact.FactType(rand.Intn(4)), }) } factHashes, factMap := hashFactList(factList) var contacts []*Contact var expectedContacts []contact.Contact for i, hash := range factHashes { contacts = append(contacts, &Contact{ UserID: id.NewIdFromString("user", id.User, t).Marshal(), PubKey: []byte{byte(i + 1)}, TrigFacts: []*HashFact{hash}, }) expectedContacts = append(expectedContacts, contact.Contact{ ID: id.NewIdFromString("user", id.User, t), DhPubKey: m.grp.NewIntFromBytes([]byte{byte(i + 1)}), Facts: fact.FactList{factMap[string(hash.Hash)]}, }) } // Generate expected Send message payload, err := proto.Marshal(&SearchResponse{Contacts: contacts}) if err != nil { t.Fatalf("Failed to marshal LookupSend: %+v", err) } m.searchResponseHandler(factMap, callback, payload, nil) select { case results := <-callbackChan: if results.err != nil { t.Errorf("Callback returned an error: %+v", results.err) } if !reflect.DeepEqual(expectedContacts, results.c) { t.Errorf("Callback returned incorrect Contacts."+ "\nexpected: %+v\nreceived: %+v", expectedContacts, results.c) } case <-time.NewTimer(50 * time.Millisecond).C: t.Error("Callback time out.") } } // Happy path: error is returned on callback when passed into function. func TestManager_searchResponseHandler_CallbackError(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} callbackChan := make(chan struct { c []contact.Contact err error }) callback := func(c []contact.Contact, err error) { callbackChan <- struct { c []contact.Contact err error }{c: c, err: err} } testErr := errors.New("search failure") m.searchResponseHandler(map[string]fact.Fact{}, callback, []byte{}, testErr) select { case results := <-callbackChan: if results.err == nil || !strings.Contains(results.err.Error(), testErr.Error()) { t.Errorf("Callback failed to return error."+ "\nexpected: %+v\nreceived: %+v", testErr, results.err) } case <-time.NewTimer(50 * time.Millisecond).C: t.Error("Callback time out.") } } // Error path: SearchResponse message contains an error. func TestManager_searchResponseHandler_MessageError(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} callbackChan := make(chan struct { c []contact.Contact err error }) callback := func(c []contact.Contact, err error) { callbackChan <- struct { c []contact.Contact err error }{c: c, err: err} } // Generate expected Send message testErr := "SearchResponse error occurred" payload, err := proto.Marshal(&SearchResponse{Error: testErr}) if err != nil { t.Fatalf("Failed to marshal LookupSend: %+v", err) } m.searchResponseHandler(map[string]fact.Fact{}, callback, payload, nil) select { case results := <-callbackChan: if results.err == nil || !strings.Contains(results.err.Error(), testErr) { t.Errorf("Callback failed to return error."+ "\nexpected: %s\nreceived: %+v", testErr, results.err) } case <-time.NewTimer(50 * time.Millisecond).C: t.Error("Callback time out.") } } // Error path: contact is malformed and cannot be parsed. func TestManager_searchResponseHandler_ParseContactError(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} callbackChan := make(chan struct { c []contact.Contact err error }) callback := func(c []contact.Contact, err error) { callbackChan <- struct { c []contact.Contact err error }{c: c, err: err} } var contacts []*Contact for i := 0; i < 10; i++ { contacts = append(contacts, &Contact{ UserID: []byte{byte(i + 1)}, }) } // Generate expected Send message payload, err := proto.Marshal(&SearchResponse{Contacts: contacts}) if err != nil { t.Fatalf("Failed to marshal LookupSend: %+v", err) } m.searchResponseHandler(nil, callback, payload, nil) select { case results := <-callbackChan: if results.err == nil || !strings.Contains(results.err.Error(), "failed to parse Contact user ID") { t.Errorf("Callback failed to return error: %+v", results.err) } case <-time.NewTimer(50 * time.Millisecond).C: t.Error("Callback time out.") } } // Happy path. func Test_hashFactList(t *testing.T) { var factList fact.FactList var expectedHashFacts []*HashFact expectedHashMap := make(map[string]fact.Fact) for i := 0; i < 10; i++ { f := fact.Fact{ Fact: fmt.Sprintf("fact %d", i), T: fact.FactType(rand.Intn(4)), } factList = append(factList, f) expectedHashFacts = append(expectedHashFacts, &HashFact{ Hash: factID.Fingerprint(f), Type: int32(f.T), }) expectedHashMap[string(factID.Fingerprint(f))] = f } hashFacts, hashMap := hashFactList(factList) if !reflect.DeepEqual(expectedHashFacts, hashFacts) { t.Errorf("hashFactList() failed to return the expected hash facts."+ "\nexpected: %+v\nreceived: %+v", expectedHashFacts, hashFacts) } if !reflect.DeepEqual(expectedHashMap, hashMap) { t.Errorf("hashFactList() failed to return the expected hash map."+ "\nexpected: %+v\nreceived: %+v", expectedHashMap, hashMap) } } // Happy path. func TestManager_parseContacts(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} // Generate fact list var factList fact.FactList for i := 0; i < 10; i++ { factList = append(factList, fact.Fact{ Fact: fmt.Sprintf("fact %d", i), T: fact.FactType(rand.Intn(4)), }) } factHashes, factMap := hashFactList(factList) var contacts []*Contact var expectedContacts []contact.Contact for i, hash := range factHashes { contacts = append(contacts, &Contact{ UserID: id.NewIdFromString("user", id.User, t).Marshal(), PubKey: []byte{byte(i + 1)}, TrigFacts: []*HashFact{hash}, }) expectedContacts = append(expectedContacts, contact.Contact{ ID: id.NewIdFromString("user", id.User, t), DhPubKey: m.grp.NewIntFromBytes([]byte{byte(i + 1)}), Facts: fact.FactList{factMap[string(hash.Hash)]}, }) } testContacts, err := m.parseContacts(contacts, factMap) if err != nil { t.Errorf("parseContacts() returned an error: %+v", err) } if !reflect.DeepEqual(expectedContacts, testContacts) { t.Errorf("parseContacts() did not return the expected contacts."+ "\nexpected: %+v\nreceived: %+v", expectedContacts, testContacts) } } func TestManager_parseContacts_username(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} // Generate fact list var factList fact.FactList for i := 0; i < 10; i++ { factList = append(factList, fact.Fact{ Fact: fmt.Sprintf("fact %d", i), T: fact.FactType(rand.Intn(4)), }) } factHashes, factMap := hashFactList(factList) var contacts []*Contact var expectedContacts []contact.Contact for i, hash := range factHashes { contacts = append(contacts, &Contact{ UserID: id.NewIdFromString("user", id.User, t).Marshal(), Username: "zezima", PubKey: []byte{byte(i + 1)}, TrigFacts: []*HashFact{hash}, }) expectedContacts = append(expectedContacts, contact.Contact{ ID: id.NewIdFromString("user", id.User, t), DhPubKey: m.grp.NewIntFromBytes([]byte{byte(i + 1)}), Facts: fact.FactList{{"zezima", fact.Username}, factMap[string(hash.Hash)]}, }) } testContacts, err := m.parseContacts(contacts, factMap) if err != nil { t.Errorf("parseContacts() returned an error: %+v", err) } if !reflect.DeepEqual(expectedContacts, testContacts) { t.Errorf("parseContacts() did not return the expected contacts."+ "\nexpected: %+v\nreceived: %+v", expectedContacts, testContacts) } } // Error path: provided contact IDs are malformed and cannot be unmarshaled. func TestManager_parseContacts_IdUnmarshalError(t *testing.T) { m := &Manager{grp: cyclic.NewGroup(large.NewInt(107), large.NewInt(2))} contacts := []*Contact{{UserID: []byte("invalid ID")}} _, err := m.parseContacts(contacts, nil) if err == nil || !strings.Contains(err.Error(), "failed to parse Contact user ID") { t.Errorf("parseContacts() did not return an error when IDs are invalid: %+v", err) } } // mockSingleSearch is used to test the search function, which uses the single- // use manager. It adheres to the SingleInterface interface. type mockSingleSearch struct { } func (s *mockSingleSearch) TransmitSingleUse(partner contact.Contact, payload []byte, _ string, _ uint8, callback single.ReplyComm, _ time.Duration) error { searchMsg := &SearchSend{} if err := proto.Unmarshal(payload, searchMsg); err != nil { return errors.Errorf("Failed to unmarshal SearchSend: %+v", err) } searchResponse := &SearchResponse{ Contacts: []*Contact{{ UserID: partner.ID.Marshal(), PubKey: partner.DhPubKey.Bytes(), }}, } msg, err := proto.Marshal(searchResponse) if err != nil { return errors.Errorf("Failed to marshal SearchResponse: %+v", err) } callback(msg, nil) return nil } func (s *mockSingleSearch) StartProcesses() (stoppable.Stoppable, error) { return stoppable.NewSingle(""), nil }