From 8c95f96f9796ef2d4354aef59265222885437ab1 Mon Sep 17 00:00:00 2001 From: Raquel Hernandez Date: Tue, 21 Jul 2015 23:45:07 -0400 Subject: [PATCH] Adding instagram provider as requested in #10 --- providers/instagram/instagram.go | 117 ++++++++++++++++++++ providers/instagram/instagram_test.go | 151 ++++++++++++++++++++++++++ providers/instagram/user.go | 85 +++++++++++++++ providers/instagram/user_test.go | 68 ++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 providers/instagram/instagram.go create mode 100644 providers/instagram/instagram_test.go create mode 100644 providers/instagram/user.go create mode 100644 providers/instagram/user_test.go diff --git a/providers/instagram/instagram.go b/providers/instagram/instagram.go new file mode 100644 index 0000000..275561f --- /dev/null +++ b/providers/instagram/instagram.go @@ -0,0 +1,117 @@ +package instagram + +import ( + "github.com/stretchr/gomniauth" + "github.com/stretchr/gomniauth/common" + "github.com/stretchr/gomniauth/oauth2" + "github.com/stretchr/objx" + "net/http" +) + +const ( + instagramDefaultScope string = "basic" + instagramName string = "instagram" + instagramDisplayName string = "Instagram" + instagramAuthURL string = "https://api.instagram.com/oauth/authorize" + instagramTokenURL string = "https://api.instagram.com/oauth/access_token" + instagramEndpointProfile string = "https://api.instagram.com/v1/users/self" +) + +// InstagramProvider implements the Provider interface and provides Instagram +// OAuth2 communication capabilities. +type InstagramProvider struct { + config *common.Config + tripperFactory common.TripperFactory +} + +func New(clientId, clientSecret, redirectUrl string) *InstagramProvider { + + p := new(InstagramProvider) + p.config = &common.Config{Map: objx.MSI( + oauth2.OAuth2KeyAuthURL, instagramAuthURL, + oauth2.OAuth2KeyTokenURL, instagramTokenURL, + oauth2.OAuth2KeyClientID, clientId, + oauth2.OAuth2KeySecret, clientSecret, + oauth2.OAuth2KeyRedirectUrl, redirectUrl, + oauth2.OAuth2KeyScope, instagramDefaultScope, + oauth2.OAuth2KeyAccessType, oauth2.OAuth2AccessTypeOnline, + oauth2.OAuth2KeyApprovalPrompt, oauth2.OAuth2ApprovalPromptAuto, + oauth2.OAuth2KeyResponseType, oauth2.OAuth2KeyCode)} + return p +} + +// TripperFactory gets an OAuth2TripperFactory +func (provider *InstagramProvider) TripperFactory() common.TripperFactory { + + if provider.tripperFactory == nil { + provider.tripperFactory = new(oauth2.OAuth2TripperFactory) + } + + return provider.tripperFactory +} + +// PublicData gets a public readable view of this provider. +func (provider *InstagramProvider) PublicData(options map[string]interface{}) (interface{}, error) { + return gomniauth.ProviderPublicData(provider, options) +} + +// Name is the unique name for this provider. +func (provider *InstagramProvider) Name() string { + return instagramName +} + +// DisplayName is the human readable name for this provider. +func (provider *InstagramProvider) DisplayName() string { + return instagramDisplayName +} + +// GetBeginAuthURL gets the URL that the client must visit in order +// to begin the authentication process. +// +// The state argument contains anything you wish to have sent back to your +// callback endpoint. +// The options argument takes any options used to configure the auth request +// sent to the provider. In the case of OAuth2, the options map can contain: +// 1. A "scope" key providing the desired scope(s). It will be merged with the default scope. +func (provider *InstagramProvider) GetBeginAuthURL(state *common.State, options objx.Map) (string, error) { + if options != nil { + scope := oauth2.MergeScopes(options.Get(oauth2.OAuth2KeyScope).Str(), instagramDefaultScope) + provider.config.Set(oauth2.OAuth2KeyScope, scope) + } + return oauth2.GetBeginAuthURLWithBase(provider.config.Get(oauth2.OAuth2KeyAuthURL).Str(), state, provider.config) +} + +// Get makes an authenticated request and returns the data in the +// response as a data map. +func (provider *InstagramProvider) Get(creds *common.Credentials, endpoint string) (objx.Map, error) { + return oauth2.Get(provider, creds, endpoint) +} + +// GetUser uses the specified common.Credentials to access the users profile +// from the remote provider, and builds the appropriate User object. +func (provider *InstagramProvider) GetUser(creds *common.Credentials) (common.User, error) { + + profileData, err := provider.Get(creds, instagramEndpointProfile) + + if err != nil { + return nil, err + } + + // build user + user := NewUser(profileData, creds, provider) + + return user, nil +} + +// CompleteAuth takes a map of arguments that are used to +// complete the authorisation process, completes it, and returns +// the appropriate Credentials. +func (provider *InstagramProvider) CompleteAuth(data objx.Map) (*common.Credentials, error) { + return oauth2.CompleteAuth(provider.TripperFactory(), data, provider.config, provider) +} + +// GetClient returns an authenticated http.Client that can be used to make requests to +// protected Instagram resources +func (provider *InstagramProvider) GetClient(creds *common.Credentials) (*http.Client, error) { + return oauth2.GetClient(provider.TripperFactory(), creds, provider) +} diff --git a/providers/instagram/instagram_test.go b/providers/instagram/instagram_test.go new file mode 100644 index 0000000..04b5394 --- /dev/null +++ b/providers/instagram/instagram_test.go @@ -0,0 +1,151 @@ +package instagram + +import ( + "github.com/stretchr/gomniauth/common" + "github.com/stretchr/gomniauth/oauth2" + "github.com/stretchr/gomniauth/test" + "github.com/stretchr/objx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "io/ioutil" + "net/http" + "strings" + "testing" +) + +func TestInstagramImplementrsProvider(t *testing.T) { + + var provider common.Provider + provider = new(InstagramProvider) + + assert.NotNil(t, provider) + +} + +func TestGetUser(t *testing.T) { + + g := New("clientID", "secret", "http://myapp.com/") + creds := &common.Credentials{Map: objx.MSI()} + + testTripperFactory := new(test.TestTripperFactory) + testTripper := new(test.TestTripper) + testTripperFactory.On("NewTripper", mock.Anything, g).Return(testTripper, nil) + testResponse := new(http.Response) + testResponse.Header = make(http.Header) + testResponse.Header.Set("Content-Type", "application/json") + testResponse.StatusCode = 200 + testResponse.Body = ioutil.NopCloser(strings.NewReader(`{ + "meta": { + "code": 200 + }, + "data": { + "username": "maggit", + "bio": "Programer who loves golang", + "website": "http://website.com", + "profile_picture": "http://myinsta.com", + "full_name": "Raquel H", + "counts": { + "media": 1232323, + "followed_by": 123, + "follows": 123 + }, + "id": "12345678" + } +}`)) + testTripper.On("RoundTrip", mock.Anything).Return(testResponse, nil) + + g.tripperFactory = testTripperFactory + + user, err := g.GetUser(creds) + + if assert.NoError(t, err) && assert.NotNil(t, user) { + + assert.Equal(t, user.Name(), "Raquel H") + assert.Equal(t, user.AuthCode(), "") // doesn't come from instagram + assert.Equal(t, user.Nickname(), "maggit") + assert.Equal(t, user.AvatarURL(), "http://myinsta.com") + assert.Equal(t, user.Data()["website"], "http://website.com") + + instagramCreds := user.ProviderCredentials()[instagramName] + if assert.NotNil(t, instagramCreds) { + assert.Equal(t, "uniqueid", instagramCreds.Get(common.CredentialsKeyID).Str()) + } + + } + +} + +func TestNewInstagram(t *testing.T) { + + g := New("clientID", "secret", "http://myapp.com/") + + if assert.NotNil(t, g) { + + // check config + if assert.NotNil(t, g.config) { + + assert.Equal(t, "clientID", g.config.Get(oauth2.OAuth2KeyClientID).Data()) + assert.Equal(t, "secret", g.config.Get(oauth2.OAuth2KeySecret).Data()) + assert.Equal(t, "http://myapp.com/", g.config.Get(oauth2.OAuth2KeyRedirectUrl).Data()) + assert.Equal(t, instagramDefaultScope, g.config.Get(oauth2.OAuth2KeyScope).Data()) + + assert.Equal(t, instagramAuthURL, g.config.Get(oauth2.OAuth2KeyAuthURL).Data()) + assert.Equal(t, instagramTokenURL, g.config.Get(oauth2.OAuth2KeyTokenURL).Data()) + + } + + } + +} + +func TestInstagramTripperFactory(t *testing.T) { + + g := New("clientID", "secret", "http://myapp.com/") + g.tripperFactory = nil + + f := g.TripperFactory() + + if assert.NotNil(t, f) { + assert.Equal(t, f, g.tripperFactory) + } + +} + +func TestInstagramName(t *testing.T) { + g := New("clientID", "secret", "http://myapp.com/") + assert.Equal(t, instagramName, g.Name()) +} + +func TestInstagramGetBeginAuthURL(t *testing.T) { + + common.SetSecurityKey("ABC123") + + state := &common.State{Map: objx.MSI("after", "http://www.stretchr.com/")} + + g := New("clientID", "secret", "http://myapp.com/") + + url, err := g.GetBeginAuthURL(state, nil) + + if assert.NoError(t, err) { + assert.Contains(t, url, "client_id=clientID") + assert.Contains(t, url, "redirect_uri=http%3A%2F%2Fmyapp.com%2F") + assert.Contains(t, url, "scope="+instagramDefaultScope) + assert.Contains(t, url, "access_type="+oauth2.OAuth2AccessTypeOnline) + assert.Contains(t, url, "approval_prompt="+oauth2.OAuth2ApprovalPromptAuto) + } + + state = &common.State{Map: objx.MSI("after", "http://www.stretchr.com/")} + + g = New("clientID", "secret", "http://myapp.com/") + + url, err = g.GetBeginAuthURL(state, objx.MSI(oauth2.OAuth2KeyScope, "avatar")) + + if assert.NoError(t, err) { + assert.Contains(t, url, "client_id=clientID") + assert.Contains(t, url, "redirect_uri=http%3A%2F%2Fmyapp.com%2F") + assert.Contains(t, url, "scope=avatar+"+instagramDefaultScope) + assert.Contains(t, url, "access_type="+oauth2.OAuth2AccessTypeOnline) + assert.Contains(t, url, "approval_prompt="+oauth2.OAuth2ApprovalPromptAuto) + } + +} diff --git a/providers/instagram/user.go b/providers/instagram/user.go new file mode 100644 index 0000000..ae76e59 --- /dev/null +++ b/providers/instagram/user.go @@ -0,0 +1,85 @@ +package instagram + +import ( + "github.com/stretchr/gomniauth/common" + "github.com/stretchr/objx" + "strconv" +) + +const ( + instagramKeyID string = "id" + instagramKeyName string = "full_name" + instagramKeyNickname string = "username" + instagramKeyAvatarUrl string = "profile_picture" +) + +type User struct { + data objx.Map +} + +// NewUser builds a new User object for Instagram. +func NewUser(data objx.Map, creds *common.Credentials, provider common.Provider) *User { + user := &User{data} + + creds.Set(common.CredentialsKeyID, data[instagramKeyID]) + // set provider credentials + user.data[common.UserKeyProviderCredentials] = map[string]*common.Credentials{ + provider.Name(): creds, + } + + return user +} + +// Name gets the users full name. +func (u *User) Name() string { + return u.Data().Get(instagramKeyName).Str() + +} + +// Nickname gets the users nickname or username. +func (u *User) Nickname() string { + return u.Data().Get(instagramKeyNickname).Str() + +} + +// Instagram API doesn't return email +func (u *User) Email() string { + return "" +} + +// AvatarURL gets the URL of an image representing the user. +func (u *User) AvatarURL() string { + return u.Data().Get(instagramKeyAvatarUrl).Str() +} + +// ProviderCredentials gets a map of Credentials (by provider name). +func (u *User) ProviderCredentials() map[string]*common.Credentials { + return u.Data().Get(common.UserKeyProviderCredentials).Data().(map[string]*common.Credentials) +} + +// IDForProvider gets the ID value for the specified provider name for +// this user from the ProviderCredentials data. +func (u *User) IDForProvider(provider string) string { + id := u.ProviderCredentials()[provider].Get(common.CredentialsKeyID).Data() + switch id.(type) { + case string: + return id.(string) + case float64: + return strconv.FormatFloat(id.(float64), 'f', 0, 64) + } + return "" +} + +// AuthCode gets this user's globally unique ID (generated by the host program) +func (u *User) AuthCode() string { + return u.Data().Get(common.UserKeyAuthCode).Str() +} + +// GetValue gets any User field by name. +func (u *User) Data() objx.Map { + return u.data +} + +func (u *User) PublicData(options map[string]interface{}) (publicData interface{}, err error) { + return u.data, nil +} diff --git a/providers/instagram/user_test.go b/providers/instagram/user_test.go new file mode 100644 index 0000000..8f69668 --- /dev/null +++ b/providers/instagram/user_test.go @@ -0,0 +1,68 @@ +package instagram + +import ( + "github.com/stretchr/gomniauth/common" + "github.com/stretchr/gomniauth/oauth2" + "github.com/stretchr/gomniauth/test" + "github.com/stretchr/objx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" +) + +func TestUserInterface(t *testing.T) { + + var user common.User = new(User) + + assert.NotNil(t, user) + +} + +func TestNewUser(t *testing.T) { + + testProvider := new(test.TestProvider) + testProvider.On("Name").Return("providerName") + + data := objx.MSI( + instagramKeyID, "123435467890", + instagramKeyName, "Raquel", + instagramKeyNickname, "maggit", + instagramKeyAvatarUrl, "http://instagram.com/") + creds := &common.Credentials{Map: objx.MSI(oauth2.OAuth2KeyAccessToken, "ABC12345")} + + user := NewUser(data, creds, testProvider) + + if assert.NotNil(t, user) { + + assert.Equal(t, data, user.Data()) + + assert.Equal(t, "Raquel", user.Name()) + assert.Equal(t, "maggit", user.Nickname()) + assert.Equal(t, "http://instagram.com/", user.AvatarURL()) + + // check provider credentials + creds := user.ProviderCredentials()[testProvider.Name()] + if assert.NotNil(t, creds) { + assert.Equal(t, "ABC12345", creds.Get(oauth2.OAuth2KeyAccessToken).Str()) + assert.Equal(t, "123435467890", creds.Get(common.CredentialsKeyID).Str()) + } + + } + + mock.AssertExpectationsForObjects(t, testProvider.Mock) + +} + +func TestIDForProvider(t *testing.T) { + + user := new(User) + user.data = objx.MSI( + common.UserKeyProviderCredentials, + map[string]*common.Credentials{ + "instagram": &common.Credentials{Map: objx.MSI(common.CredentialsKeyID, "instagramid")}, + "google": &common.Credentials{Map: objx.MSI(common.CredentialsKeyID, "googleid")}}) + + assert.Equal(t, "instagramid", user.IDForProvider("instagram")) + assert.Equal(t, "googleid", user.IDForProvider("google")) + +}