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,6 +3,8 @@

import ( "net/http" "net/http/httptest" + "testing" + "testing/synctest" "time" "github.com/gofrs/uuid/v5"

@@ -39,7 +41,7 @@ {

name: "content only", inp: apiv1NoteCreateRequest{Content: e.uuid()}, //nolint:exhaustruct assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusCreated) + e.Equal(http.StatusCreated, r.Code) var body apiv1NoteCreateResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -58,7 +60,7 @@ Slug: e.uuid() + "fuker",

Content: e.uuid(), }, assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusCreated) + e.Equal(http.StatusCreated, r.Code) var body apiv1NoteCreateResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -94,7 +96,7 @@ Slug: "read",

Content: e.uuid(), }, assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusBadRequest) + e.Equal(http.StatusBadRequest, r.Code) var body errorResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -109,7 +111,7 @@ Slug: "unread",

Content: e.uuid(), }, assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusBadRequest) + e.Equal(http.StatusBadRequest, r.Code) var body errorResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -124,7 +126,7 @@ Slug: "",

Content: e.uuid(), }, assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusCreated) + e.Equal(http.StatusCreated, r.Code) var body apiv1NoteCreateResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -165,7 +167,7 @@ BurnBeforeExpiration: true,

ExpiresAt: time.Now().Add(time.Hour), }, assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusCreated) + e.Equal(http.StatusCreated, r.Code) var body apiv1NoteCreateResponse e.readBodyAndUnjsonify(r.Body, &body)

@@ -194,6 +196,7 @@ ExpiresAt time.Time `json:"expires_at"`

} func (e *AppTestSuite) TestNoteV1_Get() { + // create note content := e.uuid() httpResp := e.httpRequest( http.MethodPost,

@@ -207,17 +210,123 @@

var bodyCreated apiv1NoteCreateResponse e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated) - httpResp = e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) - e.Equal(httpResp.Code, http.StatusOK) + // read note + httpResp2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusOK, httpResp2.Code) var body apiv1NoteGetResponse - e.readBodyAndUnjsonify(httpResp.Body, &body) + e.readBodyAndUnjsonify(httpResp2.Body, &body) e.Equal(content, body.Content) dbNote := e.getNoteBySlug(bodyCreated.Slug) - e.Equal(dbNote.Content, "") - e.Equal(dbNote.ReadAt.IsZero(), false) + e.Empty(dbNote.Content) + e.False(dbNote.ReadAt.IsZero()) +} + +func (e *AppTestSuite) TestNoteV1_Get_alreadyRead() { + // create note + content := e.uuid() + httpRespCreated := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{Content: content}), //nolint:exhaustruct + ) + e.Equal(http.StatusCreated, httpRespCreated.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated) + + // read note + httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(httpRespRead.Code, http.StatusOK) + + var bodyRead apiv1NoteGetResponse + e.readBodyAndUnjsonify(httpRespRead.Body, &bodyRead) + + e.Equal(content, bodyRead.Content) + + dbNote := e.getNoteBySlug(bodyCreated.Slug) + e.Empty(dbNote.Content) + e.False(dbNote.ReadAt.IsZero()) + + // read note once again + httpRespRead2 := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusNotFound, httpRespRead2.Code) + + var bodyRead2 apiv1NoteGetResponse + e.readBodyAndUnjsonify(httpRespRead2.Body, &bodyRead2) + + dbNote2 := e.getNoteBySlug(bodyCreated.Slug) + e.Empty(dbNote2.Content) + + e.Empty(bodyRead2.Content) + e.Equal(dbNote2.ReadAt.Unix(), bodyRead2.ReadAt.Unix()) + e.Equal(dbNote2.CreatedAt.Unix(), bodyRead2.CreatedAt.Unix()) + e.Equal(dbNote2.ExpiresAt.Unix(), bodyRead2.ExpiresAt.Unix()) +} + +func (e *AppTestSuite) TestNoteV1_Get_ShouldNotBurnBeforeExpiration() { + // create note + content := e.uuid() + httpRespCreated := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: content, + ExpiresAt: time.Now().Add(time.Hour), + BurnBeforeExpiration: true, + }), + ) + e.Equal(http.StatusCreated, httpRespCreated.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated) + + // read note + httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusOK, httpRespRead.Code) + + var bodyRead apiv1NoteGetResponse + e.readBodyAndUnjsonify(httpRespRead.Body, &bodyRead) + + e.Equal(content, bodyRead.Content) + + dbNote := e.getNoteBySlug(bodyCreated.Slug) + e.Equal(content, dbNote.Content) + e.True(dbNote.ReadAt.IsZero()) +} + +func (e *AppTestSuite) TestNoteV1_Get_ShouldBurnBeforeExpiration() { + // synctest is used here to ensure proper synchronization and isolation of test execution + // it still feels wrong to use synctest in e2e test, but it works nonetheless + synctest.Test(e.T(), func(_ *testing.T) { + // create note + content := e.uuid() + httpRespCreated := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: content, + ExpiresAt: time.Now().Add(time.Hour), + BurnBeforeExpiration: true, + }), + ) + e.Equal(http.StatusCreated, httpRespCreated.Code) + + var bodyCreated apiv1NoteCreateResponse + e.readBodyAndUnjsonify(httpRespCreated.Body, &bodyCreated) + + time.Sleep(2 * time.Hour) + + // read note + httpRespRead := e.httpRequest(http.MethodGet, "/api/v1/note/"+bodyCreated.Slug, nil) + e.Equal(http.StatusGone, httpRespRead.Code) + + dbNote := e.getNoteBySlug(bodyCreated.Slug) + e.Equal(content, dbNote.Content) + e.True(dbNote.ReadAt.IsZero()) + }) } type apiv1NoteGetWithPasswordRequest struct {
M internal/jwtutil/jwtutil_test.go

