all repos

onasty @ bf8dc57

a one-time notes service

onasty/e2e/apiv1_auth_test.go (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
feat(api): change email (#191)..., 9 months ago
1
package e2e_test
2
3
import (
4
	"crypto/rand"
5
	"encoding/hex"
6
	"fmt"
7
	"net/http"
8
	"time"
9
10
	"github.com/gofrs/uuid/v5"
11
	"github.com/olexsmir/onasty/internal/hasher"
12
	"github.com/olexsmir/onasty/internal/models"
13
)
14
15
type apiv1AuthSignUpRequest struct {
16
	Email    string `json:"email"`
17
	Password string `json:"password"`
18
}
19
20
func (e *AppTestSuite) TestAuthV1_SignUP() {
21
	email := e.uuid() + "test@test.com"
22
	password := "password"
23
24
	httpResp := e.httpRequest(
25
		http.MethodPost,
26
		"/api/v1/auth/signup",
27
		e.jsonify(apiv1AuthSignUpRequest{
28
			Email:    email,
29
			Password: password,
30
		}),
31
	)
32
33
	dbUser := e.getUserByEmail(email)
34
	hashedPasswd, err := e.hasher.Hash(password)
35
	e.require.NoError(err)
36
37
	e.Equal(http.StatusCreated, httpResp.Code)
38
	e.Equal(dbUser.Email, email)
39
	e.Equal(dbUser.Password, hashedPasswd)
40
}
41
42
func (e *AppTestSuite) TestAuthV1_SignUP_badrequest() {
43
	tests := []struct {
44
		name     string
45
		email    string
46
		password string
47
	}{
48
		{name: "all fields empty", email: "", password: ""},
49
		{name: "non valid email", email: "email", password: "password"},
50
		{name: "non valid password", email: "test@test.com", password: "12345"},
51
	}
52
	for _, t := range tests {
53
		httpResp := e.httpRequest(
54
			http.MethodPost,
55
			"/api/v1/auth/signup",
56
			e.jsonify(apiv1AuthSignUpRequest{
57
				Email:    t.email,
58
				Password: t.password,
59
			}),
60
		)
61
62
		e.Equal(http.StatusBadRequest, httpResp.Code)
63
	}
64
}
65
66
type (
67
	apiv1AuthSignInRequest struct {
68
		Email    string `json:"email"`
69
		Password string `json:"password"`
70
	}
71
	apiv1AuthSignInResponse struct {
72
		AccessToken  string `json:"access_token"`
73
		RefreshToken string `json:"refresh_token"`
74
	}
75
)
76
77
func (e *AppTestSuite) TestAuthV1_VerifyEmail() {
78
	email := e.uuid() + "email@email.com"
79
	password := "qwerty"
80
81
	httpResp := e.httpRequest(
82
		http.MethodPost,
83
		"/api/v1/auth/signup",
84
		e.jsonify(apiv1AuthSignUpRequest{
85
			Email:    email,
86
			Password: password,
87
		}),
88
	)
89
90
	e.Equal(http.StatusCreated, httpResp.Code)
91
92
	user := e.getLastUserByEmail(email)
93
	token := e.getVerificationTokenByUserID(user.ID)
94
	e.Equal(token.Token, mockMailStore[email])
95
96
	httpResp = e.httpRequest(http.MethodGet, "/api/v1/auth/verify/"+token.Token, nil)
97
	e.Equal(http.StatusOK, httpResp.Code)
98
99
	user = e.getLastUserByEmail(email)
100
	e.Equal(user.Activated, true)
101
}
102
103
type apiv1AuthResendVerificationEmailRequest struct {
104
	Email string `json:"email"`
105
}
106
107
func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail() {
108
	email, password := e.uuid()+"email@email.com", e.uuid()
109
110
	// create test user
111
	signUpHTTPResp := e.httpRequest(
112
		http.MethodPost,
113
		"/api/v1/auth/signup",
114
		e.jsonify(apiv1AuthSignUpRequest{
115
			Email:    email,
116
			Password: password,
117
		}),
118
	)
119
120
	e.Equal(http.StatusCreated, signUpHTTPResp.Code)
121
122
	// handle sending of the email
123
	httpResp := e.httpRequest(
124
		http.MethodPost,
125
		"/api/v1/auth/resend-verification-email",
126
		e.jsonify(apiv1AuthResendVerificationEmailRequest{
127
			Email: email,
128
		}),
129
	)
130
131
	e.Equal(http.StatusOK, httpResp.Code)
132
	e.NotEmpty(mockMailStore[email])
133
}
134
135
func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail_wrong() {
136
	email, password := e.uuid()+"@"+e.uuid()+".com", "password"
137
	e.insertUser(email, password, true)
138
139
	tests := []struct {
140
		name         string
141
		email        string
142
		expectedCode int
143
		expectedMsg  string
144
	}{
145
		{
146
			name:         "already verified account",
147
			email:        email,
148
			expectedCode: http.StatusBadRequest,
149
			expectedMsg:  models.ErrUserIsAlreadyVerified.Error(),
150
		},
151
		{
152
			name:         "user not found",
153
			email:        e.uuid() + "@at.com",
154
			expectedCode: http.StatusBadRequest,
155
			expectedMsg:  models.ErrUserNotFound.Error(),
156
		},
157
	}
158
159
	for _, t := range tests {
160
		httpResp := e.httpRequest(
161
			http.MethodPost,
162
			"/api/v1/auth/resend-verification-email",
163
			e.jsonify(apiv1AuthResendVerificationEmailRequest{
164
				Email: t.email,
165
			}))
166
167
		e.Equal(httpResp.Code, t.expectedCode)
168
169
		var body errorResponse
170
		e.readBodyAndUnjsonify(httpResp.Body, &body)
171
		e.Equal(body.Message, t.expectedMsg)
172
173
		e.Empty(mockMailStore[t.email])
174
	}
175
}
176
177
func (e *AppTestSuite) TestAuthV1_SignIn() {
178
	email := e.uuid() + "email@email.com"
179
	password := "qwerty"
180
	uid := e.insertUser(email, password, true)
181
182
	httpResp := e.httpRequest(
183
		http.MethodPost,
184
		"/api/v1/auth/signin",
185
		e.jsonify(apiv1AuthSignInRequest{
186
			Email:    email,
187
			Password: password,
188
		}),
189
	)
190
191
	var body apiv1AuthSignInResponse
192
	e.readBodyAndUnjsonify(httpResp.Body, &body)
193
194
	session := e.getLastSessionByUserID(uid)
195
	parsedToken := e.parseJwtToken(body.AccessToken)
196
197
	e.Equal(http.StatusOK, httpResp.Code)
198
	e.Equal(body.RefreshToken, session.RefreshToken)
199
	e.Equal(parsedToken.UserID, uid.String())
200
}
201
202
func (e *AppTestSuite) TestAuthV1_SignIn_wrong() {
203
	password := "password"
204
	email := e.uuid() + "@test.com"
205
	e.insertUser(email, "password", true)
206
207
	unactivatedEmail := e.uuid() + "@test.com"
208
	e.insertUser(unactivatedEmail, password, false)
209
210
	//exhaustruct:ignore
211
	tests := []struct {
212
		name         string
213
		email        string
214
		password     string
215
		expectedCode int
216
217
		expectMsg   bool
218
		expectedMsg string
219
	}{
220
		{
221
			name:         "inactivated user",
222
			email:        unactivatedEmail,
223
			password:     password,
224
			expectedCode: http.StatusBadRequest,
225
			expectMsg:    true,
226
			expectedMsg:  models.ErrUserIsNotActivated.Error(),
227
		},
228
		{
229
			name:         "wrong email",
230
			email:        "wrong@email.com",
231
			password:     password,
232
			expectedCode: http.StatusBadRequest,
233
			expectedMsg:  models.ErrUserWrongCredentials.Error(),
234
		},
235
		{
236
			name:         "wrong password",
237
			email:        email,
238
			password:     "wrong-wrong",
239
			expectedCode: http.StatusBadRequest,
240
			expectedMsg:  models.ErrUserWrongCredentials.Error(),
241
		},
242
	}
243
244
	for _, t := range tests {
245
		httpResp := e.httpRequest(
246
			http.MethodPost,
247
			"/api/v1/auth/signin",
248
			e.jsonify(apiv1AuthSignInRequest{
249
				Email:    t.email,
250
				Password: t.password,
251
			}),
252
		)
253
254
		if t.expectMsg {
255
			var body errorResponse
256
			e.readBodyAndUnjsonify(httpResp.Body, &body)
257
258
			e.Equal(body.Message, t.expectedMsg)
259
		}
260
261
		e.Equal(t.expectedCode, httpResp.Code)
262
	}
263
}
264
265
type apiv1AuthRefreshTokensRequest struct {
266
	RefreshToken string `json:"refresh_token"`
267
}
268
269
func (e *AppTestSuite) TestAuthV1_RefreshTokens() {
270
	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password")
271
	httpResp := e.httpRequest(
272
		http.MethodPost,
273
		"/api/v1/auth/refresh-tokens",
274
		e.jsonify(apiv1AuthRefreshTokensRequest{
275
			RefreshToken: toks.RefreshToken,
276
		}),
277
	)
278
279
	var body apiv1AuthSignInResponse
280
	e.readBodyAndUnjsonify(httpResp.Body, &body)
281
282
	sessionDB := e.getLastSessionByUserID(uid)
283
	e.Equal(e.parseJwtToken(body.AccessToken).UserID, uid.String())
284
285
	e.Equal(httpResp.Code, http.StatusOK)
286
	e.NotEqual(toks.RefreshToken, body.RefreshToken)
287
	e.Equal(body.RefreshToken, sessionDB.RefreshToken)
288
}
289
290
func (e *AppTestSuite) TestAuthV1_RefreshTokens_wrong() {
291
	httpResp := e.httpRequest(
292
		http.MethodPost,
293
		"/api/v1/auth/refresh-tokens",
294
		e.jsonify(apiv1AuthRefreshTokensRequest{
295
			RefreshToken: e.uuid(),
296
		}),
297
	)
298
299
	e.Equal(httpResp.Code, http.StatusBadRequest)
300
}
301
302
type apiV1AuthLogoutRequest struct {
303
	RefreshToken string `json:"refresh_token"`
304
}
305
306
func (e *AppTestSuite) TestAuthV1_Logout() {
307
	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password")
308
309
	sessionDB := e.getLastSessionByUserID(uid)
310
	e.NotEmpty(sessionDB.RefreshToken)
311
312
	httpResp := e.httpRequest(
313
		http.MethodPost,
314
		"/api/v1/auth/logout",
315
		e.jsonify(apiV1AuthLogoutRequest{
316
			RefreshToken: toks.RefreshToken,
317
		}),
318
		toks.AccessToken,
319
	)
320
	e.Equal(httpResp.Code, http.StatusNoContent)
321
322
	sessionDB = e.getLastSessionByUserID(uid)
323
	e.Empty(sessionDB.RefreshToken)
324
}
325
326
func (e *AppTestSuite) TestAuthV1_LogoutAll() {
327
	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", "password")
328
329
	var res int
330
	query := "select count(*) from sessions where user_id = $1"
331
332
	err := e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res)
333
	e.require.NoError(err)
334
	e.NotZero(res)
335
336
	httpResp := e.httpRequest(http.MethodPost, "/api/v1/auth/logout/all", nil, toks.AccessToken)
337
	e.Equal(httpResp.Code, http.StatusNoContent)
338
339
	err = e.postgresDB.QueryRow(e.ctx, query, uid).Scan(&res)
340
	e.require.NoError(err)
341
	e.Zero(res)
342
}
343
344
type apiv1AuthChangePasswordRequest struct {
345
	CurrentPassword string `json:"current_password"`
346
	NewPassword     string `json:"new_password"`
347
}
348
349
func (e *AppTestSuite) TestAuthV1_ChangePassword() {
350
	password := e.uuid()
351
	newPassword := e.uuid()
352
	email := e.uuid() + "@test.com"
353
	_, toks := e.createAndSingIn(email, password)
354
355
	httpResp := e.httpRequest(
356
		http.MethodPost,
357
		"/api/v1/auth/change-password",
358
		e.jsonify(apiv1AuthChangePasswordRequest{
359
			CurrentPassword: password,
360
			NewPassword:     newPassword,
361
		}),
362
		toks.AccessToken,
363
	)
364
365
	e.Equal(httpResp.Code, http.StatusOK)
366
367
	userDB := e.getUserByEmail(email)
368
	e.NoError(e.hasher.Compare(userDB.Password, newPassword))
369
}
370
371
func (e *AppTestSuite) TestAuthV1_ChangePassword_wrongPassword() {
372
	password := e.uuid()
373
	newPassword := e.uuid()
374
	email := e.uuid() + "@test.com"
375
	_, toks := e.createAndSingIn(email, password)
376
377
	httpResp := e.httpRequest(
378
		http.MethodPost,
379
		"/api/v1/auth/change-password",
380
		e.jsonify(apiv1AuthChangePasswordRequest{
381
			CurrentPassword: e.uuid(),
382
			NewPassword:     newPassword,
383
		}),
384
		toks.AccessToken,
385
	)
386
387
	e.Equal(http.StatusBadRequest, httpResp.Code)
388
389
	userDB := e.getUserByEmail(email)
390
391
	err := e.hasher.Compare(userDB.Password, newPassword)
392
	e.ErrorIs(err, hasher.ErrMismatchedHashes)
393
}
394
395
type (
396
	apiV1AuthResetPasswordRequest struct {
397
		Email string `json:"email"`
398
	}
399
	apiV1AuthSetPasswordRequest struct {
400
		Password string `json:"password"`
401
	}
402
)
403
404
func (e *AppTestSuite) TestAuthV1_ResetPassword() {
405
	email := e.uuid() + "@test.com"
406
	uid, _ := e.createAndSingIn(email, "password")
407
408
	httpResp := e.httpRequest(
409
		http.MethodPost,
410
		"/api/v1/auth/reset-password",
411
		e.jsonify(apiV1AuthResetPasswordRequest{
412
			Email: email,
413
		}),
414
	)
415
416
	e.Equal(httpResp.Code, http.StatusOK)
417
418
	token := e.getResetPasswordTokenByUserID(uid)
419
	e.Empty(token.UsedAt)
420
	e.Equal(mockMailStore[email], token.Token)
421
422
	// set new password
423
	password := e.uuid()
424
	httpResp = e.httpRequest(
425
		http.MethodPost,
426
		"/api/v1/auth/reset-password/"+token.Token,
427
		e.jsonify(apiV1AuthSetPasswordRequest{
428
			Password: password,
429
		}),
430
	)
431
432
	dbUser := e.getUserByEmail(email)
433
	e.Equal(httpResp.Code, http.StatusOK)
434
	e.NoError(e.hasher.Compare(dbUser.Password, password))
435
436
	token = e.getResetPasswordTokenByUserID(uid)
437
	e.NotEmpty(token.UsedAt)
438
}
439
440
func (e *AppTestSuite) TestAuthV1_ResetPassword_nonExistentUser() {
441
	_, _ = e.createAndSingIn(e.uuid()+"@test.com", "password")
442
	httpResp := e.httpRequest(
443
		http.MethodPost,
444
		"/api/v1/auth/reset-password",
445
		e.jsonify(apiV1AuthResetPasswordRequest{
446
			Email: e.uuid() + "@testing.com",
447
		}),
448
	)
449
450
	e.Equal(httpResp.Code, http.StatusBadRequest)
451
}
452
453
type apiv1AuthChangeEmailRequest struct {
454
	NewEmail string `json:"new_email"`
455
}
456
457
func (e *AppTestSuite) TestAuthV1_ChangeEmail() {
458
	oldEmail, newEmail := e.randomEmail(), e.randomEmail()
459
	uid, toks := e.createAndSingIn(oldEmail, e.uuid())
460
461
	// request email change
462
	httpResp := e.httpRequest(
463
		http.MethodPost,
464
		"/api/v1/auth/change-email",
465
		e.jsonify(apiv1AuthChangeEmailRequest{
466
			NewEmail: newEmail,
467
		}),
468
		toks.AccessToken,
469
	)
470
	e.Equal(http.StatusOK, httpResp.Code)
471
472
	token := e.getChangeEmailTokenByUserID(uid)
473
	e.Empty(token.UsedAt)
474
	e.Equal(mockMailStore[oldEmail], token.Token)
475
476
	// confirm email change
477
	httpResp = e.httpRequest(http.MethodGet, "/api/v1/auth/change-email/"+token.Token, nil)
478
	e.Equal(http.StatusOK, httpResp.Code)
479
480
	updatedToken := e.getChangeEmailTokenByUserID(uid)
481
	e.NotEmpty(updatedToken.UsedAt)
482
483
	dbUser := e.getUserByEmail(token.Extra)
484
	e.Equal(dbUser.Email, newEmail)
485
}
486
487
func (e *AppTestSuite) TestAuthV1_ChangeEmail_wrongSameEmail() {
488
	email := e.randomEmail()
489
	_, toks := e.createAndSingIn(email, e.uuid())
490
491
	// request email change
492
	httpResp := e.httpRequest(
493
		http.MethodPost,
494
		"/api/v1/auth/change-email",
495
		e.jsonify(apiv1AuthChangeEmailRequest{
496
			NewEmail: email,
497
		}),
498
		toks.AccessToken,
499
	)
500
	e.Equal(http.StatusBadRequest, httpResp.Code)
501
502
	var body errorResponse
503
	e.readBodyAndUnjsonify(httpResp.Body, &body)
504
505
	e.Equal(body.Message, models.ErrUserEmailIsAlreadyInUse.Error())
506
}
507
508
type getMeResponse struct {
509
	Email        string    `json:"email"`
510
	CreatedAt    time.Time `json:"created_at"`
511
	LastLoginAt  time.Time `json:"last_login_at"`
512
	NotesCreated int       `json:"notes_created"`
513
}
514
515
func (e *AppTestSuite) TestApiV1_getMe() {
516
	email := e.uuid() + "@test.com"
517
	uid, toks := e.createAndSingIn(email, "password")
518
519
	httpResp := e.httpRequest(http.MethodGet, "/api/v1/me", nil, toks.AccessToken)
520
521
	e.Equal(httpResp.Code, http.StatusOK)
522
523
	var body getMeResponse
524
	e.readBodyAndUnjsonify(httpResp.Body, &body)
525
526
	e.Equal(email, body.Email)
527
	e.NotZero(body.CreatedAt)
528
	e.NotZero(body.LastLoginAt)
529
530
	var notesCount int
531
	err := e.postgresDB.
532
		QueryRow(e.ctx, "select count(*) from notes_authors where user_id = $1", uid).
533
		Scan(&notesCount)
534
	e.require.NoError(err)
535
536
	e.Equal(body.NotesCreated, notesCount)
537
}
538
539
// createAndSingIn creates an activated user, logs them in,
540
// and returns their userID along with access and refresh tokens.
541
func (e *AppTestSuite) createAndSingIn(
542
	email, password string,
543
) (uuid.UUID, apiv1AuthSignInResponse) {
544
	uid := e.insertUser(email, password, true)
545
	httpResp := e.httpRequest(
546
		http.MethodPost,
547
		"/api/v1/auth/signin",
548
		e.jsonify(apiv1AuthSignInRequest{
549
			Email:    email,
550
			Password: password,
551
		}),
552
	)
553
554
	e.Equal(httpResp.Code, http.StatusOK)
555
556
	var body apiv1AuthSignInResponse
557
	e.readBodyAndUnjsonify(httpResp.Body, &body)
558
559
	return uid, body
560
}
561
562
func (e *AppTestSuite) randomEmail() string {
563
	b := make([]byte, 4)
564
	_, _ = rand.Read(b)
565
	return fmt.Sprintf("user-%s@test.local", hex.EncodeToString(b))
566
}