Skip to content

Commit ad51e76

Browse files
authoredJun 8, 2022
Merge pull request #216 from SkynetLabs/ivo/email_type
Introduce an Email type that handles capitalization.
2 parents 4cd88bb + 0dcf2b3 commit ad51e76

17 files changed

+252
-108
lines changed
 

‎api/auth_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/SkynetLabs/skynet-accounts/database"
1212
"github.com/SkynetLabs/skynet-accounts/jwt"
13+
"github.com/SkynetLabs/skynet-accounts/types"
1314
"github.com/sirupsen/logrus"
1415
"gitlab.com/NebulousLabs/errors"
1516
"gitlab.com/NebulousLabs/fastrand"
@@ -47,7 +48,7 @@ func TestTokenFromRequest(t *testing.T) {
4748
if err != nil {
4849
t.Fatal(err)
4950
}
50-
tk, err := jwt.TokenForUser(t.Name()+"@siasky.net", t.Name()+"_sub")
51+
tk, err := jwt.TokenForUser(types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"_sub")
5152
if err != nil {
5253
t.Fatal(err)
5354
}
@@ -97,7 +98,7 @@ func TestTokenFromRequest(t *testing.T) {
9798

9899
// Token from request with a header and a cookie. Expect the header to take
99100
// precedence.
100-
tk2, err := jwt.TokenForUser(t.Name()+"2@siasky.net", t.Name()+"2_sub")
101+
tk2, err := jwt.TokenForUser(types.NewEmail(t.Name()+"2@siasky.net"), t.Name()+"2_sub")
101102
if err != nil {
102103
t.Fatal(err)
103104
}

‎api/handlers.go

+17-16
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/SkynetLabs/skynet-accounts/lib"
2020
"github.com/SkynetLabs/skynet-accounts/metafetcher"
2121
"github.com/SkynetLabs/skynet-accounts/skynet"
22+
"github.com/SkynetLabs/skynet-accounts/types"
2223
"github.com/julienschmidt/httprouter"
2324
jwt2 "github.com/lestrrat-go/jwx/jwt"
2425
"gitlab.com/NebulousLabs/errors"
@@ -125,16 +126,16 @@ type (
125126

126127
// credentialsPOST defines the standard credentials package we expect.
127128
credentialsPOST struct {
128-
Email string `json:"email"`
129-
Password string `json:"password"`
129+
Email types.Email `json:"email"`
130+
Password string `json:"password"`
130131
}
131132

132133
// userUpdatePUT defines the fields of the User record that can be changed
133134
// externally, e.g. by calling `PUT /user`.
134135
userUpdatePUT struct {
135-
Email string `json:"email,omitempty"`
136-
Password string `json:"password,omitempty"`
137-
StripeID string `json:"stripeCustomerId,omitempty"`
136+
Email types.Email `json:"email,omitempty"`
137+
Password string `json:"password,omitempty"`
138+
StripeID string `json:"stripeCustomerId,omitempty"`
138139
}
139140
)
140141

@@ -231,7 +232,7 @@ func (api *API) loginPOSTChallengeResponse(w http.ResponseWriter, req *http.Requ
231232
}
232233

233234
// loginPOSTCredentials is a helper that handles logins with credentials.
234-
func (api *API) loginPOSTCredentials(w http.ResponseWriter, req *http.Request, email, password string) {
235+
func (api *API) loginPOSTCredentials(w http.ResponseWriter, req *http.Request, email types.Email, password string) {
235236
// Fetch the user with that email, if they exist.
236237
u, err := api.staticDB.UserByEmail(req.Context(), email)
237238
if err != nil {
@@ -388,8 +389,8 @@ func (api *API) registerPOST(_ *database.User, w http.ResponseWriter, req *http.
388389
api.WriteError(w, errors.AddContext(err, "failed to parse request body"), http.StatusBadRequest)
389390
return
390391
}
391-
parsed, err := mail.ParseAddress(payload.Email)
392-
if err != nil || payload.Email != parsed.Address {
392+
parsed, err := mail.ParseAddress(payload.Email.String())
393+
if err != nil || payload.Email.String() != parsed.Address {
393394
api.WriteError(w, errors.New("invalid email provided"), http.StatusBadRequest)
394395
return
395396
}
@@ -616,8 +617,8 @@ func (api *API) userPOST(_ *database.User, w http.ResponseWriter, req *http.Requ
616617
api.WriteError(w, errors.New("email is required"), http.StatusBadRequest)
617618
return
618619
}
619-
parsed, err := mail.ParseAddress(payload.Email)
620-
if err != nil || payload.Email != parsed.Address {
620+
parsed, err := mail.ParseAddress(payload.Email.String())
621+
if err != nil || payload.Email.String() != parsed.Address {
621622
api.WriteError(w, errors.New("invalid email provided"), http.StatusBadRequest)
622623
return
623624
}
@@ -714,8 +715,8 @@ func (api *API) userPUT(u *database.User, w http.ResponseWriter, req *http.Reque
714715

715716
var changedEmail bool
716717
if payload.Email != "" {
717-
parsed, err := mail.ParseAddress(payload.Email)
718-
if err != nil || payload.Email != parsed.Address {
718+
parsed, err := mail.ParseAddress(payload.Email.String())
719+
if err != nil || payload.Email.String() != parsed.Address {
719720
api.WriteError(w, errors.New("invalid email provided"), http.StatusBadRequest)
720721
return
721722
}
@@ -995,10 +996,10 @@ func (api *API) userRecoverRequestPOST(_ *database.User, w http.ResponseWriter,
995996
return
996997
}
997998

998-
// Read and parse the request body.
999-
var payload struct {
1000-
Email string `json:"email"`
1001-
}
999+
// Read and parse the request body. We do not expect a password but we want
1000+
// to use the same email parsing approach in all cases where we get an email
1001+
// address from the user.
1002+
var payload credentialsPOST
10021003
err = parseRequestBodyJSON(req.Body, LimitBodySizeSmall, &payload)
10031004
if err != nil {
10041005
err = errors.AddContext(err, "failed to parse request body")

‎api/upload.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/SkynetLabs/skynet-accounts/database"
8+
"github.com/SkynetLabs/skynet-accounts/types"
89
"github.com/julienschmidt/httprouter"
910
"gitlab.com/NebulousLabs/errors"
1011
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -14,7 +15,7 @@ type (
1415
// UploaderInfo gives information about a user who created an upload.
1516
UploaderInfo struct {
1617
UserID primitive.ObjectID
17-
Email string
18+
Email types.Email
1819
Sub string
1920
StripeID string
2021
}

‎database/user.go

+11-10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/SkynetLabs/skynet-accounts/hash"
1111
"github.com/SkynetLabs/skynet-accounts/lib"
1212
"github.com/SkynetLabs/skynet-accounts/skynet"
13+
"github.com/SkynetLabs/skynet-accounts/types"
1314
"gitlab.com/NebulousLabs/errors"
1415
"gitlab.com/SkynetLabs/skyd/build"
1516
"go.mongodb.org/mongo-driver/bson"
@@ -109,7 +110,7 @@ type (
109110
// ID is auto-generated by Mongo on insert. We will usually use it in
110111
// its ID.Hex() form.
111112
ID primitive.ObjectID `bson:"_id,omitempty" json:"-"`
112-
Email string `bson:"email" json:"email"`
113+
Email types.Email `bson:"email" json:"email"`
113114
EmailConfirmationToken string `bson:"email_confirmation_token,omitempty" json:"-"`
114115
EmailConfirmationTokenExpiration time.Time `bson:"email_confirmation_token_expiration,omitempty" json:"-"`
115116
PasswordHash string `bson:"password_hash" json:"-"`
@@ -140,8 +141,8 @@ type (
140141
)
141142

142143
// UserByEmail returns the user with the given username.
143-
func (db *DB) UserByEmail(ctx context.Context, email string) (*User, error) {
144-
users, err := db.managedUsersByField(ctx, "email", email)
144+
func (db *DB) UserByEmail(ctx context.Context, email types.Email) (*User, error) {
145+
users, err := db.managedUsersByField(ctx, "email", email.String())
145146
if err != nil {
146147
return nil, err
147148
}
@@ -263,14 +264,14 @@ func (db *DB) UserConfirmEmail(ctx context.Context, token string) (*User, error)
263264
//
264265
// The new user is created as "unconfirmed" and a confirmation email is sent to
265266
// the address they provided.
266-
func (db *DB) UserCreate(ctx context.Context, emailAddr, pass, sub string, tier int) (*User, error) {
267+
func (db *DB) UserCreate(ctx context.Context, emailAddr types.Email, pass, sub string, tier int) (*User, error) {
267268
// Ensure the email is valid if it's passed. We allow empty emails.
268269
if emailAddr != "" {
269-
addr, err := mail.ParseAddress(emailAddr)
270+
addr, err := mail.ParseAddress(emailAddr.String())
270271
if err != nil {
271272
return nil, errors.AddContext(err, "invalid email address")
272273
}
273-
emailAddr = addr.Address
274+
emailAddr = types.NewEmail(addr.Address)
274275
}
275276
if sub == "" {
276277
return nil, errors.New("empty sub is not allowed")
@@ -367,14 +368,14 @@ func (db *DB) UserCreateEmailConfirmation(ctx context.Context, uID primitive.Obj
367368
//
368369
// The new user is created as "unconfirmed" and a confirmation email is sent to
369370
// the address they provided.
370-
func (db *DB) UserCreatePK(ctx context.Context, emailAddr, pass, sub string, pk PubKey, tier int) (*User, error) {
371+
func (db *DB) UserCreatePK(ctx context.Context, emailAddr types.Email, pass, sub string, pk PubKey, tier int) (*User, error) {
371372
// Validate the email.
372-
parsed, err := mail.ParseAddress(emailAddr)
373-
if err != nil || parsed.Address != emailAddr {
373+
parsed, err := mail.ParseAddress(emailAddr.String())
374+
if err != nil || parsed.Address != emailAddr.String() {
374375
return nil, errors.AddContext(err, "invalid email address")
375376
}
376377
// Check for an existing user with this email.
377-
users, err := db.managedUsersByField(ctx, "email", emailAddr)
378+
users, err := db.managedUsersByField(ctx, "email", emailAddr.String())
378379
if err != nil && !errors.Contains(err, ErrUserNotFound) {
379380
return nil, errors.AddContext(err, "failed to query DB")
380381
}

‎email/mailer.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/SkynetLabs/skynet-accounts/database"
7+
"github.com/SkynetLabs/skynet-accounts/types"
78
)
89

910
/**
@@ -35,15 +36,15 @@ func (em Mailer) Send(ctx context.Context, m database.EmailMessage) error {
3536

3637
// SendAddressConfirmationEmail sends a new email to the given email address
3738
// with a link to confirm the ownership of the address.
38-
func (em Mailer) SendAddressConfirmationEmail(ctx context.Context, email, token string) error {
39-
m := confirmEmailEmail(email, token)
39+
func (em Mailer) SendAddressConfirmationEmail(ctx context.Context, email types.Email, token string) error {
40+
m := confirmEmailEmail(email.String(), token)
4041
return em.Send(ctx, *m)
4142
}
4243

4344
// SendRecoverAccountEmail sends a new email to the given email address
4445
// with a link to recover the account.
45-
func (em Mailer) SendRecoverAccountEmail(ctx context.Context, email, token string) error {
46-
m := recoverAccountEmail(email, token)
46+
func (em Mailer) SendRecoverAccountEmail(ctx context.Context, email types.Email, token string) error {
47+
m := recoverAccountEmail(email.String(), token)
4748
return em.Send(ctx, *m)
4849
}
4950

@@ -52,7 +53,7 @@ func (em Mailer) SendRecoverAccountEmail(ctx context.Context, email, token strin
5253
// recover a Skynet account but their email is not in our system. The main
5354
// reason to do that is because the user might have forgotten which email they
5455
// used for signing up.
55-
func (em Mailer) SendAccountAccessAttemptedEmail(ctx context.Context, email string) error {
56-
m := accountAccessAttemptedEmail(email)
56+
func (em Mailer) SendAccountAccessAttemptedEmail(ctx context.Context, email types.Email) error {
57+
m := accountAccessAttemptedEmail(email.String())
5758
return em.Send(ctx, *m)
5859
}

‎jwt/jwt.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io/ioutil"
77
"time"
88

9+
"github.com/SkynetLabs/skynet-accounts/types"
910
"github.com/lestrrat-go/jwx/jwa"
1011
"github.com/lestrrat-go/jwx/jwk"
1112
"github.com/lestrrat-go/jwx/jwt"
@@ -74,7 +75,7 @@ func ContextWithToken(ctx context.Context, token jwt.Token) context.Context {
7475
//
7576
// The tokens generated by this function are a slimmed down version of the ones
7677
// described in ValidateToken's docstring.
77-
func TokenForUser(email, sub string) (jwt.Token, error) {
78+
func TokenForUser(email types.Email, sub string) (jwt.Token, error) {
7879
sigAlgo, key, err := signatureAlgoAndKey()
7980
if err != nil {
8081
return nil, err
@@ -252,15 +253,15 @@ func signatureAlgoAndKey() (jwa.SignatureAlgorithm, jwk.Key, error) {
252253

253254
// tokenForUser is a helper method that puts together an unsigned token based
254255
// on the provided values.
255-
func tokenForUser(emailAddr, sub string) (jwt.Token, error) {
256+
func tokenForUser(emailAddr types.Email, sub string) (jwt.Token, error) {
256257
if emailAddr == "" || sub == "" {
257258
return nil, errors.New("email and sub cannot be empty")
258259
}
259260
session := tokenSession{
260261
Active: true,
261262
Identity: tokenIdentity{
262263
Traits: tokenTraits{
263-
Email: emailAddr,
264+
Email: emailAddr.String(),
264265
},
265266
},
266267
}

‎jwt/jwt_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/SkynetLabs/skynet-accounts/types"
910
"github.com/lestrrat-go/jwx/jwa"
1011
"github.com/lestrrat-go/jwx/jwt"
1112
"github.com/sirupsen/logrus"
@@ -19,7 +20,7 @@ func TestJWT(t *testing.T) {
1920
if err != nil {
2021
t.Fatal(err)
2122
}
22-
email := t.Name() + "@siasky.net"
23+
email := types.NewEmail(t.Name() + "@siasky.net")
2324
sub := "this is a sub"
2425
fakeSub := "fake sub"
2526
tk, err := TokenForUser(email, sub)
@@ -59,7 +60,7 @@ func TestValidateToken_Expired(t *testing.T) {
5960
if err != nil {
6061
t.Fatal(err)
6162
}
62-
email := t.Name() + "@siasky.net"
63+
email := types.NewEmail(t.Name() + "@siasky.net")
6364
sub := "this is a sub"
6465
// Fetch the tools we need in order to craft a custom token.
6566
key, found := AccountsJWKS.Get(0)
@@ -81,7 +82,7 @@ func TestValidateToken_Expired(t *testing.T) {
8182
Active: true,
8283
Identity: tokenIdentity{
8384
Traits: tokenTraits{
84-
Email: email,
85+
Email: email.String(),
8586
},
8687
},
8788
}

‎test/api/api_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/SkynetLabs/skynet-accounts/api"
1212
"github.com/SkynetLabs/skynet-accounts/database"
1313
"github.com/SkynetLabs/skynet-accounts/test"
14+
"github.com/SkynetLabs/skynet-accounts/types"
1415
"gitlab.com/NebulousLabs/fastrand"
1516
"go.sia.tech/siad/build"
1617

@@ -33,9 +34,9 @@ func TestWithDBSession(t *testing.T) {
3334
t.Fatal("Failed to instantiate API.", err)
3435
}
3536

36-
emailSuccess := t.Name() + "success@siasky.net"
37-
emailSuccessJSON := t.Name() + "success_json@siasky.net"
38-
emailFailure := t.Name() + "failure@siasky.net"
37+
emailSuccess := types.NewEmail(t.Name() + "success@siasky.net")
38+
emailSuccessJSON := types.NewEmail(t.Name() + "success_json@siasky.net")
39+
emailFailure := types.NewEmail(t.Name() + "failure@siasky.net")
3940

4041
// This handler successfully creates a user in the DB and exits with
4142
// a success status code. We expect the user to exist in the DB after
@@ -52,7 +53,7 @@ func TestWithDBSession(t *testing.T) {
5253
t.Fatal("Failed to fetch user from DB.", err)
5354
}
5455
if u.Email != emailSuccess {
55-
t.Fatalf("Expected email %s, got %s.", emailSuccess, u.Email)
56+
t.Fatalf("Expected email '%v', got '%v'.", emailSuccess, u.Email)
5657
}
5758
testAPI.WriteSuccess(w)
5859
}
@@ -147,7 +148,7 @@ func TestUserTierCache(t *testing.T) {
147148
}
148149
}()
149150

150-
emailAddr := test.DBNameForTest(t.Name()) + "@siasky.net"
151+
emailAddr := types.NewEmail(test.DBNameForTest(t.Name()) + "@siasky.net")
151152
password := hex.EncodeToString(fastrand.Bytes(16))
152153
u, err := test.CreateUser(at, emailAddr, password)
153154
if err != nil {
@@ -165,7 +166,7 @@ func TestUserTierCache(t *testing.T) {
165166
if err != nil {
166167
t.Fatal(err)
167168
}
168-
r, _, err := at.LoginCredentialsPOST(emailAddr, password)
169+
r, _, err := at.LoginCredentialsPOST(emailAddr.String(), password)
169170
if err != nil {
170171
t.Fatal(err)
171172
}

‎test/api/apikeys_test.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/SkynetLabs/skynet-accounts/api"
99
"github.com/SkynetLabs/skynet-accounts/database"
1010
"github.com/SkynetLabs/skynet-accounts/test"
11+
"github.com/SkynetLabs/skynet-accounts/types"
1112
"gitlab.com/NebulousLabs/fastrand"
1213
"go.sia.tech/siad/modules"
1314
)
@@ -16,7 +17,8 @@ import (
1617
// API keys.
1718
func testPrivateAPIKeysFlow(t *testing.T, at *test.AccountsTester) {
1819
name := test.DBNameForTest(t.Name())
19-
r, body, err := at.UserPOST(name+"@siasky.net", name+"_pass")
20+
email := types.NewEmail(name + "@siasky.net")
21+
r, body, err := at.UserPOST(email.String(), name+"_pass")
2022
if err != nil {
2123
t.Fatal(err, string(body))
2224
}
@@ -96,8 +98,8 @@ func testPrivateAPIKeysFlow(t *testing.T, at *test.AccountsTester) {
9698
func testPrivateAPIKeysUsage(t *testing.T, at *test.AccountsTester) {
9799
name := test.DBNameForTest(t.Name())
98100
// Create a test user.
99-
email := name + "@siasky.net"
100-
r, _, err := at.UserPOST(email, name+"_pass")
101+
email := types.NewEmail(name + "@siasky.net")
102+
r, _, err := at.UserPOST(email.String(), name+"_pass")
101103
if err != nil {
102104
t.Fatal(err)
103105
}
@@ -138,7 +140,8 @@ func testPrivateAPIKeysUsage(t *testing.T, at *test.AccountsTester) {
138140
// API keys.
139141
func testPublicAPIKeysFlow(t *testing.T, at *test.AccountsTester) {
140142
name := test.DBNameForTest(t.Name())
141-
r, body, err := at.UserPOST(name+"@siasky.net", name+"_pass")
143+
email := types.NewEmail(name + "@siasky.net")
144+
r, body, err := at.UserPOST(email.String(), name+"_pass")
142145
if err != nil {
143146
t.Fatal(err, string(body))
144147
}
@@ -234,8 +237,8 @@ func testPublicAPIKeysFlow(t *testing.T, at *test.AccountsTester) {
234237
func testPublicAPIKeysUsage(t *testing.T, at *test.AccountsTester) {
235238
name := test.DBNameForTest(t.Name())
236239
// Create a test user.
237-
email := name + "@siasky.net"
238-
r, _, err := at.UserPOST(email, name+"_pass")
240+
email := types.NewEmail(name + "@siasky.net")
241+
r, _, err := at.UserPOST(email.String(), name+"_pass")
239242
if err != nil {
240243
t.Fatal(err)
241244
}
@@ -318,7 +321,8 @@ func testPublicAPIKeysUsage(t *testing.T, at *test.AccountsTester) {
318321
func testAPIKeysAcceptance(t *testing.T, at *test.AccountsTester) {
319322
name := test.DBNameForTest(t.Name())
320323
// Create a test user.
321-
r, _, err := at.UserPOST(name+"@siasky.net", name+"_pass")
324+
email := types.NewEmail(name + "@siasky.net")
325+
r, _, err := at.UserPOST(email.String(), name+"_pass")
322326
if err != nil {
323327
t.Fatal(err)
324328
}

‎test/api/challenge_test.go

+9-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/SkynetLabs/skynet-accounts/database"
1111
"github.com/SkynetLabs/skynet-accounts/test"
12+
"github.com/SkynetLabs/skynet-accounts/types"
1213
"gitlab.com/NebulousLabs/errors"
1314
"gitlab.com/NebulousLabs/fastrand"
1415
"go.sia.tech/siad/crypto"
@@ -45,8 +46,8 @@ func testRegistration(t *testing.T, at *test.AccountsTester) {
4546
// Solve the challenge.
4647
response := append(chBytes, append([]byte(database.ChallengeTypeRegister), []byte(database.PortalName)...)...)
4748
sig := ed25519.Sign(sk[:], response)
48-
emailStr := name + "@siasky.net"
49-
u, status, err := at.RegisterPOST(response, sig, emailStr)
49+
emailStr := types.NewEmail(name + "@siasky.net")
50+
u, status, err := at.RegisterPOST(response, sig, emailStr.String())
5051
if err != nil {
5152
t.Fatalf("Failed to register. Status %d, error '%s'", status, err)
5253
}
@@ -89,8 +90,8 @@ func testLogin(t *testing.T, at *test.AccountsTester) {
8990
}
9091
response := append(chBytes, append([]byte(database.ChallengeTypeRegister), []byte(database.PortalName)...)...)
9192
sig := ed25519.Sign(sk[:], response)
92-
emailStr := name + "@siasky.net"
93-
u, status, err := at.RegisterPOST(response, sig, emailStr)
93+
emailStr := types.NewEmail(name + "@siasky.net")
94+
u, status, err := at.RegisterPOST(response, sig, emailStr.String())
9495
if err != nil {
9596
t.Fatalf("Failed to validate the response. Status %d, error '%s'", status, err)
9697
}
@@ -119,8 +120,8 @@ func testLogin(t *testing.T, at *test.AccountsTester) {
119120
// Solve the challenge.
120121
response = append(chBytes, append([]byte(database.ChallengeTypeLogin), []byte(database.PortalName)...)...)
121122
sig = ed25519.Sign(sk[:], response)
122-
emailStr = name + "@siasky.net"
123-
r, b, err := at.LoginPubKeyPOST(response, sig, emailStr)
123+
emailStr = types.NewEmail(name + "@siasky.net")
124+
r, b, err := at.LoginPubKeyPOST(response, sig, emailStr.String())
124125
if err != nil {
125126
t.Fatalf("Failed to login. Status %d, body '%s', error '%s'", r.StatusCode, string(b), err)
126127
}
@@ -164,7 +165,7 @@ func testUserAddPubKey(t *testing.T, at *test.AccountsTester) {
164165

165166
// Request a challenge with a pubKey that belongs to another user.
166167
_, pk2 := crypto.GenerateKeyPair()
167-
_, err = at.DB.UserCreatePK(at.Ctx, name+"_other@siasky.net", "", name+"_other_sub", pk2[:], database.TierFree)
168+
_, err = at.DB.UserCreatePK(at.Ctx, types.NewEmail(name+"_other@siasky.net"), "", name+"_other_sub", pk2[:], database.TierFree)
168169
if err != nil {
169170
t.Fatal(err)
170171
}
@@ -198,7 +199,7 @@ func testUserAddPubKey(t *testing.T, at *test.AccountsTester) {
198199
// Try to solve the challenge while logged in as a different user.
199200
// NOTE: This will consume the challenge and the user will need to request
200201
// a new one.
201-
r, b, err := at.UserPOST(name+"_user3@siasky.net", name+"_pass")
202+
r, b, err := at.UserPOST(types.NewEmail(name+"_user3@siasky.net").String(), name+"_pass")
202203
if err != nil || r.StatusCode != http.StatusOK {
203204
t.Fatal(r.Status, err, string(b))
204205
}

‎test/api/handlers_test.go

+50-23
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/SkynetLabs/skynet-accounts/jwt"
1818
"github.com/SkynetLabs/skynet-accounts/skynet"
1919
"github.com/SkynetLabs/skynet-accounts/test"
20+
"github.com/SkynetLabs/skynet-accounts/types"
2021
"gitlab.com/NebulousLabs/errors"
2122
"gitlab.com/NebulousLabs/fastrand"
2223
"gitlab.com/SkynetLabs/skyd/skymodules"
@@ -101,7 +102,7 @@ func testHandlerHealthGET(t *testing.T, at *test.AccountsTester) {
101102
func testHandlerUserPOST(t *testing.T, at *test.AccountsTester) {
102103
// Use the test's name as an email-compatible identifier.
103104
name := test.DBNameForTest(t.Name())
104-
emailAddr := name + "@siasky.net"
105+
emailAddr := types.NewEmail(name + "@siasky.net")
105106
password := hex.EncodeToString(fastrand.Bytes(16))
106107
// Try to create a user with a missing email.
107108
_, _, err := at.UserPOST("", password)
@@ -119,12 +120,12 @@ func testHandlerUserPOST(t *testing.T, at *test.AccountsTester) {
119120
t.Fatalf("Expected user creation to fail with '%s', got '%s'. Body: '%s'", badRequest, err, string(b))
120121
}
121122
// Try to create a user with an empty password.
122-
_, b, err = at.UserPOST(emailAddr, "")
123+
_, b, err = at.UserPOST(emailAddr.String(), "")
123124
if err == nil || !strings.Contains(err.Error(), badRequest) {
124125
t.Fatalf("Expected user creation to fail with '%s', got '%s'. Body: '%s", badRequest, err, string(b))
125126
}
126127
// Create a user.
127-
_, b, err = at.UserPOST(emailAddr, password)
128+
_, b, err = at.UserPOST(emailAddr.String(), password)
128129
if err != nil {
129130
t.Fatalf("User creation failed. Error: '%s'. Body: '%s' ", err.Error(), string(b))
130131
}
@@ -147,23 +148,23 @@ func testHandlerUserPOST(t *testing.T, at *test.AccountsTester) {
147148
}
148149
}(u)
149150
// Log in with that user in order to make sure it exists.
150-
_, b, err = at.LoginCredentialsPOST(emailAddr, password)
151+
_, b, err = at.LoginCredentialsPOST(emailAddr.String(), password)
151152
if err != nil {
152153
t.Fatalf("Login failed. Error: '%s'. Body: '%s'", err.Error(), string(b))
153154
}
154155
// try to create a user with an already taken email
155-
_, b, err = at.UserPOST(emailAddr, "password")
156+
_, b, err = at.UserPOST(emailAddr.String(), "password")
156157
if err == nil || !strings.Contains(err.Error(), badRequest) {
157158
t.Fatalf("Expected user creation to fail with '%s', got '%s'. Body: '%s'", badRequest, err, string(b))
158159
}
159160
}
160161

161162
// testHandlerLoginPOST tests the /login endpoint.
162163
func testHandlerLoginPOST(t *testing.T, at *test.AccountsTester) {
163-
emailAddr := test.DBNameForTest(t.Name()) + "@siasky.net"
164+
emailAddr := types.NewEmail(test.DBNameForTest(t.Name()) + "@siasky.net")
164165
password := hex.EncodeToString(fastrand.Bytes(16))
165166
// Try logging in with a non-existent user.
166-
_, _, err := at.LoginCredentialsPOST(emailAddr, password)
167+
_, _, err := at.LoginCredentialsPOST(emailAddr.String(), password)
167168
if err == nil || !strings.Contains(err.Error(), unauthorized) {
168169
t.Fatalf("Expected '%s', got '%s'", unauthorized, err)
169170
}
@@ -177,7 +178,7 @@ func testHandlerLoginPOST(t *testing.T, at *test.AccountsTester) {
177178
}
178179
}()
179180
// Login with an existing user.
180-
r, _, err := at.LoginCredentialsPOST(emailAddr, password)
181+
r, _, err := at.LoginCredentialsPOST(emailAddr.String(), password)
181182
if err != nil {
182183
t.Fatal(err)
183184
}
@@ -186,6 +187,12 @@ func testHandlerLoginPOST(t *testing.T, at *test.AccountsTester) {
186187
if c == nil {
187188
t.Fatal("Expected a cookie.")
188189
}
190+
// Login with an email with a different capitalisation.
191+
// Expect this to succeed.
192+
_, _, err = at.LoginCredentialsPOST(strings.ToUpper(emailAddr.String()), password)
193+
if err != nil {
194+
t.Fatal(err)
195+
}
189196
// Make sure the returned cookie is usable for making requests.
190197
at.SetCookie(c)
191198
defer at.ClearCredentials()
@@ -222,7 +229,7 @@ func testHandlerLoginPOST(t *testing.T, at *test.AccountsTester) {
222229
t.Fatalf("Expected %s, got %s", unauthorized, err)
223230
}
224231
// Try logging in with a bad password.
225-
_, _, err = at.LoginCredentialsPOST(emailAddr, "bad password")
232+
_, _, err = at.LoginCredentialsPOST(emailAddr.String(), "bad password")
226233
if err == nil || !strings.Contains(err.Error(), unauthorized) {
227234
t.Fatalf("Expected '%s', got '%s'", unauthorized, err)
228235
}
@@ -295,17 +302,17 @@ func testUserPUT(t *testing.T, at *test.AccountsTester) {
295302
}
296303
// Check if we can login with the new password.
297304
params := url.Values{}
298-
params.Set("email", u.Email)
305+
params.Set("email", u.Email.String())
299306
params.Set("password", pw)
300307
// Try logging in with a non-existent user.
301-
_, _, err = at.LoginCredentialsPOST(u.Email, pw)
308+
_, _, err = at.LoginCredentialsPOST(u.Email.String(), pw)
302309
if err != nil {
303310
t.Fatal(err)
304311
}
305312

306313
// Update the user's email.
307-
emailAddr := name + "_new@siasky.net"
308-
_, status, err = at.UserPUT(emailAddr, "", "")
314+
emailAddr := types.NewEmail(name + "_new@siasky.net")
315+
_, status, err = at.UserPUT(emailAddr.String(), "", "")
309316
if err != nil || status != http.StatusOK {
310317
t.Fatal(status, err)
311318
}
@@ -323,14 +330,34 @@ func testUserPUT(t *testing.T, at *test.AccountsTester) {
323330
t.Fatalf("Expected the user to have a non-empty confirmation token, got '%s'", u3.EmailConfirmationToken)
324331
}
325332
// Expect to find a confirmation email queued for sending.
326-
filer := bson.M{"to": emailAddr}
333+
filer := bson.M{"to": emailAddr.String()}
327334
_, msgs, err := at.DB.FindEmails(at.Ctx, filer, &options.FindOptions{})
328335
if err != nil {
329336
t.Fatal(err)
330337
}
331338
if len(msgs) != 1 || msgs[0].Subject != "Please verify your email address" {
332339
t.Fatal("Expected to find a single confirmation email but didn't.")
333340
}
341+
// Update the user's email to a mixed-case string, expect it to be persisted
342+
// as lowercase only.
343+
emailStr := name + "_ThIsIsMiXeDcAsE@siasky.net"
344+
_, status, err = at.UserPUT(emailStr, "", "")
345+
if err != nil || status != http.StatusOK {
346+
t.Fatal(status, err)
347+
}
348+
// Fetch the user by the mixed-case email. Expect this to succeed because we
349+
// cast the email to lowercase in the UserPUT handler.
350+
u4, err := at.DB.UserByEmail(at.Ctx, types.NewEmail(emailStr))
351+
if err != nil {
352+
t.Fatal(err)
353+
}
354+
// Make sure the email field is lowercase. Make sure to not use String()
355+
// because that will cast it to lowercase even if it's not.
356+
// We disable gocritic here, so it doesn't suggest to use strings.EqualFold().
357+
//nolint:gocritic
358+
if string(u4.Email) != strings.ToLower(emailStr) {
359+
t.Fatalf("Expected the email to be '%s', got '%s", strings.ToLower(emailStr), u4.Email)
360+
}
334361
}
335362

336363
// testUserDELETE tests the DELETE /user endpoint.
@@ -719,8 +746,8 @@ func testUserAccountRecovery(t *testing.T, at *test.AccountsTester) {
719746
// person requesting a recovery and they just forgot which email they used
720747
// to sign up. While we can't tell them that, we can indicate tht recovery
721748
// process works as expected and they should try their other emails.
722-
attemptedEmail := hex.EncodeToString(fastrand.Bytes(16)) + "@siasky.net"
723-
_, err = at.UserRecoverRequestPOST(attemptedEmail)
749+
attemptedEmail := types.NewEmail(hex.EncodeToString(fastrand.Bytes(16)) + "@siasky.net")
750+
_, err = at.UserRecoverRequestPOST(attemptedEmail.String())
724751
if err != nil {
725752
t.Fatal(err)
726753
}
@@ -736,8 +763,8 @@ func testUserAccountRecovery(t *testing.T, at *test.AccountsTester) {
736763
// Request recovery with a valid email. We expect there to be a single email
737764
// with the recovery token. The email is unconfirmed but we don't mind that.
738765
bodyParams := url.Values{}
739-
bodyParams.Set("email", u.Email)
740-
_, err = at.UserRecoverRequestPOST(u.Email)
766+
bodyParams.Set("email", u.Email.String())
767+
_, err = at.UserRecoverRequestPOST(u.Email.String())
741768
if err != nil {
742769
t.Fatal(err)
743770
}
@@ -809,7 +836,7 @@ func testUserAccountRecovery(t *testing.T, at *test.AccountsTester) {
809836
t.Fatal(err)
810837
}
811838
// Make sure the user's password is now successfully changed.
812-
_, b, err := at.LoginCredentialsPOST(u.Email, newPassword)
839+
_, b, err := at.LoginCredentialsPOST(u.Email.String(), newPassword)
813840
if err != nil {
814841
t.Fatal(err, string(b))
815842
}
@@ -940,7 +967,7 @@ func testUserFlow(t *testing.T, at *test.AccountsTester) {
940967
queryParams.Set("email", emailAddr)
941968
queryParams.Set("password", password)
942969
// Create a user.
943-
u, err := test.CreateUser(at, queryParams.Get("email"), queryParams.Get("password"))
970+
u, err := test.CreateUser(at, types.NewEmail(queryParams.Get("email")), queryParams.Get("password"))
944971
if err != nil {
945972
t.Fatal(err)
946973
}
@@ -975,14 +1002,14 @@ func testUserFlow(t *testing.T, at *test.AccountsTester) {
9751002
}
9761003
at.SetCookie(c)
9771004
// Change the user's email.
978-
newEmail := name + "_new@siasky.net"
979-
_, _, err = at.UserPUT(newEmail, "", "")
1005+
newEmail := types.NewEmail(name + "_new@siasky.net")
1006+
_, _, err = at.UserPUT(newEmail.String(), "", "")
9801007
if err != nil {
9811008
t.Fatalf("Failed to update user. Error: %s", err.Error())
9821009
}
9831010
// Grab the new cookie. It has changed because of the user edit.
9841011
at.ClearCredentials()
985-
r, _, err = at.LoginCredentialsPOST(newEmail, password)
1012+
r, _, err = at.LoginCredentialsPOST(newEmail.String(), password)
9861013
if err != nil {
9871014
t.Fatal(err)
9881015
}

‎test/api/upload_test.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/SkynetLabs/skynet-accounts/database"
88
"github.com/SkynetLabs/skynet-accounts/test"
9+
"github.com/SkynetLabs/skynet-accounts/types"
910
"go.mongodb.org/mongo-driver/bson/primitive"
1011
)
1112

@@ -14,14 +15,14 @@ func testUploadInfo(t *testing.T, at *test.AccountsTester) {
1415
// Create two test users.
1516
name := test.DBNameForTest(t.Name())
1617
name2 := name + "2"
17-
email := name + "@siasky.net"
18-
email2 := name2 + "@siasky.net"
19-
r, _, err := at.UserPOST(email, name+"_pass")
18+
email := types.NewEmail(name + "@siasky.net")
19+
email2 := types.NewEmail(name2 + "@siasky.net")
20+
r, _, err := at.UserPOST(email.String(), name+"_pass")
2021
if err != nil {
2122
t.Fatal(err)
2223
}
2324
c1 := test.ExtractCookie(r)
24-
r, _, err = at.UserPOST(email2, name2+"_pass")
25+
r, _, err = at.UserPOST(email2.String(), name2+"_pass")
2526
if err != nil {
2627
t.Fatal(err)
2728
}

‎test/database/user_test.go

+13-12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/SkynetLabs/skynet-accounts/lib"
1212
"github.com/SkynetLabs/skynet-accounts/skynet"
1313
"github.com/SkynetLabs/skynet-accounts/test"
14+
"github.com/SkynetLabs/skynet-accounts/types"
1415
"gitlab.com/NebulousLabs/errors"
1516
"gitlab.com/NebulousLabs/fastrand"
1617
"go.mongodb.org/mongo-driver/bson/primitive"
@@ -27,7 +28,7 @@ func TestUserByEmail(t *testing.T) {
2728
t.Fatal(err)
2829
}
2930

30-
email := t.Name() + "@siasky.net"
31+
email := types.NewEmail(t.Name() + "@siasky.net")
3132
pass := t.Name() + "password"
3233
sub := t.Name() + "sub"
3334
// Ensure we don't have a user with this email and the method handles that
@@ -122,7 +123,7 @@ func TestUserByPubKey(t *testing.T) {
122123
}
123124

124125
// Create a user with this pubkey.
125-
u, err := db.UserCreatePK(ctx, name+"@siasky.net", name+"pass", name+"sub", pk, database.TierFree)
126+
u, err := db.UserCreatePK(ctx, types.NewEmail(name+"@siasky.net"), name+"pass", name+"sub", pk, database.TierFree)
126127
if err != nil {
127128
t.Fatal(err)
128129
}
@@ -172,7 +173,7 @@ func TestUserByStripeID(t *testing.T) {
172173
t.Fatalf("Expected error %v, got %v.\n", database.ErrUserNotFound, err)
173174
}
174175
// Create a test user with the respective StripeID.
175-
u, err := db.UserCreate(ctx, t.Name()+"@siasky.net", t.Name()+"pass", t.Name()+"sub", database.TierFree)
176+
u, err := db.UserCreate(ctx, types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"pass", t.Name()+"sub", database.TierFree)
176177
if err != nil {
177178
t.Fatal(err)
178179
}
@@ -248,7 +249,7 @@ func TestUserConfirmEmail(t *testing.T) {
248249
if err != nil {
249250
t.Fatal("Failed to connect to the DB:", err)
250251
}
251-
emailAddr := t.Name() + "@siasky.net"
252+
emailAddr := types.NewEmail(t.Name() + "@siasky.net")
252253
// Create a user with this email.
253254
u, err := db.UserCreate(ctx, emailAddr, "password", "sub", database.TierFree)
254255
if err != nil {
@@ -287,7 +288,7 @@ func TestUserCreate(t *testing.T) {
287288
t.Fatal(err)
288289
}
289290

290-
email := t.Name() + "@siasky.net"
291+
email := types.NewEmail(t.Name() + "@siasky.net")
291292
pass := t.Name() + "pass"
292293
sub := t.Name() + "sub"
293294

@@ -315,7 +316,7 @@ func TestUserCreate(t *testing.T) {
315316
if fu == nil {
316317
t.Fatal("Expected to find a user but didn't.")
317318
}
318-
newEmail := t.Name() + "_new@siasky.net"
319+
newEmail := types.NewEmail(t.Name() + "_new@siasky.net")
319320
newPass := t.Name() + "pass_new"
320321
newSub := t.Name() + "sub_new"
321322
// Try to create a user with an email which is already in use.
@@ -338,7 +339,7 @@ func TestUserCreateEmailConfirmation(t *testing.T) {
338339
if err != nil {
339340
t.Fatal(err)
340341
}
341-
u, err := db.UserCreate(ctx, t.Name()+"@siasky.net", t.Name()+"pass", t.Name()+"sub", database.TierFree)
342+
u, err := db.UserCreate(ctx, types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"pass", t.Name()+"sub", database.TierFree)
342343
if err != nil {
343344
t.Fatal(err)
344345
}
@@ -408,7 +409,7 @@ func TestUserSave(t *testing.T) {
408409
username := t.Name()
409410
// Case: save a user that doesn't exist in the DB.
410411
u := &database.User{
411-
Email: username + "@siasky.net",
412+
Email: types.NewEmail(username + "@siasky.net"),
412413
Sub: t.Name() + "sub",
413414
Tier: database.TierFree,
414415
}
@@ -424,7 +425,7 @@ func TestUserSave(t *testing.T) {
424425
t.Fatalf("Expected user id %s, got %s.", u.ID.Hex(), u1.ID.Hex())
425426
}
426427
// Case: save a user that does exist in the DB.
427-
u.Email = username + "_changed@siasky.net"
428+
u.Email = types.NewEmail(username + "_changed@siasky.net")
428429
u.Tier = database.TierPremium80
429430
err = db.UserSave(ctx, u)
430431
if err != nil {
@@ -453,7 +454,7 @@ func TestUserSetStripeID(t *testing.T) {
453454

454455
stripeID := t.Name() + "stripeid"
455456
// Create a test user with the respective StripeID.
456-
u, err := db.UserCreate(ctx, t.Name()+"@siasky.net", t.Name()+"pass", t.Name()+"sub", database.TierFree)
457+
u, err := db.UserCreate(ctx, types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"pass", t.Name()+"sub", database.TierFree)
457458
if err != nil {
458459
t.Fatal(err)
459460
}
@@ -485,7 +486,7 @@ func TestUserPubKey(t *testing.T) {
485486
t.Fatal(err)
486487
}
487488
// Create a test user.
488-
u, err := db.UserCreate(ctx, t.Name()+"@siasky.net", t.Name()+"pass", t.Name()+"sub", database.TierFree)
489+
u, err := db.UserCreate(ctx, types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"pass", t.Name()+"sub", database.TierFree)
489490
if err != nil {
490491
t.Fatal(err)
491492
}
@@ -569,7 +570,7 @@ func TestUserSetTier(t *testing.T) {
569570
t.Fatal(err)
570571
}
571572
// Create a test user with the respective StripeID.
572-
u, err := db.UserCreate(ctx, t.Name()+"@siasky.net", t.Name()+"pass", t.Name()+"sub", database.TierFree)
573+
u, err := db.UserCreate(ctx, types.NewEmail(t.Name()+"@siasky.net"), t.Name()+"pass", t.Name()+"sub", database.TierFree)
573574
if err != nil {
574575
t.Fatal(err)
575576
}

‎test/email/sender_test.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/SkynetLabs/skynet-accounts/email"
1313
"github.com/SkynetLabs/skynet-accounts/test"
14+
"github.com/SkynetLabs/skynet-accounts/types"
1415
"github.com/sirupsen/logrus"
1516
"go.mongodb.org/mongo-driver/bson"
1617
"go.mongodb.org/mongo-driver/mongo/options"
@@ -43,7 +44,7 @@ func TestSender(t *testing.T) {
4344
mailer := email.NewMailer(db)
4445

4546
// Send an email.
46-
to := t.Name() + "@siasky.net"
47+
to := types.NewEmail(t.Name() + "@siasky.net")
4748
token := t.Name()
4849
err = mailer.SendAddressConfirmationEmail(ctx, to, token)
4950
if err != nil {
@@ -101,7 +102,7 @@ func TestContendingSenders(t *testing.T) {
101102
t.Fatal("Failed to purge email collection:", err)
102103
}
103104
}()
104-
targetAddr := t.Name() + "@siasky.net"
105+
targetAddr := types.NewEmail(t.Name() + "@siasky.net")
105106
numMsgs := 200
106107
// count will hold the total number of messages sent.
107108
var count int32
@@ -111,7 +112,9 @@ func TestContendingSenders(t *testing.T) {
111112
generator := func(n int) {
112113
m := email.NewMailer(db)
113114
for i := 0; i < n; i++ {
114-
err1 := m.SendAddressConfirmationEmail(ctx, targetAddr, targetAddr)
115+
// We'll use the target email address as token because it doesn't
116+
// matter what we use.
117+
err1 := m.SendAddressConfirmationEmail(ctx, targetAddr, targetAddr.String())
115118
if err1 != nil {
116119
t.Error("Failed to send email.", err1)
117120
return

‎test/utils.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/SkynetLabs/skynet-accounts/database"
12+
"github.com/SkynetLabs/skynet-accounts/types"
1213
"gitlab.com/NebulousLabs/errors"
1314
"gitlab.com/NebulousLabs/fastrand"
1415
"gitlab.com/SkynetLabs/skyd/skymodules"
@@ -78,9 +79,9 @@ func DBTestCredentials() database.DBCredentials {
7879
}
7980

8081
// CreateUser is a helper method which simplifies the creation of test users
81-
func CreateUser(at *AccountsTester, emailAddr, password string) (*User, error) {
82+
func CreateUser(at *AccountsTester, emailAddr types.Email, password string) (*User, error) {
8283
// Create a user.
83-
_, _, err := at.UserPOST(emailAddr, password)
84+
_, _, err := at.UserPOST(emailAddr.String(), password)
8485
if err != nil {
8586
return nil, errors.AddContext(err, "user creation failed")
8687
}
@@ -97,15 +98,15 @@ func CreateUser(at *AccountsTester, emailAddr, password string) (*User, error) {
9798
// function that deletes the user.
9899
func CreateUserAndLogin(at *AccountsTester, name string) (*User, *http.Cookie, error) {
99100
// Use the test's name as an email-compatible identifier.
100-
email := DBNameForTest(name) + "@siasky.net"
101+
email := types.NewEmail(DBNameForTest(name) + "@siasky.net")
101102
password := hex.EncodeToString(fastrand.Bytes(16))
102103
// Create a user.
103104
u, err := CreateUser(at, email, password)
104105
if err != nil {
105106
return nil, nil, err
106107
}
107108
// Log in with that user in order to make sure it exists.
108-
r, _, err := at.LoginCredentialsPOST(email, password)
109+
r, _, err := at.LoginCredentialsPOST(email.String(), password)
109110
if err != nil {
110111
return nil, nil, err
111112
}

‎types/email.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
Package types provides various service-wide types.
3+
4+
These types are used by more than one subsystem and should not provide
5+
subsystem-specific behaviours, e.g. database-specific serialization.
6+
The exception to this rule should be test methods which should allow testing
7+
with all kinds of input.
8+
*/
9+
10+
package types
11+
12+
import (
13+
"encoding/json"
14+
"strings"
15+
)
16+
17+
type (
18+
// Email is a string type with some extra rules about its casing (it always
19+
// gets converted to lowercase). All subsystems working with emails should
20+
// use this type in the signatures of their exported methods and functions.
21+
Email string
22+
)
23+
24+
// NewEmail creates a new Email.
25+
func NewEmail(s string) Email {
26+
return Email(strings.ToLower(s))
27+
}
28+
29+
// MarshalJSON defines a custom marshaller for this type.
30+
func (e Email) MarshalJSON() ([]byte, error) {
31+
return json.Marshal(e.String())
32+
}
33+
34+
// UnmarshalJSON defines a custom unmarshaller for this type.
35+
// The only custom part is the fact that we cast the email to lower case.
36+
func (e *Email) UnmarshalJSON(data []byte) error {
37+
var s string
38+
if err := json.Unmarshal(data, &s); err != nil {
39+
return err
40+
}
41+
*e = NewEmail(s)
42+
return nil
43+
}
44+
45+
// String is a fmt.Stringer implementation for Email.
46+
func (e Email) String() string {
47+
return strings.ToLower(string(e))
48+
}

‎types/email_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// TestEmail_String ensures that stringifying an Email will result in a
11+
// lowercase string.
12+
func TestEmail_String(t *testing.T) {
13+
s := "mIxEdCaSeStRiNg"
14+
e := Email(s)
15+
if e.String() != strings.ToLower(s) {
16+
t.Fatalf("Expected '%s', got '%s'", strings.ToLower(s), e)
17+
}
18+
}
19+
20+
// TestEmail_MarshalJSON ensures that marshalling an Email will result in a
21+
// lower case representation.
22+
func TestEmail_MarshalJSON(t *testing.T) {
23+
s := "EmAiL@eXaMpLe.CoM"
24+
e := Email(s)
25+
b, err := json.Marshal(e)
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
// We expect these bytes to match the source.
30+
expectedJSON := fmt.Sprintf("\"%s\"", strings.ToLower(s))
31+
if string(b) != expectedJSON {
32+
t.Fatalf("Expected '%s', got '%s'", expectedJSON, string(b))
33+
}
34+
}
35+
36+
// TestEmail_UnmarshalJSON ensures that unmarshalling an Email will result in a
37+
// lower case string, even if the marshalled data was mixed-case.
38+
func TestEmail_UnmarshalJSON(t *testing.T) {
39+
// Manually craft a mixed-case JSON representation of an Email.
40+
b := []byte(`"EmAiL@eXaMpLe.CoM"`)
41+
var e Email
42+
err := json.Unmarshal(b, &e)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
// We expect the unmarshalled email to be lowercase only.
47+
if string(e) != strings.ToLower(string(b[1:len(b)-1])) {
48+
t.Fatalf("Expected to get a lowercase version of '%s', i.e. '%s' but got '%s'", e, strings.ToLower(string(e)), e)
49+
}
50+
}

0 commit comments

Comments
 (0)
Please sign in to comment.