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