Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions providers/steam/steam.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,46 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
// buildUserObject is an internal function to build a goth.User object
// based in the data stored in r
func buildUserObject(r io.Reader, u goth.User) (goth.User, error) {
// Response object from Steam
apiResponse := struct {
bits, err := io.ReadAll(r)
if err != nil {
return u, err
}

// Decode the envelope only enough to reach the player object. Keep the
// player payload as RawMessage so we can decode it twice -- once into a
// typed struct for goth.User fields, once into u.RawData so callers can
// access Steam-specific fields that don't have a slot on goth.User
// (timecreated, primaryclanid, communityvisibilitystate, etc.) without
// mirroring every Steam field on the User struct.
envelope := struct {
Response struct {
Players []struct {
UserID string `json:"steamid"`
NickName string `json:"personaname"`
Name string `json:"realname"`
AvatarURL string `json:"avatarfull"`
LocationCountryCode string `json:"loccountrycode"`
LocationStateCode string `json:"locstatecode"`
} `json:"players"`
Players []json.RawMessage `json:"players"`
} `json:"response"`
}{}

err := json.NewDecoder(r).Decode(&apiResponse)
if err != nil {
if err := json.Unmarshal(bits, &envelope); err != nil {
return u, err
}

if l := len(apiResponse.Response.Players); l != 1 {
if l := len(envelope.Response.Players); l != 1 {
return u, fmt.Errorf("Expected one player in API response. Got %d.", l)
}
playerRaw := envelope.Response.Players[0]

player := struct {
UserID string `json:"steamid"`
NickName string `json:"personaname"`
Name string `json:"realname"`
AvatarURL string `json:"avatarfull"`
LocationCountryCode string `json:"loccountrycode"`
LocationStateCode string `json:"locstatecode"`
}{}
if err := json.Unmarshal(playerRaw, &player); err != nil {
return u, err
}
if err := json.Unmarshal(playerRaw, &u.RawData); err != nil {
return u, err
}

player := apiResponse.Response.Players[0]
u.UserID = player.UserID
u.Name = player.Name
if len(player.Name) == 0 {
Expand Down
103 changes: 103 additions & 0 deletions providers/steam/steam_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package steam_test

import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"

Expand Down Expand Up @@ -50,6 +54,105 @@ func Test_SessionFromJSON(t *testing.T) {
a.Equal(s.ResponseNonce, "2016-03-13T16:56:30ZJ8tlKVquwHi9ZSPV4ElU5PY2dmI=")
}

func Test_FetchUser(t *testing.T) {
// Regression test for the gap that left goth.User.RawData empty for the
// steam provider (originally raised in PR #518). Beyond the six typed
// fields the provider already mapped, RawData should expose the full
// Steam player payload so callers can read fields without a slot on
// goth.User -- communityvisibilitystate, primaryclanid, timecreated, etc.
apiUserSummaryPath := "/ISteamUser/GetPlayerSummaries/v0002/?key=%s&steamids=%s"

t.Parallel()
a := assert.New(t)
p := provider()
session, err := p.UnmarshalSession(`{"AuthURL":"https://steamcommunity.com/openid/login?openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&openid.realm=%3A%2F%2F&openid.return_to=%2Ffoo","SteamID":"1234567890","CallbackURL":"http://localhost:3030/","ResponseNonce":"2016-03-13T16:56:30ZJ8tlKVquwHi9ZSPV4ElU5PY2dmI="}`)
a.NoError(err)

expectedPath := fmt.Sprintf(apiUserSummaryPath, p.APIKey, "1234567890")

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
a.Equal("application/json", r.Header.Get("Accept"))
a.Equal(http.MethodGet, r.Method)
a.Equal(expectedPath, r.URL.RequestURI())
_, _ = w.Write([]byte(testUserSummaryBody))
}))
defer ts.Close()

p.HTTPClient = ts.Client()
p.HTTPClient.Transport = &httpTestTransport{server: ts}

user, err := p.FetchUser(session)
a.NoError(err)

// Typed fields land where they always did.
a.Equal("76561197960435530", user.UserID)
a.Equal("Robin", user.NickName)
a.Equal("Robin Walker", user.Name)
a.Equal("https://avatars.steamstatic.com/81b5478529dce13bf24b55ac42c1af7058aaf7a9_full.jpg", user.AvatarURL)
a.Equal("No email is provided by the Steam API", user.Email)
a.Equal("No description is provided by the Steam API", user.Description)
a.Equal("WA, US", user.Location)

// RawData mirrors the six typed fields ...
a.Equal("76561197960435530", user.RawData["steamid"])
a.Equal("Robin", user.RawData["personaname"])
a.Equal("Robin Walker", user.RawData["realname"])
a.Equal("https://avatars.steamstatic.com/81b5478529dce13bf24b55ac42c1af7058aaf7a9_full.jpg", user.RawData["avatarfull"])
a.Equal("US", user.RawData["loccountrycode"])
a.Equal("WA", user.RawData["locstatecode"])
// ... and also the fields without a slot on goth.User. These are the
// ones consumers had no other way to reach before; locking them in
// prevents a future refactor from silently shrinking the payload.
a.EqualValues(3, user.RawData["communityvisibilitystate"])
a.EqualValues(1, user.RawData["profilestate"])
a.EqualValues(0, user.RawData["personastate"])
a.Equal("103582791429521408", user.RawData["primaryclanid"])
a.EqualValues(1063407589, user.RawData["timecreated"])
a.Equal("https://steamcommunity.com/id/robinwalker/", user.RawData["profileurl"])
}

func provider() *steam.Provider {
return steam.New(os.Getenv("STEAM_KEY"), "/foo")
}

type httpTestTransport struct {
server *httptest.Server
}

func (t *httpTestTransport) RoundTrip(req *http.Request) (*http.Response, error) {
uri, err := url.Parse(t.server.URL)
if err != nil {
return nil, err
}

req.URL.Scheme = uri.Scheme
req.URL.Host = uri.Host

return http.DefaultTransport.RoundTrip(req)
}

// Reference: https://developer.valvesoftware.com/wiki/Steam_Web_API
// Extended beyond the six fields the typed struct extracts so the test also
// guards the RawData fallthrough for fields that have no slot on goth.User.
var testUserSummaryBody = `{
"response": {
"players": [
{
"steamid": "76561197960435530",
"communityvisibilitystate": 3,
"profilestate": 1,
"personaname": "Robin",
"profileurl": "https://steamcommunity.com/id/robinwalker/",
"avatar": "https://avatars.steamstatic.com/81b5478529dce13bf24b55ac42c1af7058aaf7a9.jpg",
"avatarmedium": "https://avatars.steamstatic.com/81b5478529dce13bf24b55ac42c1af7058aaf7a9_medium.jpg",
"avatarfull": "https://avatars.steamstatic.com/81b5478529dce13bf24b55ac42c1af7058aaf7a9_full.jpg",
"personastate": 0,
"primaryclanid": "103582791429521408",
"timecreated": 1063407589,
"realname": "Robin Walker",
"loccountrycode": "US",
"locstatecode": "WA"
}
]
}
}`