1package anaconda_test 2 3import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "reflect" 14 "strconv" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/ChimeraCoder/anaconda" 20) 21 22var CONSUMER_KEY = os.Getenv("CONSUMER_KEY") 23var CONSUMER_SECRET = os.Getenv("CONSUMER_SECRET") 24var ACCESS_TOKEN = os.Getenv("ACCESS_TOKEN") 25var ACCESS_TOKEN_SECRET = os.Getenv("ACCESS_TOKEN_SECRET") 26 27var api *anaconda.TwitterApi 28 29var testBase string 30 31func init() { 32 // Initialize api so it can be used even when invidual tests are run in isolation 33 anaconda.SetConsumerKey(CONSUMER_KEY) 34 anaconda.SetConsumerSecret(CONSUMER_SECRET) 35 api = anaconda.NewTwitterApi(ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 36 37 if CONSUMER_KEY != "" && CONSUMER_SECRET != "" && ACCESS_TOKEN != "" && ACCESS_TOKEN_SECRET != "" { 38 return 39 } 40 41 mux := http.NewServeMux() 42 server := httptest.NewServer(mux) 43 44 parsed, _ := url.Parse(server.URL) 45 testBase = parsed.String() 46 api.SetBaseUrl(testBase) 47 48 var endpointElems [][]string 49 filepath.Walk("json", func(path string, info os.FileInfo, err error) error { 50 if !info.IsDir() { 51 elems := strings.Split(path, string(os.PathSeparator))[1:] 52 endpointElems = append(endpointElems, elems) 53 } 54 55 return nil 56 }) 57 58 for _, elems := range endpointElems { 59 endpoint := strings.Replace("/"+path.Join(elems...), "_id_", "?id=", -1) 60 filename := filepath.Join(append([]string{"json"}, elems...)...) 61 62 mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { 63 // if one filename is the prefix of another, the prefix will always match 64 // check if there is a more specific filename that matches this request 65 66 // create local variable to avoid closing over `filename` 67 sourceFilename := filename 68 69 r.ParseForm() 70 form := strings.Replace(r.Form.Encode(), "=", "_", -1) 71 form = strings.Replace(form, "&", "_", -1) 72 specific := sourceFilename + "_" + form 73 _, err := os.Stat(specific) 74 if err == nil { 75 sourceFilename = specific 76 } else { 77 if err != nil && !os.IsNotExist(err) { 78 fmt.Fprintf(w, "error: %s", err) 79 return 80 } 81 } 82 83 f, err := os.Open(sourceFilename) 84 if err != nil { 85 // either the file does not exist 86 // or something is seriously wrong with the testing environment 87 fmt.Fprintf(w, "error: %s", err) 88 } 89 defer f.Close() 90 91 // TODO not a hack 92 if sourceFilename == filepath.Join("json", "statuses", "show.json_id_404409873170841600_tweet_mode_extended") { 93 bts, err := ioutil.ReadAll(f) 94 if err != nil { 95 http.Error(w, err.Error(), http.StatusInternalServerError) 96 97 } 98 http.Error(w, string(bts), http.StatusNotFound) 99 return 100 101 } 102 103 io.Copy(w, f) 104 }) 105 } 106} 107 108// Test_TwitterCredentials tests that non-empty Twitter credentials are set 109// Without this, all following tests will fail 110func Test_TwitterCredentials(t *testing.T) { 111 if CONSUMER_KEY == "" || CONSUMER_SECRET == "" || ACCESS_TOKEN == "" || ACCESS_TOKEN_SECRET == "" { 112 t.Logf("Using HTTP mock responses (API credentials are invalid: at least one is empty)") 113 } else { 114 t.Logf("Tests will query the live Twitter API (API credentials are all non-empty)") 115 } 116} 117 118// Test that creating a TwitterApi client creates a client with non-empty OAuth credentials 119func Test_TwitterApi_NewTwitterApi(t *testing.T) { 120 anaconda.SetConsumerKey(CONSUMER_KEY) 121 anaconda.SetConsumerSecret(CONSUMER_SECRET) 122 apiLocal := anaconda.NewTwitterApi(ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 123 124 if apiLocal.Credentials == nil { 125 t.Fatalf("Twitter Api client has empty (nil) credentials") 126 } 127} 128 129// Test that creating a TwitterApi client creates a client with non-empty OAuth credentials 130func Test_TwitterApi_NewTwitterApiWithCredentials(t *testing.T) { 131 apiLocal := anaconda.NewTwitterApiWithCredentials(ACCESS_TOKEN, ACCESS_TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET) 132 133 if apiLocal.Credentials == nil { 134 t.Fatalf("Twitter Api client has empty (nil) credentials") 135 } 136} 137 138// Test that the GetSearch function actually works and returns non-empty results 139func Test_TwitterApi_GetSearch(t *testing.T) { 140 search_result, err := api.GetSearch("golang", nil) 141 if err != nil { 142 t.Fatal(err) 143 } 144 145 // Unless something is seriously wrong, there should be at least two tweets 146 if len(search_result.Statuses) < 2 { 147 t.Fatalf("Expected 2 or more tweets, and found %d", len(search_result.Statuses)) 148 } 149 150 // Check that at least one tweet is non-empty 151 for _, tweet := range search_result.Statuses { 152 if tweet.Text != "" { 153 return 154 } 155 fmt.Print(tweet.Text) 156 } 157 158 t.Fatalf("All %d tweets had empty text", len(search_result.Statuses)) 159} 160 161// Test that a valid user can be fetched 162// and that unmarshalling works properly 163func Test_GetUser(t *testing.T) { 164 const username = "chimeracoder" 165 166 users, err := api.GetUsersLookup(username, nil) 167 if err != nil { 168 t.Fatalf("GetUsersLookup returned error: %s", err.Error()) 169 } 170 171 if len(users) != 1 { 172 t.Fatalf("Expected one user and received %d", len(users)) 173 } 174 175 // If all attributes are equal to the zero value for that type, 176 // then the original value was not valid 177 if reflect.DeepEqual(users[0], anaconda.User{}) { 178 t.Fatalf("Received %#v", users[0]) 179 } 180} 181 182func Test_GetFavorites(t *testing.T) { 183 v := url.Values{} 184 v.Set("screen_name", "chimeracoder") 185 favorites, err := api.GetFavorites(v) 186 if err != nil { 187 t.Fatalf("GetFavorites returned error: %s", err.Error()) 188 } 189 190 if len(favorites) == 0 { 191 t.Fatalf("GetFavorites returned no favorites") 192 } 193 194 if reflect.DeepEqual(favorites[0], anaconda.Tweet{}) { 195 t.Fatalf("GetFavorites returned %d favorites and the first one was empty", len(favorites)) 196 } 197} 198 199// Test that a valid tweet can be fetched properly 200// and that unmarshalling of tweet works without error 201func Test_GetTweet(t *testing.T) { 202 const tweetId = 303777106620452864 203 const tweetText = `golang-syd is in session. Dave Symonds is now talking about API design and protobufs. #golang http://t.co/eSq3ROwu` 204 205 tweet, err := api.GetTweet(tweetId, nil) 206 if err != nil { 207 t.Fatalf("GetTweet returned error: %s", err.Error()) 208 } 209 210 if tweet.Text != tweetText { 211 t.Fatalf("Tweet %d contained incorrect text. Received: %s", tweetId, tweet.Text) 212 } 213 214 // Check the entities 215 expectedEntities := anaconda.Entities{Hashtags: []struct { 216 Indices []int `json:"indices"` 217 Text string `json:"text"` 218 }{struct { 219 Indices []int `json:"indices"` 220 Text string `json:"text"` 221 }{Indices: []int{86, 93}, Text: "golang"}}, Urls: []struct { 222 Indices []int `json:"indices"` 223 Url string `json:"url"` 224 Display_url string `json:"display_url"` 225 Expanded_url string `json:"expanded_url"` 226 }{}, User_mentions: []struct { 227 Name string `json:"name"` 228 Indices []int `json:"indices"` 229 Screen_name string `json:"screen_name"` 230 Id int64 `json:"id"` 231 Id_str string `json:"id_str"` 232 }{}, Media: []anaconda.EntityMedia{anaconda.EntityMedia{ 233 Id: 303777106628841472, 234 Id_str: "303777106628841472", 235 Media_url: "http://pbs.twimg.com/media/BDc7q0OCEAAoe2C.jpg", 236 Media_url_https: "https://pbs.twimg.com/media/BDc7q0OCEAAoe2C.jpg", 237 Url: "http://t.co/eSq3ROwu", 238 Display_url: "pic.twitter.com/eSq3ROwu", 239 Expanded_url: "http://twitter.com/go_nuts/status/303777106620452864/photo/1", 240 Sizes: anaconda.MediaSizes{Medium: anaconda.MediaSize{W: 600, 241 H: 450, 242 Resize: "fit"}, 243 Thumb: anaconda.MediaSize{W: 150, 244 H: 150, 245 Resize: "crop"}, 246 Small: anaconda.MediaSize{W: 340, 247 H: 255, 248 Resize: "fit"}, 249 Large: anaconda.MediaSize{W: 1024, 250 H: 768, 251 Resize: "fit"}}, 252 Type: "photo", 253 Indices: []int{94, 254 114}}}} 255 if !reflect.DeepEqual(tweet.Entities, expectedEntities) { 256 t.Fatalf("Tweet entities differ") 257 } 258} 259 260func Test_GetQuotedTweet(t *testing.T) { 261 const tweetId = 738567564641599489 262 const tweetText = `Well, this has certainly come a long way! https://t.co/QomzRzwcti` 263 const quotedID = 284377451625340928 264 const quotedText = `Just created gojson - a simple tool for turning JSON data into Go structs! http://t.co/QM6k9AUV #golang` 265 266 tweet, err := api.GetTweet(tweetId, nil) 267 if err != nil { 268 t.Fatalf("GetTweet returned error: %s", err.Error()) 269 } 270 271 if tweet.Text != tweetText { 272 t.Fatalf("Tweet %d contained incorrect text. Received: %s", tweetId, tweet.Text) 273 } 274 275 if tweet.QuotedStatusID != quotedID { 276 t.Fatalf("Expected quoted status %d, received %d", quotedID, tweet.QuotedStatusID) 277 } 278 279 if tweet.QuotedStatusIdStr != strconv.Itoa(quotedID) { 280 t.Fatalf("Expected quoted status %d (as string), received %s", quotedID, tweet.QuotedStatusIdStr) 281 } 282 283 if tweet.QuotedStatus.Text != quotedText { 284 t.Fatalf("Expected quoted status text %#v, received %#v", quotedText, tweet.QuotedStatus.Text) 285 } 286} 287 288// This assumes that the current user has at least two pages' worth of followers 289func Test_GetFollowersListAll(t *testing.T) { 290 result := api.GetFollowersListAll(nil) 291 i := 0 292 293 for page := range result { 294 if i == 2 { 295 return 296 } 297 298 if page.Error != nil { 299 t.Fatalf("Receved error from GetFollowersListAll: %s", page.Error) 300 } 301 302 if page.Followers == nil || len(page.Followers) == 0 { 303 t.Fatalf("Received invalid value for page %d of followers: %v", i, page.Followers) 304 } 305 i++ 306 } 307} 308 309// This assumes that the current user has at least two pages' worth of followers 310func Test_GetFollowersIdsAll(t *testing.T) { 311 result := api.GetFollowersIdsAll(nil) 312 i := 0 313 314 for page := range result { 315 if i == 2 { 316 return 317 } 318 319 if page.Error != nil { 320 t.Fatalf("Receved error from GetFollowersIdsAll: %s", page.Error) 321 } 322 323 if page.Ids == nil || len(page.Ids) == 0 { 324 t.Fatalf("Received invalid value for page %d of followers: %v", i, page.Ids) 325 } 326 i++ 327 } 328} 329 330// This assumes that the current user has at least two pages' worth of friends 331func Test_GetFriendsIdsAll(t *testing.T) { 332 result := api.GetFriendsIdsAll(nil) 333 i := 0 334 335 for page := range result { 336 if i == 2 { 337 return 338 } 339 340 if page.Error != nil { 341 t.Fatalf("Receved error from GetFriendsIdsAll : %s", page.Error) 342 } 343 344 if page.Ids == nil || len(page.Ids) == 0 { 345 t.Fatalf("Received invalid value for page %d of friends : %v", i, page.Ids) 346 } 347 i++ 348 } 349} 350 351// Test that setting the delay actually changes the stored delay value 352func Test_TwitterApi_SetDelay(t *testing.T) { 353 const OLD_DELAY = 1 * time.Second 354 const NEW_DELAY = 20 * time.Second 355 api.EnableThrottling(OLD_DELAY, 4) 356 357 delay := api.GetDelay() 358 if delay != OLD_DELAY { 359 t.Fatalf("Expected initial delay to be the default delay (%s)", anaconda.DEFAULT_DELAY.String()) 360 } 361 362 api.SetDelay(NEW_DELAY) 363 364 if newDelay := api.GetDelay(); newDelay != NEW_DELAY { 365 t.Fatalf("Attempted to set delay to %s, but delay is now %s (original delay: %s)", NEW_DELAY, newDelay, delay) 366 } 367} 368 369func Test_TwitterApi_TwitterErrorDoesNotExist(t *testing.T) { 370 371 // Try fetching a tweet that no longer exists (was deleted) 372 const DELETED_TWEET_ID = 404409873170841600 373 374 tweet, err := api.GetTweet(DELETED_TWEET_ID, nil) 375 if err == nil { 376 t.Fatalf("Expected an error when fetching tweet with id %d but got none - tweet object is %+v", DELETED_TWEET_ID, tweet) 377 } 378 379 apiErr, ok := err.(*anaconda.ApiError) 380 if !ok { 381 t.Fatalf("Expected an *anaconda.ApiError, and received error message %s, (%+v)", err.Error(), err) 382 } 383 384 terr, ok := apiErr.Decoded.First().(anaconda.TwitterError) 385 386 if !ok { 387 t.Fatalf("TwitterErrorResponse.First() should return value of type TwitterError, not %s", reflect.TypeOf(apiErr.Decoded.First())) 388 } 389 390 if code := terr.Code; code != anaconda.TwitterErrorDoesNotExist && code != anaconda.TwitterErrorDoesNotExist2 { 391 if code == anaconda.TwitterErrorRateLimitExceeded { 392 t.Fatalf("Rate limit exceeded during testing - received error code %d instead of %d", anaconda.TwitterErrorRateLimitExceeded, anaconda.TwitterErrorDoesNotExist) 393 } else { 394 395 t.Fatalf("Expected Twitter to return error code %d, and instead received error code %d", anaconda.TwitterErrorDoesNotExist, code) 396 } 397 } 398} 399 400func Test_DMScreenName(t *testing.T) { 401 to, err := api.GetSelf(url.Values{}) 402 if err != nil { 403 t.Error(err) 404 } 405 _, err = api.PostDMToScreenName("Test the anaconda lib", to.ScreenName) 406 if err != nil { 407 t.Error(err) 408 return 409 } 410} 411 412// Test that the client can be used to throttle to an arbitrary duration 413func Test_TwitterApi_Throttling(t *testing.T) { 414 const MIN_DELAY = 15 * time.Second 415 416 api.EnableThrottling(MIN_DELAY, 5) 417 oldDelay := api.GetDelay() 418 api.SetDelay(MIN_DELAY) 419 420 now := time.Now() 421 _, err := api.GetSearch("golang", nil) 422 if err != nil { 423 t.Fatalf("GetSearch yielded error %s", err.Error()) 424 } 425 _, err = api.GetSearch("anaconda", nil) 426 if err != nil { 427 t.Fatalf("GetSearch yielded error %s", err.Error()) 428 } 429 after := time.Now() 430 431 if difference := after.Sub(now); difference < MIN_DELAY { 432 t.Fatalf("Expected delay of at least %s. Actual delay: %s", MIN_DELAY.String(), difference.String()) 433 } 434 435 // Reset the delay to its previous value 436 api.SetDelay(oldDelay) 437} 438