all repos

onasty @ e21d37bab459675c3789549338bb699769f1687b

a one-time notes service

onasty/e2e/apiv1_notes_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
refactor!: rename "burn before expiration" to "keep before expiration" (#199)..., 9 months ago
1
package e2e_test
2
3
import (
4
	"net/http"
5
	"net/http/httptest"
6
	"testing"
7
	"testing/synctest"
8
	"time"
9
10
	"github.com/gofrs/uuid/v5"
11
	"github.com/olexsmir/onasty/internal/models"
12
)
13
14
type (
15
	apiv1NoteCreateRequest struct {
16
		Content              string    `json:"content"`
17
		Slug                 string    `json:"slug"`
18
		Password             string    `json:"password"`
19
		KeepBeforeExpiration bool      `json:"keep_before_expiration"`
20
		ExpiresAt            time.Time `json:"expires_at"`
21
	}
22
	apiv1NoteCreateResponse struct {
23
		Slug string `json:"slug"`
24
	}
25
)
26
27
func (e *AppTestSuite) TestNoteV1_Create() {
28
	tests := []struct {
29
		name   string
30
		inp    apiv1NoteCreateRequest
31
		assert func(*httptest.ResponseRecorder, apiv1NoteCreateRequest)
32
	}{
33
		{
34
			name: "empty request",
35
			inp:  apiv1NoteCreateRequest{}, //nolint:exhaustruct
36
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
37
				e.Equal(r.Code, http.StatusBadRequest)
38
			},
39
		},
40
		{
41
			name: "content only",
42
			inp:  apiv1NoteCreateRequest{Content: e.uuid()}, //nolint:exhaustruct
43
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
44
				e.Equal(http.StatusCreated, r.Code)
45
46
				var body apiv1NoteCreateResponse
47
				e.readBodyAndUnjsonify(r.Body, &body)
48
49
				_, err := uuid.FromString(body.Slug)
50
				e.require.NoError(err)
51
52
				dbNote := e.getNoteBySlug(body.Slug)
53
				e.NotEmpty(dbNote)
54
			},
55
		},
56
		{
57
			name: "set slug",
58
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
59
				Slug:    e.uuid() + "fuker",
60
				Content: e.uuid(),
61
			},
62
			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {
63
				e.Equal(http.StatusCreated, r.Code)
64
65
				var body apiv1NoteCreateResponse
66
				e.readBodyAndUnjsonify(r.Body, &body)
67
68
				dbNote := e.getNoteBySlug(inp.Slug)
69
				e.NotEmpty(dbNote)
70
			},
71
		},
72
		{
73
			name: "invalid slug, with space",
74
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
75
				Slug:    e.uuid() + "fuker fuker",
76
				Content: e.uuid(),
77
			},
78
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
79
				e.Equal(http.StatusBadRequest, r.Code)
80
			},
81
		},
82
		{
83
			name: "invalid slug, with slash",
84
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
85
				Slug:    e.uuid() + "fuker/fuker",
86
				Content: e.uuid(),
87
			},
88
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
89
				e.Equal(http.StatusBadRequest, r.Code)
90
			},
91
		},
92
		{
93
			name: "invalid slug, 'read'",
94
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
95
				Slug:    "read",
96
				Content: e.uuid(),
97
			},
98
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
99
				e.Equal(http.StatusBadRequest, r.Code)
100
101
				var body errorResponse
102
				e.readBodyAndUnjsonify(r.Body, &body)
103
104
				e.Equal(models.ErrNoteSlugIsAlreadyInUse.Error(), body.Message)
105
			},
106
		},
107
		{
108
			name: "invalid slug, 'unread'",
109
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
110
				Slug:    "unread",
111
				Content: e.uuid(),
112
			},
113
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
114
				e.Equal(http.StatusBadRequest, r.Code)
115
116
				var body errorResponse
117
				e.readBodyAndUnjsonify(r.Body, &body)
118
119
				e.Equal(models.ErrNoteSlugIsAlreadyInUse.Error(), body.Message)
120
			},
121
		},
122
		{
123
			name: "slug provided but empty",
124
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
125
				Slug:    "",
126
				Content: e.uuid(),
127
			},
128
			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {
129
				e.Equal(http.StatusCreated, r.Code)
130
131
				var body apiv1NoteCreateResponse
132
				e.readBodyAndUnjsonify(r.Body, &body)
133
134
				dbNote := e.getNoteBySlug(body.Slug)
135
				e.NotEmpty(dbNote)
136
				e.Equal(inp.Content, dbNote.Content)
137
			},
138
		},
139
		{
140
			name: "keep before expiration, but without expiration time",
141
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
142
				Content:              e.uuid(),
143
				KeepBeforeExpiration: true,
144
			},
145
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
146
				var body errorResponse
147
				e.readBodyAndUnjsonify(r.Body, &body)
