all repos

onasty @ 7a1da0fa4be762f19bbb5136f14dcf26bdb922fd

a one-time notes service

onasty/e2e/apiv1_auth_test.go (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
refactor: require only email for resending verification email (#165)..., 11 months ago
1
package e2e_test
2
3
import (
4
	"net/http"
5
	"time"
6
7
	"github.com/gofrs/uuid/v5"
8
	"github.com/olexsmir/onasty/internal/hasher"
9
	"github.com/olexsmir/onasty/internal/models"
10
)
11
12
type apiv1AuthSignUpRequest struct {
13
	Email    string `json:"email"`
14
	Password string `json:"password"`
15
}
16
17
func (e *AppTestSuite) TestAuthV1_SignUP() {
18
	email := e.uuid() + "test@test.com"
19
	password := "password"
20
21
	httpResp := e.httpRequest(
22
		http.MethodPost,
23
		"/api/v1/auth/signup",
24
		e.jsonify(apiv1AuthSignUpRequest{
25
			Email:    email,
26
			Password: password,
27
		}),
28
	)
29
30
	dbUser := e.getUserByEmail(email)
31
	hashedPasswd, err := e.hasher.Hash(password)
32
	e.require.NoError(err)
33
34
	e.Equal(http.StatusCreated, httpResp.Code)
35
	e.Equal(dbUser.Email, email)
36
	e.Equal(dbUser.Password, hashedPasswd)
37
}
38
39
func (e *AppTestSuite) TestAuthV1_SignUP_badrequest() {
40
	tests := []struct {
41
		name     string
42
		email    string
43
		password string
44
	}{
45
		{name: "all fields empty", email: "", password: ""},
46
		{name: "non valid email", email: "email", password: "password"},
47
		{name: "non valid password", email: "test@test.com", password: "12345"},
48
	}
49
	for _, t := range tests {
50
		httpResp := e.httpRequest(
51
			http.MethodPost,
52
			"/api/v1/auth/signup",
53
			e.jsonify(apiv1AuthSignUpRequest{
54
				Email:    t.email,
55
				Password: t.password,
56
			}),
57
		)
58
59
		e.Equal(http.StatusBadRequest, httpResp.Code)
60
	}
61
}
62
63
type (
64
	apiv1AuthSignInRequest struct {
65
		Email    string `json:"email"`
66
		Password string `json:"password"`
67
	}
68
	apiv1AuthSignInResponse struct {
69
		AccessToken  string `json:"access_token"`
70
		RefreshToken string `json:"refresh_token"`
71
	}
72
)
73
74
func (e *AppTestSuite) TestAuthV1_VerifyEmail() {
75
	email := e.uuid() + "email@email.com"
76
	password := "qwerty"
77
78
	httpResp := e.httpRequest(
79
		http.MethodPost,
80
		"/api/v1/auth/signup",
81
		e.jsonify(apiv1AuthSignUpRequest{
82
			Email:    email,
83
			Password: password,
84
		}),
85
	)
86
87
	e.Equal(http.StatusCreated, httpResp.Code)
88
89
	user := e.getLastUserByEmail(email)
90
	token := e.getVerificationTokenByUserID(user.ID)
91
	e.Equal(token.Token, mockMailStore[email])
92
93
	httpResp = e.httpRequest(http.MethodGet, "/api/v1/auth/verify/"+token.Token, nil)
94
	e.Equal(http.StatusOK, httpResp.Code)
95
96
	user = e.getLastUserByEmail(email)
97
	e.Equal(user.Activated, true)
98
}
99
100
type apiv1AuthResendVerificationEmailRequest struct {
101
	Email string `json:"email"`
102
}
103
104
func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail() {
105
	email, password := e.uuid()+"email@email.com", e.uuid()
106
107
	// create test user
108
	signUpHTTPResp := e.httpRequest(
109
		http.MethodPost,
110
		"/api/v1/auth/signup",
111
		e.jsonify(apiv1AuthSignUpRequest{
112
			Email:    email,
113
			Password: password,
114
		}),
115
	)
116
117
	e.Equal(http.StatusCreated, signUpHTTPResp.Code)
118
119
	// handle sending of the email
120
	httpResp := e.httpRequest(
121
		http.MethodPost,
122
		"/api/v1/auth/resend-verification-email",
123
		e.jsonify(apiv1AuthResendVerificationEmailRequest{
124
			Email: email,
125
		}),
126
	)
127
128
	e.Equal(http.StatusOK, httpResp.Code)
129
	e.NotEmpty(mockMailStore[email])
130
}
131
132
func (e *AppTestSuite) TestAuthV1_ResendVerificationEmail_wrong() {
133
	email, password := e.uuid()+"@"+e.uuid()+".com", "password"
134
	e.insertUser(email, password, true)
135
136
	tests := []struct {
137
		name         string
138
		email        string
139
		expectedCode int
140
		expectedMsg  string
141
	}{
142
		{
143
			name:         "already verified account",
144
			email:        email,
145
			expectedCode: http.StatusBadRequest,
146
			expectedMsg:  models.ErrUserIsAlreadyVerified.Error(),
147
		},
148
		{
149
			name:         "user not found",
150
			email:        e.uuid() + "@at.com",
151
			expectedCode: http.StatusBadRequest,
152
			expectedMsg:  models.ErrUserNotFound.Error(),
153
		},
154
	}
155
156
	for _, t := range tests {
157
		httpResp := e.httpRequest(
158
			http.MethodPost,
159
			"/api/v1/auth/resend-verification-email",
160
			e.jsonify(apiv1AuthResendVerificationEmailRequest{
161
				Email: t.email,
162
			}))
163
164
		e.Equal(httpResp.Code, t.expectedCode)
165
166
		var body errorResponse
167
		e.readBodyAndUnjsonify(httpResp.Body, &body)
168
		e.Equal(body.Message, t.expectedMsg)
169
170
		e.Empty(mockMailStore[t.email])
171
	}
172
}
173
174
func (e *AppTestSuite) TestAuthV1_SignIn() {
175
	email := e.uuid() + "email@email.com"
176
	password := "qwerty"
177
	uid := e.insertUser(email, password, true)
178
179
	httpResp := e.httpRequest(
180
		http.MethodPost,
181
		"/api/v1/auth/signin",
182
		e.jsonify(apiv1AuthSignInRequest{
183
			Email:    email,
184
			Password: password,
185
		}),
186
	)
187
188
	var body apiv1AuthSignInResponse
189
	e.readBodyAndUnjsonify(httpResp.Body, &body)
190
191
	session := e.getLastSessionByUserID(uid)
192
	parsedToken := e.parseJwtToken(body.AccessToken)
193
194
	e.Equal(http.StatusOK, httpResp.Code)
195
	e.Equal(body.RefreshToken, session.RefreshToken)
196
	e.Equal(parsedToken.UserID, uid.String())
197
}
198
199
func (e *AppTestSuite) TestAuthV1_SignIn_wrong() {
200
	password := "password"
201
	email := e.uuid() + "@test.com"
202
	e.insertUser(email, "password", true)
203
204
	unactivatedEmail := e.uuid() + "@test.com"
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:     password,
229
			expectedCode: http.StatusBadRequest,
230
			expectedMsg:  models.ErrUserWrongCredentials.Error(),
231
		},
232
		{
233
			name:         "wrong password",
234
			email:        email,
235
			password:     "wrong-wrong",
236
			expectedCode: http.StatusBadRequest,
237
			expectedMsg:  models.ErrUserWrongCredentials.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.uuid()+"@test.com", "password")
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.uuid()+"@test.com", "password")
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.uuid()+"@test.com", "password")
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
	password := e.uuid()
