all repos

onasty @ d2c87a81b9c7e589cbe6848e117470645d68e192

a one-time notes service

onasty/e2e/apiv1_auth_test.go (view raw)

Olexandr Smirnov Olexandr Smirnov
olexsmir@gmail.com
fix(api): change password exposes internal error (#194)..., 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
	oldPassword, newPassword := e.uuid(), e.uuid()
351
	email := e.uuid() + "@test.com"
352
	_, toks := e.createAndSingIn(email, oldPassword)
353
354
	httpResp := e.httpRequest(
355
		http.MethodPost,
356
		"/api/v1/auth/change-password",
357
		e.jsonify(apiv1AuthChangePasswordRequest{
358
			CurrentPassword: oldPassword,
359
			NewPassword:     newPassword,
360
		}),
361
		toks.AccessToken,
362
	)
363
364
	e.Equal(httpResp.Code, http.StatusOK)
365
366
	userDB := e.getUserByEmail(email)
367
	e.NoError(e.hasher.Compare(userDB.Password, newPassword))
368
}
369
370
func (e *AppTestSuite) TestAuthV1_ChangePassword_wrongPassword() {
371
	password := e.uuid()
372
	newPassword := e.uuid()
373
	email := e.uuid() + "@test.com"
374
	_, toks := e.createAndSingIn(email, password)
375
376
	httpResp := e.httpRequest(
377
		http.MethodPost,
378
		"/api/v1/auth/change-password",
379
		e.jsonify(apiv1AuthChangePasswordRequest{
380
			CurrentPassword: e.uuid(),
381
			NewPassword:     newPassword,
382
		}),
383
		toks.AccessToken,
384
	)
385
386
	e.Equal(http.StatusBadRequest, httpResp.Code)
387
388
	var body errorResponse
389
	e.readBodyAndUnjsonify(httpResp.Body, &body)
390
	e.Equal(models.ErrUserWrongCredentials.Error(), body.Message)
391
392
	userDB := e.getUserByEmail(email)
393
394
	err := e.hasher.Compare(userDB.Password, newPassword)
395
	e.ErrorIs(err, hasher.ErrMismatchedHashes)
396
}
397
398
type (
399
	apiV1AuthResetPasswordRequest struct {
400
		Email string `json:"email"`
401
	}
402
	apiV1AuthSetPasswordRequest struct {
403
		Password string `json:"password"`
404
	}
405
)
406
407
func (e *AppTestSuite) TestAuthV1_ResetPassword() {
408
	email := e.uuid() + "@test.com"
409
	uid, _ := e.createAndSingIn(email, "password")
410
411
	httpResp := e.httpRequest(
412
		http.MethodPost,
413
		"/api/v1/auth/reset-password",
414
		e.jsonify(apiV1AuthResetPasswordRequest{
415
			Email: email,
416
		}),
417
	)
418
419
	e.Equal(httpResp.Code, http.StatusOK)
420
421
	token := e.getResetPasswordTokenByUserID(uid)
422
	e.Empty(token.UsedAt)
423
	e.Equal(mockMailStore[email], token.Token)
424
425
	// set new password
426
	password := e.uuid()
427
	httpResp = e.httpRequest(
428
		http.MethodPost,
429
		"/api/v1/auth/reset-password/"+token.Token,
430
		e.jsonify(apiV1AuthSetPasswordRequest{
431
			Password: password,
432
		}),
433
	)
434
435
	dbUser := e.getUserByEmail(email)
436
	e.Equal(httpResp.Code, http.StatusOK)
437
	e.NoError(e.hasher.Compare(dbUser.Password, password))
438
439
	token = e.getResetPasswordTokenByUserID(uid)
440
	e.NotEmpty(token.UsedAt)
441
}
442
443
func (e *AppTestSuite) TestAuthV1_ResetPassword_nonExistentUser() {
444
	_, _ = e.createAndSingIn(e.uuid()+"@test.com", "password")
445
	httpResp := e.httpRequest(
446
		http.MethodPost,
447
		"/api/v1/auth/reset-password",
448
		e.jsonify(apiV1AuthResetPasswordRequest{
449
			Email: e.uuid() + "@testing.com",
450
		}),
451
	)
452
453
	e.Equal(httpResp.Code, http.StatusBadRequest)
454
}
455
456
type apiv1AuthChangeEmailRequest struct {
457
	NewEmail string `json:"new_email"`
458
}
459
460
func (e *AppTestSuite) TestAuthV1_ChangeEmail() {
461
	oldEmail, newEmail := e.randomEmail(), e.randomEmail()
462
	uid, toks := e.createAndSingIn(oldEmail, e.uuid())
463
464
	// request email change
465
	httpResp := e.httpRequest(
466
		http.MethodPost,
467
		"/api/v1/auth/change-email",
468
		e.jsonify(apiv1AuthChangeEmailRequest{
469
			NewEmail: newEmail,
470
		}),
471
		toks.AccessToken,
472
	)
473
	e.Equal(http.StatusOK, httpResp.Code)
474
475
	token := e.getChangeEmailTokenByUserID(uid)
476
	e.Empty(token.UsedAt)
477
	e.Equal(mockMailStore[oldEmail], token.Token)
478
479
	// confirm email change
480
	httpResp = e.httpRequest(http.MethodGet, "/api/v1/auth/change-email/"+token.Token, nil)
481
	e.Equal(http.StatusOK, httpResp.Code)
482
483
	updatedToken := e.getChangeEmailTokenByUserID(uid)
484
	e.NotEmpty(updatedToken.UsedAt)
485
486
	dbUser := e.getUserByEmail(token.Extra)
487
	e.Equal(dbUser.Email, newEmail)
488
}
489
490
func (e *AppTestSuite) TestAuthV1_ChangeEmail_wrongSameEmail() {
491
	email := e.randomEmail()
492
	_, toks := e.createAndSingIn(email, e.uuid())
493
494
	// request email change
495
	httpResp := e.httpRequest(
496
		http.MethodPost,
497
		"/api/v1/auth/change-email",
498
		e.jsonify(apiv1AuthChangeEmailRequest{
499
			NewEmail: email,
500
		}),
501
		toks.AccessToken,
502
	)
503
	e.Equal(http.StatusBadRequest, httpResp.Code)
504
505
	var body errorResponse
506
	e.readBodyAndUnjsonify(httpResp.Body, &body)
507
508
	e.Equal(body.Message, models.ErrUserEmailIsAlreadyInUse.Error())
509
}
510
511
type getMeResponse struct {
512
	Email        string    `json:"email"`
513
	CreatedAt    time.Time `json:"created_at"`
514
	LastLoginAt  time.Time `json:"last_login_at"`
515
	NotesCreated int       `json:"notes_created"`
516
}
517
518
func (e *AppTestSuite) TestApiV1_getMe() {
519
	email := e.uuid() + "@test.com"
520
	uid, toks := e.createAndSingIn(email, "password")
521
522
	httpResp := e.httpRequest(http.MethodGet, "/api/v1/me", nil, toks.AccessToken)
523
524
	e.Equal(httpResp.Code, http.StatusOK)
525
526
	var body getMeResponse
527
	e.readBodyAndUnjsonify(httpResp.Body, &body)
528
529
	e.Equal(email, body.Email)
530
	e.NotZero(body.CreatedAt)
531
	e.NotZero(body.LastLoginAt)
532
533
	var notesCount int
534
	err := e.postgresDB.
535
		QueryRow(e.ctx, "select count(*) from notes_authors where user_id = $1", uid).
536
		Scan(&notesCount)
537
	e.require.NoError(err)
538
539
	e.Equal(body.NotesCreated, notesCount)
540
}
541
542
// createAndSingIn creates an activated user, logs them in,
543
// and returns their userID along with access and refresh tokens.
544
func (e *AppTestSuite) createAndSingIn(
545
	email, password string,
546
) (uuid.UUID, apiv1AuthSignInResponse) {
547
	uid := e.insertUser(email, password, true)
548
	httpResp := e.httpRequest(
549
		http.MethodPost,
550
		"/api/v1/auth/signin",
551
		e.jsonify(apiv1AuthSignInRequest{
552
			Email:    email,
553
			Password: password,
554
		}),
555
	)
556
557
	e.Equal(httpResp.Code, http.StatusOK)
558
559
	var body apiv1AuthSignInResponse
560
	e.readBodyAndUnjsonify(httpResp.Body, &body)
561
562
	return uid, body
563
}
564
565
func (e *AppTestSuite) randomEmail() string {
566
	b := make([]byte, 4)
567
	_, _ = rand.Read(b)
568
	return fmt.Sprintf("user-%s@test.local", hex.EncodeToString(b))
569
}