From a617cf50eebef286ae045dfdfa1b29ac917d8e4f Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 7 Dec 2014 18:36:53 -0800 Subject: [PATCH] first commit --- apistructs.go | 41 +++++++ apistucts_test.go | 29 +++++ joiner.go | 53 ++++++++ joiner_test.go | 70 +++++++++++ twitter.go | 88 ++++++++++++++ twitterApi.go | 204 +++++++++++++++++++++++++++++++ twitterApi_test.go | 21 ++++ twitter_test.go | 293 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 799 insertions(+) create mode 100644 apistructs.go create mode 100644 apistucts_test.go create mode 100644 joiner.go create mode 100644 joiner_test.go create mode 100644 twitter.go create mode 100644 twitterApi.go create mode 100644 twitterApi_test.go create mode 100644 twitter_test.go diff --git a/apistructs.go b/apistructs.go new file mode 100644 index 0000000..c671093 --- /dev/null +++ b/apistructs.go @@ -0,0 +1,41 @@ +package main + +type idHolder struct { + Id uint64 `json:"id"` +} + +type User struct { + ScreenName string `json:"screen_name"` + Id uint64 `json:"id"` +} + +type FollowerList struct { + NextCursor string `json:"next_cursor_str"` + Followers []*User `json:"users"` +} + +func (f *FollowerList) GetFollowerScreenNames() []string { + ret := make([]string, len(f.Followers)) + for i, u := range f.Followers { + ret[i] = u.ScreenName + } + return ret +} + +type FollowerIDList struct { + NextCursor string `json:"next_cursor_str"` + Followers []uint64 `json:"ids"` +} + +type TwitterErr struct { + Msg string + Status int +} + +func (err *TwitterErr) Error() string { + return err.Msg +} + +func NewTwitterErr(msg string, status int) *TwitterErr { + return &TwitterErr{msg, status} +} diff --git a/apistucts_test.go b/apistucts_test.go new file mode 100644 index 0000000..b329d0e --- /dev/null +++ b/apistucts_test.go @@ -0,0 +1,29 @@ +package main + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestUserJsonification(t *testing.T) { + jsonForm := `{"screen_name": "boblechef", "id": 2920819021}` + expectedUser := &User{"boblechef", 2920819021} + u := new(User) + if err := json.Unmarshal([]byte(jsonForm), u); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(u, expectedUser) { + t.Fail() + } +} + +func TestFollowerListJsonification(t *testing.T) { + jsonForm := `{"next_cursor_str": "3333", "users":[ {"screen_name": "boblechef", "id": 2920819021}]}` + expectedUserList := &FollowerList{"3333", []*User{&User{"boblechef", 2920819021}}} + l := new(FollowerList) + if err := json.Unmarshal([]byte(jsonForm), l); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(l, expectedUserList) { + t.Fail() + } +} diff --git a/joiner.go b/joiner.go new file mode 100644 index 0000000..7615cb5 --- /dev/null +++ b/joiner.go @@ -0,0 +1,53 @@ +package main + +import "sync" + +// returns all the element x that are in c1 and c2. can retuns many time +// the same x. +func intersection(c1, c2 <-chan uint64) <-chan uint64 { + var wg sync.WaitGroup + out := make(chan uint64) + + mc1, mc2 := make(map[uint64]bool), make(map[uint64]bool) + + filter := func(c <-chan uint64, mapToAdd, mapToVerify map[uint64]bool) { + for n := range c { + mapToAdd[n] = true + if _, ok := mapToVerify[n]; ok { + out <- n + } + } + wg.Done() + } + wg.Add(2) + go filter(c1, mc1, mc2) + go filter(c2, mc2, mc1) + + go func() { + wg.Wait() + close(out) + }() + return out +} + +// remove all duplicates in inputC +func uniq(inputC <-chan uint64) <-chan uint64 { + m := make(map[uint64]bool) + c := make(chan uint64) + go func() { + for n := range inputC { + if _, ok := m[n]; !ok { + m[n] = true + c <- n + } + } + close(c) + }() + return c +} + +// returns the intersection of a and b without repetitions +func Intersection(a, b <-chan uint64) <-chan uint64 { + return uniq(intersection(a, b)) + +} diff --git a/joiner_test.go b/joiner_test.go new file mode 100644 index 0000000..528f719 --- /dev/null +++ b/joiner_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "reflect" + "testing" +) + +func makeUInt64Channel(numbers ...uint64) <-chan uint64 { + c := make(chan uint64) + go func() { + for _, n := range numbers { + c <- n + } + close(c) + }() + return c +} + +func readUInt64Channel(c <-chan uint64) []uint64 { + ret := make([]uint64, 0) + for n := range c { + ret = append(ret, n) + } + return ret +} + +func equalsAsMultiSet(a, b []uint64) bool { + aSet, bSet := make(map[uint64]int), make(map[uint64]int) + for _, e := range a { + if _, ok := aSet[e]; ok { + aSet[e]++ + } else { + aSet[e] = 1 + } + } + + for _, e := range b { + if _, ok := aSet[e]; ok { + bSet[e]++ + } else { + bSet[e] = 1 + } + } + return reflect.DeepEqual(aSet, bSet) +} + +func TestUniq(t *testing.T) { + c := makeUInt64Channel(1, 3, 2, 1, 4, 3, 3, 2, 1) + expt := []uint64{1, 3, 2, 4} + if ret := readUInt64Channel(uniq(c)); !reflect.DeepEqual(expt, ret) { + t.Fail() + } +} + +func TestIntersection(t *testing.T) { + c := Intersection(makeUInt64Channel(1, 2, 3, 4), makeUInt64Channel(1, 2, 3, 4)) + if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{1, 2, 3, 4}) { + t.Error("needed {1, 2, 3 , 4} received ", s) + } + + c = Intersection(makeUInt64Channel(1, 2), makeUInt64Channel(1, 2, 3, 4)) + if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{1, 2}) { + t.Error("needed {1, 2} received ", s) + } + + c = Intersection(makeUInt64Channel(3, 4), makeUInt64Channel(1, 2, 3, 4)) + if s := readUInt64Channel(c); !equalsAsMultiSet(s, []uint64{3, 4}) { + t.Error("needed {3 , 4} received ", s) + } +} diff --git a/twitter.go b/twitter.go new file mode 100644 index 0000000..2f010de --- /dev/null +++ b/twitter.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "sync" +) + +type FollowerGetter interface { + GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList + GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList + GetScreenNameOfUsersByIds(ids []uint64) <-chan string +} + +func GetFollowerScreenNames(followerGetter FollowerGetter, screenName string) <-chan string { + followerC := make(chan string) + go func() { + nextCursor := "-1" + for nextCursor != "0" && nextCursor != "" { + followers := <-followerGetter.GetFollowerByCursor(screenName, nextCursor) + nextCursor = followers.NextCursor + for _, follower := range followers.GetFollowerScreenNames() { + followerC <- follower + } + } + close(followerC) + }() + return followerC +} + +func GetFollowerIds(followerGetter FollowerGetter, screenName string) <-chan uint64 { + followerC := make(chan uint64) + go func() { + nextCursor := "-1" + for nextCursor != "0" && nextCursor != "" { + followers := <-followerGetter.GetGetFollowerIdsByCursor(screenName, nextCursor) + nextCursor = followers.NextCursor + for _, follower := range followers.Followers { + followerC <- follower + } + } + close(followerC) + }() + return followerC +} + +func GetTwitterScreenNameByIds(followerGetter FollowerGetter, idsC <-chan uint64) <-chan string { + screenNameC := make(chan string) + var wg sync.WaitGroup + + produceScreenName := func(buffer []uint64) { + for screenName := range followerGetter.GetScreenNameOfUsersByIds(buffer) { + screenNameC <- screenName + } + wg.Done() + } + + go func() { + buffer := make([]uint64, 0, 100) + for id := range idsC { + buffer = append(buffer, id) + if len(buffer) >= 95 { + wg.Add(1) + go produceScreenName(buffer) + buffer = make([]uint64, 0, 100) + } + } + wg.Add(1) + go produceScreenName(buffer) + go func() { + wg.Wait() + close(screenNameC) + }() + }() + return screenNameC +} + +func GetFollowerIdsOfBothAccounts(followerGetter FollowerGetter, screenName1, screenName2 string) <-chan uint64 { + return Intersection(GetFollowerIds(followerGetter, screenName1), GetFollowerIds(followerGetter, screenName1)) +} + +func main() { + token := "AAAAAAAAAAAAAAAAAAAAAPwfcQAAAAAAzkou%2FHjJNJmwdepeRq0c%2Bi3Nx6o%3DXofLt7SVvc99ulETLRA3yS2lYo8smfc6tACxEYsLUmGsrNbc9J" + t := NewTwitterApi(TWITTER_API_URL, token) + ff := Intersection(GetFollowerIds(t, "Shopify"), GetFollowerIds(t, "ShopifyPicks")) + for screenName := range GetTwitterScreenNameByIds(t, ff) { + fmt.Println(screenName) + } +} diff --git a/twitterApi.go b/twitterApi.go new file mode 100644 index 0000000..9d48e67 --- /dev/null +++ b/twitterApi.go @@ -0,0 +1,204 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + "time" +) + +const TWITTER_API_URL = "https://api.twitter.com/1.1" + +type TwitterApi struct { + BaseUrl string + AccessToken string +} + +func NewTwitterApi(baseUrl, accesToken string) *TwitterApi { + return &TwitterApi{baseUrl, accesToken} +} + +func (t *TwitterApi) encodeParams(params map[string]string) string { + + q := url.Values{} + for k, v := range params { + q.Set(k, v) + } + return q.Encode() + +} + +func (t *TwitterApi) createGetPathAndParams(apiEndpoint string, params map[string]string) string { + u, err := url.Parse(apiEndpoint) + if err != nil { + panic(err) + } + q := u.Query() + for k, v := range params { + q.Set(k, v) + } + u.RawQuery = q.Encode() + return u.String() +} + +func (t *TwitterApi) GetBase64EncodedBearerTokenCredentials(consumerKey, consumerSecret string) string { + data := []byte(consumerKey + ":" + consumerSecret) + return base64.URLEncoding.EncodeToString(data) +} + +func (t *TwitterApi) Get(path_ string) (resp *http.Response, err error) { + req, err := http.NewRequest("GET", t.BaseUrl+path_, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+t.AccessToken) + req.Header.Set("content-type", "application/json; charset=utf-8") + return http.DefaultClient.Do(req) +} + +func (t *TwitterApi) Post(path_ string, body string) (resp *http.Response, err error) { + req, err := http.NewRequest("POST", t.BaseUrl+path_, bytes.NewBufferString(body)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+t.AccessToken) + req.Header.Set("content-type", "application/json; charset=utf-8") + return http.DefaultClient.Do(req) +} + +func (t *TwitterApi) GetAndDeserialize(path string, params map[string]string, v interface{}) (err error) { + defer func() { + if err == io.EOF { + err = nil + } + }() + if r, err := t.Get(t.createGetPathAndParams(path, params)); err != nil { + return err + } else if r.StatusCode/100 != 2 { + if errMsg, err := ioutil.ReadAll(r.Body); err != nil { + return errors.New("error getting endpoint and cannot deserialize error message") + } else { + return NewTwitterErr(string(errMsg), r.StatusCode) + } + } else { + d := json.NewDecoder(r.Body) + return d.Decode(v) + } +} + +func (t *TwitterApi) PostAndDeserialize(path string, params map[string]string, v interface{}) (err error) { + defer func() { + if err == io.EOF { + err = nil + } + }() + if r, err := t.Post(path, t.encodeParams(params)); err != nil { + return err + } else if r.StatusCode/100 != 2 { + if errMsg, err := ioutil.ReadAll(r.Body); err != nil { + return errors.New("error getting endpoint and cannot deserialize error message") + } else { + return NewTwitterErr(string(errMsg), r.StatusCode) + } + } else { + d := json.NewDecoder(r.Body) + return d.Decode(v) + } +} + +func (t *TwitterApi) GetTwitterIdByScreenName(sceenName string) (id uint64, err error) { + ids := make([]*idHolder, 0, 1) + params := map[string]string{"screen_name": sceenName, "include_entities": "id"} + apiPath := "/users/lookup.json" + if err := t.GetAndDeserialize(apiPath, params, &ids); err != nil { + return 0, err + } else if len(ids) < 1 { + return 0, errors.New("cannot find user with screen name " + sceenName) + } + return ids[0].Id, nil +} + +func (t *TwitterApi) GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList { + followerListC := make(chan *FollowerList) + go func() { + params := map[string]string{"screen_name": screenName, "count": "200", "skip_status": "true", "cursor": cursor} + apiPath := "/followers/list.json" + followers := new(FollowerList) + if err := t.GetAndDeserialize(apiPath, params, followers); err == nil { + followerListC <- followers + } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { + log.Println("api limit reached, need to sleep") + time.Sleep(5 * 60 * time.Second) + followerListC <- <-t.GetFollowerByCursor(screenName, cursor) + } else { + log.Println(err) + followerListC <- nil + } + }() + return followerListC +} + +func (t *TwitterApi) GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList { + followerListC := make(chan *FollowerIDList) + + go func() { + params := map[string]string{"screen_name": screenName, "count": "5000", "cursor": cursor} + apiPath := "/followers/ids.json" + followers := new(FollowerIDList) + if err := t.GetAndDeserialize(apiPath, params, followers); err == nil { + followerListC <- followers + } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { + log.Println("api limit reached, need to sleep") + time.Sleep(5 * 60 * time.Second) + followerListC <- <-t.GetGetFollowerIdsByCursor(screenName, cursor) + } else { + log.Println(err) + followerListC <- nil + } + }() + + return followerListC +} + +func (t *TwitterApi) GetScreenNameOfUsersByIds(ids []uint64) <-chan string { + if len(ids) >= 100 { + log.Println("GetScreenNameOfUsersByIds received a list of more that 100 ids. This is not supported by twitter") + } + screenNameC := make(chan string) + go func() { + path := "/users/lookup.json" + params := map[string]string{"include_entities": "false", "user_id": t.asCommaSeparatedString(ids)} + users := make([]*User, 0, len(ids)) + if err := t.PostAndDeserialize(path, params, users); err == nil { + for _, user := range users { + screenNameC <- user.ScreenName + } + } else if twitterErr, ok := err.(*TwitterErr); ok && twitterErr.Status == 429 { + log.Println("api limit reached, need to sleep") + time.Sleep(5 * 60 * time.Second) + screenNameC <- <-t.GetScreenNameOfUsersByIds(ids) + } else { + log.Println(err) + screenNameC <- "" + } + close(screenNameC) + + }() + return screenNameC +} + +func (t *TwitterApi) asCommaSeparatedString(lst []uint64) string { + lstAsString := make([]string, len(lst)) + for i, n := range lst { + lstAsString[i] = fmt.Sprintf("%v", n) + } + return strings.Join(lstAsString, ",") +} diff --git a/twitterApi_test.go b/twitterApi_test.go new file mode 100644 index 0000000..87a753f --- /dev/null +++ b/twitterApi_test.go @@ -0,0 +1,21 @@ +package main + +import "testing" + +func TestcreateGetPathAndParams(t *testing.T) { + tw := NewTwitterApi("twitter.com", "access_token") + p := map[string]string{"hello": "world"} + if u := tw.createGetPathAndParams("bob/foo", p); u != "bob/foo?hello=world" { + t.Error("bad url", u) + } +} + +func TestGetBase64EncodedBearerTokenCredentials(t *testing.T) { + key := "xvz1evFS4wEEPTGEFPHBog" + secret := "L8qq9PZyRg6ieKGEKhZolGC0vJWLw8iEJ88DRdyOg" + expt := "eHZ6MWV2RlM0d0VFUFRHRUZQSEJvZzpMOHFxOVBaeVJnNmllS0dFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw==" + tapi := new(TwitterApi) + if res := tapi.GetBase64EncodedBearerTokenCredentials(key, secret); res != expt { + t.Error("expected: ", expt, "\nbut received:", res) + } +} diff --git a/twitter_test.go b/twitter_test.go new file mode 100644 index 0000000..f20fb84 --- /dev/null +++ b/twitter_test.go @@ -0,0 +1,293 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestGet(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Error("bad request verb") + } + if auth := r.Header.Get("Authorization"); auth != "Bearer access_token" { + t.Error("bad authorization in header :", auth) + } + if contentType := r.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" { + t.Error("bad content-type in header ", contentType) + } + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + + if res, err := tw.Get("/"); err != nil { + t.Error(err) + } else { + ioutil.ReadAll(res.Body) + } +} + +func TestPost(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Error("bad request verb") + } + if auth := r.Header.Get("Authorization"); auth != "Bearer access_token" { + t.Error("bad authorization in header :", auth) + } + if contentType := r.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" { + t.Error("bad content-type in header ", contentType) + } + if body, err := ioutil.ReadAll(r.Body); err != nil { + t.Error(err) + } else if string(body) != `{"data": "is_hugue"}` { + t.Error("bad body", string(body)) + } + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + + if res, err := tw.Post("/", `{"data": "is_hugue"}`); err != nil { + t.Error(err) + } else { + ioutil.ReadAll(res.Body) + } +} + +func TestGetAndDeserializeError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + fmt.Fprint(w, `{"error": "bar"}`) + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + m := make(map[string]string) + if err := tw.GetAndDeserialize("/foo", map[string]string{}, &m); err == nil { + t.Error("shoud have an error message here") + } else if err.Error() != `{"error": "bar"}` { + t.Error("error message should propagate here") + } else if terr, ok := err.(*TwitterErr); !ok { + t.Error("the error should be a twitter error") + } else if terr.Status != 404 { + t.Error("bad status") + } +} + +func TestGetAndDeserialize(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("foo") != "bar" { + t.Error("bad params") + } + fmt.Fprint(w, `{"foo": "bar"}`) + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + m := make(map[string]string) + if err := tw.GetAndDeserialize("/foo", map[string]string{"foo": "bar"}, &m); err != nil { + t.Error(err) + } + if m["foo"] != "bar" { + t.Fail() + } +} + +func TestPostAndDeserialize(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if body, err := ioutil.ReadAll(r.Body); err != nil { + t.Error(err) + } else if string(body) != `val=7` { + t.Error("bad body") + } + fmt.Fprint(w, `{"foo": "bar"}`) + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + m := make(map[string]string) + + if err := tw.PostAndDeserialize("/foo", map[string]string{"val": "7"}, &m); err != nil { + t.Error(err) + } + if m["foo"] != "bar" { + t.Fail() + } +} + +func TestGetTwitterIdByScreenName(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if url_ := r.URL.String(); url_ != "/users/lookup.json?include_entities=id&screen_name=bobLeChef" { + t.Error("bad url :", url_) + } + fmt.Fprint(w, `[{"id": 1492}]`) + + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + + if id, err := tw.GetTwitterIdByScreenName("bobLeChef"); err != nil { + t.Error(err) + } else if id != 1492 { + t.Error("found bad id") + } +} + +func TestGetFollowerByCursor(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if url_ := r.URL.String(); url_ != "/followers/list.json?count=200&cursor=89&screen_name=bobLeChef&skip_status=true" { + t.Error("bad url :", url_) + } + fmt.Fprint(w, `{"users": [{"id": 1492, "screen_name": "bob_le_chef"}], "next_cursor_str": "1793"}`) + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + + followersListC := tw.GetFollowerByCursor("bobLeChef", "89") + followers := <-followersListC + + if followers.NextCursor != "1793" { + t.Error("bad cursor") + } else if len(followers.Followers) != 1 { + t.Error("bad number of followers") + } else if user := followers.Followers[0]; !reflect.DeepEqual(user, &User{"bob_le_chef", 1492}) { + t.Error("bad user", user) + } +} + +func TestGetFollowerIdsByCursor(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if url_ := r.URL.String(); url_ != "/followers/ids.json?count=5000&cursor=89&screen_name=bobLeChef" { + t.Error("bad url :", url_) + } + fmt.Fprint(w, `{"ids": [1492], "next_cursor_str": "1793"}`) + })) + defer ts.Close() + tw := NewTwitterApi(ts.URL, "access_token") + + followersListC := tw.GetGetFollowerIdsByCursor("bobLeChef", "89") + + followers := <-followersListC + + if followers.NextCursor != "1793" { + t.Error("bad cursor") + } else if len(followers.Followers) != 1 { + t.Error("bad number of followers", len(followers.Followers)) + } else if id := followers.Followers[0]; id != 1492 { + t.Error("bad id") + } +} + +type MockFollowerGetter struct { + T *testing.T +} + +func (m *MockFollowerGetter) GetFollowerByCursor(screenName, cursor string) <-chan *FollowerList { + followerListC := make(chan *FollowerList) + go func() { + u1 := &User{"nat", 78789} + u2 := &User{"jude", 78789} + u3 := &User{"alice", 78789} + u4 := &User{"bob", 78789} + switch cursor { + case "-1": + followerListC <- &FollowerList{"1", []*User{u1, u2}} + case "1": + followerListC <- &FollowerList{"0", []*User{u3, u4}} + case "0": + default: + m.T.Error("bad cursor", cursor) + } + }() + return followerListC +} + +func (m *MockFollowerGetter) GetGetFollowerIdsByCursor(screenName, cursor string) <-chan *FollowerIDList { + followerListC := make(chan *FollowerIDList) + go func() { + switch cursor { + case "-1": + followerListC <- &FollowerIDList{"1", []uint64{1, 2}} + case "1": + followerListC <- &FollowerIDList{"0", []uint64{3, 4}} + case "0": + default: + m.T.Error("bad cursor", cursor) + } + }() + return followerListC +} + +func (m *MockFollowerGetter) GetScreenNameOfUsersByIds(ids []uint64) <-chan string { + ret := make(chan string) + go func() { + for _, id := range ids { + ret <- fmt.Sprintf("%v", id) + } + close(ret) + }() + return ret +} + +func readAllStringFromChannel(c <-chan string) []string { + ret := make([]string, 0) + for s := range c { + ret = append(ret, s) + } + return ret +} + +func readAllUInt64FromChannel(c <-chan uint64) []uint64 { + ret := make([]uint64, 0) + for s := range c { + ret = append(ret, s) + } + return ret +} + +func TestGetFollowerScreenNames(t *testing.T) { + fg := &MockFollowerGetter{t} + followerC := GetFollowerScreenNames(fg, "justinBieber") + followers := readAllStringFromChannel(followerC) + expected := []string{"nat", "jude", "alice", "bob"} + if !reflect.DeepEqual(followers, expected) { + t.Fail() + } +} + +func TestGetFollowerIds(t *testing.T) { + fg := &MockFollowerGetter{t} + ids := readAllUInt64FromChannel(GetFollowerIds(fg, "justinBieber")) + expected := []uint64{1, 2, 3, 4} + if !reflect.DeepEqual(ids, expected) { + t.Error(ids) + } +} + +func TestAsCommaSeparatedString(t *testing.T) { + tw := new(TwitterApi) + if s := tw.asCommaSeparatedString([]uint64{10765432100123456789, 78}); s != "10765432100123456789,78" { + t.Error(s) + } +} + +func uint64Range(n int) []uint64 { + ret := make([]uint64, n) + for i := 0; i < n; i++ { + ret[i] = uint64(i) + } + return ret +} + +func TestGetTwitterScreenNameByIds(t *testing.T) { + fg := &MockFollowerGetter{t} + ids := uint64Range(233) + ret := make(map[string]bool) + for name := range GetTwitterScreenNameByIds(fg, makeUInt64Channel(ids...)) { + ret[name] = true + } + if len(ret) != 233 { + t.Fail() + } +}