twitter_test.go 13 KB

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