all repos

onasty @ 3f33118

a one-time notes service

onasty/e2e/apiv1_auth_test.go (view raw)

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