@@ -2,6 +2,7 @@ package jwtutil

import ( "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert"

@@ -46,15 +47,19 @@ assert.Equal(t, payload, parsedPayload)

} func TestJWTUtil_Parse_expired(t *testing.T) { - ttl := 100 * time.Millisecond - jwt := NewJWTUtil("key", ttl) - payload := Payload{UserID: "qwerty"} + ttl := 24 * time.Hour - token, err := jwt.AccessToken(payload) - require.NoError(t, err) - assert.NotEmpty(t, token) + synctest.Test(t, func(t *testing.T) { + jwt := NewJWTUtil("key", ttl) + payload := Payload{UserID: "qwerty"} - time.Sleep(ttl) - _, err = jwt.Parse(token) - require.Error(t, err) + token, err := jwt.AccessToken(payload) + require.NoError(t, err) + assert.NotEmpty(t, token) + + time.Sleep(2 * ttl) + + _, err = jwt.Parse(token) + require.EqualError(t, err, ErrTokenExpired.Error()) + }) }
M internal/service/notesrv/notesrv.go

@@ -143,6 +143,7 @@ }

// since not every note should be burn before expiration // we return early if it's not + // TODO: fix naming if note.ShouldBeBurnt() { return respNote, nil }

@@ -230,8 +231,8 @@ return n.noterepo.DeleteNoteBySlug(ctx, slug, authorID)

} func (n *NoteSrv) getNote(ctx context.Context, inp GetNoteBySlugInput) (models.Note, error) { - if r, err := n.cache.GetNote(ctx, inp.Slug); err == nil { - return r, nil + if note, err := n.cache.GetNote(ctx, inp.Slug); err == nil { + return note, nil } note, err := n.getNoteFromDBasedOnInput(ctx, inp)
M internal/transport/http/ratelimit/ratelimit_test.go

@@ -30,7 +30,7 @@ limiter := newLimiter(10, 20, time.Minute)

limiter.getVisitor("192.168.9.1") assert.Len(t, limiter.visitors, 1) - time.Sleep(61 * time.Second) + time.Sleep(2 * time.Minute) limiter.cleanupVisitors() assert.Empty(t, limiter.visitors)