Skip to content

Commit

Permalink
Adding instagram provider as requested in stretchr#10
Browse files Browse the repository at this point in the history
  • Loading branch information
maggit committed Jul 22, 2015
1 parent 82c20a1 commit 8c95f96
Show file tree
Hide file tree
Showing 4 changed files with 421 additions and 0 deletions.
117 changes: 117 additions & 0 deletions providers/instagram/instagram.go
Original file line number Diff line number Diff line change
@@ -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)
}
151 changes: 151 additions & 0 deletions providers/instagram/instagram_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

}
85 changes: 85 additions & 0 deletions providers/instagram/user.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 8c95f96

Please sign in to comment.