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
        70
             - usestdlibvars # detects the possibility to use variables/constants from the Go standard library

      
        71
        71
             - wastedassign # finds wasted assignment statements

      
        72
        72
             - whitespace # detects leading and trailing whitespace

      
        
        73
        +    - inamedparam # peports interfaces with unnamed method parameters

      
        
        74
        +    - forcetypeassert # finds forced type assertions.

      
        
        75
        +    - exhaustruct # checks if all structure fields are initialized.

      
        
        76
        +    - err113 # forbids usage of dynamic errors

      
        
        77
        +    - contextcheck # check whether the function uses a non-inherited context

      
        
        78
        +    - ireturn # accept interfaces, return concrete types

      
        73
        79
         

      
        74
        80
         linters-settings:

      
        75
        81
           cyclop:

      ···
        122
        128
           nakedret:

      
        123
        129
             # the gods will judge me but I just don't like naked returns at all

      
        124
        130
             max-func-lines: 0

      
        
        131
        +

      
        
        132
        +  exhaustruct:

      
        
        133
        +    exclude:

      
        
        134
        +      - 'log/slog\.HandlerOptions'

      
        
        135
        +      - 'net/http\.Server'

      
        
        136
        +

      
        
        137
        +      - 'github.com/golang-jwt/jwt/v5\.RegisteredClaims'

      
        125
        138
         

      
        126
        139
         issues:

      
        127
        140
           # Maximum count of issues with the same text.

      
