Compare commits

..

3 Commits

Author SHA1 Message Date
Renovate 6572aadebe chore(deps): update module github.com/swaggo/swag to v1.8.8
continuous-integration/drone/pr Build is failing Details
2022-11-24 08:02:16 +00:00
Dawid Wysokiński 5872ead643 chore: update deps (#13)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #13
2022-11-24 05:57:14 +00:00
Dawid Wysokiński 8e8e7e1f94 feat: add a new REST endpoint - PUT /api/v1/user/sessions/:server (#8)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #8
2022-11-24 05:17:32 +00:00
29 changed files with 986 additions and 112 deletions

View File

@ -14,7 +14,7 @@ steps:
services:
- name: database
image: postgres:14.5
image: postgres:14.6
environment:
POSTGRES_DB: sessions
POSTGRES_PASSWORD: sessions

View File

@ -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 {

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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),

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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)
}

View File

@ -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
})
}

58
internal/bundb/session.go Normal file
View File

@ -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
}
}

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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,
},
}
}

View File

@ -7,6 +7,7 @@ import (
var (
ErrRequired = errors.New("cannot be blank")
ErrBase64 = errors.New("must be encoded in Base64")
)
type ErrorCode uint8

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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,
},
}
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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")

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)
})
}
}

View File

@ -114,5 +114,4 @@ func TestUser_getAuthenticated(t *testing.T) {
assertJSONResponse(t, resp, tt.expectedStatus, tt.expectedResponse, tt.target)
})
}
}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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