148
149
				e.Equal(models.ErrNoteCannotBeKept.Error(), body.Message)
150
			},
151
		},
152
		{
153
			name: "set password",
154
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
155
				Content:  e.uuid(),
156
				Password: e.uuid(),
157
			},
158
			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {
159
				e.Equal(http.StatusCreated, r.Code)
160
			},
161
		},
162
		{
163
			name: "all possible fields",
164
			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct
165
				Content:              e.uuid(),
166
				KeepBeforeExpiration: true,
167
				ExpiresAt:            time.Now().Add(time.Hour),
168
			},
169
			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {
170
				e.Equal(http.StatusCreated, r.Code)
171
172
				var body apiv1NoteCreateResponse
173
				e.readBodyAndUnjsonify(r.Body, &body)
174
175
				dbNote := e.getNoteBySlug(body.Slug)
176
				e.NotEmpty(dbNote)
177
178
				e.Equal(dbNote.Content, inp.Content)
179
				e.Equal(dbNote.KeepBeforeExpiration, inp.KeepBeforeExpiration)
180
				e.Equal(dbNote.ExpiresAt.Unix(), inp.ExpiresAt.Unix())
181
			},
182
		},
183
	}
184
185
	for _, tt := range tests {
186
		httpResp := e.httpRequest(http.MethodPost, "/api/v1/note", e.jsonify(tt.inp))
187
		tt.assert(httpResp, tt.inp)
188
	}
189
}
190
191
type apiv1NoteGetResponse struct {
192
	Content   string     `json:"content"`
193
	ReadAt    *time.Time `json:"read_at"`
194
	CreatedAt time.Time  `json:"created_at"`
195
	ExpiresAt time.Time  `json:"expires_at"`
196
}
197
198
func (e *AppTestSuite) TestNoteV1_Get() {
199
	// create note
200
	content := e.uuid()
201
	httpResp := e.httpRequest(
202
		http.MethodPost,
203
		"/api/v1/note",
204
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
205
			Content: content,
206
		}),
207
	)
208
	e.Equal(http.StatusCreated, httpResp.Code)
209
210
	var bodyCreated apiv1NoteCreateResponse
211
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
212
213
	// read note
214
	httpResp2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
215
	e.Equal(http.StatusOK, httpResp2.Code)
216
217
	var body apiv1NoteGetResponse
218
	e.readBodyAndUnjsonify(httpResp2.Body, &body)
219
220
	e.Equal(content, body.Content)
221
222
	dbNote := e.getNoteBySlug(bodyCreated.Slug)
223
	e.Empty(dbNote.Content)
224
	e.False(dbNote.ReadAt.IsZero())
225
}
226
227
func (e *AppTestSuite) TestNoteV1_Get_alreadyRead() {
228
	// create note
229
	content := e.uuid()
230
	httpRespCreated := e.httpRequest(
231
		http.MethodPost,
232
		"/api/v1/note",
233
		e.jsonify(apiv1NoteCreateRequest{Content: content}), //nolint:exhaustruct
234
	)
235
	e.Equal(http.StatusCreated, httpRespCreated.Code)
236
237
	var bodyCreated apiv1NoteCreateResponse
238
	e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)
239
240
	// read note
241
	httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
242
	e.Equal(httpRespRead.Code, http.StatusOK)
243
244
	var bodyRead apiv1NoteGetResponse
245
	e.readBodyAndUnjsonify(httpRespRead.Body, &bodyRead)
246
247
	e.Equal(content, bodyRead.Content)
248
249
	dbNote := e.getNoteBySlug(bodyCreated.Slug)
250
	e.Empty(dbNote.Content)
251
	e.False(dbNote.ReadAt.IsZero())
252
253
	// read note once again
254
	httpRespRead2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
255
	e.Equal(http.StatusNotFound, httpRespRead2.Code)
256
257
	var bodyRead2 apiv1NoteGetResponse
258
	e.readBodyAndUnjsonify(httpRespRead2.Body, &bodyRead2)
259
260
	dbNote2 := e.getNoteBySlug(bodyCreated.Slug)
261
	e.Empty(dbNote2.Content)
262
263
	e.Empty(bodyRead2.Content)
264
	e.Equal(dbNote2.ReadAt.Unix(), bodyRead2.ReadAt.Unix())
265
	e.Equal(dbNote2.CreatedAt.Unix(), bodyRead2.CreatedAt.Unix())
266
	e.Equal(dbNote2.ExpiresAt.Unix(), bodyRead2.ExpiresAt.Unix())
267
}
268
269
func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBeKeptBeforeExpiration() {
270
	// create note
271
	content := e.uuid()
272
	httpRespCreated := e.httpRequest(
273
		http.MethodPost,
274
		"/api/v1/note",
275
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
276
			Content:              content,
277
			ExpiresAt:            time.Now().Add(time.Hour),
278
			KeepBeforeExpiration: true,
279
		}),
