Skip to content

Commit db40e28

Browse files
cstocktonChris Stockton
andauthored
chore: expand e2e phone flow tests and rename helpers (#2288)
Increase tests coverage and verify phone change functionality in response to supabase/supabase#40797 - Add defensive change to anonymous rate limits test - This function relies on low anon rate limit (loops over rate limit) - Rename getAccessToken to getEmailAccessToken and add generic getAccessToken helper - Rename signupAndConfirm to signupAndConfirmEmail - Expand phone signup and phone change e2e tests: - Capture and verify OTP from SendSMS hook - Validate one-time token creation and user state transitions - Add full phone change flow, including OTP verification - Update MFA-related tests to use new helper names Co-authored-by: Chris Stockton <[email protected]>
1 parent 5f2e279 commit db40e28

File tree

2 files changed

+195
-22
lines changed

2 files changed

+195
-22
lines changed

internal/api/anonymous_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/supabase/auth/internal/conf"
1717
mail "github.com/supabase/auth/internal/mailer"
1818
"github.com/supabase/auth/internal/models"
19+
"github.com/supabase/auth/internal/storage"
1920
)
2021

2122
type AnonymousTestSuite struct {
@@ -25,9 +26,14 @@ type AnonymousTestSuite struct {
2526
}
2627

2728
func TestAnonymous(t *testing.T) {
28-
api, config, err := setupAPIForTest()
29-
require.NoError(t, err)
29+
cb := func(cfg *conf.GlobalConfiguration, _ *storage.Connection) {
30+
if cfg != nil {
31+
cfg.RateLimitAnonymousUsers = 5
32+
}
33+
}
3034

35+
api, config, err := setupAPIForTestWithCallback(cb)
36+
require.NoError(t, err)
3137
ts := &AnonymousTestSuite{
3238
API: api,
3339
Config: config,

internal/api/e2e_test.go

Lines changed: 187 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -124,23 +124,31 @@ func runVerifyAfterUserCreatedHook(
124124
return latest
125125
}
126126

127-
func getAccessToken(
127+
func getEmailAccessToken(
128128
ctx context.Context,
129129
t *testing.T,
130130
inst *e2ehooks.Instance,
131131
email, pass string,
132132
) *api.AccessTokenResponse {
133-
req := &api.PasswordGrantParams{
133+
return getAccessToken(ctx, t, inst, &api.PasswordGrantParams{
134134
Email: email,
135135
Password: pass,
136-
}
136+
})
137+
}
137138

139+
func getAccessToken(
140+
ctx context.Context,
141+
t *testing.T,
142+
inst *e2ehooks.Instance,
143+
req *api.PasswordGrantParams,
144+
) *api.AccessTokenResponse {
138145
res := new(api.AccessTokenResponse)
139146
err := e2eapi.Do(ctx, http.MethodPost, inst.APIServer.URL+"/token?grant_type=password", req, res)
140147
require.NoError(t, err)
141148
return res
142149
}
143-
func signupAndConfirm(
150+
151+
func signupAndConfirmEmail(
144152
ctx context.Context,
145153
t *testing.T,
146154
inst *e2ehooks.Instance,
@@ -249,21 +257,180 @@ func TestE2EHooks(t *testing.T) {
249257
})
250258

251259
t.Run("SignupPhone", func(t *testing.T) {
260+
defer inst.HookRecorder.AfterUserCreated.ClearCalls()
252261
defer inst.HookRecorder.BeforeUserCreated.ClearCalls()
262+
defer inst.HookRecorder.SendSMS.ClearCalls()
253263

254-
phone := genPhone()
255-
req := &api.SignupParams{
256-
Phone: phone,
257-
Password: defaultPassword,
264+
var currentUser *models.User
265+
{
266+
phone := genPhone()
267+
req := &api.SignupParams{
268+
Phone: phone,
269+
Password: defaultPassword,
270+
}
271+
signupUser := new(models.User)
272+
{
273+
err := e2eapi.Do(
274+
ctx, http.MethodPost, inst.APIServer.URL+"/signup", req, signupUser)
275+
require.NoError(t, err)
276+
require.Equal(t, phone, signupUser.Phone.String())
277+
}
278+
279+
// load the hook call
280+
calls := inst.HookRecorder.SendSMS.GetCalls()
281+
require.Equal(t, 1, len(calls))
282+
call := calls[0]
283+
284+
hookReq := &v0hooks.SendSMSInput{}
285+
err = call.Unmarshal(hookReq)
286+
require.NoError(t, err)
287+
288+
latestUser, err := models.FindUserByID(inst.Conn, signupUser.ID)
289+
require.NoError(t, err)
290+
require.NotNil(t, latestUser)
291+
292+
otp := hookReq.SMS.OTP
293+
otpHash := crypto.GenerateTokenHash(
294+
signupUser.GetPhone(), hookReq.SMS.OTP)
295+
296+
ott, err := models.FindOneTimeToken(
297+
inst.Conn,
298+
otpHash,
299+
models.ConfirmationToken)
300+
require.NoError(t, err)
301+
require.Equal(t, signupUser.ID.String(), ott.UserID.String())
302+
require.Equal(t, signupUser.Phone.String(), ott.RelatesTo)
303+
304+
{
305+
req := &api.VerifyParams{
306+
Type: "sms",
307+
Token: otp,
308+
Phone: phone,
309+
}
310+
res := new(models.User)
311+
312+
body := new(bytes.Buffer)
313+
err = json.NewEncoder(body).Encode(req)
314+
require.NoError(t, err)
315+
316+
httpReq, err := http.NewRequestWithContext(
317+
ctx, "POST", "/verify", body)
318+
require.NoError(t, err)
319+
320+
httpRes, err := inst.Do(httpReq)
321+
require.NoError(t, err)
322+
require.Equal(t, 200, httpRes.StatusCode)
323+
324+
err = json.NewDecoder(httpRes.Body).Decode(res)
325+
require.NoError(t, err)
326+
}
327+
328+
{
329+
// setup phone change
330+
latestUser, err = models.FindUserByID(inst.Conn, signupUser.ID)
331+
require.NoError(t, err)
332+
require.NotNil(t, latestUser)
333+
currentUser = latestUser
334+
}
258335
}
259-
res := new(models.User)
260-
err := e2eapi.Do(ctx, http.MethodPost, inst.APIServer.URL+"/signup", req, res)
261-
require.NoError(t, err)
262-
require.Equal(t, phone, res.Phone.String())
263336

264-
runVerifyBeforeUserCreatedHook(t, inst, res)
265-
runVerifyAfterUserCreatedHook(t, inst, res)
337+
t.Run("PhoneChange", func(t *testing.T) {
338+
inst.HookRecorder.BeforeUserCreated.ClearCalls()
339+
inst.HookRecorder.SendSMS.ClearCalls()
266340

341+
currentAccessToken := getAccessToken(ctx, t, inst,
342+
&api.PasswordGrantParams{
343+
Phone: string(currentUser.Phone),
344+
Password: defaultPassword,
345+
})
346+
347+
curPhone := currentUser.Phone.String()
348+
newPhone := genPhone()
349+
{
350+
req := &api.UserUpdateParams{
351+
Phone: newPhone,
352+
}
353+
res := new(models.User)
354+
355+
body := new(bytes.Buffer)
356+
err = json.NewEncoder(body).Encode(req)
357+
require.NoError(t, err)
358+
359+
httpReq, err := http.NewRequestWithContext(
360+
ctx, "PUT", "/user", body)
361+
require.NoError(t, err)
362+
363+
httpRes, err := inst.DoAuth(httpReq, currentAccessToken.Token)
364+
require.NoError(t, err)
365+
require.Equal(t, 200, httpRes.StatusCode)
366+
367+
err = json.NewDecoder(httpRes.Body).Decode(res)
368+
require.NoError(t, err)
369+
370+
currentUser = res
371+
}
372+
373+
var otp string
374+
{
375+
require.Equal(t, curPhone, currentUser.Phone.String())
376+
require.Equal(t, newPhone, currentUser.PhoneChange)
377+
378+
calls := inst.HookRecorder.SendSMS.GetCalls()
379+
require.Equal(t, 1, len(calls))
380+
call := calls[0]
381+
382+
hookReq := &v0hooks.SendSMSInput{}
383+
err = call.Unmarshal(hookReq)
384+
require.NoError(t, err)
385+
386+
require.Equal(t, currentUser.ID, hookReq.User.ID)
387+
require.Equal(t, currentUser.Aud, hookReq.User.Aud)
388+
require.Equal(t, currentUser.Phone, hookReq.User.Phone)
389+
require.Equal(t, currentUser.AppMetaData, hookReq.User.AppMetaData)
390+
391+
otp = hookReq.SMS.OTP
392+
otpHash := crypto.GenerateTokenHash(
393+
currentUser.PhoneChange, hookReq.SMS.OTP)
394+
395+
ott, err := models.FindOneTimeToken(
396+
inst.Conn,
397+
otpHash,
398+
models.PhoneChangeToken)
399+
require.NoError(t, err)
400+
require.Equal(t, currentUser.ID.String(), ott.UserID.String())
401+
require.Equal(t, currentUser.PhoneChange, ott.RelatesTo)
402+
403+
latestUser, err := models.FindUserByID(inst.Conn, currentUser.ID)
404+
require.NoError(t, err)
405+
require.NotNil(t, latestUser)
406+
407+
currentUser = latestUser
408+
}
409+
410+
{
411+
req := &api.VerifyParams{
412+
Type: "phone_change",
413+
Token: otp,
414+
Phone: currentUser.PhoneChange,
415+
}
416+
res := new(models.User)
417+
418+
body := new(bytes.Buffer)
419+
err = json.NewEncoder(body).Encode(req)
420+
require.NoError(t, err)
421+
422+
httpReq, err := http.NewRequestWithContext(
423+
ctx, "POST", "/verify", body)
424+
require.NoError(t, err)
425+
426+
httpRes, err := inst.Do(httpReq)
427+
require.NoError(t, err)
428+
require.Equal(t, 200, httpRes.StatusCode)
429+
430+
err = json.NewDecoder(httpRes.Body).Decode(res)
431+
require.NoError(t, err)
432+
}
433+
})
267434
})
268435

269436
t.Run("SignupAnonymously", func(t *testing.T) {
@@ -422,7 +589,7 @@ func TestE2EHooks(t *testing.T) {
422589
mfaUser = runVerifyBeforeUserCreatedHook(t, inst, mfaUser)
423590
runVerifyAfterUserCreatedHook(t, inst, mfaUser)
424591
require.NotNil(t, mfaUser)
425-
mfaUserAccessToken = getAccessToken(
592+
mfaUserAccessToken = getEmailAccessToken(
426593
ctx, t, inst, string(mfaUser.Email), defaultPassword)
427594

428595
phone := genPhone()
@@ -813,7 +980,7 @@ func TestE2EHooks(t *testing.T) {
813980
require.NoError(t, err)
814981
defer inst.Close()
815982

816-
signupAndConfirm(ctx, t, inst)
983+
signupAndConfirmEmail(ctx, t, inst)
817984
})
818985

819986
t.Run("SecureEmailChange=Enabled", func(t *testing.T) {
@@ -829,11 +996,11 @@ func TestE2EHooks(t *testing.T) {
829996
// test requires this flag
830997
require.True(t, inst.Config.Mailer.SecureEmailChangeEnabled)
831998

832-
signupUser := signupAndConfirm(ctx, t, inst)
999+
signupUser := signupAndConfirmEmail(ctx, t, inst)
8331000
currentUser := signupUser
8341001

8351002
// get access token
836-
currentAccessToken := getAccessToken(
1003+
currentAccessToken := getEmailAccessToken(
8371004
ctx, t, inst, string(currentUser.Email), defaultPassword)
8381005

8391006
// update email
@@ -1045,11 +1212,11 @@ func TestE2EHooks(t *testing.T) {
10451212
// test requires this flag
10461213
require.False(t, inst.Config.Mailer.SecureEmailChangeEnabled)
10471214

1048-
signupUser := signupAndConfirm(ctx, t, inst)
1215+
signupUser := signupAndConfirmEmail(ctx, t, inst)
10491216
currentUser := signupUser
10501217

10511218
// get access token
1052-
currentAccessToken := getAccessToken(
1219+
currentAccessToken := getEmailAccessToken(
10531220
ctx, t, inst, string(currentUser.Email), defaultPassword)
10541221

10551222
// update email

0 commit comments

Comments
 (0)