M cmd/server/main.go
···
        
        1
        +//nolint:err113 // all errors are shown to the user so it's ok for them to be dynamic

      
        1
        2
         package main

      
        2
        3
         

      
        3
        4
         import (

      
M e2e/apiv1_auth_test.go
···
        1
        
        -package e2e

      
        
        1
        +package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"net/http"

      ···
        47
        47
         		{name: "all fiels empty", email: "", password: "", username: ""},

      
        48
        48
         		{

      
        49
        49
         			name:     "non valid email",

      
        
        50
        +			username: "testing",

      
        50
        51
         			email:    "email",

      
        51
        52
         			password: "password",

      
        52
        53
         		},

      ···
        214
        215
         	unactivatedEmail := e.uuid() + "@test.com"

      
        215
        216
         	e.insertUserIntoDB(e.uuid(), unactivatedEmail, password, false)

      
        216
        217
         

      
        
        218
        +	//exhaustruct:ignore

      
        217
        219
         	tests := []struct {

      
        218
        220
         		name         string

      
        219
        221
         		email        string

      
M e2e/apiv1_notes_test.go
···
        1
        
        -package e2e

      
        
        1
        +package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"net/http"

      ···
        28
        28
         	}{

      
        29
        29
         		{

      
        30
        30
         			name: "empty request",

      
        31
        
        -			inp:  apiv1NoteCreateRequest{},

      
        
        31
        +			inp:  apiv1NoteCreateRequest{}, //nolint:exhaustruct

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

      
        33
        33
         				e.Equal(r.Code, http.StatusBadRequest)

      
        34
        34
         			},

      
        35
        35
         		},

      
        36
        36
         		{

      
        37
        37
         			name: "content only",

      
        38
        
        -			inp:  apiv1NoteCreateRequest{Content: e.uuid()},

      
        
        38
        +			inp:  apiv1NoteCreateRequest{Content: e.uuid()}, //nolint:exhaustruct

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

      
        40
        40
         				e.Equal(r.Code, http.StatusCreated)

      
        41
        41
         

      ···
        51
        51
         		},

      
        52
        52
         		{

      
        53
        53
         			name: "set slug",

      
        54
        
        -			inp: apiv1NoteCreateRequest{

      
        
        54
        +			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        55
        55
         				Slug:    e.uuid() + "fuker",

      
        56
        56
         				Content: e.uuid(),

      
        57
        57
         			},

      ···
        67
        67
         		},

      
        68
        68
         		{

      
        69
        69
         			name: "all possible fields",

      
        70
        
        -			inp: apiv1NoteCreateRequest{

      
        
        70
        +			inp: apiv1NoteCreateRequest{ //nolint:exhaustruct

      
        71
        71
         				Content:              e.uuid(),

      
        72
        72
         				BurnBeforeExpiration: true,

      
        73
        73
         				ExpiresAt:            time.Now().Add(time.Hour),

      ···
        96
        96
         

      
        97
        97
         func (e *AppTestSuite) TestNoteV1_Create_authorized() {

      
        98
        98
         	uid, toks := e.createAndSingIn(e.uuid()+"@test.com", e.uuid(), "password")

      
        99
        
        -	httpResp := e.httpRequest(http.MethodPost, "/api/v1/note", e.jsonify(apiv1NoteCreateRequest{

      
        100
        
        -		Content: "some random ass content for the test",

      
        101
        
        -	}), toks.AccessToken)

      
        
        99
        +	httpResp := e.httpRequest(

      
        
        100
        +		http.MethodPost,

      
        
        101
        +		"/api/v1/note",

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

      
        
        103
        +			Content: "some random ass content for the test",

      
        
        104
        +		}),

      
        
        105
        +		toks.AccessToken,

      
        
        106
        +	)

      
        102
        107
         

      
        103
        108
         	var body apiv1NoteCreateResponse

      
        104
        109
         	e.readBodyAndUnjsonify(httpResp.Body, &body)

      ···
        118
        123
         

      
        119
        124
         func (e *AppTestSuite) TestNoteV1_Get() {

      
        120
        125
         	content := e.uuid()

      
        121
        
        -	httpResp := e.httpRequest(http.MethodPost, "/api/v1/note", e.jsonify(apiv1NoteCreateRequest{

      
        122
        
        -		Content: content,

      
        123
        
        -	}))

      
        
        126
        +	httpResp := e.httpRequest(

      
        
        127
        +		http.MethodPost,

      
        
        128
        +		"/api/v1/note",

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

      
        
        130
        +			Content: content,

      
        
        131
        +		}),

      
        
        132
        +	)

      
        124
        133
         	e.Equal(http.StatusCreated, httpResp.Code)

      
        125
        134
         

      
        126
        135
         	var bodyCreated apiv1NoteCreateResponse

      
M e2e/e2e_test.go
···
        1
        
        -package e2e

      
        
        1
        +package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"context"

      ···
        152
        152
         

      
        153
        153
         	// run migrations

      
        154
        154
         	sdb := stdlib.OpenDBFromPool(db.Pool)

      
        155
        
        -	driver, err := pgx.WithInstance(sdb, &pgx.Config{})

      
        
        155
        +	driver, err := pgx.WithInstance(sdb, &pgx.Config{}) //nolint:exhaustruct

      
        156
        156
         	e.require.NoError(err)

      
        157
        157
         

      
        158
        158
         	m, err := migrate.NewWithDatabaseInstance(

      ···
        175
        175
         }

      
        176
        176
         

      
        177
        177
         func (e *AppTestSuite) getConfig() *config.Config {

      
        178
        
        -	return &config.Config{

      
        
        178
        +	return &config.Config{ //nolint:exhaustruct

      
        179
        179
         		AppEnv:              "testing",

      
        180
        180
         		ServerPort:          "3000",

      
        181
        181
         		PasswordSalt:        "salty-password",

      
M e2e/e2e_utils_db_test.go
···
        1
        
        -package e2e

      
        
        1
        +package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"errors"

      ···
        63
        63
         	err = e.postgresDB.QueryRow(e.ctx, query, args...).

      
        64
        64
         		Scan(&session.RefreshToken, &session.ExpiresAt)

      
        65
        65
         	if errors.Is(err, pgx.ErrNoRows) {

      
        66
        
        -		return models.Session{}

      
        
        66
        +		return models.Session{} //nolint:exhaustruct

      
        67
        67
         	}

      
        68
        68
         

      
        69
        69
         	e.require.NoError(err)

      ···
        84
        84
         	err = e.postgresDB.QueryRow(e.ctx, query, args...).

      
        85
        85
         		Scan(&u.ID, &u.Username, &u.Activated, &u.Email, &u.Password)

      
        86
        86
         	if errors.Is(err, pgx.ErrNoRows) {

      
        87
        
        -		return models.User{}

      
        
        87
        +		return models.User{} //nolint:exhaustruct

      
        88
        88
         	}

      
        89
        89
         

      
        90
        90
         	e.require.NoError(err)

      ···
        103
        103
         	err = e.postgresDB.QueryRow(e.ctx, query, args...).

      
        104
        104
         		Scan(&note.ID, &note.Content, &note.Slug, &note.BurnBeforeExpiration, &note.CreatedAt, &note.ExpiresAt)

      
        105
        105
         	if errors.Is(err, pgx.ErrNoRows) {

      
        106
        
        -		return models.Note{}

      
        
        106
        +		return models.Note{} //nolint:exhaustruct

      
        107
        107
         	}

      
        108
        108
         

      
        109
        109
         	e.require.NoError(err)

      ···
        128
        128
         	var na noteAuthorModel

      
        129
        129
         	err = e.postgresDB.QueryRow(e.ctx, qeuery, args...).Scan(&na.noteID, &na.userID)

      
        130
        130
         	if errors.Is(err, pgx.ErrNoRows) {

      
        131
        
        -		return noteAuthorModel{}

      
        
        131
        +		return noteAuthorModel{} //nolint:exhaustruct

      
        132
        132
         	}

      
        133
        133
         

      
        134
        134
         	e.require.NoError(err)

      
M e2e/e2e_utils_test.go
···
        1
        
        -package e2e

      
        
        1
        +package e2e_test

      
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"bytes"

      
M internal/config/config.go
···
        68
        68
         func mustParseDurationOrPanic(dur string) time.Duration {

      
        69
        69
         	d, err := time.ParseDuration(dur)

      
        70
        70
         	if err != nil {

      
        71
        
        -		panic(errors.Join(errors.New("cannot time.ParseDuration"), err))

      
        
        71
        +		panic(errors.Join(errors.New("cannot time.ParseDuration"), err)) //nolint:err113

      
        72
        72
         	}

      
        73
        73
         

      
        74
        74
         	return d

      
M internal/hasher/hasher.go
···
        2
        2
         

      
        3
        3
         type Hasher interface {

      
        4
        4
         	// Hash takes a string as input and returns its hash

      
        5
        
        -	Hash(string) (string, error)

      
        
        5
        +	Hash(str string) (string, error)

      
        6
        6
         }

      
M internal/jwtutil/jwtutil.go
···
        9
        9
         	"github.com/golang-jwt/jwt/v5"

      
        10
        10
         )

      
        11
        11
         

      
        
        12
        +var ErrUnexpectedSigningMethod = errors.New("unexpected signing method")

      
        
        13
        +

      
        12
        14
         type JWTTokenizer interface {

      
        13
        15
         	// AccessToken generates a new access token with the given payload

      
        14
        16
         	AccessToken(pl Payload) (string, error)

      ···
        58
        60
         	var claims jwt.RegisteredClaims

      
        59
        61
         	_, err := jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (interface{}, error) {

      
        60
        62
         		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {

      
        61
        
        -			return nil, errors.New("unexpected signing method")

      
        
        63
        +			return nil, ErrUnexpectedSigningMethod

      
        62
        64
         		}

      
        63
        65
         		return []byte(j.signingKey), nil

      
        64
        66
         	})

      
M internal/mailer/testing_mailer_test.go
···
        20
        20
         }

      
        21
        21
         

      
        22
        22
         func TestMailer_GetLastSentEmailToEmail(t *testing.T) {

      
        
        23
        +	email := "test@mail.com"

      
        
        24
        +	content := "content"

      
        
        25
        +

      
        23
        26
         	m := NewTestMailer()

      
        24
        27
         	assert.Empty(t, m.emails)

      
        25
        28
         

      
        26
        
        -	email := "test@mail.com"

      
        27
        
        -	content := "content"

      
        28
        
        -	err := m.Send(context.TODO(), email, "", content)

      
        29
        
        -	require.NoError(t, err)

      
        
        29
        +	m.emails[email] = content

      
        30
        30
         

      
        31
        31
         	c := m.GetLastSentEmailToEmail(email)

      
        32
        32
         	assert.Equal(t, content, c)

      
M internal/models/notes_test.go
···
        17
        17
         		// NOTE: there no need to test if note is expired since it tested in IsExpired test

      
        18
        18
         		{

      
        19
        19
         			name: "ok",

      
        20
        
        -			note: Note{

      
        
        20
        +			note: Note{ //nolint:exhaustruct

      
        21
        21
         				Content:   "some wired ass content",

      
        22
        22
         				ExpiresAt: time.Now().Add(time.Hour),

      
        23
        23
         			},

      
        24
        24
         			willError: false,

      
        
        25
        +			error:     nil,

      
        25
        26
         		},

      
        26
        27
         		{

      
        27
        28
         			name:      "content missing",

      
        28
        
        -			note:      Note{Content: ""},

      
        
        29
        +			note:      Note{Content: ""}, //nolint:exhaustruct

      
        29
        30
         			willError: true,

      
        30
        31
         			error:     ErrNoteContentIsEmpty,

      
        31
        32
         		},

      ···
        51
        52
         	}{

      
        52
        53
         		{

      
        53
        54
         			name:     "expired",

      
        54
        
        -			note:     Note{ExpiresAt: time.Now().Add(-time.Hour)},

      
        
        55
        +			note:     Note{ExpiresAt: time.Now().Add(-time.Hour)}, //nolint:exhaustruct

      
        55
        56
         			expected: true,

      
        56
        57
         		},

      
        57
        58
         		{

      
        58
        59
         			name:     "not expired",

      
        59
        
        -			note:     Note{ExpiresAt: time.Now().Add(time.Hour)},

      
        
        60
        +			note:     Note{ExpiresAt: time.Now().Add(time.Hour)}, //nolint:exhaustruct

      
        60
        61
         			expected: false,

      
        61
        62
         		},

      
        62
        63
         		{

      
        63
        64
         			name:     "zero expiration",

      
        64
        
        -			note:     Note{ExpiresAt: time.Time{}},

      
        
        65
        +			note:     Note{ExpiresAt: time.Time{}}, //nolint:exhaustruct

      
        65
        66
         			expected: false,

      
        66
        67
         		},

      
        67
        68
         	}

      ···
        81
        82
         	}{

      
        82
        83
         		{

      
        83
        84
         			name: "should be burnt",

      
        84
        
        -			note: Note{

      
        
        85
        +			note: Note{ //nolint:exhaustruct

      
        85
        86
         				BurnBeforeExpiration: true,

      
        86
        87
         				ExpiresAt:            time.Now().Add(time.Hour),

      
        87
        88
         			},

      ···
        89
        90
         		},

      
        90
        91
         		{

      
        91
        92
         			name: "could not be burnt, no expiration time",

      
        92
        
        -			note: Note{

      
        
        93
        +			note: Note{ //nolint:exhaustruct

      
        93
        94
         				BurnBeforeExpiration: true,

      
        94
        95
         				ExpiresAt:            time.Time{},

      
        95
        96
         			},

      ···
        97
        98
         		},

      
        98
        99
         		{

      
        99
        100
         			name: "could not be burnt, burn when expiration and burn is false",

      
        100
        
        -			note: Note{

      
        
        101
        +			note: Note{ //nolint:exhaustruct

      
        101
        102
         				BurnBeforeExpiration: false,

      
        102
        103
         				ExpiresAt:            time.Time{},

      
        103
        104
         			},

      
M internal/models/user.go
···
        34
        34
         	// NOTE: there's probably a better way to validate emails

      
        35
        35
         	_, err := mail.ParseAddress(u.Email)

      
        36
        36
         	if err != nil {

      
        37
        
        -		return errors.New("user: invalid email")

      
        
        37
        +		return errors.New("user: invalid email") //nolint:err113

      
        38
        38
         	}

      
        39
        39
         

      
        40
        40
         	if len(u.Password) < 6 {

      
        41
        
        -		return errors.New("user: password too short, minimum 6 chars")

      
        
        41
        +		return errors.New("user: password too short, minimum 6 chars") //nolint:err113

      
        42
        42
         	}

      
        43
        43
         

      
        44
        44
         	if len(u.Username) == 0 {

      
        45
        
        -		return errors.New("user: username is required")

      
        
        45
        +		return errors.New("user: username is required") //nolint:err113

      
        46
        46
         	}

      
        47
        47
         

      
        48
        48
         	return nil

      
M internal/models/user_test.go
···
        54
        54
         

      
        55
        55
         	for _, tt := range tests {

      
        56
        56
         		t.Run(tt.name, func(t *testing.T) {

      
        57
        
        -			err := User{

      
        
        57
        +			err := User{ //nolint:exhaustruct

      
        58
        58
         				Username: tt.username,

      
        59
        59
         				Email:    tt.email,

      
        60
        60
         				Password: tt.password,

      
M internal/service/notesrv/notesrv.go
···
        23
        23
         	noterepo noterepo.NoteStorer

      
        24
        24
         }

      
        25
        25
         

      
        26
        
        -func New(noterepo noterepo.NoteStorer) NoteServicer {

      
        
        26
        +func New(noterepo noterepo.NoteStorer) *NoteSrv {

      
        27
        27
         	return &NoteSrv{

      
        28
        28
         		noterepo: noterepo,

      
        29
        29
         	}

      ···
        62
        62
         	}

      
        63
        63
         

      
        64
        64
         	// TODO: there should be a better way to do it

      
        65
        
        -	m := models.Note{

      
        
        65
        +	m := models.Note{ //nolint:exhaustruct

      
        66
        66
         		ExpiresAt:            note.ExpiresAt,

      
        67
        67
         		BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        68
        68
         	}

      
M internal/service/usersrv/usersrv.go
···
        55
        55
         	jwtTokenizer jwtutil.JWTTokenizer,

      
        56
        56
         	mailer mailer.Mailer,

      
        57
        57
         	refreshTokenTTL, verificationTokenTTL time.Duration,

      
        58
        
        -) UserServicer {

      
        
        58
        +) *UserSrv {

      
        59
        59
         	return &UserSrv{

      
        60
        60
         		userstore:            userstore,

      
        61
        61
         		sessionstore:         sessionstore,

      ···
        93
        93
         	// TODO: handle the error that might be returned

      
        94
        94
         	// i dont think that tehre's need to handle the error, just log it

      
        95
        95
         	bgCtx, bgCancel := context.WithTimeout(context.Background(), 10*time.Second)

      
        96
        
        -	go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, vtok) //nolint:errcheck

      
        
        96
        +	go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, vtok) //nolint:errcheck,contextcheck

      
        97
        97
         

      
        98
        98
         	return uid, nil

      
        99
        99
         }

      ···
        211
        211
         	}

      
        212
        212
         

      
        213
        213
         	bgCtx, bgCancel := context.WithTimeout(context.Background(), 10*time.Second)

      
        214
        
        -	go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, token) //nolint:errcheck

      
        
        214
        +	go u.sendVerificationEmail(bgCtx, bgCancel, inp.Email, token) //nolint:errcheck,contextcheck

      
        215
        215
         

      
        216
        216
         	return nil

      
        217
        217
         }

      
M internal/store/psql/noterepo/noterepo.go
···
        26
        26
         	db *psqlutil.DB

      
        27
        27
         }

      
        28
        28
         

      
        29
        
        -func New(db *psqlutil.DB) NoteStorer {

      
        
        29
        +func New(db *psqlutil.DB) *NoteRepo {

      
        30
        30
         	return &NoteRepo{db}

      
        31
        31
         }

      
        32
        32
         

      
M internal/store/psql/sessionrepo/sessionrepo.go
···
        25
        25
         	db *psqlutil.DB

      
        26
        26
         }

      
        27
        27
         

      
        28
        
        -func New(db *psqlutil.DB) SessionStorer {

      
        
        28
        +func New(db *psqlutil.DB) *SessionRepo {

      
        29
        29
         	return &SessionRepo{

      
        30
        30
         		db: db,

      
        31
        31
         	}

      
M internal/transport/http/apiv1/auth.go
···
        22
        22
         		return

      
        23
        23
         	}

      
        24
        24
         

      
        25
        
        -	user := models.User{

      
        
        25
        +	user := models.User{ //nolint:exhaustruct

      
        26
        26
         		Username:    req.Username,

      
        27
        27
         		Email:       req.Email,

      
        28
        28
         		Password:    req.Password,

      
M internal/transport/http/apiv1/middleware.go
···
        82
        82
         	if !exists {

      
        83
        83
         		return uuid.Nil

      
        84
        84
         	}

      
        85
        
        -	return userID.(uuid.UUID)

      
        
        85
        +

      
        
        86
        +	uid, ok := userID.(uuid.UUID)

      
        
        87
        +	if !ok {

      
        
        88
        +		return uuid.Nil

      
        
        89
        +	}

      
        
        90
        +

      
        
        91
        +	return uid

      
        86
        92
         }

      
        87
        93
         

      
        88
        94
         func (a *APIV1) validateAuthorizedUser(ctx context.Context, accessToken string) (uuid.UUID, error) {

      
M internal/transport/http/apiv1/note.go
···
        27
        27
         		return

      
        28
        28
         	}

      
        29
        29
         

      
        30
        
        -	note := models.Note{

      
        
        30
        +	note := models.Note{ //nolint:exhaustruct

      
        31
        31
         		Content:              req.Content,

      
        32
        32
         		Slug:                 req.Slug,

      
        33
        33
         		BurnBeforeExpiration: req.BurnBeforeExpiration,