diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa8db3..219a790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ely.skinsystem.{hostname}.app.mojang_textures.usernames.textures_miss` - All incoming requests are now logging to the console in [Apache Common Log Format](http://httpd.apache.org/docs/2.2/logs.html#common). -- Added `/healthcheck` endpoint (at the moment checks are only available for the batch Mojang UUIDs provider). +- Added `/healthcheck` endpoint. - Graceful server shutdown. - Panics in http are now logged in Sentry. diff --git a/db/commons.go b/db/commons.go deleted file mode 100644 index 136137c..0000000 --- a/db/commons.go +++ /dev/null @@ -1,9 +0,0 @@ -package db - -type ParamRequired struct { - Param string -} - -func (e ParamRequired) Error() string { - return "Required parameter not provided" -} diff --git a/db/factory.go b/db/factory.go deleted file mode 100644 index 7baf993..0000000 --- a/db/factory.go +++ /dev/null @@ -1,12 +0,0 @@ -package db - -import ( - "github.com/elyby/chrly/http" - "github.com/elyby/chrly/mojangtextures" -) - -type RepositoriesCreator interface { - CreateSkinsRepository() (http.SkinsRepository, error) - CreateCapesRepository() (http.CapesRepository, error) - CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) -} diff --git a/db/filesystem.go b/db/filesystem.go deleted file mode 100644 index e03b479..0000000 --- a/db/filesystem.go +++ /dev/null @@ -1,68 +0,0 @@ -package db - -import ( - "github.com/elyby/chrly/http" - "os" - "path" - "strings" - - "github.com/elyby/chrly/model" - "github.com/elyby/chrly/mojangtextures" -) - -type FilesystemFactory struct { - BasePath string - CapesDirName string -} - -func (f FilesystemFactory) CreateSkinsRepository() (http.SkinsRepository, error) { - panic("skins repository not supported for this storage type") -} - -func (f FilesystemFactory) CreateCapesRepository() (http.CapesRepository, error) { - if err := f.validateFactoryConfig(); err != nil { - return nil, err - } - - return &filesStorage{path: path.Join(f.BasePath, f.CapesDirName)}, nil -} - -func (f FilesystemFactory) CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) { - panic("implement me") -} - -func (f FilesystemFactory) validateFactoryConfig() error { - if f.BasePath == "" { - return &ParamRequired{"basePath"} - } - - if f.CapesDirName == "" { - f.CapesDirName = "capes" - } - - return nil -} - -type filesStorage struct { - path string -} - -func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) { - if username == "" { - return nil, nil - } - - capePath := path.Join(repository.path, strings.ToLower(username)+".png") - file, err := os.Open(capePath) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - - return nil, err - } - - return &model.Cape{ - File: file, - }, nil -} diff --git a/db/fs/fs.go b/db/fs/fs.go new file mode 100644 index 0000000..646e199 --- /dev/null +++ b/db/fs/fs.go @@ -0,0 +1,37 @@ +package fs + +import ( + "os" + "path" + "strings" + + "github.com/elyby/chrly/model" +) + +func New(basePath string) (*Filesystem, error) { + return &Filesystem{path: basePath}, nil +} + +type Filesystem struct { + path string +} + +func (f *Filesystem) FindCapeByUsername(username string) (*model.Cape, error) { + if username == "" { + return nil, nil + } + + capePath := path.Join(f.path, strings.ToLower(username)+".png") + file, err := os.Open(capePath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + + return nil, err + } + + return &model.Cape{ + File: file, + }, nil +} diff --git a/db/redis.go b/db/redis/redis.go similarity index 75% rename from db/redis.go rename to db/redis/redis.go index 899eca1..8b4aa76 100644 --- a/db/redis.go +++ b/db/redis/redis.go @@ -1,10 +1,9 @@ -package db +package redis import ( "bytes" "compress/zlib" "encoding/json" - "fmt" "io" "strconv" "strings" @@ -14,69 +13,29 @@ import ( "github.com/mediocregopher/radix.v2/redis" "github.com/mediocregopher/radix.v2/util" - "github.com/elyby/chrly/http" "github.com/elyby/chrly/model" "github.com/elyby/chrly/mojangtextures" ) -type RedisFactory struct { - Host string - Port int - PoolSize int - pool *pool.Pool -} - -func (f *RedisFactory) CreateSkinsRepository() (http.SkinsRepository, error) { - return f.createInstance() -} - -func (f *RedisFactory) CreateCapesRepository() (http.CapesRepository, error) { - panic("capes repository not supported for this storage type") -} - -func (f *RedisFactory) CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) { - return f.createInstance() -} - -func (f *RedisFactory) createInstance() (*redisDb, error) { - p, err := f.getPool() +func New(addr string, poolSize int) (*Redis, error) { + conn, err := pool.New("tcp", addr, poolSize) if err != nil { return nil, err } - return &redisDb{p}, nil -} - -func (f *RedisFactory) getPool() (*pool.Pool, error) { - if f.pool == nil { - if f.Host == "" { - return nil, &ParamRequired{"host"} - } - - if f.Port == 0 { - return nil, &ParamRequired{"port"} - } - - addr := fmt.Sprintf("%s:%d", f.Host, f.Port) - conn, err := pool.New("tcp", addr, f.PoolSize) - if err != nil { - return nil, err - } - - f.pool = conn - } - - return f.pool, nil -} - -type redisDb struct { - pool *pool.Pool + return &Redis{ + pool: conn, + }, nil } const accountIdToUsernameKey = "hash:username-to-account-id" const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid" -func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { +type Redis struct { + pool *pool.Pool +} + +func (db *Redis) FindSkinByUsername(username string) (*model.Skin, error) { conn, err := db.pool.Get() if err != nil { return nil, err @@ -86,66 +45,6 @@ func (db *redisDb) FindByUsername(username string) (*model.Skin, error) { return findByUsername(username, conn) } -func (db *redisDb) FindByUserId(id int) (*model.Skin, error) { - conn, err := db.pool.Get() - if err != nil { - return nil, err - } - defer db.pool.Put(conn) - - return findByUserId(id, conn) -} - -func (db *redisDb) Save(skin *model.Skin) error { - conn, err := db.pool.Get() - if err != nil { - return err - } - defer db.pool.Put(conn) - - return save(skin, conn) -} - -func (db *redisDb) RemoveByUserId(id int) error { - conn, err := db.pool.Get() - if err != nil { - return err - } - defer db.pool.Put(conn) - - return removeByUserId(id, conn) -} - -func (db *redisDb) RemoveByUsername(username string) error { - conn, err := db.pool.Get() - if err != nil { - return err - } - defer db.pool.Put(conn) - - return removeByUsername(username, conn) -} - -func (db *redisDb) GetUuid(username string) (string, error) { - conn, err := db.pool.Get() - if err != nil { - return "", err - } - defer db.pool.Put(conn) - - return findMojangUuidByUsername(username, conn) -} - -func (db *redisDb) StoreUuid(username string, uuid string) error { - conn, err := db.pool.Get() - if err != nil { - return err - } - defer db.pool.Put(conn) - - return storeMojangUuid(username, uuid, conn) -} - func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { redisKey := buildUsernameKey(username) response := conn.Cmd("GET", redisKey) @@ -174,6 +73,16 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { return skin, nil } +func (db *Redis) FindSkinByUserId(id int) (*model.Skin, error) { + conn, err := db.pool.Get() + if err != nil { + return nil, err + } + defer db.pool.Put(conn) + + return findByUserId(id, conn) +} + func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { response := conn.Cmd("HGET", accountIdToUsernameKey, id) if response.IsType(redis.Nil) { @@ -188,42 +97,14 @@ func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { return findByUsername(username, conn) } -func removeByUserId(id int, conn util.Cmder) error { - record, err := findByUserId(id, conn) +func (db *Redis) SaveSkin(skin *model.Skin) error { + conn, err := db.pool.Get() if err != nil { return err } + defer db.pool.Put(conn) - conn.Cmd("MULTI") - - conn.Cmd("HDEL", accountIdToUsernameKey, id) - if record != nil { - conn.Cmd("DEL", buildUsernameKey(record.Username)) - } - - conn.Cmd("EXEC") - - return nil -} - -func removeByUsername(username string, conn util.Cmder) error { - record, err := findByUsername(username, conn) - if err != nil { - return err - } - - if record == nil { - return nil - } - - conn.Cmd("MULTI") - - conn.Cmd("DEL", buildUsernameKey(record.Username)) - conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId) - - conn.Cmd("EXEC") - - return nil + return save(skin, conn) } func save(skin *model.Skin, conn util.Cmder) error { @@ -249,6 +130,74 @@ func save(skin *model.Skin, conn util.Cmder) error { return nil } +func (db *Redis) RemoveSkinByUserId(id int) error { + conn, err := db.pool.Get() + if err != nil { + return err + } + defer db.pool.Put(conn) + + return removeByUserId(id, conn) +} + +func removeByUserId(id int, conn util.Cmder) error { + record, err := findByUserId(id, conn) + if err != nil { + return err + } + + conn.Cmd("MULTI") + + conn.Cmd("HDEL", accountIdToUsernameKey, id) + if record != nil { + conn.Cmd("DEL", buildUsernameKey(record.Username)) + } + + conn.Cmd("EXEC") + + return nil +} + +func (db *Redis) RemoveSkinByUsername(username string) error { + conn, err := db.pool.Get() + if err != nil { + return err + } + defer db.pool.Put(conn) + + return removeByUsername(username, conn) +} + +func removeByUsername(username string, conn util.Cmder) error { + record, err := findByUsername(username, conn) + if err != nil { + return err + } + + if record == nil { + return nil + } + + conn.Cmd("MULTI") + + conn.Cmd("DEL", buildUsernameKey(record.Username)) + conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId) + + conn.Cmd("EXEC") + + return nil +} + +func (db *Redis) GetUuid(username string) (string, error) { + conn, err := db.pool.Get() + if err != nil { + return "", err + } + defer db.pool.Put(conn) + + return findMojangUuidByUsername(username, conn) +} + func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) { response := conn.Cmd("HGET", mojangUsernameToUuidKey, strings.ToLower(username)) if response.IsType(redis.Nil) { @@ -266,6 +215,16 @@ func findMojangUuidByUsername(username string, conn util.Cmder) (string, error) return parts[0], nil } +func (db *Redis) StoreUuid(username string, uuid string) error { + conn, err := db.pool.Get() + if err != nil { + return err + } + defer db.pool.Put(conn) + + return storeMojangUuid(username, uuid, conn) +} + func storeMojangUuid(username string, uuid string, conn util.Cmder) error { value := uuid + ":" + strconv.FormatInt(time.Now().Unix(), 10) res := conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value) @@ -276,6 +235,15 @@ func storeMojangUuid(username string, uuid string, conn util.Cmder) error { return nil } +func (db *Redis) Ping() error { + r := db.pool.Cmd("PING") + if r.Err != nil { + return r.Err + } + + return nil +} + func buildUsernameKey(username string) string { return "username:" + strings.ToLower(username) } @@ -291,14 +259,14 @@ func zlibEncode(str []byte) []byte { func zlibDecode(bts []byte) ([]byte, error) { buff := bytes.NewReader(bts) - reader, readError := zlib.NewReader(buff) - if readError != nil { - return nil, readError + reader, err := zlib.NewReader(buff) + if err != nil { + return nil, err } resultBuffer := new(bytes.Buffer) _, _ = io.Copy(resultBuffer, reader) - reader.Close() + _ = reader.Close() return resultBuffer.Bytes(), nil } diff --git a/di/db.go b/di/db.go index 6b49a8c..8f0738c 100644 --- a/di/db.go +++ b/di/db.go @@ -1,61 +1,68 @@ package di import ( + "fmt" + "path" + "github.com/goava/di" "github.com/spf13/viper" - . "github.com/elyby/chrly/db" + "github.com/elyby/chrly/db/fs" + "github.com/elyby/chrly/db/redis" + es "github.com/elyby/chrly/eventsubscribers" "github.com/elyby/chrly/http" "github.com/elyby/chrly/mojangtextures" ) -var db = di.Options( - di.Provide(newRedisFactory), - di.Provide(newFSFactory), - di.Provide(newSkinsRepository), - di.Provide(newCapesRepository), - di.Provide(newMojangUUIDsRepository), - di.Provide(newMojangSignedTexturesStorage), -) - -func newRedisFactory(config *viper.Viper) *RedisFactory { - config.SetDefault("storage.redis.host", "localhost") - config.SetDefault("storage.redis.port", 6379) - config.SetDefault("storage.redis.poll", 10) - - return &RedisFactory{ - Host: config.GetString("storage.redis.host"), - Port: config.GetInt("storage.redis.port"), - PoolSize: config.GetInt("storage.redis.poolSize"), - } -} - -func newFSFactory(config *viper.Viper) *FilesystemFactory { - config.SetDefault("storage.filesystem.basePath", "data") - config.SetDefault("storage.filesystem.capesDirName", "capes") - - return &FilesystemFactory{ - BasePath: config.GetString("storage.filesystem.basePath"), - CapesDirName: config.GetString("storage.filesystem.capesDirName"), - } -} - // v4 had the idea that it would be possible to separate backends for storing skins and capes. // But in v5 the storage will be unified, so this is just temporary constructors before large reworking. // // Since there are no options for selecting target backends, // all constants in this case point to static specific implementations. +var db = di.Options( + di.Provide(newRedis, + di.As(new(http.SkinsRepository)), + di.As(new(mojangtextures.UuidsStorage)), + ), + di.Provide(newFSFactory, + di.As(new(http.CapesRepository)), + ), + di.Provide(newMojangSignedTexturesStorage), +) -func newSkinsRepository(factory *RedisFactory) (http.SkinsRepository, error) { - return factory.CreateSkinsRepository() +func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error) { + config.SetDefault("storage.redis.host", "localhost") + config.SetDefault("storage.redis.port", 6379) + config.SetDefault("storage.redis.poll", 10) + + conn, err := redis.New( + fmt.Sprintf("%s:%d", config.GetString("storage.redis.host"), config.GetInt("storage.redis.port")), + config.GetInt("storage.redis.poolSize"), + ) + if err != nil { + return nil, err + } + + if err := container.Provide(func() *namedHealthChecker { + return &namedHealthChecker{ + Name: "redis", + Checker: es.DatabaseChecker(conn), + } + }); err != nil { + return nil, err + } + + return conn, nil } -func newCapesRepository(factory *FilesystemFactory) (http.CapesRepository, error) { - return factory.CreateCapesRepository() -} +func newFSFactory(config *viper.Viper) (*fs.Filesystem, error) { + config.SetDefault("storage.filesystem.basePath", "data") + config.SetDefault("storage.filesystem.capesDirName", "capes") -func newMojangUUIDsRepository(factory *RedisFactory) (mojangtextures.UuidsStorage, error) { - return factory.CreateMojangUuidsRepository() + return fs.New(path.Join( + config.GetString("storage.filesystem.basePath"), + config.GetString("storage.filesystem.capesDirName"), + )) } func newMojangSignedTexturesStorage() mojangtextures.TexturesStorage { diff --git a/di/handlers.go b/di/handlers.go index 0fb5fdf..2c21bb2 100644 --- a/di/handlers.go +++ b/di/handlers.go @@ -87,7 +87,7 @@ func newHandlerFactory( checkersOptions[i] = healthcheck.WithChecker(checker.Name, checker.Checker) } - router.Handle("/healthcheck", healthcheck.Handler()).Methods("GET") + router.Handle("/healthcheck", healthcheck.Handler(checkersOptions...)).Methods("GET") } return router, nil diff --git a/eventsubscribers/health_checkers.go b/eventsubscribers/health_checkers.go index 9c33db4..2eb4788 100644 --- a/eventsubscribers/health_checkers.go +++ b/eventsubscribers/health_checkers.go @@ -11,6 +11,26 @@ import ( "github.com/elyby/chrly/api/mojang" ) +type Pingable interface { + Ping() error +} + +func DatabaseChecker(connection Pingable) healthcheck.CheckerFunc { + return func(ctx context.Context) error { + done := make(chan error) + go func() { + done <- connection.Ping() + }() + + select { + case <-ctx.Done(): + return errors.New("check timeout") + case err := <-done: + return err + } + } +} + func MojangBatchUuidsProviderResponseChecker(dispatcher Subscriber, resetDuration time.Duration) healthcheck.CheckerFunc { var mutex sync.Mutex var lastCallErr error diff --git a/eventsubscribers/health_checkers_test.go b/eventsubscribers/health_checkers_test.go index 5e95f63..5ca269c 100644 --- a/eventsubscribers/health_checkers_test.go +++ b/eventsubscribers/health_checkers_test.go @@ -7,18 +7,56 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/dispatcher" ) +type pingableMock struct { + mock.Mock +} + +func (p *pingableMock) Ping() error { + args := p.Called() + return args.Error(0) +} + +func TestDatabaseChecker(t *testing.T) { + t.Run("no error", func(t *testing.T) { + p := &pingableMock{} + p.On("Ping").Return(nil) + checker := DatabaseChecker(p) + assert.Nil(t, checker(context.Background())) + }) + + t.Run("with error", func(t *testing.T) { + err := errors.New("mock error") + p := &pingableMock{} + p.On("Ping").Return(err) + checker := DatabaseChecker(p) + assert.Equal(t, err, checker(context.Background())) + }) + + t.Run("context timeout", func(t *testing.T) { + p := &pingableMock{} + waitChan := make(chan time.Time, 1) + p.On("Ping").WaitUntil(waitChan).Return(nil) + + ctx, _ := context.WithTimeout(context.Background(), 0) + checker := DatabaseChecker(p) + assert.Errorf(t, checker(ctx), "check timeout") + close(waitChan) + }) +} + func TestMojangBatchUuidsProviderChecker(t *testing.T) { t.Run("empty state", func(t *testing.T) { d := dispatcher.New() checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond) assert.Nil(t, checker(context.Background())) }) - + // t.Run("when no error occurred", func(t *testing.T) { d := dispatcher.New() checker := MojangBatchUuidsProviderResponseChecker(d, time.Millisecond) diff --git a/http/api.go b/http/api.go index 75fe9a9..a93c26b 100644 --- a/http/api.go +++ b/http/api.go @@ -92,7 +92,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) { record.MojangTextures = req.Form.Get("mojangTextures") record.MojangSignature = req.Form.Get("mojangSignature") - err = ctx.SkinsRepo.Save(record) + err = ctx.SkinsRepo.SaveSkin(record) if err != nil { ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err)) apiServerError(resp) @@ -104,13 +104,13 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) { func (ctx *Api) deleteSkinByUserIdHandler(resp http.ResponseWriter, req *http.Request) { id, _ := strconv.Atoi(mux.Vars(req)["id"]) - skin, err := ctx.SkinsRepo.FindByUserId(id) + skin, err := ctx.SkinsRepo.FindSkinByUserId(id) ctx.deleteSkin(skin, err, resp) } func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.Request) { username := mux.Vars(req)["username"] - skin, err := ctx.SkinsRepo.FindByUsername(username) + skin, err := ctx.SkinsRepo.FindSkinByUsername(username) ctx.deleteSkin(skin, err, resp) } @@ -126,7 +126,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter return } - err = ctx.SkinsRepo.RemoveByUserId(skin.UserId) + err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId) if err != nil { ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err)) apiServerError(resp) @@ -137,7 +137,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter } func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.Skin, error) { - record, err := ctx.SkinsRepo.FindByUserId(identityId) + record, err := ctx.SkinsRepo.FindSkinByUserId(identityId) if err != nil { return nil, err } @@ -146,7 +146,7 @@ func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.S // The username may have changed in the external database, // so we need to remove the old association if record.Username != username { - _ = ctx.SkinsRepo.RemoveByUserId(identityId) + _ = ctx.SkinsRepo.RemoveSkinByUserId(identityId) record.Username = username } @@ -155,14 +155,14 @@ func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.S // If the requested id was not found, then username was reassigned to another user // who has not uploaded his data to Chrly yet - record, err = ctx.SkinsRepo.FindByUsername(username) + record, err = ctx.SkinsRepo.FindSkinByUsername(username) if err != nil { return nil, err } // If the target username does exist, clear it as it will be reassigned to the new user if record != nil { - _ = ctx.SkinsRepo.RemoveByUsername(username) + _ = ctx.SkinsRepo.RemoveSkinByUsername(username) record.UserId = identityId return record, nil diff --git a/http/api_test.go b/http/api_test.go index fe4f1cb..3863f60 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -88,9 +88,9 @@ var postSkinTestsCases = []*postSkinTestCase{ "url": {"http://example.com/skin.png"}, }.Encode()), BeforeTest: func(suite *apiTestSuite) { - suite.SkinsRepository.On("FindByUserId", 1).Return(nil, nil) - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) - suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { suite.Equal(1, model.UserId) suite.Equal("mock_username", model.Username) suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) @@ -120,8 +120,8 @@ var postSkinTestsCases = []*postSkinTestCase{ "url": {"http://textures-server.com/skin.png"}, }.Encode()), BeforeTest: func(suite *apiTestSuite) { - suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) - suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { suite.Equal(1, model.UserId) suite.Equal("mock_username", model.Username) suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) @@ -151,10 +151,10 @@ var postSkinTestsCases = []*postSkinTestCase{ "url": {"http://example.com/skin.png"}, }.Encode()), BeforeTest: func(suite *apiTestSuite) { - suite.SkinsRepository.On("FindByUserId", 2).Return(nil, nil) - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) - suite.SkinsRepository.On("RemoveByUsername", "mock_username").Times(1).Return(nil) - suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { + suite.SkinsRepository.On("FindSkinByUserId", 2).Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveSkinByUsername", "mock_username").Times(1).Return(nil) + suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { suite.Equal(2, model.UserId) suite.Equal("mock_username", model.Username) suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) @@ -180,9 +180,9 @@ var postSkinTestsCases = []*postSkinTestCase{ "url": {"http://example.com/skin.png"}, }.Encode()), BeforeTest: func(suite *apiTestSuite) { - suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) - suite.SkinsRepository.On("RemoveByUserId", 1).Times(1).Return(nil) - suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool { + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveSkinByUserId", 1).Times(1).Return(nil) + suite.SkinsRepository.On("SaveSkin", mock.MatchedBy(func(model *model.Skin) bool { suite.Equal(1, model.UserId) suite.Equal("changed_username", model.Username) suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) @@ -208,9 +208,9 @@ var postSkinTestsCases = []*postSkinTestCase{ "url": {"http://textures-server.com/skin.png"}, }.Encode()), BeforeTest: func(suite *apiTestSuite) { - suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) err := errors.New("mock error") - suite.SkinsRepository.On("Save", mock.Anything).Return(err) + suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(err) suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool { return cErr.Error() == "unable to save record to the repository: mock error" && errors.Is(cErr, err) @@ -235,7 +235,7 @@ var postSkinTestsCases = []*postSkinTestCase{ }.Encode()), BeforeTest: func(suite *apiTestSuite) { err := errors.New("mock error") - suite.SkinsRepository.On("FindByUserId", 1).Return(nil, err) + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, err) suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool { return cErr.Error() == "error on requesting a skin from the repository: mock error" && errors.Is(cErr, err) @@ -352,8 +352,8 @@ func (suite *apiTestSuite) TestPostSkin() { func (suite *apiTestSuite) TestDeleteByUserId() { suite.RunSubTest("Delete skin by its identity id", func() { - suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) - suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveSkinByUserId", 1).Once().Return(nil) req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil) w := httptest.NewRecorder() @@ -368,7 +368,7 @@ func (suite *apiTestSuite) TestDeleteByUserId() { }) suite.RunSubTest("Try to remove not exists identity id", func() { - suite.SkinsRepository.On("FindByUserId", 1).Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, nil) req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil) w := httptest.NewRecorder() @@ -391,8 +391,8 @@ func (suite *apiTestSuite) TestDeleteByUserId() { func (suite *apiTestSuite) TestDeleteByUsername() { suite.RunSubTest("Delete skin by its identity username", func() { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) - suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveSkinByUserId", 1).Once().Return(nil) req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil) w := httptest.NewRecorder() @@ -407,7 +407,7 @@ func (suite *apiTestSuite) TestDeleteByUsername() { }) suite.RunSubTest("Try to remove not exists identity username", func() { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil) w := httptest.NewRecorder() diff --git a/http/skinsystem.go b/http/skinsystem.go index a390b60..59976c6 100644 --- a/http/skinsystem.go +++ b/http/skinsystem.go @@ -14,15 +14,15 @@ import ( ) type SkinsRepository interface { - FindByUsername(username string) (*model.Skin, error) - FindByUserId(id int) (*model.Skin, error) - Save(skin *model.Skin) error - RemoveByUserId(id int) error - RemoveByUsername(username string) error + FindSkinByUsername(username string) (*model.Skin, error) + FindSkinByUserId(id int) (*model.Skin, error) + SaveSkin(skin *model.Skin) error + RemoveSkinByUserId(id int) error + RemoveSkinByUsername(username string) error } type CapesRepository interface { - FindByUsername(username string) (*model.Cape, error) + FindCapeByUsername(username string) (*model.Cape, error) } type MojangTexturesProvider interface { @@ -54,7 +54,7 @@ func (ctx *Skinsystem) Handler() *mux.Router { func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) { username := parseUsername(mux.Vars(request)["username"]) - rec, err := ctx.SkinsRepo.FindByUsername(username) + rec, err := ctx.SkinsRepo.FindSkinByUsername(username) if err == nil && rec != nil && rec.SkinId != 0 { http.Redirect(response, request, rec.Url, 301) return @@ -91,7 +91,7 @@ func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *htt func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) { username := parseUsername(mux.Vars(request)["username"]) - rec, err := ctx.CapesRepo.FindByUsername(username) + rec, err := ctx.CapesRepo.FindCapeByUsername(username) if err == nil && rec != nil { request.Header.Set("Content-Type", "image/png") _, _ = io.Copy(response, rec.File) @@ -131,8 +131,8 @@ func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *ht username := parseUsername(mux.Vars(request)["username"]) var textures *mojang.TexturesResponse - skin, skinErr := ctx.SkinsRepo.FindByUsername(username) - cape, capeErr := ctx.CapesRepo.FindByUsername(username) + skin, skinErr := ctx.SkinsRepo.FindSkinByUsername(username) + cape, capeErr := ctx.CapesRepo.FindCapeByUsername(username) if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) { textures = &mojang.TexturesResponse{} if skinErr == nil && skin != nil && skin.SkinId != 0 { @@ -185,7 +185,7 @@ func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, reque var responseData *mojang.SignedTexturesResponse - rec, err := ctx.SkinsRepo.FindByUsername(username) + rec, err := ctx.SkinsRepo.FindSkinByUsername(username) if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" { responseData = &mojang.SignedTexturesResponse{ Id: strings.Replace(rec.Uuid, "-", "", -1), diff --git a/http/skinsystem_test.go b/http/skinsystem_test.go index 49e01e3..f2cb229 100644 --- a/http/skinsystem_test.go +++ b/http/skinsystem_test.go @@ -26,7 +26,7 @@ type skinsRepositoryMock struct { mock.Mock } -func (m *skinsRepositoryMock) FindByUsername(username string) (*model.Skin, error) { +func (m *skinsRepositoryMock) FindSkinByUsername(username string) (*model.Skin, error) { args := m.Called(username) var result *model.Skin if casted, ok := args.Get(0).(*model.Skin); ok { @@ -36,7 +36,7 @@ func (m *skinsRepositoryMock) FindByUsername(username string) (*model.Skin, erro return result, args.Error(1) } -func (m *skinsRepositoryMock) FindByUserId(id int) (*model.Skin, error) { +func (m *skinsRepositoryMock) FindSkinByUserId(id int) (*model.Skin, error) { args := m.Called(id) var result *model.Skin if casted, ok := args.Get(0).(*model.Skin); ok { @@ -46,17 +46,17 @@ func (m *skinsRepositoryMock) FindByUserId(id int) (*model.Skin, error) { return result, args.Error(1) } -func (m *skinsRepositoryMock) Save(skin *model.Skin) error { +func (m *skinsRepositoryMock) SaveSkin(skin *model.Skin) error { args := m.Called(skin) return args.Error(0) } -func (m *skinsRepositoryMock) RemoveByUserId(id int) error { +func (m *skinsRepositoryMock) RemoveSkinByUserId(id int) error { args := m.Called(id) return args.Error(0) } -func (m *skinsRepositoryMock) RemoveByUsername(username string) error { +func (m *skinsRepositoryMock) RemoveSkinByUsername(username string) error { args := m.Called(username) return args.Error(0) } @@ -65,7 +65,7 @@ type capesRepositoryMock struct { mock.Mock } -func (m *capesRepositoryMock) FindByUsername(username string) (*model.Cape, error) { +func (m *capesRepositoryMock) FindCapeByUsername(username string) (*model.Cape, error) { args := m.Called(username) var result *model.Cape if casted, ok := args.Get(0).(*model.Cape); ok { @@ -155,7 +155,7 @@ var skinsTestsCases = []*skinsystemTestCase{ { Name: "Username exists in the local storage", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(301, response.StatusCode) @@ -165,7 +165,7 @@ var skinsTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -176,7 +176,7 @@ var skinsTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -186,7 +186,7 @@ var skinsTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage and doesn't exists on Mojang", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -210,7 +210,7 @@ func (suite *skinsystemTestSuite) TestSkin() { } suite.RunSubTest("Pass username with png extension", func() { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil) w := httptest.NewRecorder() @@ -257,7 +257,7 @@ var capesTestsCases = []*skinsystemTestCase{ { Name: "Username exists in the local storage", BeforeTest: func(suite *skinsystemTestSuite) { - suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -269,7 +269,7 @@ var capesTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures", BeforeTest: func(suite *skinsystemTestSuite) { - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -280,7 +280,7 @@ var capesTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage, but exists on Mojang and has no textures", BeforeTest: func(suite *skinsystemTestSuite) { - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -290,7 +290,7 @@ var capesTestsCases = []*skinsystemTestCase{ { Name: "Username doesn't exists on the local storage and doesn't exists on Mojang", BeforeTest: func(suite *skinsystemTestSuite) { - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -314,7 +314,7 @@ func (suite *skinsystemTestSuite) TestCape() { } suite.RunSubTest("Pass username with png extension", func() { - suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil) w := httptest.NewRecorder() @@ -363,8 +363,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username exists and has skin, no cape", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -380,8 +380,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username exists and has slim skin, no cape", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -400,8 +400,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username exists and has cape, no skin", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -417,8 +417,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username exists and has both skin and cape", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -437,8 +437,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username not exists, but Mojang profile available", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -458,8 +458,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username not exists, but Mojang profile available, but there is no textures", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(false, false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -469,8 +469,8 @@ var texturesTestsCases = []*skinsystemTestCase{ { Name: "Username not exists and Mojang profile unavailable", BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) - suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) + suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -512,7 +512,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ Name: "Username exists", AllowProxy: false, BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(200, response.StatusCode) @@ -539,7 +539,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ Name: "Username not exists", AllowProxy: false, BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(204, response.StatusCode) @@ -554,7 +554,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ skinModel := createSkinModel("mock_username", true) skinModel.MojangTextures = "" skinModel.MojangSignature = "" - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(skinModel, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { suite.Equal(204, response.StatusCode) @@ -566,7 +566,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ Name: "Username not exists, but Mojang profile is available and proxying is enabled", AllowProxy: true, BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { @@ -593,7 +593,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{ Name: "Username not exists, Mojang profile is unavailable too and proxying is enabled", AllowProxy: true, BeforeTest: func(suite *skinsystemTestSuite) { - suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, nil) + suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil) suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) }, AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {