5 files changed,
94 insertions(+),
10 deletions(-)
Author:
Olexandr Smirnov
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-08-13 19:14:09 +0300
Parent:
bc92b82
M
e2e/apiv1_notes_test.go
@@ -6,6 +6,7 @@ "net/http/httptest"
"time" "github.com/gofrs/uuid/v5" + "github.com/olexsmir/onasty/internal/models" ) type (@@ -67,13 +68,80 @@ e.NotEmpty(dbNote)
}, }, { + name: "invalid slug, with space", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Slug: e.uuid() + "fuker fuker", + Content: e.uuid(), + }, + assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { + e.Equal(http.StatusBadRequest, r.Code) + }, + }, + { + name: "invalid slug, with slash", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Slug: e.uuid() + "fuker/fuker", + Content: e.uuid(), + }, + assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { + e.Equal(http.StatusBadRequest, r.Code) + }, + }, + { + name: "invalid slug, 'read'", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Slug: "read", + Content: e.uuid(), + }, + assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { + e.Equal(r.Code, http.StatusBadRequest) + + var body errorResponse + e.readBodyAndUnjsonify(r.Body, &body) + + e.Equal(models.ErrNoteSlugIsAlreadyInUse.Error(), body.Message) + }, + }, + { + name: "invalid slug, 'unread'", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Slug: "unread", + Content: e.uuid(), + }, + assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { + e.Equal(r.Code, http.StatusBadRequest) + + var body errorResponse + e.readBodyAndUnjsonify(r.Body, &body) + + e.Equal(models.ErrNoteSlugIsAlreadyInUse.Error(), body.Message) + }, + }, + { + name: "slug provided but empty", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Slug: "", + Content: e.uuid(), + }, + assert: func(r *httptest.ResponseRecorder, inp apiv1NoteCreateRequest) { + e.Equal(r.Code, http.StatusCreated) + + var body apiv1NoteCreateResponse + e.readBodyAndUnjsonify(r.Body, &body) + + dbNote := e.getNoteBySlug(body.Slug) + e.NotEmpty(dbNote) + e.Equal(inp.Content, dbNote.Content) + }, + }, + { name: "set password", inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Content: e.uuid(), Password: e.uuid(), }, assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { - e.Equal(r.Code, http.StatusCreated) + e.Equal(http.StatusCreated, r.Code) }, }, {
M
internal/models/note.go
@@ -2,11 +2,17 @@ package models
import ( "errors" - "strings" + "regexp" "time" "github.com/gofrs/uuid/v5" ) + +// read and unread are not allowed because those slugs might and will be interpreted as api routes +var notAllowedSlugs = map[string]struct{}{ + "read": {}, + "unread": {}, +} var ( ErrNoteContentIsEmpty = errors.New("note: content is empty")@@ -27,17 +33,23 @@ CreatedAt time.Time
ExpiresAt time.Time } +var slugPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + func (n Note) Validate() error { if n.Content == "" { return ErrNoteContentIsEmpty } - if n.Slug == "" || strings.Contains(n.Slug, " ") { + if !slugPattern.MatchString(n.Slug) { return ErrNoteSlugIsInvalid } if n.IsExpired() { return ErrNoteExpired + } + + if _, exists := notAllowedSlugs[n.Slug]; exists { + return ErrNoteSlugIsAlreadyInUse } return nil
M
internal/models/note_test.go
@@ -44,12 +44,18 @@ }
assert.EqualError(t, n.Validate(), ErrNoteExpired.Error()) }) t.Run("should fail if slug is empty", func(t *testing.T) { - n := Note{Content: "the content", Slug: ""} + n := Note{Content: "the content", Slug: " "} assert.EqualError(t, n.Validate(), ErrNoteSlugIsInvalid.Error()) }) - t.Run("should fail if slug is empty", func(t *testing.T) { - n := Note{Content: "the content", Slug: " "} + t.Run("should fail if slug has '/'", func(t *testing.T) { + n := Note{Content: "the content", Slug: "asdf/asdf"} assert.EqualError(t, n.Validate(), ErrNoteSlugIsInvalid.Error()) + }) + t.Run("should fail if slug one of not allowed slugs", func(t *testing.T) { + for notAllowedSlug := range notAllowedSlugs { + n := Note{Content: "the content", Slug: notAllowedSlug} + assert.EqualError(t, n.Validate(), ErrNoteSlugIsAlreadyInUse.Error()) + } }) }
M
internal/transport/http/apiv1/apiv1.go
@@ -75,11 +75,8 @@
authorized := note.Group("", a.authorizedMiddleware) { authorized.GET("", a.getNotesHandler) - - // FIXME: those links make slugs `read` and `unread` unavailable authorized.GET("/read", a.getReadNotesHandler) authorized.GET("/unread", a.getUnReadNotesHandler) - authorized.PATCH(":slug/expires", a.updateNoteHandler) authorized.PATCH(":slug/password", a.setNotePasswordHandler) authorized.DELETE(":slug", a.deleteNoteHandler)
M
internal/transport/http/apiv1/response.go
@@ -32,7 +32,8 @@ errors.Is(err, models.ErrUserWrongCredentials) ||
// notes errors.Is(err, notesrv.ErrNotePasswordNotProvided) || errors.Is(err, models.ErrNoteContentIsEmpty) || - errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) { + errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) || + errors.Is(err, models.ErrNoteSlugIsInvalid) { newError(c, http.StatusBadRequest, err.Error()) return }