all repos

onasty @ 1c67ba5

a one-time notes service
21 files changed, 84 insertions(+), 50 deletions(-)
chore(golangci-lint): upgrade config (#14)

* chore(linter): upgrade go linter

* chore(golangci-lint): disable linters that arent that useful

* refactor(hasher): add name in interface parameters

* refactor(e2e): move it all to _test package

* refactor(mailer): some usless refactoring i guess

* refactor: make `ireturn` happy

* refactor: make `forcetypeassert` happy

* chore: disable gci

* refactor: make `err113` happy

* refactor: make `exhaustruct` happy

* refactor: make `contextcheck` happy

* fix typo

* refactor: move err declaration to the top of a file
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2024-09-12 14:20:21 +0300
Parent: 0c026f2
M .golangci.yaml

@@ -70,6 +70,12 @@ - unparam # reports unused function parameters

- usestdlibvars # detects the possibility to use variables/constants from the Go standard library - wastedassign # finds wasted assignment statements - whitespace # detects leading and trailing whitespace + - inamedparam # peports interfaces with unnamed method parameters + - forcetypeassert # finds forced type assertions. + - exhaustruct # checks if all structure fields are initialized. + - err113 # forbids usage of dynamic errors + - contextcheck # check whether the function uses a non-inherited context + - ireturn # accept interfaces, return concrete types linters-settings: cyclop:

@@ -122,6 +128,13 @@

nakedret: # the gods will judge me but I just don't like naked returns at all max-func-lines: 0 + + exhaustruct: + exclude: + - 'log/slog\.HandlerOptions' + - 'net/http\.Server' + + - 'github.com/golang-jwt/jwt/v5\.RegisteredClaims' issues: # Maximum count of issues with the same text.
M cmd/server/main.go

@@ -1,3 +1,4 @@

+//nolint:err113 // all errors are shown to the user so it's ok for them to be dynamic package main import (
M e2e/apiv1_auth_test.go

@@ -1,4 +1,4 @@

-package e2e +package e2e_test import ( "net/http"

@@ -47,6 +47,7 @@ }{

{name: "all fiels empty", email: "", password: "", username: ""}, { name: "non valid email", + username: "testing", email: "email", password: "password", },

@@ -214,6 +215,7 @@

unactivatedEmail := e.uuid() + "@test.com" e.insertUserIntoDB(e.uuid(), unactivatedEmail, password, false) + //exhaustruct:ignore tests := []struct { name string email string
M e2e/apiv1_notes_test.go

@@ -1,4 +1,4 @@

-package e2e +package e2e_test import ( "net/http"

@@ -28,14 +28,14 @@ assert func(*httptest.ResponseRecorder, apiv1NoteCreateRequest)

}{ { name: "empty request", - inp: apiv1NoteCreateRequest{}, + inp: apiv1NoteCreateRequest{}, //nolint:exhaustruct assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { e.Equal(r.Code, http.StatusBadRequest) }, }, { name: "content only", - inp: apiv1NoteCreateRequest{Content: e.uuid()}, + inp: apiv1NoteCreateRequest{Content: e.uuid()}, //nolint:exhaustruct assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { e.Equal(r.Code, http.StatusCreated)

@@ -51,7 +51,7 @@ },

}, { name: "set slug", - inp: apiv1NoteCreateRequest{ + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Slug: e.uuid() + "fuker", Content: e.uuid(), },

@@ -67,7 +67,7 @@ },

}, { name: "all possible fields", - inp: apiv1NoteCreateRequest{ + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Content: e.uuid(), BurnBeforeExpiration: true, ExpiresAt: time.Now().Add(time.Hour),

@@ -96,9 +96,14 @@ }

func (e *AppTestSuite) TestNoteV1_Create_authorized() { uid, toks := e.createAndSingIn(e.uuid()+"@test.com", e.uuid(), "password") - httpResp := e.httpRequest(http.MethodPost, "/api/v1/note", e.jsonify(apiv1NoteCreateRequest{ - Content: "some random ass content for the test", - }), toks.AccessToken) + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: "some random ass content for the test", + }), + toks.AccessToken, + ) var body apiv1NoteCreateResponse e.readBodyAndUnjsonify(httpResp.Body, &body)

@@ -118,9 +123,13 @@ }

func (e *AppTestSuite) TestNoteV1_Get() { content := e.uuid() - httpResp := e.httpRequest(http.MethodPost, "/api/v1/note", e.jsonify(apiv1NoteCreateRequest{ - Content: content, - })) + httpResp := e.httpRequest( + http.MethodPost, + "/api/v1/note", + e.jsonify(apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: content, + }), + ) e.Equal(http.StatusCreated, httpResp.Code) var bodyCreated apiv1NoteCreateResponse
M e2e/e2e_test.go

@@ -1,4 +1,4 @@

-package e2e +package e2e_test import ( "context"

@@ -152,7 +152,7 @@ e.require.NoError(err)

// run migrations sdb := stdlib.OpenDBFromPool(db.Pool) - driver, err := pgx.WithInstance(sdb, &pgx.Config{}) + driver, err := pgx.WithInstance(sdb, &pgx.Config{}) //nolint:exhaustruct e.require.NoError(err) m, err := migrate.NewWithDatabaseInstance(

@@ -175,7 +175,7 @@ })))

} func (e *AppTestSuite) getConfig() *config.Config { - return &config.Config{ + return &config.Config{ //nolint:exhaustruct AppEnv: "testing", ServerPort: "3000", PasswordSalt: "salty-password",
M e2e/e2e_utils_db_test.go

@@ -1,4 +1,4 @@

-package e2e +package e2e_test import ( "errors"

@@ -63,7 +63,7 @@ var session models.Session

err = e.postgresDB.QueryRow(e.ctx, query, args...). Scan(&session.RefreshToken, &session.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { - return models.Session{} + return models.Session{} //nolint:exhaustruct } e.require.NoError(err)

@@ -84,7 +84,7 @@ var u models.User

err = e.postgresDB.QueryRow(e.ctx, query, args...). Scan(&u.ID, &u.Username, &u.Activated, &u.Email, &u.Password) if errors.Is(err, pgx.ErrNoRows) { - return models.User{} + return models.User{} //nolint:exhaustruct } e.require.NoError(err)

@@ -103,7 +103,7 @@ var note models.Note

err = e.postgresDB.QueryRow(e.ctx, query, args...). Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.CreatedAt, &note.ExpiresAt) if errors.Is(err, pgx.ErrNoRows) { - return models.Note{} + return models.Note{} //nolint:exhaustruct } e.require.NoError(err)

@@ -128,7 +128,7 @@

var na noteAuthorModel err = e.postgresDB.QueryRow(e.ctx, qeuery, args...).Scan(&na.noteID, &na.userID) if errors.Is(err, pgx.ErrNoRows) { - return noteAuthorModel{} + return noteAuthorModel{} //nolint:exhaustruct } e.require.NoError(err)
M e2e/e2e_utils_test.go

@@ -1,4 +1,4 @@

-package e2e +package e2e_test import ( "bytes"
M internal/config/config.go

@@ -68,7 +68,7 @@

func mustParseDurationOrPanic(dur string) time.Duration { d, err := time.ParseDuration(dur) if err != nil { - panic(errors.Join(errors.New("cannot time.ParseDuration"), err)) + panic(errors.Join(errors.New("cannot time.ParseDuration"), err)) //nolint:err113 } return d
M internal/hasher/hasher.go

@@ -2,5 +2,5 @@ package hasher

type Hasher interface { // Hash takes a string as input and returns its hash - Hash(string) (string, error) + Hash(str string) (string, error) }
M internal/jwtutil/jwtutil.go

@@ -9,6 +9,8 @@

"github.com/golang-jwt/jwt/v5" ) +var ErrUnexpectedSigningMethod = errors.New("unexpected signing method") + type JWTTokenizer interface { // AccessToken generates a new access token with the given payload AccessToken(pl Payload) (string, error)

@@ -58,7 +60,7 @@ func (j *JWTUtil) Parse(token string) (Payload, error) {

var claims jwt.RegisteredClaims _, err := jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (interface{}, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.New("unexpected signing method") + return nil, ErrUnexpectedSigningMethod } return []byte(j.signingKey), nil })
M internal/mailer/testing_mailer_test.go

@@ -20,13 +20,13 @@ assert.Equal(t, "content", m.emails[email])

} func TestMailer_GetLastSentEmailToEmail(t *testing.T) { + email := "test@mail.com" + content := "content" + m := NewTestMailer() assert.Empty(t, m.emails) - email := "test@mail.com" - content := "content" - err := m.Send(context.TODO(), email, "", content) - require.NoError(t, err) + m.emails[email] = content c := m.GetLastSentEmailToEmail(email) assert.Equal(t, content, c)
M internal/models/notes_test.go

@@ -17,15 +17,16 @@ }{

// NOTE: there no need to test if note is expired since it tested in IsExpired test { name: "ok", - note: Note{ + note: Note{ //nolint:exhaustruct Content: "some wired ass content", ExpiresAt: time.Now().Add(time.Hour), }, willError: false, + error: nil, }, { name: "content missing", - note: Note{Content: ""}, + note: Note{Content: ""}, //nolint:exhaustruct willError: true, error: ErrNoteContentIsEmpty, },

@@ -51,17 +52,17 @@ expected bool

}{ { name: "expired", - note: Note{ExpiresAt: time.Now().Add(-time.Hour)}, + note: Note{ExpiresAt: time.Now().Add(-time.Hour)}, //nolint:exhaustruct expected: true, }, { name: "not expired", - note: Note{ExpiresAt: time.Now().Add(time.Hour)}, + note: Note{ExpiresAt: time.Now().Add(time.Hour)}, //nolint:exhaustruct expected: false, }, { name: "zero expiration", - note: Note{ExpiresAt: time.Time{}}, + note: Note{ExpiresAt: time.Time{}}, //nolint:exhaustruct expected: false, }, }

@@ -81,7 +82,7 @@ expected bool

}{ { name: "should be burnt", - note: Note{ + note: Note{ //nolint:exhaustruct BurnBeforeExpiration: true, ExpiresAt: time.Now().Add(time.Hour), },

@@ -89,7 +90,7 @@ expected: true,

}, { name: "could not be burnt, no expiration time", - note: Note{ + note: Note{ //nolint:exhaustruct BurnBeforeExpiration: true, ExpiresAt: time.Time{}, },

@@ -97,7 +98,7 @@ expected: false,

}, { name: "could not be burnt, burn when expiration and burn is false", - note: Note{ + note: Note{ //nolint:exhaustruct BurnBeforeExpiration: false, ExpiresAt: time.Time{}, },
M internal/models/user.go

@@ -34,15 +34,15 @@ func (u User) Validate() error {

// NOTE: there's probably a better way to validate emails _, err := mail.ParseAddress(u.Email) if err != nil { - return errors.New("user: invalid email") + return errors.New("user: invalid email") //nolint:err113 } if len(u.Password) < 6 { - return errors.New("user: password too short, minimum 6 chars") + return errors.New("user: password too short, minimum 6 chars") //nolint:err113 } if len(u.Username) == 0 { - return errors.New("user: username is required") + return errors.New("user: username is required") //nolint:err113 } return nil
M internal/models/user_test.go

@@ -54,7 +54,7 @@ }

for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := User{ + err := User{ //nolint:exhaustruct Username: tt.username, Email: tt.email, Password: tt.password,
M internal/service/notesrv/notesrv.go

@@ -23,7 +23,7 @@ type NoteSrv struct {

noterepo noterepo.NoteStorer } -func New(noterepo noterepo.NoteStorer) NoteServicer { +func New(noterepo noterepo.NoteStorer) *NoteSrv { return &NoteSrv{ noterepo: noterepo, }

@@ -62,7 +62,7 @@ return dtos.NoteDTO{}, err

} // TODO: there should be a better way to do it - m := models.Note{ + m := models.Note{ //nolint:exhaustruct ExpiresAt: note.ExpiresAt, BurnBeforeExpiration: note.BurnBeforeExpiration, }
M internal/service/usersrv/usersrv.go

@@ -55,7 +55,7 @@ hasher hasher.Hasher,

jwtTokenizer jwtutil.JWTTokenizer, mailer mailer.Mailer, refreshTokenTTL, verificationTokenTTL time.Duration, -) UserServicer { +) *UserSrv { return &UserSrv{ userstore: userstore, sessionstore: sessionstore,

@@ -93,7 +93,7 @@

// TODO: handle the error that might be returned // i dont think that tehre's need to handle the error, just log it bgCtx, bgCancel := context.WithTimeout(context.Background(), 10*time.Second) - go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, vtok) //nolint:errcheck + go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, vtok) //nolint:errcheck,contextcheck return uid, nil }

@@ -211,7 +211,7 @@ return err

} bgCtx, bgCancel := context.WithTimeout(context.Background(), 10*time.Second) - go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, token) //nolint:errcheck + go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, token) //nolint:errcheck,contextcheck return nil }
M internal/store/psql/noterepo/noterepo.go

@@ -26,7 +26,7 @@ type NoteRepo struct {

db *psqlutil.DB } -func New(db *psqlutil.DB) NoteStorer { +func New(db *psqlutil.DB) *NoteRepo { return &NoteRepo{db} }
M internal/store/psql/sessionrepo/sessionrepo.go

@@ -25,7 +25,7 @@ type SessionRepo struct {

db *psqlutil.DB } -func New(db *psqlutil.DB) SessionStorer { +func New(db *psqlutil.DB) *SessionRepo { return &SessionRepo{ db: db, }
M internal/transport/http/apiv1/auth.go

@@ -22,7 +22,7 @@ newError(c, http.StatusBadRequest, "invalid request")

return } - user := models.User{ + user := models.User{ //nolint:exhaustruct Username: req.Username, Email: req.Email, Password: req.Password,
M internal/transport/http/apiv1/middleware.go

@@ -82,7 +82,13 @@ userID, exists := c.Get(userIDCtxKey)

if !exists { return uuid.Nil } - return userID.(uuid.UUID) + + uid, ok := userID.(uuid.UUID) + if !ok { + return uuid.Nil + } + + return uid } func (a *APIV1) validateAuthorizedUser(ctx context.Context, accessToken string) (uuid.UUID, error) {
M internal/transport/http/apiv1/note.go

@@ -27,7 +27,7 @@ newError(c, http.StatusBadRequest, "invalid request")

return } - note := models.Note{ + note := models.Note{ //nolint:exhaustruct Content: req.Content, Slug: req.Slug, BurnBeforeExpiration: req.BurnBeforeExpiration,