Compare commits
3 Commits
b69395dd03
...
6572aadebe
Author | SHA1 | Date |
---|---|---|
Renovate | 6572aadebe | |
Dawid Wysokiński | 5872ead643 | |
Dawid Wysokiński | 8e8e7e1f94 |
|
@ -14,7 +14,7 @@ steps:
|
|||
|
||||
services:
|
||||
- name: database
|
||||
image: postgres:14.5
|
||||
image: postgres:14.6
|
||||
environment:
|
||||
POSTGRES_DB: sessions
|
||||
POSTGRES_PASSWORD: sessions
|
||||
|
|
|
@ -40,6 +40,12 @@ func newMigrateCmd() *cli.Command {
|
|||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
if err = migrator.Lock(c.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = migrator.Unlock(c.Context)
|
||||
}()
|
||||
|
||||
if err = migrator.Init(c.Context); err != nil {
|
||||
return fmt.Errorf("migrator.Init: %w", err)
|
||||
|
@ -77,6 +83,12 @@ func newRollbackCmd() *cli.Command {
|
|||
}()
|
||||
|
||||
migrator := migrations.NewMigrator(db)
|
||||
if err = migrator.Lock(c.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = migrator.Unlock(c.Context)
|
||||
}()
|
||||
|
||||
group, err := migrator.Rollback(c.Context)
|
||||
if err != nil {
|
||||
|
|
|
@ -91,10 +91,12 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
|||
// repos
|
||||
userRepo := bundb.NewUser(cfg.db)
|
||||
apiKeyRepo := bundb.NewAPIKey(cfg.db)
|
||||
sessionRepo := bundb.NewSession(cfg.db)
|
||||
|
||||
// services
|
||||
userSvc := service.NewUser(userRepo)
|
||||
apiKeySvc := service.NewAPIKey(apiKeyRepo, userSvc)
|
||||
sessionSvc := service.NewSession(sessionRepo, userSvc)
|
||||
|
||||
// router
|
||||
r := chi.NewRouter()
|
||||
|
@ -102,6 +104,7 @@ func newServer(cfg serverConfig) (*http.Server, error) {
|
|||
r.Mount(metaEndpointsPrefix, meta.NewRouter([]meta.Checker{bundb.NewChecker(cfg.db)}))
|
||||
r.Mount("/api", rest.NewRouter(rest.RouterConfig{
|
||||
APIKeyVerifier: apiKeySvc,
|
||||
SessionService: sessionSvc,
|
||||
CORS: rest.CORSConfig{
|
||||
Enabled: apiCfg.CORSEnabled,
|
||||
AllowedOrigins: apiCfg.CORSAllowedOrigins,
|
||||
|
|
14
go.mod
14
go.mod
|
@ -15,10 +15,10 @@ require (
|
|||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/http-swagger v1.3.3
|
||||
github.com/swaggo/swag v1.8.8
|
||||
github.com/uptrace/bun v1.1.8
|
||||
github.com/uptrace/bun/dbfixture v1.1.8
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.8
|
||||
github.com/uptrace/bun v1.1.9
|
||||
github.com/uptrace/bun/dbfixture v1.1.9
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.9
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.9
|
||||
github.com/urfave/cli/v2 v2.23.5
|
||||
go.uber.org/zap v1.23.0
|
||||
)
|
||||
|
@ -64,9 +64,9 @@ require (
|
|||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||
golang.org/x/crypto v0.3.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
32
go.sum
32
go.sum
|
@ -137,19 +137,19 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuI
|
|||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
|
||||
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
|
||||
github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
|
||||
github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.8 h1:/GgJmrJ8/c0z4R4hoEPZ5UeEhVGdvsII4JbVDLbR7Xc=
|
||||
github.com/swaggo/swag v1.8.8/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.1.8 h1:slxuaP4LYWFbPRUmTtQhfJN+6eX/6ar2HDKYTcI50SA=
|
||||
github.com/uptrace/bun v1.1.8/go.mod h1:iT89ESdV3uMupD9ixt6Khidht+BK0STabK/LeZE+B84=
|
||||
github.com/uptrace/bun/dbfixture v1.1.8 h1:cDRqII+emIgmmhWSebdAZWu5vKmuIw+PZ/arf0oYJdo=
|
||||
github.com/uptrace/bun/dbfixture v1.1.8/go.mod h1:GrRDt9lIfDpMyAGIJjWNsYeRHKjr8Gr2wDl2Rsf1sR4=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8 h1:wayJhjYDPGv8tgOBLolbBtSFQ0TihFoo8E1T129UdA8=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.8/go.mod h1:nNbU8PHTjTUM+CRtGmqyBb9zcuRAB8I680/qoFSmBUk=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.8 h1:gyL22axRQfjJS2Umq0erzJnp0bLOdUE8/USKZHPQB8o=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.8/go.mod h1:4tHK0h7a/UoldBoe9J3GU4tEYjr3mkd62U3Kq3PVk3E=
|
||||
github.com/uptrace/bun v1.1.9 h1:6zs+YJcgw8oj67c+YmI8edQokDFeyR4BE/ykNWjGYYs=
|
||||
github.com/uptrace/bun v1.1.9/go.mod h1:fpYRCGyruLCyP7dNjMfqulYn4VBP/fH0enc0j0yW/Cs=
|
||||
github.com/uptrace/bun/dbfixture v1.1.9 h1:IMHiZgdVeUpXpla1nmY6Ax9GPNHkpq7DAUiuyJHeayY=
|
||||
github.com/uptrace/bun/dbfixture v1.1.9/go.mod h1:qGTgA/9RJwHD+5QFr34eyEdobV8Bl9aFnipcXQlRvik=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.9 h1:V23SU89WfjqtePLFPRXVXCwmSyYb0XKeg8Z6BMXgyHg=
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.1.9/go.mod h1:+ux7PjC4NYsNMdGE9b2ERxCi2jJai8Z8zniXFExq0Ns=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.9 h1:Dy9g/EpgOG15RP3mDAuJd/hnfXAUdBsfxpg9On27M+Y=
|
||||
github.com/uptrace/bun/driver/pgdriver v1.1.9/go.mod h1:YnPHzR4fT24PXrBTcadclXfdtkR9dYouqk2HwOiKf2g=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
|
||||
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
|
@ -180,8 +180,8 @@ go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
|
||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
|
@ -192,8 +192,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -210,8 +210,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
|
@ -25,7 +25,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), fixture.user(t, "user-1").ID)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiKey, err := repo.Create(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
|
@ -35,11 +35,11 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
assert.WithinDuration(t, time.Now(), apiKey.CreatedAt, time.Second)
|
||||
})
|
||||
|
||||
t.Run("ERR: player doesn't exist", func(t *testing.T) {
|
||||
t.Run("ERR: user doesn't exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateAPIKeyParams(uuid.NewString(), fixture.user(t, "user-1").ID+1)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiKey, err := repo.Create(context.Background(), params)
|
||||
assert.ErrorIs(t, err, domain.UserDoesNotExistError{
|
||||
|
@ -55,7 +55,7 @@ func TestAPIKey_Create(t *testing.T) {
|
|||
fixture.apiKey(t, "user-1-api-key-1").Key,
|
||||
fixture.user(t, "user-1").ID,
|
||||
)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiKey, err := repo.Create(context.Background(), params)
|
||||
var pgError pgdriver.Error
|
||||
|
|
|
@ -53,7 +53,7 @@ func newDB(tb testing.TB) *bun.DB {
|
|||
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "14.5",
|
||||
Tag: "14.6",
|
||||
Env: []string{
|
||||
fmt.Sprintf("POSTGRES_USER=%s", dsn.User.Username()),
|
||||
fmt.Sprintf("POSTGRES_PASSWORD=%s", pw),
|
||||
|
|
|
@ -24,7 +24,7 @@ func NewAPIKey(p domain.CreateAPIKeyParams) APIKey {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *APIKey) ToDomain() domain.APIKey {
|
||||
func (a APIKey) ToDomain() domain.APIKey {
|
||||
return domain.APIKey{
|
||||
ID: a.ID,
|
||||
Key: a.Key,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
bun.BaseModel `bun:"base_model,table:sessions,alias:session"`
|
||||
|
||||
ID int64 `bun:"id,pk,autoincrement,identity"`
|
||||
UserID int64 `bun:"user_id,nullzero,notnull,unique:sessions_user_id_server_key_key"`
|
||||
ServerKey string `bun:"server_key,type:varchar(10),nullzero,notnull,unique:sessions_user_id_server_key_key"`
|
||||
SID string `bun:"sid,type:varchar(512),nullzero,notnull"`
|
||||
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
|
||||
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
|
||||
}
|
||||
|
||||
func NewSession(p domain.CreateSessionParams) Session {
|
||||
return Session{
|
||||
UserID: p.UserID(),
|
||||
ServerKey: p.ServerKey(),
|
||||
SID: p.SID(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s Session) ToDomain() domain.Session {
|
||||
return domain.Session{
|
||||
ID: s.ID,
|
||||
UserID: s.UserID,
|
||||
ServerKey: s.ServerKey,
|
||||
SID: s.SID,
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var id int64 = 123
|
||||
params, err := domain.NewCreateSessionParams(
|
||||
"pl151",
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
1,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
result := model.NewSession(params)
|
||||
result.ID = id
|
||||
|
||||
apiKey := result.ToDomain()
|
||||
assert.Equal(t, id, apiKey.ID)
|
||||
assert.Equal(t, params.ServerKey(), apiKey.ServerKey)
|
||||
assert.Equal(t, params.SID(), apiKey.SID)
|
||||
assert.Equal(t, params.UserID(), apiKey.UserID)
|
||||
assert.WithinDuration(t, time.Now(), apiKey.CreatedAt, 10*time.Millisecond)
|
||||
assert.WithinDuration(t, time.Now(), apiKey.UpdatedAt, 10*time.Millisecond)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.NewCreateTable().
|
||||
Model(&model.Session{}).
|
||||
Varchar(defaultVarcharLength).
|
||||
ForeignKey(`(user_id) REFERENCES users (id) ON DELETE CASCADE`).
|
||||
Exec(ctx); err != nil {
|
||||
return fmt.Errorf("couldn't create the 'sessions' table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, func(ctx context.Context, db *bun.DB) error {
|
||||
if _, err := db.NewDropTable().
|
||||
Model(&model.Session{}).
|
||||
IfExists().
|
||||
Cascade().
|
||||
Exec(ctx); err != nil {
|
||||
return fmt.Errorf("couldn't drop the 'sessions' table: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb/internal/model"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/driver/pgdriver"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
func NewSession(db *bun.DB) *Session {
|
||||
return &Session{db: db}
|
||||
}
|
||||
|
||||
func (u *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error) {
|
||||
sess := model.NewSession(params)
|
||||
|
||||
if _, err := u.db.NewInsert().
|
||||
Model(&sess).
|
||||
On("CONFLICT ON CONSTRAINT sessions_user_id_server_key_key DO UPDATE").
|
||||
Set("sid = EXCLUDED.sid").
|
||||
Set("updated_at = EXCLUDED.updated_at").
|
||||
Returning("*").
|
||||
Exec(ctx); err != nil {
|
||||
return domain.Session{}, fmt.Errorf(
|
||||
"something went wrong while inserting session into the db: %w",
|
||||
mapCreateSessionError(err, params),
|
||||
)
|
||||
}
|
||||
|
||||
return sess.ToDomain(), nil
|
||||
}
|
||||
|
||||
func mapCreateSessionError(err error, params domain.CreateSessionParams) error {
|
||||
var pgError pgdriver.Error
|
||||
if !errors.As(err, &pgError) {
|
||||
return err
|
||||
}
|
||||
|
||||
code := pgError.Field('C')
|
||||
constraint := pgError.Field('n')
|
||||
switch {
|
||||
case code == pgerrcode.ForeignKeyViolation && constraint == "sessions_user_id_fkey":
|
||||
return domain.UserDoesNotExistError{
|
||||
ID: params.UserID(),
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/bundb"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSession_CreateOrUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db := newDB(t)
|
||||
fixture := loadFixtures(t, db)
|
||||
repo := bundb.NewSession(db)
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
createParams, err := domain.NewCreateSessionParams(
|
||||
"pl151",
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
fixture.user(t, "user-1").ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
createdSess, err := repo.CreateOrUpdate(context.Background(), createParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, createdSess.ID, int64(0))
|
||||
assert.Equal(t, createParams.UserID(), createdSess.UserID)
|
||||
assert.Equal(t, createParams.SID(), createdSess.SID)
|
||||
assert.Equal(t, createParams.ServerKey(), createdSess.ServerKey)
|
||||
assert.WithinDuration(t, time.Now(), createdSess.CreatedAt, time.Second)
|
||||
assert.WithinDuration(t, time.Now(), createdSess.UpdatedAt, time.Second)
|
||||
|
||||
updateParams, err := domain.NewCreateSessionParams(
|
||||
createParams.ServerKey(),
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
createParams.UserID(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedSess, err := repo.CreateOrUpdate(context.Background(), updateParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, createdSess.ID, updatedSess.ID)
|
||||
assert.Equal(t, createdSess.UserID, updatedSess.UserID)
|
||||
assert.Equal(t, updateParams.SID(), updatedSess.SID)
|
||||
assert.Equal(t, createdSess.ServerKey, updatedSess.ServerKey)
|
||||
assert.Equal(t, createdSess.CreatedAt, updatedSess.CreatedAt)
|
||||
assert.True(t, updatedSess.UpdatedAt.After(createdSess.UpdatedAt))
|
||||
})
|
||||
|
||||
t.Run("ERR: user doesn't exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateSessionParams(
|
||||
"pl151",
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
fixture.user(t, "user-1").ID+1111,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
sess, err := repo.CreateOrUpdate(context.Background(), params)
|
||||
assert.ErrorIs(t, err, domain.UserDoesNotExistError{
|
||||
ID: params.UserID(),
|
||||
})
|
||||
assert.Zero(t, sess)
|
||||
})
|
||||
}
|
|
@ -23,7 +23,7 @@ func TestUser_Create(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateUserParams("nameName")
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, err := repo.Create(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -5,10 +5,6 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
apiKeyUserIDMin = 1
|
||||
)
|
||||
|
||||
type APIKey struct {
|
||||
ID int64
|
||||
Key string
|
||||
|
@ -29,11 +25,11 @@ func NewCreateAPIKeyParams(key string, userID int64) (CreateAPIKeyParams, error)
|
|||
}
|
||||
}
|
||||
|
||||
if userID < apiKeyUserIDMin {
|
||||
if userID < userIDMin {
|
||||
return CreateAPIKeyParams{}, ValidationError{
|
||||
Field: "UserID",
|
||||
Err: MinError{
|
||||
Min: apiKeyUserIDMin,
|
||||
Min: userIDMin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
var (
|
||||
ErrRequired = errors.New("cannot be blank")
|
||||
ErrBase64 = errors.New("must be encoded in Base64")
|
||||
)
|
||||
|
||||
type ErrorCode uint8
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
serverMaxLen = 10
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
ServerKey string
|
||||
SID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateSessionParams struct {
|
||||
userID int64
|
||||
serverKey string
|
||||
sid string
|
||||
}
|
||||
|
||||
func NewCreateSessionParams(serverKey, sid string, userID int64) (CreateSessionParams, error) {
|
||||
if serverKey == "" {
|
||||
return CreateSessionParams{}, ValidationError{
|
||||
Field: "ServerKey",
|
||||
Err: ErrRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if len(serverKey) > serverMaxLen {
|
||||
return CreateSessionParams{}, ValidationError{
|
||||
Field: "ServerKey",
|
||||
Err: MaxLengthError{
|
||||
Max: serverMaxLen,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if sid == "" {
|
||||
return CreateSessionParams{}, ValidationError{
|
||||
Field: "SID",
|
||||
Err: ErrRequired,
|
||||
}
|
||||
}
|
||||
|
||||
if !isBase64(sid) {
|
||||
return CreateSessionParams{}, ValidationError{
|
||||
Field: "SID",
|
||||
Err: ErrBase64,
|
||||
}
|
||||
}
|
||||
|
||||
if userID < userIDMin {
|
||||
return CreateSessionParams{}, ValidationError{
|
||||
Field: "UserID",
|
||||
Err: MinError{
|
||||
Min: userIDMin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return CreateSessionParams{userID: userID, serverKey: serverKey, sid: sid}, nil
|
||||
}
|
||||
|
||||
func (c CreateSessionParams) UserID() int64 {
|
||||
return c.userID
|
||||
}
|
||||
|
||||
func (c CreateSessionParams) ServerKey() string {
|
||||
return c.serverKey
|
||||
}
|
||||
|
||||
func (c CreateSessionParams) SID() string {
|
||||
return c.sid
|
||||
}
|
||||
|
||||
func isBase64(s string) bool {
|
||||
_, err := base64.StdEncoding.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCreateSessionParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
serverKey string
|
||||
sid string
|
||||
userID int64
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
serverKey: "pl151",
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
userID: 1,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ERR: serverKey is required",
|
||||
serverKey: "",
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
userID: 1,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "ServerKey",
|
||||
Err: domain.ErrRequired,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: len(serverKey) > 10",
|
||||
serverKey: randString(11),
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
userID: 1,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "ServerKey",
|
||||
Err: domain.MaxLengthError{
|
||||
Max: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: SID is required",
|
||||
serverKey: "pl151",
|
||||
sid: "",
|
||||
userID: 1,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "SID",
|
||||
Err: domain.ErrRequired,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: SID is not a valid base64",
|
||||
serverKey: "pl151",
|
||||
sid: uuid.NewString(),
|
||||
userID: 1,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "SID",
|
||||
Err: domain.ErrBase64,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: userID < 1",
|
||||
serverKey: "pl151",
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
userID: 0,
|
||||
expectedErr: domain.ValidationError{
|
||||
Field: "UserID",
|
||||
Err: domain.MinError{
|
||||
Min: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateSessionParams(tt.serverKey, tt.sid, tt.userID)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
assert.Zero(t, params)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.serverKey, params.ServerKey())
|
||||
assert.Equal(t, tt.sid, params.SID())
|
||||
assert.Equal(t, tt.userID, params.UserID())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,8 +8,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
UsernameMinLength = 2
|
||||
UsernameMaxLength = 40
|
||||
usernameMinLength = 2
|
||||
usernameMaxLength = 40
|
||||
userIDMin = 1
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -36,20 +37,20 @@ func NewCreateUserParams(name string) (CreateUserParams, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(name) < UsernameMinLength {
|
||||
if len(name) < usernameMinLength {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: MinLengthError{
|
||||
Min: UsernameMinLength,
|
||||
Min: usernameMinLength,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(name) > UsernameMaxLength {
|
||||
if len(name) > usernameMaxLength {
|
||||
return CreateUserParams{}, ValidationError{
|
||||
Field: "Name",
|
||||
Err: MaxLengthError{
|
||||
Max: UsernameMaxLength,
|
||||
Max: usernameMaxLength,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
user := domain.User{
|
||||
ID: 111,
|
||||
Name: "Name 111",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
assertUser(t, user, model.NewUser(user))
|
||||
}
|
||||
|
||||
func TestNewGetUserResp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
user := domain.User{
|
||||
ID: 111,
|
||||
Name: "Name 111",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
assertUser(t, user, model.NewGetUserResp(user).Data)
|
||||
}
|
||||
|
||||
func assertUser(tb testing.TB, du domain.User, ru model.User) {
|
||||
tb.Helper()
|
||||
|
||||
assert.Equal(tb, du.ID, ru.ID)
|
||||
assert.Equal(tb, du.Name, ru.Name)
|
||||
assert.Equal(tb, du.CreatedAt, ru.CreatedAt)
|
||||
}
|
|
@ -10,6 +10,11 @@ import (
|
|||
|
||||
type authCtxKey struct{}
|
||||
|
||||
//counterfeiter:generate -o internal/mock/api_key_verifier.gen.go . APIKeyVerifier
|
||||
type APIKeyVerifier interface {
|
||||
Verify(ctx context.Context, key string) (domain.User, error)
|
||||
}
|
||||
|
||||
func authMiddleware(verifier APIKeyVerifier) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
|
@ -29,13 +29,9 @@ type SwaggerConfig struct {
|
|||
Schemes []string
|
||||
}
|
||||
|
||||
//counterfeiter:generate -o internal/mock/api_key_verifier.gen.go . APIKeyVerifier
|
||||
type APIKeyVerifier interface {
|
||||
Verify(ctx context.Context, key string) (domain.User, error)
|
||||
}
|
||||
|
||||
type RouterConfig struct {
|
||||
APIKeyVerifier APIKeyVerifier
|
||||
SessionService SessionService
|
||||
CORS CORSConfig
|
||||
Swagger SwaggerConfig
|
||||
}
|
||||
|
@ -43,6 +39,9 @@ type RouterConfig struct {
|
|||
func NewRouter(cfg RouterConfig) *chi.Mux {
|
||||
// handlers
|
||||
uh := userHandler{}
|
||||
sh := sessionHandler{
|
||||
svc: cfg.SessionService,
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
|
@ -73,6 +72,7 @@ func NewRouter(cfg RouterConfig) *chi.Mux {
|
|||
authMw := authMiddleware(cfg.APIKeyVerifier)
|
||||
|
||||
r.With(authMw).Get("/user", uh.getAuthenticated)
|
||||
r.With(authMw).Put("/user/sessions/{serverKey}", sh.createOrUpdate)
|
||||
})
|
||||
|
||||
return router
|
||||
|
@ -96,53 +96,53 @@ func methodNotAllowedHandler(w http.ResponseWriter, _ *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
// type internalServerError struct {
|
||||
// err error
|
||||
// }
|
||||
//
|
||||
// func (e internalServerError) Error() string {
|
||||
// return e.err.Error()
|
||||
// }
|
||||
//
|
||||
// func (e internalServerError) UserError() string {
|
||||
// return "internal server error"
|
||||
// }
|
||||
//
|
||||
// func (e internalServerError) Code() domain.ErrorCode {
|
||||
// return domain.ErrorCodeUnknown
|
||||
// }
|
||||
//
|
||||
// func (e internalServerError) Unwrap() error {
|
||||
// return e.err
|
||||
// }
|
||||
//
|
||||
// func renderErr(w http.ResponseWriter, err error) {
|
||||
// var userError domain.Error
|
||||
// if !errors.As(err, &userError) {
|
||||
// userError = internalServerError{err: err}
|
||||
// }
|
||||
// renderJSON(w, errorCodeToHTTPStatus(userError.Code()), model.ErrorResp{
|
||||
// Error: model.APIError{
|
||||
// Code: userError.Code().String(),
|
||||
// Message: userError.UserError(),
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func errorCodeToHTTPStatus(code domain.ErrorCode) int {
|
||||
// switch code {
|
||||
// case domain.ErrorCodeValidationError:
|
||||
// return http.StatusBadRequest
|
||||
// case domain.ErrorCodeEntityNotFound:
|
||||
// return http.StatusNotFound
|
||||
// case domain.ErrorCodeAlreadyExists:
|
||||
// return http.StatusConflict
|
||||
// case domain.ErrorCodeUnknown:
|
||||
// fallthrough
|
||||
// default:
|
||||
// return http.StatusInternalServerError
|
||||
// }
|
||||
// }
|
||||
type internalServerError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e internalServerError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e internalServerError) UserError() string {
|
||||
return "internal server error"
|
||||
}
|
||||
|
||||
func (e internalServerError) Code() domain.ErrorCode {
|
||||
return domain.ErrorCodeUnknown
|
||||
}
|
||||
|
||||
func (e internalServerError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func renderErr(w http.ResponseWriter, err error) {
|
||||
var domainErr domain.Error
|
||||
if !errors.As(err, &domainErr) {
|
||||
domainErr = internalServerError{err: err}
|
||||
}
|
||||
renderJSON(w, errorCodeToHTTPStatus(domainErr.Code()), model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: domainErr.Code().String(),
|
||||
Message: domainErr.UserError(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func errorCodeToHTTPStatus(code domain.ErrorCode) int {
|
||||
switch code {
|
||||
case domain.ErrorCodeValidationError:
|
||||
return http.StatusBadRequest
|
||||
case domain.ErrorCodeEntityNotFound:
|
||||
return http.StatusNotFound
|
||||
case domain.ErrorCodeAlreadyExists:
|
||||
return http.StatusConflict
|
||||
case domain.ErrorCodeUnknown:
|
||||
fallthrough
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
func renderJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
|
@ -20,35 +20,31 @@ import (
|
|||
func TestRouteNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedResp := &model.ErrorResp{
|
||||
resp := doRequest(rest.NewRouter(rest.RouterConfig{}), http.MethodGet, "/v1/"+uuid.NewString(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
assertJSONResponse(t, resp, http.StatusNotFound, &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "route-not-found",
|
||||
Message: "route not found",
|
||||
},
|
||||
}
|
||||
|
||||
resp := doRequest(rest.NewRouter(rest.RouterConfig{}), http.MethodGet, "/v1/"+uuid.NewString(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
assertJSONResponse(t, resp, http.StatusNotFound, expectedResp, &model.ErrorResp{})
|
||||
}, &model.ErrorResp{})
|
||||
}
|
||||
|
||||
func TestMethodNotAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedResp := &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "method-not-allowed",
|
||||
Message: "method not allowed",
|
||||
},
|
||||
}
|
||||
|
||||
resp := doRequest(rest.NewRouter(rest.RouterConfig{
|
||||
Swagger: rest.SwaggerConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
}), http.MethodPost, "/v1/swagger/index.html", "", nil)
|
||||
defer resp.Body.Close()
|
||||
assertJSONResponse(t, resp, http.StatusMethodNotAllowed, expectedResp, &model.ErrorResp{})
|
||||
assertJSONResponse(t, resp, http.StatusMethodNotAllowed, &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "method-not-allowed",
|
||||
Message: "method not allowed",
|
||||
},
|
||||
}, &model.ErrorResp{})
|
||||
}
|
||||
|
||||
func TestSwagger(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
sidMaxBytes = 512
|
||||
)
|
||||
|
||||
//counterfeiter:generate -o internal/mock/session_service.gen.go . SessionService
|
||||
type SessionService interface {
|
||||
CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error)
|
||||
}
|
||||
|
||||
type sessionHandler struct {
|
||||
svc SessionService
|
||||
}
|
||||
|
||||
// @ID createOrUpdateSession
|
||||
// @Summary Create or update a session
|
||||
// @Description Create or update a session
|
||||
// @Tags users,sessions
|
||||
// @Accept plain
|
||||
// @Success 204
|
||||
// @Success 400 {object} model.ErrorResp
|
||||
// @Success 401 {object} model.ErrorResp
|
||||
// @Failure 500 {object} model.ErrorResp
|
||||
// @Security ApiKeyAuth
|
||||
// @Param serverKey path string true "Server key"
|
||||
// @Param request body string true "SID"
|
||||
// @Router /user/sessions/{serverKey} [put]
|
||||
func (h *sessionHandler) createOrUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chiCtx := chi.RouteContext(ctx)
|
||||
user, _ := userFromContext(ctx)
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(r.Body, sidMaxBytes))
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
params, err := domain.NewCreateSessionParams(chiCtx.URLParam("serverKey"), string(b), user.ID)
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.svc.CreateOrUpdate(ctx, params)
|
||||
if err != nil {
|
||||
renderErr(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package rest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest/internal/mock"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/router/rest/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSession_createOrUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
apiKey := uuid.NewString()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*mock.FakeAPIKeyVerifier, *mock.FakeSessionService)
|
||||
apiKey string
|
||||
serverKey string
|
||||
sid string
|
||||
expectedStatus int
|
||||
target any
|
||||
expectedResponse any
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
sessionSvc.CreateOrUpdateReturns(domain.Session{}, nil)
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
expectedStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
name: "ERR: len(serverKey) > 10",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "012345678890",
|
||||
sid: base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: domain.ErrorCodeValidationError.String(),
|
||||
Message: "ServerKey: the length must be no more than 10",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: SID is required",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
sid: "",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: domain.ErrorCodeValidationError.String(),
|
||||
Message: "SID: cannot be blank",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: SID is not a valid base64",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: apiKey,
|
||||
serverKey: "pl151",
|
||||
sid: uuid.NewString(),
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: domain.ErrorCodeValidationError.String(),
|
||||
Message: "SID: must be encoded in Base64",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: apiKey == \"\"",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {},
|
||||
apiKey: "",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
serverKey: "pl151",
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "unauthorized",
|
||||
Message: "invalid API key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: unexpected API key",
|
||||
setup: func(apiKeySvc *mock.FakeAPIKeyVerifier, sessionSvc *mock.FakeSessionService) {
|
||||
apiKeySvc.VerifyCalls(func(ctx context.Context, key string) (domain.User, error) {
|
||||
if key != apiKey {
|
||||
return domain.User{}, domain.APIKeyNotFoundError{
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: 111,
|
||||
Name: "name",
|
||||
CreatedAt: now,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
apiKey: uuid.NewString(),
|
||||
serverKey: "pl151",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
target: &model.ErrorResp{},
|
||||
expectedResponse: &model.ErrorResp{
|
||||
Error: model.APIError{
|
||||
Code: "unauthorized",
|
||||
Message: "invalid API key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
apiKeySvc := &mock.FakeAPIKeyVerifier{}
|
||||
sessionSvc := &mock.FakeSessionService{}
|
||||
tt.setup(apiKeySvc, sessionSvc)
|
||||
|
||||
router := rest.NewRouter(rest.RouterConfig{
|
||||
APIKeyVerifier: apiKeySvc,
|
||||
SessionService: sessionSvc,
|
||||
})
|
||||
|
||||
resp := doRequest(
|
||||
router,
|
||||
http.MethodPut,
|
||||
"/v1/user/sessions/"+tt.serverKey,
|
||||
tt.apiKey,
|
||||
strings.NewReader(tt.sid),
|
||||
)
|
||||
defer resp.Body.Close()
|
||||
if tt.expectedStatus == http.StatusNoContent {
|
||||
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -114,5 +114,4 @@ func TestUser_getAuthenticated(t *testing.T) {
|
|||
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
)
|
||||
|
||||
//counterfeiter:generate -o internal/mock/session_repository.gen.go . SessionRepository
|
||||
type SessionRepository interface {
|
||||
CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error)
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
repo SessionRepository
|
||||
userSvc UserGetter
|
||||
}
|
||||
|
||||
func NewSession(repo SessionRepository, userSvc UserGetter) *Session {
|
||||
return &Session{repo: repo, userSvc: userSvc}
|
||||
}
|
||||
|
||||
func (s *Session) CreateOrUpdate(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error) {
|
||||
if _, err := s.userSvc.Get(ctx, params.UserID()); err != nil {
|
||||
if errors.Is(err, domain.UserNotFoundError{ID: params.UserID()}) {
|
||||
return domain.Session{}, domain.UserDoesNotExistError{ID: params.UserID()}
|
||||
}
|
||||
return domain.Session{}, fmt.Errorf("UserService.Get: %w", err)
|
||||
}
|
||||
|
||||
sess, err := s.repo.CreateOrUpdate(ctx, params)
|
||||
if err != nil {
|
||||
return domain.Session{}, fmt.Errorf("SessionRepository.CreateOrUpdate: %w", err)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package service_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/service"
|
||||
"gitea.dwysokinski.me/twhelp/sessions/internal/service/internal/mock"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSession_CreateOrUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateSessionParams(
|
||||
"pl151",
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
111,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
repo := &mock.FakeSessionRepository{}
|
||||
repo.CreateOrUpdateCalls(func(ctx context.Context, params domain.CreateSessionParams) (domain.Session, error) {
|
||||
return domain.Session{
|
||||
ID: 1111,
|
||||
UserID: params.UserID(),
|
||||
ServerKey: params.ServerKey(),
|
||||
SID: params.SID(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}, nil
|
||||
})
|
||||
|
||||
userSvc := &mock.FakeUserGetter{}
|
||||
userSvc.GetReturns(domain.User{
|
||||
ID: params.UserID(),
|
||||
}, nil)
|
||||
|
||||
sess, err := service.NewSession(repo, userSvc).CreateOrUpdate(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, sess.ID, int64(0))
|
||||
assert.Equal(t, params.UserID(), sess.UserID)
|
||||
assert.Equal(t, params.SID(), sess.SID)
|
||||
assert.Equal(t, params.ServerKey(), sess.ServerKey)
|
||||
assert.WithinDuration(t, time.Now(), sess.CreatedAt, 10*time.Millisecond)
|
||||
assert.WithinDuration(t, time.Now(), sess.UpdatedAt, 10*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("ERR: user doesnt exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, err := domain.NewCreateSessionParams(
|
||||
"pl151",
|
||||
base64.StdEncoding.EncodeToString([]byte(uuid.NewString())),
|
||||
111,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
userSvc := &mock.FakeUserGetter{}
|
||||
userSvc.GetReturns(domain.User{}, domain.UserNotFoundError{ID: params.UserID()})
|
||||
|
||||
sess, err := service.NewSession(nil, userSvc).CreateOrUpdate(context.Background(), params)
|
||||
assert.ErrorIs(t, err, domain.UserDoesNotExistError{ID: params.UserID()})
|
||||
assert.Zero(t, sess)
|
||||
})
|
||||
}
|
|
@ -19,9 +19,10 @@ deploy:
|
|||
- name: sessionsdb
|
||||
repo: https://charts.bitnami.com/bitnami
|
||||
remoteChart: postgresql
|
||||
version: 11.6.19
|
||||
version: 11.9.13
|
||||
wait: true
|
||||
setValues:
|
||||
image.tag: 14.6.0-debian-11-r4
|
||||
auth.username: sessions
|
||||
auth.password: sessions
|
||||
auth.database: sessions
|
||||
|
|
Loading…
Reference in New Issue