280
	)
281
	e.Equal(http.StatusCreated, httpRespCreated.Code)
282
283
	var bodyCreated apiv1NoteCreateResponse
284
	e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)
285
286
	// read note
287
	httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
288
	e.Equal(http.StatusOK, httpRespRead.Code)
289
290
	var bodyRead apiv1NoteGetResponse
291
	e.readBodyAndUnjsonify(httpRespRead.Body, &bodyRead)
292
293
	e.Equal(content, bodyRead.Content)
294
295
	dbNote := e.getNoteBySlug(bodyCreated.Slug)
296
	e.Equal(content, dbNote.Content)
297
	e.True(dbNote.ReadAt.IsZero())
298
299
	// read note again
300
	httpRespRead2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
301
	e.Equal(http.StatusOK, httpRespRead2.Code)
302
303
	var body2 apiv1NoteGetResponse
304
	e.readBodyAndUnjsonify(httpRespRead2.Body, &body2)
305
306
	dbNote2 := e.getNoteBySlug(bodyCreated.Slug)
307
	e.Equal(content, dbNote2.Content)
308
	e.Equal(body2.Content, dbNote.Content)
309
	e.True(dbNote2.ReadAt.IsZero())
310
311
	e.Equal(bodyRead, body2)
312
	e.Equal(dbNote, dbNote2)
313
}
314
315
func (e *AppTestSuite) TestNoteV1_Get_ShouldKeepBeforeExpiration_expired() {
316
	// synctest is used here to ensure proper synchronization and isolation of test execution
317
	// it still feels wrong to use synctest in e2e test, but it works nonetheless
318
	synctest.Test(e.T(), func(_ *testing.T) {
319
		// create note
320
		content := e.uuid()
321
		httpRespCreated := e.httpRequest(
322
			http.MethodPost,
323
			"/api/v1/note",
324
			e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
325
				Content:              content,
326
				ExpiresAt:            time.Now().Add(time.Hour),
327
				KeepBeforeExpiration: true,
328
			}),
329
		)
330
		e.Equal(http.StatusCreated, httpRespCreated.Code)
331
332
		var bodyCreated apiv1NoteCreateResponse
333
		e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)
334
335
		time.Sleep(2 * time.Hour)
336
337
		// read note
338
		httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
339
		e.Equal(http.StatusGone, httpRespRead.Code)
340
341
		dbNote := e.getNoteBySlug(bodyCreated.Slug)
342
		e.Equal(content, dbNote.Content)
343
		e.True(dbNote.ReadAt.IsZero())
344
	})
345
}
346
347
func (e *AppTestSuite) TestNoteV1_Get_expired() {
348
	// synctest is used here to ensure proper synchronization and isolation of test execution
349
	// it still feels wrong to use synctest in e2e test, but it works nonetheless
350
	synctest.Test(e.T(), func(_ *testing.T) {
351
		// create note
352
		content := e.uuid()
353
		httpRespCreated := e.httpRequest(
354
			http.MethodPost,
355
			"/api/v1/note",
356
			e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
357
				Content:   content,
358
				ExpiresAt: time.Now().Add(time.Hour),
359
			}),
360
		)
361
		e.Equal(http.StatusCreated, httpRespCreated.Code)
362
363
		var bodyCreated apiv1NoteCreateResponse
364
		e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)
365
366
		time.Sleep(2 * time.Hour)
367
368
		// read note
369
		httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
370
		e.Equal(http.StatusGone, httpRespRead.Code)
371
372
		dbNote := e.getNoteBySlug(bodyCreated.Slug)
373
		e.Equal(content, dbNote.Content)
374
		e.True(dbNote.ReadAt.IsZero())
375
	})
376
}
377
378
type apiv1NoteGetWithPasswordRequest struct {
379
	Password string `json:"password"`
380
}
381
382
func (e *AppTestSuite) TestNoteV1_GetWithPassword() {
383
	content := e.uuid()
384
	passwd := e.uuid()
385
	httpResp := e.httpRequest(
386
		http.MethodPost,
387
		"/api/v1/note",
388
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
389
			Content:  content,
390
			Password: passwd,
391
		}),
392
	)
393
	e.Equal(http.StatusCreated, httpResp.Code)
394
395
	var bodyCreated apiv1NoteCreateResponse
396
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
397
398
	httpResp = e.httpRequest(
399
		http.MethodPost,
400
		"/api/v1/note/"+bodyCreated.Slug+"/view",
401
		e.jsonify(apiv1NoteGetWithPasswordRequest{
402
			Password: passwd,
403
		}),
404
	)
405
	e.Equal(httpResp.Code, http.StatusOK)
406
407
	var body apiv1NoteGetResponse
408
	e.readBodyAndUnjsonify(httpResp.Body, &body)
