all repos

onasty @ d2c87a81b9c7e589cbe6848e117470645d68e192

a one-time notes service
4 files changed, 138 insertions(+), 23 deletions(-)
test(e2e): test all behaviours of 'read note' route (#198)

* test(e2e): read note that was already read

* test(e2e): test if "burn before expiration" option is respected

* fix: use correct order of arguments in Equal for some tests

* use synctest in e2e tests, yeah...

* fix: typo

* refactor: use synctest where it's needed
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-08-27 17:52:04 +0300
Parent: c24198a
M e2e/apiv1_notes_test.go
···
        3
        3
         import (

      
        4
        4
         	"net/http"

      
        5
        5
         	"net/http/httptest"

      
        
        6
        +	"testing"

      
        
        7
        +	"testing/synctest"

      
        6
        8
         	"time"

      
        7
        9
         

      
        8
        10
         	"github.com/gofrs/uuid/v5"

      ···
        39
        41
         			name: "content only",

      
        40
        42
         			inp:  apiv1NoteCreateRequest{Content: e.uuid()}, //nolint:exhaustruct

      
        41
        43
         			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {

      
        42
        
        -				e.Equal(r.Code, http.StatusCreated)

      
        
        44
        +				e.Equal(http.StatusCreated, r.Code)

      
        43
        45
         

      
        44
        46
         				var body apiv1NoteCreateResponse

      
        45
        47
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        58
        60
         				Content: e.uuid(),

      
        59
        61
         			},

      
        60
        62
         			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {

      
        61
        
        -				e.Equal(r.Code, http.StatusCreated)

      
        
        63
        +				e.Equal(http.StatusCreated, r.Code)

      
        62
        64
         

      
        63
        65
         				var body apiv1NoteCreateResponse

      
        64
        66
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        94
        96
         				Content: e.uuid(),

      
        95
        97
         			},

      
        96
        98
         			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {

      
        97
        
        -				e.Equal(r.Code, http.StatusBadRequest)

      
        
        99
        +				e.Equal(http.StatusBadRequest, r.Code)

      
        98
        100
         

      
        99
        101
         				var body errorResponse

      
        100
        102
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        109
        111
         				Content: e.uuid(),

      
        110
        112
         			},

      
        111
        113
         			assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) {

      
        112
        
        -				e.Equal(r.Code, http.StatusBadRequest)

      
        
        114
        +				e.Equal(http.StatusBadRequest, r.Code)

      
        113
        115
         

      
        114
        116
         				var body errorResponse

      
        115
        117
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        124
        126
         				Content: e.uuid(),

      
        125
        127
         			},

      
        126
        128
         			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {

      
        127
        
        -				e.Equal(r.Code, http.StatusCreated)

      
        
        129
        +				e.Equal(http.StatusCreated, r.Code)

      
        128
        130
         

      
        129
        131
         				var body apiv1NoteCreateResponse

      
        130
        132
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        165
        167
         				ExpiresAt:            time.Now().Add(time.Hour),

      
        166
        168
         			},

      
        167
        169
         			assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) {

      
        168
        
        -				e.Equal(r.Code, http.StatusCreated)

      
        
        170
        +				e.Equal(http.StatusCreated, r.Code)

      
        169
        171
         

      
        170
        172
         				var body apiv1NoteCreateResponse

      
        171
        173
         				e.readBodyAndUnjsonify(r.Body, &body)

      ···
        194
        196
         }

      
        195
        197
         

      
        196
        198
         func (e *AppTestSuite) TestNoteV1_Get() {

      
        
        199
        +	// create note

      
        197
        200
         	content := e.uuid()

      
        198
        201
         	httpResp := e.httpRequest(

      
        199
        202
         		http.MethodPost,

      ···
        207
        210
         	var bodyCreated apiv1NoteCreateResponse

      
        208
        211
         	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)

      
        209
        212
         

      
        210
        
        -	httpResp = e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)

      
        211
        
        -	e.Equal(httpResp.Code, http.StatusOK)

      
        
        213
        +	// read note

      
        
        214
        +	httpResp2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)

      
        
        215
        +	e.Equal(http.StatusOK, httpResp2.Code)

      
        212
        216
         

      
        213
        217
         	var body apiv1NoteGetResponse

      
        214
        
        -	e.readBodyAndUnjsonify(httpResp.Body, &body)

      
        
        218
        +	e.readBodyAndUnjsonify(httpResp2.Body, &body)

      
        215
        219
         

      
        216
        220
         	e.Equal(content, body.Content)

      
        217
        221
         

      
        218
        222
         	dbNote := e.getNoteBySlug(bodyCreated.Slug)

      
        219
        
        -	e.Equal(dbNote.Content, "")

      
        220
        
        -	e.Equal(dbNote.ReadAt.IsZero(), false)

      
        
        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_ShouldNotBurnBeforeExpiration() {

      
        
        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
        +			BurnBeforeExpiration: 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
        +

      
        
        300
        +func (e *AppTestSuite) TestNoteV1_Get_ShouldBurnBeforeExpiration() {

      
        
        301
        +	// synctest is used here to ensure proper synchronization and isolation of test execution

      
        
        302
        +	// it still feels wrong to use synctest in e2e test, but it works nonetheless

      
        
        303
        +	synctest.Test(e.T(), func(_ *testing.T) {

      
        
        304
        +		// create note

      
        
        305
        +		content := e.uuid()

      
        
        306
        +		httpRespCreated := e.httpRequest(

      
        
        307
        +			http.MethodPost,

      
        
        308
        +			"/api/v1/note",

      
        
        309
        +			e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        
        310
        +				Content:              content,

      
        
        311
        +				ExpiresAt:            time.Now().Add(time.Hour),

      
        
        312
        +				BurnBeforeExpiration: true,

      
        
        313
        +			}),

      
        
        314
        +		)

      
        
        315
        +		e.Equal(http.StatusCreated, httpRespCreated.Code)

      
        
        316
        +

      
        
        317
        +		var bodyCreated apiv1NoteCreateResponse

      
        
        318
        +		e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated)

      
        
        319
        +

      
        
        320
        +		time.Sleep(2 * time.Hour)

      
        
        321
        +

      
        
        322
        +		// read note

      
        
        323
        +		httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil)

      
        
        324
        +		e.Equal(http.StatusGone, httpRespRead.Code)

      
        
        325
        +

      
        
        326
        +		dbNote := e.getNoteBySlug(bodyCreated.Slug)

      
        
        327
        +		e.Equal(content, dbNote.Content)

      
        
        328
        +		e.True(dbNote.ReadAt.IsZero())

      
        
        329
        +	})

      
        221
        330
         }

      
        222
        331
         

      
        223
        332
         type apiv1NoteGetWithPasswordRequest struct {

      
M internal/jwtutil/jwtutil_test.go
···
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"testing"

      
        
        5
        +	"testing/synctest"

      
        5
        6
         	"time"

      
        6
        7
         

      
        7
        8
         	"github.com/stretchr/testify/assert"

      ···
        46
        47
         }

      
        47
        48
         

      
        48
        49
         func TestJWTUtil_Parse_expired(t *testing.T) {

      
        49
        
        -	ttl := 100 * time.Millisecond

      
        50
        
        -	jwt := NewJWTUtil("key", ttl)

      
        51
        
        -	payload := Payload{UserID: "qwerty"}

      
        
        50
        +	ttl := 24 * time.Hour

      
        52
        51
         

      
        53
        
        -	token, err := jwt.AccessToken(payload)

      
        54
        
        -	require.NoError(t, err)

      
        55
        
        -	assert.NotEmpty(t, token)

      
        
        52
        +	synctest.Test(t, func(t *testing.T) {

      
        
        53
        +		jwt := NewJWTUtil("key", ttl)

      
        
        54
        +		payload := Payload{UserID: "qwerty"}

      
        56
        55
         

      
        57
        
        -	time.Sleep(ttl)

      
        58
        
        -	_, err = jwt.Parse(token)

      
        59
        
        -	require.Error(t, err)

      
        
        56
        +		token, err := jwt.AccessToken(payload)

      
        
        57
        +		require.NoError(t, err)

      
        
        58
        +		assert.NotEmpty(t, token)

      
        
        59
        +

      
        
        60
        +		time.Sleep(2 * ttl)

      
        
        61
        +

      
        
        62
        +		_, err = jwt.Parse(token)

      
        
        63
        +		require.EqualError(t, err, ErrTokenExpired.Error())

      
        
        64
        +	})

      
        60
        65
         }

      
M internal/service/notesrv/notesrv.go
···
        143
        143
         

      
        144
        144
         	// since not every note should be burn before expiration

      
        145
        145
         	// we return early if it's not

      
        
        146
        +	// TODO: fix naming

      
        146
        147
         	if note.ShouldBeBurnt() {

      
        147
        148
         		return respNote, nil

      
        148
        149
         	}

      ···
        230
        231
         }

      
        231
        232
         

      
        232
        233
         func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (models.Note, error) {

      
        233
        
        -	if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil {

      
        234
        
        -		return r, nil

      
        
        234
        +	if note, err := n.cache.GetNote(ctx, inp.Slug); err == nil {

      
        
        235
        +		return note, nil

      
        235
        236
         	}

      
        236
        237
         

      
        237
        238
         	note, err := n.getNoteFromDBasedOnInput(ctx, inp)

      
M internal/transport/http/ratelimit/ratelimit_test.go
···
        30
        30
         		limiter.getVisitor("192.168.9.1")

      
        31
        31
         		assert.Len(t, limiter.visitors, 1)

      
        32
        32
         

      
        33
        
        -		time.Sleep(61 * time.Second)

      
        
        33
        +		time.Sleep(2 * time.Minute)

      
        34
        34
         

      
        35
        35
         		limiter.cleanupVisitors()

      
        36
        36
         		assert.Empty(t, limiter.visitors)