348
	newPassword := e.uuid()
349
	email := e.uuid() + "@test.com"
350
	_, toks := e.createAndSingIn(email, password)
351
352
	httpResp := e.httpRequest(
353
		http.MethodPost,
354
		"/api/v1/auth/change-password",
355
		e.jsonify(apiv1AuthChangePasswordRequest{
356
			CurrentPassword: password,
357
			NewPassword:     newPassword,
358
		}),
359
		toks.AccessToken,
360
	)
361
362
	e.Equal(httpResp.Code, http.StatusOK)
363
364
	userDB := e.getUserByEmail(email)
365
	e.NoError(e.hasher.Compare(userDB.Password, newPassword))
366
}
367
368
func (e *AppTestSuite) TestAuthV1_ChangePassword_wrongPassword() {
369
	password := e.uuid()
370
	newPassword := e.uuid()
371
	email := e.uuid() + "@test.com"
372
	_, toks := e.createAndSingIn(email, password)
373
374
	httpResp := e.httpRequest(
375
		http.MethodPost,
376
		"/api/v1/auth/change-password",
377
		e.jsonify(apiv1AuthChangePasswordRequest{
378
			CurrentPassword: e.uuid(),
379
			NewPassword:     newPassword,
380
		}),
381
		toks.AccessToken,
382
	)
383
384
	e.Equal(http.StatusBadRequest, httpResp.Code)
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.uuid() + "@test.com"
403
	uid, _ := e.createAndSingIn(email, "password")
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.uuid()+"@test.com", "password")
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 getMeResponse struct {
451
	Email        string    `json:"email"`
452
	CreatedAt    time.Time `json:"created_at"`
453
	LastLoginAt  time.Time `json:"last_login_at"`
454
	NotesCreated int       `json:"notes_created"`
455
}
456
457
func (e *AppTestSuite) TestApiV1_getMe() {
458
	email := e.uuid() + "@test.com"
459
	uid, toks := e.createAndSingIn(email, "password")
460
461
	httpResp := e.httpRequest(http.MethodGet, "/api/v1/me", nil, toks.AccessToken)
462
463
	e.Equal(httpResp.Code, http.StatusOK)
464
465
	var body getMeResponse
466
	e.readBodyAndUnjsonify(httpResp.Body, &body)
467
468
	e.Equal(email, body.Email)
469
	e.NotZero(body.CreatedAt)
470
	e.NotZero(body.LastLoginAt)
471
472
	var notesCount int
473
	err := e.postgresDB.
474
		QueryRow(e.ctx, "select count(*) from notes_authors where user_id = $1", uid).
475
		Scan(&notesCount)
476
	e.require.NoError(err)
477
478
	e.Equal(body.NotesCreated, notesCount)
479
}
480
481
// createAndSingIn creates an activated user, logs them in,
482
// and returns their userID along with access and refresh tokens.
483
func (e *AppTestSuite) createAndSingIn(
484
	email, password string,
485
) (uuid.UUID, apiv1AuthSignInResponse) {
486
	uid := e.insertUser(email, password, true)
487
	httpResp := e.httpRequest(
488
		http.MethodPost,
489
		"/api/v1/auth/signin",
490
		e.jsonify(apiv1AuthSignInRequest{
491
			Email:    email,
492
			Password: password,
493
		}),
494
	)
495
496
	e.Equal(httpResp.Code, http.StatusOK)
497
498
	var body apiv1AuthSignInResponse
499
	e.readBodyAndUnjsonify(httpResp.Body, &body)
500
501
	return uid, body
502
}