409
410
	e.Equal(content, body.Content)
411
412
	dbNote := e.getNoteBySlug(bodyCreated.Slug)
413
	e.Equal(dbNote.Content, "")
414
	e.Equal(dbNote.ReadAt.IsZero(), false)
415
}
416
417
func (e *AppTestSuite) TestNoteV1_GetWithPassword_wrongNoPassword() {
418
	content := e.uuid()
419
	passwd := e.uuid()
420
	httpResp := e.httpRequest(
421
		http.MethodPost,
422
		"/api/v1/note",
423
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
424
			Content:  content,
425
			Password: passwd,
426
		}),
427
	)
428
	e.Equal(http.StatusCreated, httpResp.Code)
429
430
	var bodyCreated apiv1NoteCreateResponse
431
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
432
433
	httpResp = e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
434
	e.Equal(httpResp.Code, http.StatusNotFound)
435
}
436
437
func (e *AppTestSuite) TestNoteV1_GetWithPassword_wrong() {
438
	content := e.uuid()
439
	httpResp := e.httpRequest(
440
		http.MethodPost,
441
		"/api/v1/note",
442
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
443
			Content:  content,
444
			Password: e.uuid(),
445
		}),
446
	)
447
	e.Equal(http.StatusCreated, httpResp.Code)
448
449
	var bodyCreated apiv1NoteCreateResponse
450
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
451
452
	httpResp = e.httpRequest(
453
		http.MethodPost,
454
		"/api/v1/note/"+bodyCreated.Slug+"/view",
455
		e.jsonify(apiv1NoteGetWithPasswordRequest{
456
			Password: e.uuid(),
457
		}),
458
	)
459
	e.Equal(httpResp.Code, http.StatusNotFound)
460
}
461
462
type apiv1NoteMetadataResponse struct {
463
	CreatedAt   time.Time `json:"created_at"`
464
	HasPassword bool      `json:"has_password"`
465
}
466
467
func (e *AppTestSuite) TestNoteV1_GetMetadata() {
468
	// create note
469
	httpResp := e.httpRequest(
470
		http.MethodPost,
471
		"/api/v1/note",
472
		e.jsonify(apiv1NoteCreateRequest{Content: "content"}), //nolint:exhaustruct
473
	)
474
	e.Equal(http.StatusCreated, httpResp.Code)
475
476
	var bodyCreated apiv1NoteCreateResponse
477
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
478
479
	// get metadata
480
	metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug+"/meta", []byte{})
481
	e.Equal(metaResp.Code, http.StatusOK)
482
483
	var metadata apiv1NoteMetadataResponse
484
	e.readBodyAndUnjsonify(metaResp.Body, &metadata)
485
486
	e.False(metadata.HasPassword)
487
	e.NotEmpty(metadata.CreatedAt)
488
}
489
490
func (e *AppTestSuite) TestNoteV1_GetMetadata_withPassword() {
491
	// create note
492
	httpResp := e.httpRequest(
493
		http.MethodPost,
494
		"/api/v1/note",
495
		e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct
496
			Content:  "content",
497
			Password: "pass",
498
		}),
499
	)
500
	e.Equal(http.StatusCreated, httpResp.Code)
501
502
	var bodyCreated apiv1NoteCreateResponse
503
	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)
504
505
	// get metadata
506
	metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug+"/meta", []byte{})
507
	e.Equal(http.StatusOK, metaResp.Code)
508
509
	var metadata apiv1NoteMetadataResponse
510
	e.readBodyAndUnjsonify(metaResp.Body, &metadata)
511
512
	e.True(metadata.HasPassword)
513
	e.NotEmpty(metadata.CreatedAt)
514
}
515
516
func (e *AppTestSuite) TestNoteV1_GetMetadata_notFound() {
517
	metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+e.uuid()+"/meta", []byte{})
518
	e.Equal(http.StatusNotFound, metaResp.Code)
519
}
520
521
func (e *AppTestSuite) TestNoteV1_GetMetadata_readNote() {
522
	// create note
523
	createdResp := e.httpRequest(
524
		http.MethodPost,
525
		"/api/v1/note",
526
		e.jsonify(apiv1NoteCreateRequest{Content: "content"}), //nolint:exhaustruct
527
	)
528
	e.Equal(http.StatusCreated, createdResp.Code)
529
530
	var bodyCreated apiv1NoteCreateResponse
531
	e.readBodyAndUnjsonify(createdResp.Body, &bodyCreated)
532
533
	// read note
534
	readResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)
535
	e.Equal(http.StatusOK, readResp.Code)
536
537
	// get metadata
538
	metaResp := e.httpRequest(http.MethodGet, "/api/v1/note/"+e.uuid()+"/meta", nil)
539
	e.Equal(http.StatusNotFound, metaResp.Code)
540
}