From b7370088db12d871a7c1bb4f0df89c60823dcb2c Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Tue, 19 Sep 2023 17:07:58 -0400 Subject: [PATCH 1/4] Adding custom JSON deserializer for UserInfo type --- internal/auth/authutil/user_info.go | 37 ++++++++++++++++++++++++ internal/auth/authutil/user_info_test.go | 21 +++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/internal/auth/authutil/user_info.go b/internal/auth/authutil/user_info.go index b38b3bc44..8c78e50f4 100644 --- a/internal/auth/authutil/user_info.go +++ b/internal/auth/authutil/user_info.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "net/url" + "reflect" + "strconv" "time" ) @@ -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: + panic(reflect.TypeOf(rawEmailVerified)) + } + alias.EmailVerified = &emailVerified + } + + return nil +} + // 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"} diff --git a/internal/auth/authutil/user_info_test.go b/internal/auth/authutil/user_info_test.go index 8622c7b54..2b8d3892f 100644 --- a/internal/auth/authutil/user_info_test.go +++ b/internal/auth/authutil/user_info_test.go @@ -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() @@ -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 { From 3f89c76231f229f8666454de86ed49ebd84ac044 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Tue, 19 Sep 2023 17:17:17 -0400 Subject: [PATCH 2/4] Adding error test case --- internal/auth/authutil/user_info.go | 3 ++- internal/auth/authutil/user_info_test.go | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/auth/authutil/user_info.go b/internal/auth/authutil/user_info.go index 8c78e50f4..5711f0dfe 100644 --- a/internal/auth/authutil/user_info.go +++ b/internal/auth/authutil/user_info.go @@ -2,6 +2,7 @@ package authutil import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -60,7 +61,7 @@ func (u *UserInfo) UnmarshalJSON(b []byte) error { return err } default: - panic(reflect.TypeOf(rawEmailVerified)) + return errors.New(fmt.Sprintf("unable to unmarshal JSON for email_verified field. Expected bool or string, got: %s", reflect.TypeOf(rawEmailVerified))) } alias.EmailVerified = &emailVerified } diff --git a/internal/auth/authutil/user_info_test.go b/internal/auth/authutil/user_info_test.go index 2b8d3892f..0fa1d4e22 100644 --- a/internal/auth/authutil/user_info_test.go +++ b/internal/auth/authutil/user_info_test.go @@ -65,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: unable to unmarshal JSON for email_verified field. Expected bool or string, got: float64", + httpStatus: http.StatusOK, + response: `{ "email_verified": 0 }`, + }, } for _, testCase := range testCases { From 82c39794b9ad699b266b398291b827361024a6d5 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Tue, 19 Sep 2023 17:24:55 -0400 Subject: [PATCH 3/4] Errorf instead of sprintf --- internal/auth/authutil/user_info.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/auth/authutil/user_info.go b/internal/auth/authutil/user_info.go index 5711f0dfe..5baa7dd25 100644 --- a/internal/auth/authutil/user_info.go +++ b/internal/auth/authutil/user_info.go @@ -2,7 +2,6 @@ package authutil import ( "encoding/json" - "errors" "fmt" "net/http" "net/url" @@ -61,7 +60,7 @@ func (u *UserInfo) UnmarshalJSON(b []byte) error { return err } default: - return errors.New(fmt.Sprintf("unable to unmarshal JSON for email_verified field. Expected bool or string, got: %s", reflect.TypeOf(rawEmailVerified))) + return fmt.Errorf("unable to unmarshal JSON for email_verified field. Expected bool or string, got: %s", reflect.TypeOf(rawEmailVerified)) } alias.EmailVerified = &emailVerified } From 286541ee4bf6b88ff6eec3fda610dbe83c2ba030 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Tue, 19 Sep 2023 17:27:15 -0400 Subject: [PATCH 4/4] Removing error stutter --- internal/auth/authutil/user_info.go | 2 +- internal/auth/authutil/user_info_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/auth/authutil/user_info.go b/internal/auth/authutil/user_info.go index 5baa7dd25..eebcc5fb2 100644 --- a/internal/auth/authutil/user_info.go +++ b/internal/auth/authutil/user_info.go @@ -60,7 +60,7 @@ func (u *UserInfo) UnmarshalJSON(b []byte) error { return err } default: - return fmt.Errorf("unable to unmarshal JSON for email_verified field. Expected bool or string, got: %s", reflect.TypeOf(rawEmailVerified)) + return fmt.Errorf("email_verified field expected to be bool or string, got: %s", reflect.TypeOf(rawEmailVerified)) } alias.EmailVerified = &emailVerified } diff --git a/internal/auth/authutil/user_info_test.go b/internal/auth/authutil/user_info_test.go index 0fa1d4e22..d0f79d1ec 100644 --- a/internal/auth/authutil/user_info_test.go +++ b/internal/auth/authutil/user_info_test.go @@ -67,7 +67,7 @@ func TestUserInfo(t *testing.T) { }, { name: "Email verified field not string or bool", - expect: "cannot decode response: unable to unmarshal JSON for email_verified field. Expected bool or string, got: float64", + expect: "cannot decode response: email_verified field expected to be bool or string, got: float64", httpStatus: http.StatusOK, response: `{ "email_verified": 0 }`, },