Permalink
496 lines (408 sloc) 14.3 KB
package anaconda_test
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/ChimeraCoder/anaconda"
)
var CONSUMER_KEY = os.Getenv("CONSUMER_KEY")
var CONSUMER_SECRET = os.Getenv("CONSUMER_SECRET")
var ACCESS_TOKEN = os.Getenv("ACCESS_TOKEN")
var ACCESS_TOKEN_SECRET = os.Getenv("ACCESS_TOKEN_SECRET")
var api *anaconda.TwitterApi
var testBase string
func init() {
// Initialize api so it can be used even when invidual tests are run in isolation
anaconda.SetConsumerKey(CONSUMER_KEY)
anaconda.SetConsumerSecret(CONSUMER_SECRET)
api = anaconda.NewTwitterApi(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
if CONSUMER_KEY != "" && CONSUMER_SECRET != "" && ACCESS_TOKEN != "" && ACCESS_TOKEN_SECRET != "" {
return
}
mux := http.NewServeMux()
server := httptest.NewServer(mux)
parsed, _ := url.Parse(server.URL)
testBase = parsed.String()
api.SetBaseUrl(testBase)
var endpointElems [][]string
filepath.Walk("json", func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
elems := strings.Split(path, string(os.PathSeparator))[1:]
endpointElems = append(endpointElems, elems)
}
return nil
})
for _, elems := range endpointElems {
endpoint := strings.Replace("/"+path.Join(elems...), "_id_", "?id=", -1)
filename := filepath.Join(append([]string{"json"}, elems...)...)
mux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
// if one filename is the prefix of another, the prefix will always match
// check if there is a more specific filename that matches this request
// create local variable to avoid closing over `filename`
sourceFilename := filename
r.ParseForm()
form := strings.Replace(r.Form.Encode(), "=", "_", -1)
form = strings.Replace(form, "&", "_", -1)
specific := sourceFilename + "_" + form
_, err := os.Stat(specific)
if err == nil {
sourceFilename = specific
} else {
if err != nil && !os.IsNotExist(err) {
fmt.Fprintf(w, "error: %s", err)
return
}
}
f, err := os.Open(sourceFilename)
if err != nil {
// either the file does not exist
// or something is seriously wrong with the testing environment
fmt.Fprintf(w, "error: %s", err)
}
defer f.Close()
// TODO not a hack
if sourceFilename == filepath.Join("json", "statuses", "show.json_id_404409873170841600_tweet_mode_extended") {
bts, err := ioutil.ReadAll(f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
http.Error(w, string(bts), http.StatusNotFound)
return
}
io.Copy(w, f)
})
}
}
// Test_TwitterCredentials tests that non-empty Twitter credentials are set
// Without this, all following tests will fail
func Test_TwitterCredentials(t *testing.T) {
if CONSUMER_KEY == "" || CONSUMER_SECRET == "" || ACCESS_TOKEN == "" || ACCESS_TOKEN_SECRET == "" {
t.Logf("Using HTTP mock responses (API credentials are invalid: at least one is empty)")
} else {
t.Logf("Tests will query the live Twitter API (API credentials are all non-empty)")
}
}
// Test that creating a TwitterApi client creates a client with non-empty OAuth credentials
func Test_TwitterApi_NewTwitterApi(t *testing.T) {
anaconda.SetConsumerKey(CONSUMER_KEY)
anaconda.SetConsumerSecret(CONSUMER_SECRET)
apiLocal := anaconda.NewTwitterApi(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
if apiLocal.Credentials == nil {
t.Fatalf("Twitter Api client has empty (nil) credentials")
}
}
// Test that creating a TwitterApi client creates a client with non-empty OAuth credentials
func Test_TwitterApi_NewTwitterApiWithCredentials(t *testing.T) {
apiLocal := anaconda.NewTwitterApiWithCredentials(ACCESS_TOKEN, ACCESS_TOKEN_SECRET, CONSUMER_KEY, CONSUMER_SECRET)
if apiLocal.Credentials == nil {
t.Fatalf("Twitter Api client has empty (nil) credentials")
}
}
// Test that the GetSearch function actually works and returns non-empty results
func Test_TwitterApi_GetSearch(t *testing.T) {
search_result, err := api.GetSearch("golang", nil)
if err != nil {
t.Fatal(err)
}
// Unless something is seriously wrong, there should be at least two tweets
if len(search_result.Statuses) < 2 {
t.Fatalf("Expected 2 or more tweets, and found %d", len(search_result.Statuses))
}
// Check that at least one tweet is non-empty
for _, tweet := range search_result.Statuses {
if tweet.Text != "" {
return
}
fmt.Print(tweet.Text)
}
t.Fatalf("All %d tweets had empty text", len(search_result.Statuses))
}
// Test that a valid user can be fetched
// and that unmarshalling works properly
func Test_GetUser(t *testing.T) {
const username = "chimeracoder"
users, err := api.GetUsersLookup(username, nil)
if err != nil {
t.Fatalf("GetUsersLookup returned error: %s", err.Error())
}
if len(users) != 1 {
t.Fatalf("Expected one user and received %d", len(users))
}
// If all attributes are equal to the zero value for that type,
// then the original value was not valid
if reflect.DeepEqual(users[0], anaconda.User{}) {
t.Fatalf("Received %#v", users[0])
}
}
func Test_GetFavorites(t *testing.T) {
v := url.Values{}
v.Set("screen_name", "chimeracoder")
favorites, err := api.GetFavorites(v)
if err != nil {
t.Fatalf("GetFavorites returned error: %s", err.Error())
}
if len(favorites) == 0 {
t.Fatalf("GetFavorites returned no favorites")
}
if reflect.DeepEqual(favorites[0], anaconda.Tweet{}) {
t.Fatalf("GetFavorites returned %d favorites and the first one was empty", len(favorites))
}
}
// Test that a valid tweet can be fetched properly
// and that unmarshalling of tweet works without error
func Test_GetTweet(t *testing.T) {
const tweetId = 303777106620452864
const tweetText = `golang-syd is in session. Dave Symonds is now talking about API design and protobufs. #golang http://t.co/eSq3ROwu`
tweet, err := api.GetTweet(tweetId, nil)
if err != nil {
t.Fatalf("GetTweet returned error: %s", err.Error())
}
if tweet.Text != tweetText {
t.Fatalf("Tweet %d contained incorrect text. Received: %s", tweetId, tweet.Text)
}
// Check the entities
expectedEntities := anaconda.Entities{Hashtags: []struct {
Indices []int `json:"indices"`
Text string `json:"text"`
}{struct {
Indices []int `json:"indices"`
Text string `json:"text"`
}{Indices: []int{86, 93}, Text: "golang"}}, Urls: []struct {
Indices []int `json:"indices"`
Url string `json:"url"`
Display_url string `json:"display_url"`
Expanded_url string `json:"expanded_url"`
}{}, User_mentions: []struct {
Name string `json:"name"`
Indices []int `json:"indices"`
Screen_name string `json:"screen_name"`
Id int64 `json:"id"`
Id_str string `json:"id_str"`
}{}, Media: []anaconda.EntityMedia{anaconda.EntityMedia{
Id: 303777106628841472,
Id_str: "303777106628841472",
Media_url: "http://pbs.twimg.com/media/BDc7q0OCEAAoe2C.jpg",
Media_url_https: "https://pbs.twimg.com/media/BDc7q0OCEAAoe2C.jpg",
Url: "http://t.co/eSq3ROwu",
Display_url: "pic.twitter.com/eSq3ROwu",
Expanded_url: "http://twitter.com/go_nuts/status/303777106620452864/photo/1",
Sizes: anaconda.MediaSizes{Medium: anaconda.MediaSize{W: 600,
H: 450,
Resize: "fit"},
Thumb: anaconda.MediaSize{W: 150,
H: 150,
Resize: "crop"},
Small: anaconda.MediaSize{W: 340,
H: 255,
Resize: "fit"},
Large: anaconda.MediaSize{W: 1024,
H: 768,
Resize: "fit"}},
Type: "photo",
Indices: []int{94,
114}}}}
if !reflect.DeepEqual(tweet.Entities, expectedEntities) {
t.Fatalf("Tweet entities differ")
}
}
func Test_GetQuotedTweet(t *testing.T) {
const tweetId = 738567564641599489
const tweetText = `Well, this has certainly come a long way! https://t.co/QomzRzwcti`
const quotedID = 284377451625340928
const quotedText = `Just created gojson - a simple tool for turning JSON data into Go structs! http://t.co/QM6k9AUV #golang`
tweet, err := api.GetTweet(tweetId, nil)
if err != nil {
t.Fatalf("GetTweet returned error: %s", err.Error())
}
if tweet.Text != tweetText {
t.Fatalf("Tweet %d contained incorrect text. Received: %s", tweetId, tweet.Text)
}
if tweet.QuotedStatusID != quotedID {
t.Fatalf("Expected quoted status %d, received %d", quotedID, tweet.QuotedStatusID)
}
if tweet.QuotedStatusIdStr != strconv.Itoa(quotedID) {
t.Fatalf("Expected quoted status %d (as string), received %s", quotedID, tweet.QuotedStatusIdStr)
}
if tweet.QuotedStatus.Text != quotedText {
t.Fatalf("Expected quoted status text %#v, received %#v", quotedText, tweet.QuotedStatus.Text)
}
}
// This assumes that the current user has at least two pages' worth of followers
func Test_GetFollowersListAll(t *testing.T) {
result := api.GetFollowersListAll(nil)
i := 0
for page := range result {
if i == 2 {
return
}
if page.Error != nil {
t.Fatalf("Receved error from GetFollowersListAll: %s", page.Error)
}
if page.Followers == nil || len(page.Followers) == 0 {
t.Fatalf("Received invalid value for page %d of followers: %v", i, page.Followers)
}
i++
}
}
// This assumes that the current user has at least two pages' worth of followers
func Test_GetFollowersIdsAll(t *testing.T) {
result := api.GetFollowersIdsAll(nil)
i := 0
for page := range result {
if i == 2 {
return
}
if page.Error != nil {
t.Fatalf("Receved error from GetFollowersIdsAll: %s", page.Error)
}
if page.Ids == nil || len(page.Ids) == 0 {
t.Fatalf("Received invalid value for page %d of followers: %v", i, page.Ids)
}
i++
}
}
// This assumes that the current user has at least two pages' worth of friends
func Test_GetFriendsIdsAll(t *testing.T) {
result := api.GetFriendsIdsAll(nil)
i := 0
for page := range result {
if i == 2 {
return
}
if page.Error != nil {
t.Fatalf("Receved error from GetFriendsIdsAll : %s", page.Error)
}
if page.Ids == nil || len(page.Ids) == 0 {
t.Fatalf("Received invalid value for page %d of friends : %v", i, page.Ids)
}
i++
}
}
// Test that setting the delay actually changes the stored delay value
func Test_TwitterApi_SetDelay(t *testing.T) {
const OLD_DELAY = 1 * time.Second
const NEW_DELAY = 20 * time.Second
api.EnableThrottling(OLD_DELAY, 4)
delay := api.GetDelay()
if delay != OLD_DELAY {
t.Fatalf("Expected initial delay to be the default delay (%s)", anaconda.DEFAULT_DELAY.String())
}
api.SetDelay(NEW_DELAY)
if newDelay := api.GetDelay(); newDelay != NEW_DELAY {
t.Fatalf("Attempted to set delay to %s, but delay is now %s (original delay: %s)", NEW_DELAY, newDelay, delay)
}
}
func Test_TwitterApi_TwitterErrorDoesNotExist(t *testing.T) {
// Try fetching a tweet that no longer exists (was deleted)
const DELETED_TWEET_ID = 404409873170841600
tweet, err := api.GetTweet(DELETED_TWEET_ID, nil)
if err == nil {
t.Fatalf("Expected an error when fetching tweet with id %d but got none - tweet object is %+v", DELETED_TWEET_ID, tweet)
}
apiErr, ok := err.(*anaconda.ApiError)
if !ok {
t.Fatalf("Expected an *anaconda.ApiError, and received error message %s, (%+v)", err.Error(), err)
}
terr, ok := apiErr.Decoded.First().(anaconda.TwitterError)
if !ok {
t.Fatalf("TwitterErrorResponse.First() should return value of type TwitterError, not %s", reflect.TypeOf(apiErr.Decoded.First()))
}
if code := terr.Code; code != anaconda.TwitterErrorDoesNotExist && code != anaconda.TwitterErrorDoesNotExist2 {
if code == anaconda.TwitterErrorRateLimitExceeded {
t.Fatalf("Rate limit exceeded during testing - received error code %d instead of %d", anaconda.TwitterErrorRateLimitExceeded, anaconda.TwitterErrorDoesNotExist)
} else {
t.Fatalf("Expected Twitter to return error code %d, and instead received error code %d", anaconda.TwitterErrorDoesNotExist, code)
}
}
}
func Test_DMScreenName(t *testing.T) {
to, err := api.GetSelf(url.Values{})
if err != nil {
t.Error(err)
}
_, err = api.PostDMToScreenName("Test the anaconda lib", to.ScreenName)
if err != nil {
t.Error(err)
return
}
}
// This assumes that the current user has at least one list, and the first list includes the current user as a member.
func Test_RemoveUserFromList(t *testing.T) {
user, err := api.GetSelf(url.Values{})
if err != nil {
t.Fatalf("GetSelf returned error: %s", err.Error())
}
lists, err := api.GetListsOwnedBy(user.Id, nil)
if err != nil {
t.Fatalf("GetListsOwnedBy returned error: %s", err.Error())
}
if len(lists) == 0 {
t.Fatalf("GetListsOwnedBy returned no lists")
}
list, err := api.RemoveUserFromList(user.ScreenName, lists[0].Id, nil)
if err != nil {
t.Fatalf("RemoveUserFromList returned error: %s", err.Error())
}
// If all attributes are equal to the zero value for that type,
// then the original value was not valid
if reflect.DeepEqual(list, anaconda.List{}) {
t.Fatalf("Received %#v", list)
}
}
// This assumes that the current user has at least one list, and the first list includes the current user as a member.
func Test_RemoveMultipleUsersFromList(t *testing.T) {
user, err := api.GetSelf(url.Values{})
if err != nil {
t.Fatalf("GetSelf returned error: %s", err.Error())
}
lists, err := api.GetListsOwnedBy(user.Id, nil)
if err != nil {
t.Fatalf("GetListsOwnedBy returned error: %s", err.Error())
}
if len(lists) == 0 {
t.Fatalf("GetListsOwnedBy returned no lists")
}
users := []string{user.ScreenName}
list, err := api.RemoveMultipleUsersFromList(users, lists[0].Id, nil)
if err != nil {
t.Fatalf("RemoveMultipleUsersFromList returned error: %s", err.Error())
}
// If all attributes are equal to the zero value for that type,
// then the original value was not valid
if reflect.DeepEqual(list, anaconda.List{}) {
t.Fatalf("Received %#v", list)
}
}
// Test that the client can be used to throttle to an arbitrary duration
func Test_TwitterApi_Throttling(t *testing.T) {
const MIN_DELAY = 15 * time.Second
api.EnableThrottling(MIN_DELAY, 5)
oldDelay := api.GetDelay()
api.SetDelay(MIN_DELAY)
now := time.Now()
_, err := api.GetSearch("golang", nil)
if err != nil {
t.Fatalf("GetSearch yielded error %s", err.Error())
}
_, err = api.GetSearch("anaconda", nil)
if err != nil {
t.Fatalf("GetSearch yielded error %s", err.Error())
}
after := time.Now()
if difference := after.Sub(now); difference < MIN_DELAY {
t.Fatalf("Expected delay of at least %s. Actual delay: %s", MIN_DELAY.String(), difference.String())
}
// Reset the delay to its previous value
api.SetDelay(oldDelay)
}