Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding custom JSON deserializer for UserInfo type #851

Merged
Merged
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
37 changes: 37 additions & 0 deletions internal/auth/authutil/user_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"
"time"
)

Expand All @@ -31,6 +33,41 @@ type UserInfo struct {
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

// UnmarshalJSON is a custom deserializer for the UserInfo type.
// A custom solution is necessary due to possible inconsistencies in value types.
func (u *UserInfo) UnmarshalJSON(b []byte) error {
type userInfo UserInfo
type userAlias struct {
*userInfo
RawEmailVerified interface{} `json:"email_verified,omitempty"`
}

alias := &userAlias{(*userInfo)(u), nil}

err := json.Unmarshal(b, alias)
if err != nil {
return err
}

if alias.RawEmailVerified != nil {
var emailVerified bool
switch rawEmailVerified := alias.RawEmailVerified.(type) {
case bool:
emailVerified = rawEmailVerified
case string:
emailVerified, err = strconv.ParseBool(rawEmailVerified)
if err != nil {
return err
}
default:
return fmt.Errorf("email_verified field expected to be bool or string, got: %s", reflect.TypeOf(rawEmailVerified))
}
alias.EmailVerified = &emailVerified
}

return nil
}
Comment on lines +52 to +69
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😞 It's unfortunate that we have to do this...


// FetchUserInfo fetches and parses user information with the provided access token.
func FetchUserInfo(httpClient *http.Client, baseDomain, token string) (*UserInfo, error) {
endpoint := url.URL{Scheme: "https", Host: baseDomain, Path: "/userinfo"}
Expand Down
27 changes: 26 additions & 1 deletion internal/auth/authutil/user_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestUserInfo(t *testing.T) {
assert.Equal(t, "Bearer token", r.Header.Get("authorization"))

w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"name": "Joe Bloggs"}`)
io.WriteString(w, `{"name": "Joe Bloggs","email_verified":true}`)
}))

defer ts.Close()
Expand All @@ -27,6 +27,25 @@ func TestUserInfo(t *testing.T) {

assert.NoError(t, err)
assert.Equal(t, "Joe Bloggs", *user.Name)
assert.Equal(t, true, *user.EmailVerified)
})

t.Run("Successfully call user info endpoint with string encoded email verified field", func(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer token", r.Header.Get("authorization"))

w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"email_verified":"true"}`)
}))

defer ts.Close()
parsedURL, err := url.Parse(ts.URL)
assert.NoError(t, err)

user, err := FetchUserInfo(ts.Client(), parsedURL.Host, "token")

assert.NoError(t, err)
assert.Equal(t, true, *user.EmailVerified)
})

testCases := []struct {
Expand All @@ -46,6 +65,12 @@ func TestUserInfo(t *testing.T) {
httpStatus: http.StatusOK,
response: `{ "foo": "bar" `,
},
{
name: "Email verified field not string or bool",
expect: "cannot decode response: email_verified field expected to be bool or string, got: float64",
httpStatus: http.StatusOK,
response: `{ "email_verified": 0 }`,
},
}

for _, testCase := range testCases {
Expand Down
Loading