From 1e91aef0a657517a519492992bb96a1706d66144 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Wed, 1 Jan 2020 23:42:45 +0300 Subject: [PATCH] Rework http app structure, get rid of the golang/mock package, rewrite http tests --- Gopkg.lock | 9 - Gopkg.toml | 4 - cmd/serve.go | 2 +- db/commons.go | 16 - db/factory.go | 6 +- db/filesystem.go | 10 +- db/redis.go | 16 +- http/api.go | 259 ---- http/api_test.go | 501 -------- http/cape.go | 51 - http/cape_test.go | 163 --- http/http.go | 110 +- http/http_test.go | 108 +- http/not_found.go | 17 - http/not_found_test.go | 27 - http/signed_textures.go | 52 - http/signed_textures_test.go | 141 --- http/skin.go | 49 - http/skin_test.go | 158 --- http/skinsystem.go | 503 ++++++++ http/skinsystem_test.go | 1092 +++++++++++++++++ http/textures.go | 61 - http/textures_test.go | 194 --- interfaces/auth.go | 7 - interfaces/mock_interfaces/mock_auth.go | 45 - interfaces/mock_interfaces/mock_interfaces.go | 131 -- interfaces/mock_wd/mock_wd.go | 218 ---- interfaces/repositories.go | 22 - 28 files changed, 1669 insertions(+), 2303 deletions(-) delete mode 100644 http/api.go delete mode 100644 http/api_test.go delete mode 100644 http/cape.go delete mode 100644 http/cape_test.go delete mode 100644 http/not_found.go delete mode 100644 http/not_found_test.go delete mode 100644 http/signed_textures.go delete mode 100644 http/signed_textures_test.go delete mode 100644 http/skin.go delete mode 100644 http/skin_test.go create mode 100644 http/skinsystem.go create mode 100644 http/skinsystem_test.go delete mode 100644 http/textures.go delete mode 100644 http/textures_test.go delete mode 100644 interfaces/auth.go delete mode 100644 interfaces/mock_interfaces/mock_auth.go delete mode 100644 interfaces/mock_interfaces/mock_interfaces.go delete mode 100644 interfaces/mock_wd/mock_wd.go delete mode 100644 interfaces/repositories.go diff --git a/Gopkg.lock b/Gopkg.lock index 5b022fc..8416f5d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -46,14 +46,6 @@ pruneopts = "" revision = "919484f041ea21e7e27be291cee1d6af7bc98864" -[[projects]] - digest = "1:530233672f656641b365f8efb38ed9fba80e420baff2ce87633813ab3755ed6d" - name = "github.com/golang/mock" - packages = ["gomock"] - pruneopts = "" - revision = "51421b967af1f557f93a59e0057aaf15ca02e29c" - version = "v1.2.0" - [[projects]] digest = "1:65c7ed49d9f36dd4752e43013323fa9229db60b29aa4f5a75aaecda3130c74e2" name = "github.com/gorilla/mux" @@ -310,7 +302,6 @@ "github.com/SermoDigital/jose/crypto", "github.com/SermoDigital/jose/jws", "github.com/getsentry/raven-go", - "github.com/golang/mock/gomock", "github.com/gorilla/mux", "github.com/h2non/gock", "github.com/mediocregopher/radix.v2/pool", diff --git a/Gopkg.toml b/Gopkg.toml index dd2f585..a260730 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -42,10 +42,6 @@ ignored = ["github.com/elyby/chrly"] name = "github.com/stretchr/testify" version = "^1.3.0" -[[constraint]] - name = "github.com/golang/mock" - version = "^1.0.0" - [[constraint]] name = "github.com/h2non/gock" version = "^1.0.6" diff --git a/cmd/serve.go b/cmd/serve.go index e06a66b..481e219 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -91,7 +91,7 @@ var serveCmd = &cobra.Command{ } logger.Info("Mojang's textures queue is successfully initialized") - cfg := &http.Config{ + cfg := &http.Skinsystem{ ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")), SkinsRepo: skinsRepo, CapesRepo: capesRepo, diff --git a/db/commons.go b/db/commons.go index 55be43e..136137c 100644 --- a/db/commons.go +++ b/db/commons.go @@ -7,19 +7,3 @@ type ParamRequired struct { func (e ParamRequired) Error() string { return "Required parameter not provided" } - -type SkinNotFoundError struct { - Who string -} - -func (e SkinNotFoundError) Error() string { - return "Skin data not found." -} - -type CapeNotFoundError struct { - Who string -} - -func (e CapeNotFoundError) Error() string { - return "Cape file not found." -} diff --git a/db/factory.go b/db/factory.go index 337d8bf..03c9923 100644 --- a/db/factory.go +++ b/db/factory.go @@ -1,9 +1,9 @@ package db import ( + "github.com/elyby/chrly/http" "github.com/spf13/viper" - "github.com/elyby/chrly/interfaces" "github.com/elyby/chrly/mojangtextures" ) @@ -12,8 +12,8 @@ type StorageFactory struct { } type RepositoriesCreator interface { - CreateSkinsRepository() (interfaces.SkinsRepository, error) - CreateCapesRepository() (interfaces.CapesRepository, error) + CreateSkinsRepository() (http.SkinsRepository, error) + CreateCapesRepository() (http.CapesRepository, error) CreateMojangUuidsRepository() (mojangtextures.UuidsStorage, error) } diff --git a/db/filesystem.go b/db/filesystem.go index 4a996c3..a9c9030 100644 --- a/db/filesystem.go +++ b/db/filesystem.go @@ -1,11 +1,11 @@ package db import ( + "github.com/elyby/chrly/http" "os" "path" "strings" - "github.com/elyby/chrly/interfaces" "github.com/elyby/chrly/model" "github.com/elyby/chrly/mojangtextures" ) @@ -15,11 +15,11 @@ type FilesystemFactory struct { CapesDirName string } -func (f FilesystemFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) { +func (f FilesystemFactory) CreateSkinsRepository() (http.SkinsRepository, error) { panic("skins repository not supported for this storage type") } -func (f FilesystemFactory) CreateCapesRepository() (interfaces.CapesRepository, error) { +func (f FilesystemFactory) CreateCapesRepository() (http.CapesRepository, error) { if err := f.validateFactoryConfig(); err != nil { return nil, err } @@ -49,13 +49,13 @@ type filesStorage struct { func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) { if username == "" { - return nil, &CapeNotFoundError{username} + return nil, &http.CapeNotFoundError{username} } capePath := path.Join(repository.path, strings.ToLower(username)+".png") file, err := os.Open(capePath) if err != nil { - return nil, &CapeNotFoundError{username} + return nil, &http.CapeNotFoundError{username} } return &model.Cape{ diff --git a/db/redis.go b/db/redis.go index cb657b0..d07e5b6 100644 --- a/db/redis.go +++ b/db/redis.go @@ -5,6 +5,7 @@ import ( "compress/zlib" "encoding/json" "fmt" + "github.com/elyby/chrly/http" "io" "strconv" "strings" @@ -14,7 +15,6 @@ import ( "github.com/mediocregopher/radix.v2/redis" "github.com/mediocregopher/radix.v2/util" - "github.com/elyby/chrly/interfaces" "github.com/elyby/chrly/model" "github.com/elyby/chrly/mojangtextures" ) @@ -26,11 +26,11 @@ type RedisFactory struct { pool *pool.Pool } -func (f *RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) { +func (f *RedisFactory) CreateSkinsRepository() (http.SkinsRepository, error) { return f.createInstance() } -func (f *RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) { +func (f *RedisFactory) CreateCapesRepository() (http.CapesRepository, error) { panic("capes repository not supported for this storage type") } @@ -148,13 +148,13 @@ func (db *redisDb) StoreUuid(username string, uuid string) error { func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { if username == "" { - return nil, &SkinNotFoundError{username} + return nil, &http.SkinNotFoundError{username} } redisKey := buildUsernameKey(username) response := conn.Cmd("GET", redisKey) if !response.IsType(redis.Str) { - return nil, &SkinNotFoundError{username} + return nil, &http.SkinNotFoundError{username} } encodedResult, err := response.Bytes() @@ -181,7 +181,7 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) { func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { response := conn.Cmd("HGET", accountIdToUsernameKey, id) if !response.IsType(redis.Str) { - return nil, &SkinNotFoundError{"unknown"} + return nil, &http.SkinNotFoundError{"unknown"} } username, _ := response.Str() @@ -192,7 +192,7 @@ func findByUserId(id int, conn util.Cmder) (*model.Skin, error) { func removeByUserId(id int, conn util.Cmder) error { record, err := findByUserId(id, conn) if err != nil { - if _, ok := err.(*SkinNotFoundError); !ok { + if _, ok := err.(*http.SkinNotFoundError); !ok { return err } } @@ -212,7 +212,7 @@ func removeByUserId(id int, conn util.Cmder) error { func removeByUsername(username string, conn util.Cmder) error { record, err := findByUsername(username, conn) if err != nil { - if _, ok := err.(*SkinNotFoundError); ok { + if _, ok := err.(*http.SkinNotFoundError); ok { return nil } diff --git a/http/api.go b/http/api.go deleted file mode 100644 index 9a41401..0000000 --- a/http/api.go +++ /dev/null @@ -1,259 +0,0 @@ -package http - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "regexp" - "strconv" - - "github.com/elyby/chrly/auth" - "github.com/elyby/chrly/db" - "github.com/elyby/chrly/interfaces" - "github.com/elyby/chrly/model" - - "github.com/gorilla/mux" - "github.com/mono83/slf/wd" - "github.com/thedevsaddam/govalidator" -) - -//noinspection GoSnakeCaseUsage -const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" - -var regexUuidAny = regexp.MustCompile(UUID_ANY) - -func init() { - govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error { - if message == "" { - message = "Skin uploading is temporary unavailable" - } - - return errors.New(message) - }) - - // Add ability to validate any possible uuid form - govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error { - str := value.(string) - if !regexUuidAny.MatchString(str) { - if message == "" { - message = fmt.Sprintf("The %s field must contain valid UUID", field) - } - - return errors.New(message) - } - - return nil - }) -} - -func (cfg *Config) PostSkin(resp http.ResponseWriter, req *http.Request) { - cfg.Logger.IncCounter("api.skins.post.request", 1) - validationErrors := validatePostSkinRequest(req) - if validationErrors != nil { - cfg.Logger.IncCounter("api.skins.post.validation_failed", 1) - apiBadRequest(resp, validationErrors) - return - } - - identityId, _ := strconv.Atoi(req.Form.Get("identityId")) - username := req.Form.Get("username") - - record, err := findIdentity(cfg.SkinsRepo, identityId, username) - if err != nil { - cfg.Logger.Error("Error on requesting a skin from the repository: :err", wd.ErrParam(err)) - apiServerError(resp) - return - } - - skinId, _ := strconv.Atoi(req.Form.Get("skinId")) - is18, _ := strconv.ParseBool(req.Form.Get("is1_8")) - isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim")) - - record.Uuid = req.Form.Get("uuid") - record.SkinId = skinId - record.Is1_8 = is18 - record.IsSlim = isSlim - record.Url = req.Form.Get("url") - record.MojangTextures = req.Form.Get("mojangTextures") - record.MojangSignature = req.Form.Get("mojangSignature") - - err = cfg.SkinsRepo.Save(record) - if err != nil { - cfg.Logger.Error("Unable to save record to the repository: :err", wd.ErrParam(err)) - apiServerError(resp) - return - } - - cfg.Logger.IncCounter("api.skins.post.success", 1) - resp.WriteHeader(http.StatusCreated) -} - -func (cfg *Config) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) { - cfg.Logger.IncCounter("api.skins.delete.request", 1) - id, _ := strconv.Atoi(mux.Vars(req)["id"]) - skin, err := cfg.SkinsRepo.FindByUserId(id) - if err != nil { - cfg.Logger.IncCounter("api.skins.delete.not_found", 1) - apiNotFound(resp, "Cannot find record for requested user id") - return - } - - cfg.deleteSkin(skin, resp) -} - -func (cfg *Config) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) { - cfg.Logger.IncCounter("api.skins.delete.request", 1) - username := mux.Vars(req)["username"] - skin, err := cfg.SkinsRepo.FindByUsername(username) - if err != nil { - cfg.Logger.IncCounter("api.skins.delete.not_found", 1) - apiNotFound(resp, "Cannot find record for requested username") - return - } - - cfg.deleteSkin(skin, resp) -} - -func (cfg *Config) AuthenticationMiddleware(handler http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - cfg.Logger.IncCounter("authentication.challenge", 1) - err := cfg.Auth.Check(req) - if err != nil { - if _, ok := err.(*auth.Unauthorized); ok { - cfg.Logger.IncCounter("authentication.failed", 1) - apiForbidden(resp, err.Error()) - } else { - cfg.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err)) - apiServerError(resp) - } - - return - } - - cfg.Logger.IncCounter("authentication.success", 1) - handler.ServeHTTP(resp, req) - }) -} - -func (cfg *Config) deleteSkin(skin *model.Skin, resp http.ResponseWriter) { - err := cfg.SkinsRepo.RemoveByUserId(skin.UserId) - if err != nil { - cfg.Logger.Error("Cannot delete skin by error: :err", wd.ErrParam(err)) - apiServerError(resp) - return - } - - cfg.Logger.IncCounter("api.skins.delete.success", 1) - resp.WriteHeader(http.StatusNoContent) -} - -func validatePostSkinRequest(request *http.Request) map[string][]string { - const maxMultipartMemory int64 = 32 << 20 - const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both" - - _ = request.ParseMultipartForm(maxMultipartMemory) - - validationRules := govalidator.MapData{ - "identityId": {"required", "numeric", "min:1"}, - "username": {"required"}, - "uuid": {"required", "uuid_any"}, - "skinId": {"required", "numeric", "min:1"}, - "url": {"url"}, - "file:skin": {"ext:png", "size:24576", "mime:image/png"}, - "is1_8": {"bool"}, - "isSlim": {"bool"}, - } - - shouldAppendSkinRequiredError := false - url := request.Form.Get("url") - _, _, skinErr := request.FormFile("skin") - if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) { - shouldAppendSkinRequiredError = true - } else if skinErr == nil { - validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable") - } else if url != "" { - validationRules["is1_8"] = append(validationRules["is1_8"], "required") - validationRules["isSlim"] = append(validationRules["isSlim"], "required") - } - - mojangTextures := request.Form.Get("mojangTextures") - if mojangTextures != "" { - validationRules["mojangSignature"] = []string{"required"} - } - - validator := govalidator.New(govalidator.Options{ - Request: request, - Rules: validationRules, - RequiredDefault: false, - FormSize: maxMultipartMemory, - }) - validationResults := validator.Validate() - if shouldAppendSkinRequiredError { - validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage) - validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage) - } - - if len(validationResults) != 0 { - return validationResults - } - - return nil -} - -func findIdentity(repo interfaces.SkinsRepository, identityId int, username string) (*model.Skin, error) { - var record *model.Skin - record, err := repo.FindByUserId(identityId) - if err != nil { - if _, isSkinNotFound := err.(*db.SkinNotFoundError); !isSkinNotFound { - return nil, err - } - - record, err = repo.FindByUsername(username) - if err == nil { - _ = repo.RemoveByUsername(username) - record.UserId = identityId - } else { - record = &model.Skin{ - UserId: identityId, - Username: username, - } - } - } else if record.Username != username { - _ = repo.RemoveByUserId(identityId) - record.Username = username - } - - return record, nil -} - -func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) { - resp.WriteHeader(http.StatusBadRequest) - resp.Header().Set("Content-Type", "application/json") - result, _ := json.Marshal(map[string]interface{}{ - "errors": errorsPerField, - }) - _, _ = resp.Write(result) -} - -func apiForbidden(resp http.ResponseWriter, reason string) { - resp.WriteHeader(http.StatusForbidden) - resp.Header().Set("Content-Type", "application/json") - result, _ := json.Marshal(map[string]interface{}{ - "error": reason, - }) - _, _ = resp.Write(result) -} - -func apiNotFound(resp http.ResponseWriter, reason string) { - resp.WriteHeader(http.StatusNotFound) - resp.Header().Set("Content-Type", "application/json") - result, _ := json.Marshal([]interface{}{ - reason, - }) - _, _ = resp.Write(result) -} - -func apiServerError(resp http.ResponseWriter) { - resp.WriteHeader(http.StatusInternalServerError) -} diff --git a/http/api_test.go b/http/api_test.go deleted file mode 100644 index 01cd917..0000000 --- a/http/api_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package http - -import ( - "bytes" - "encoding/base64" - "io/ioutil" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/elyby/chrly/auth" - "github.com/elyby/chrly/db" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" -) - -func TestConfig_PostSkin(t *testing.T) { - t.Run("Upload new identity with textures info", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - resultModel := createSkinModel("mock_user", false) - resultModel.SkinId = 5 - resultModel.Url = "http://example.com/skin.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"1"}, - "username": {"mock_user"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://example.com/skin.png"}, - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(1).Return(nil, &db.SkinNotFoundError{Who: "unknown"}) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{Who: "mock_user"}) - mocks.Skins.EXPECT().Save(resultModel).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(201, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Upload new identity with skin file", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - part, _ := writer.CreateFormFile("skin", "char.png") - _, _ = part.Write(loadSkinFile()) - - _ = writer.WriteField("identityId", "1") - _ = writer.WriteField("username", "mock_user") - _ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3") - _ = writer.WriteField("skinId", "5") - - err := writer.Close() - if err != nil { - panic(err) - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", body) - req.Header.Add("Content-Type", writer.FormDataContentType()) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(400, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "errors": { - "skin": [ - "Skin uploading is temporary unavailable" - ] - } - }`, string(response)) - }) - - t.Run("Keep the same identityId, uuid and username, but change textures information", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - resultModel := createSkinModel("mock_user", false) - resultModel.SkinId = 5 - resultModel.Url = "http://textures-server.com/skin.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) - mocks.Skins.EXPECT().Save(resultModel).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) - - form := url.Values{ - "identityId": {"1"}, - "username": {"mock_user"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://textures-server.com/skin.png"}, - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(201, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Keep the same uuid and username, but change identityId", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - resultModel := createSkinModel("mock_user", false) - resultModel.UserId = 2 - resultModel.SkinId = 5 - resultModel.Url = "http://example.com/skin.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"2"}, - "username": {"mock_user"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://example.com/skin.png"}, - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{Who: "unknown"}) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Skins.EXPECT().RemoveByUsername("mock_user").Return(nil) - mocks.Skins.EXPECT().Save(resultModel).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(201, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Keep the same identityId and uuid, but change username", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - resultModel := createSkinModel("changed_username", false) - resultModel.SkinId = 5 - resultModel.Url = "http://example.com/skin.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"1"}, - "username": {"changed_username"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://example.com/skin.png"}, - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) - mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) - mocks.Skins.EXPECT().Save(resultModel).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.success", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(201, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Get errors about required fields", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - form := url.Values{ - "mojangTextures": {"someBase64EncodedString"}, - } - - req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.post.validation_failed", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(400, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "errors": { - "identityId": [ - "The identityId field is required", - "The identityId field must be numeric", - "The identityId field must be minimum 1 char" - ], - "skinId": [ - "The skinId field is required", - "The skinId field must be numeric", - "The skinId field must be minimum 1 char" - ], - "username": [ - "The username field is required" - ], - "uuid": [ - "The uuid field is required", - "The uuid field must contain valid UUID" - ], - "url": [ - "One of url or skin should be provided, but not both" - ], - "skin": [ - "One of url or skin should be provided, but not both" - ], - "mojangSignature": [ - "The mojangSignature field is required" - ] - } - }`, string(response)) - }) - - t.Run("Perform request without authorization", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("POST", "http://chrly/api/skins", nil) - req.Header.Add("Authorization", "Bearer invalid.jwt.token") - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{Reason: "Cannot parse passed JWT token"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(403, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "error": "Cannot parse passed JWT token" - }`, string(response)) - }) -} - -func TestConfig_DeleteSkinByUserId(t *testing.T) { - t.Run("Delete skin by its identity id", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(1).Return(createSkinModel("mock_user", false), nil) - mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(204, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Try to remove not exists identity id", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:2", nil) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{Who: "unknown"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(404, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`[ - "Cannot find record for requested user id" - ]`, string(response)) - }) -} - -func TestConfig_DeleteSkinByUsername(t *testing.T) { - t.Run("Delete skin by its identity username", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user", nil) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Skins.EXPECT().RemoveByUserId(1).Return(nil) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.success", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(204, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Empty(response) - }) - - t.Run("Try to remove not exists identity username", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user_2", nil) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{Who: "mock_user_2"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.success", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.request", int64(1)) - mocks.Log.EXPECT().IncCounter("api.skins.delete.not_found", int64(1)) - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(404, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`[ - "Cannot find record for requested username" - ]`, string(response)) - }) -} - -func TestConfig_Authenticate(t *testing.T) { - t.Run("Test behavior when signing key is not set", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("POST", "http://localhost", nil) - w := httptest.NewRecorder() - - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{Reason: "signing key not available"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) - - res := config.AuthenticationMiddleware(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {})) - res.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(403, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "error": "signing key not available" - }`, string(response)) - }) -} - -// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png -var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==") - -func loadSkinFile() []byte { - result := make([]byte, 92) - _, err := base64.StdEncoding.Decode(result, OnePxPng) - if err != nil { - panic(err) - } - - return result -} diff --git a/http/cape.go b/http/cape.go deleted file mode 100644 index 9bba411..0000000 --- a/http/cape.go +++ /dev/null @@ -1,51 +0,0 @@ -package http - -import ( - "io" - "net/http" - - "github.com/gorilla/mux" -) - -func (cfg *Config) Cape(response http.ResponseWriter, request *http.Request) { - if mux.Vars(request)["converted"] == "" { - cfg.Logger.IncCounter("capes.request", 1) - } - - username := parseUsername(mux.Vars(request)["username"]) - rec, err := cfg.CapesRepo.FindByUsername(username) - if err == nil { - request.Header.Set("Content-Type", "image/png") - _, _ = io.Copy(response, rec.File) - return - } - - mojangTextures, err := cfg.MojangTexturesProvider.GetForUsername(username) - if err != nil || mojangTextures == nil { - response.WriteHeader(http.StatusNotFound) - return - } - - texturesProp := mojangTextures.DecodeTextures() - cape := texturesProp.Textures.Cape - if cape == nil { - response.WriteHeader(http.StatusNotFound) - return - } - - http.Redirect(response, request, cape.Url, 301) -} - -func (cfg *Config) CapeGET(response http.ResponseWriter, request *http.Request) { - cfg.Logger.IncCounter("capes.get_request", 1) - username := request.URL.Query().Get("name") - if username == "" { - response.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(request)["username"] = username - mux.Vars(request)["converted"] = "1" - - cfg.Cape(response, request) -} diff --git a/http/cape_test.go b/http/cape_test.go deleted file mode 100644 index ededa15..0000000 --- a/http/cape_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package http - -import ( - "bytes" - "image" - "image/png" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "github.com/elyby/chrly/db" - "github.com/elyby/chrly/model" -) - -type capesTestCase struct { - Name string - RequestUrl string - ExpectedLogKey string - ExistsInLocalStorage bool - ExistsInMojang bool - HasCapeInMojangResp bool - AssertResponse func(assert *testify.Assertions, resp *http.Response) -} - -var capesTestCases = []*capesTestCase{ - { - Name: "Obtain cape for known username", - ExistsInLocalStorage: true, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(200, resp.StatusCode) - responseData, _ := ioutil.ReadAll(resp.Body) - assert.Equal(createCape(), responseData) - assert.Equal("image/png", resp.Header.Get("Content-Type")) - }, - }, - { - Name: "Obtain cape for unknown username that exists in Mojang and has a cape", - ExistsInLocalStorage: false, - ExistsInMojang: true, - HasCapeInMojangResp: true, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(301, resp.StatusCode) - assert.Equal("http://mojang/cape.png", resp.Header.Get("Location")) - }, - }, - { - Name: "Obtain cape for unknown username that exists in Mojang, but don't has a cape", - ExistsInLocalStorage: false, - ExistsInMojang: true, - HasCapeInMojangResp: false, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(404, resp.StatusCode) - }, - }, - { - Name: "Obtain cape for unknown username that doesn't exists in Mojang", - ExistsInLocalStorage: false, - ExistsInMojang: false, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(404, resp.StatusCode) - }, - }, -} - -func TestConfig_Cape(t *testing.T) { - performTest := func(t *testing.T, testCase *capesTestCase) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter(testCase.ExpectedLogKey, int64(1)) - if testCase.ExistsInLocalStorage { - mocks.Capes.EXPECT().FindByUsername("mock_username").Return(&model.Cape{ - File: bytes.NewReader(createCape()), - }, nil) - } else { - mocks.Capes.EXPECT().FindByUsername("mock_username").Return(nil, &db.CapeNotFoundError{Who: "mock_username"}) - } - - if testCase.ExistsInMojang { - textures := createTexturesResponse(false, testCase.HasCapeInMojangResp) - mocks.MojangProvider.On("GetForUsername", "mock_username").Return(textures, nil) - } else { - mocks.MojangProvider.On("GetForUsername", "mock_username").Return(nil, nil) - } - - req := httptest.NewRequest("GET", testCase.RequestUrl, nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - testCase.AssertResponse(assert, resp) - } - - t.Run("Normal API", func(t *testing.T) { - for _, testCase := range capesTestCases { - testCase.RequestUrl = "http://chrly/cloaks/mock_username" - testCase.ExpectedLogKey = "capes.request" - t.Run(testCase.Name, func(t *testing.T) { - performTest(t, testCase) - }) - } - }) - - t.Run("GET fallback API", func(t *testing.T) { - for _, testCase := range capesTestCases { - testCase.RequestUrl = "http://chrly/cloaks?name=mock_username" - testCase.ExpectedLogKey = "capes.get_request" - t.Run(testCase.Name, func(t *testing.T) { - performTest(t, testCase) - }) - } - - t.Run("Should trim trailing slash", func(t *testing.T) { - assert := testify.New(t) - - req := httptest.NewRequest("GET", "http://chrly/cloaks/?name=notch", nil) - w := httptest.NewRecorder() - - (&Config{}).CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://chrly/cloaks?name=notch", resp.Header.Get("Location")) - }) - - t.Run("Return error when name is not provided", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1)) - - req := httptest.NewRequest("GET", "http://chrly/cloaks", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(400, resp.StatusCode) - }) - }) -} - -// Cape md5: 424ff79dce9940af89c28ad80de8aaad -func createCape() []byte { - img := image.NewAlpha(image.Rect(0, 0, 64, 32)) - writer := &bytes.Buffer{} - _ = png.Encode(writer, img) - pngBytes, _ := ioutil.ReadAll(writer) - - return pngBytes -} diff --git a/http/http.go b/http/http.go index bc1edc4..8286b28 100644 --- a/http/http.go +++ b/http/http.go @@ -1,83 +1,22 @@ package http import ( - "fmt" - "net" + "encoding/json" "net/http" "os" "os/signal" - "strings" "syscall" - "time" - - "github.com/gorilla/mux" - "github.com/mono83/slf/wd" - - "github.com/elyby/chrly/interfaces" ) -type Config struct { - ListenSpec string +func NotFound(response http.ResponseWriter, _ *http.Request) { + data, _ := json.Marshal(map[string]string{ + "status": "404", + "message": "Not Found", + }) - SkinsRepo interfaces.SkinsRepository - CapesRepo interfaces.CapesRepository - MojangTexturesProvider interfaces.MojangTexturesProvider - Logger wd.Watchdog - Auth interfaces.AuthChecker -} - -func (cfg *Config) Run() error { - cfg.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", cfg.ListenSpec)) - - listener, err := net.Listen("tcp", cfg.ListenSpec) - if err != nil { - return err - } - - server := &http.Server{ - ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, - MaxHeaderBytes: 1 << 16, - Handler: cfg.CreateHandler(), - } - - go server.Serve(listener) - - s := waitForSignal() - cfg.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s)) - - return nil -} - -func (cfg *Config) CreateHandler() http.Handler { - router := mux.NewRouter().StrictSlash(true) - - router.HandleFunc("/skins/{username}", cfg.Skin).Methods("GET") - router.HandleFunc("/cloaks/{username}", cfg.Cape).Methods("GET").Name("cloaks") - router.HandleFunc("/textures/{username}", cfg.Textures).Methods("GET") - router.HandleFunc("/textures/signed/{username}", cfg.SignedTextures).Methods("GET") - // Legacy - router.HandleFunc("/skins", cfg.SkinGET).Methods("GET") - router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET") - // API - apiRouter := router.PathPrefix("/api").Subrouter() - apiRouter.Use(cfg.AuthenticationMiddleware) - apiRouter.Handle("/skins", http.HandlerFunc(cfg.PostSkin)).Methods("POST") - apiRouter.Handle("/skins/id:{id:[0-9]+}", http.HandlerFunc(cfg.DeleteSkinByUserId)).Methods("DELETE") - apiRouter.Handle("/skins/{username}", http.HandlerFunc(cfg.DeleteSkinByUsername)).Methods("DELETE") - // 404 - router.NotFoundHandler = http.HandlerFunc(cfg.NotFound) - - return router -} - -func parseUsername(username string) string { - const suffix = ".png" - if strings.HasSuffix(username, suffix) { - username = strings.TrimSuffix(username, suffix) - } - - return username + response.Header().Set("Content-Type", "application/json") + response.WriteHeader(http.StatusNotFound) + _, _ = response.Write(data) } func waitForSignal() os.Signal { @@ -86,3 +25,34 @@ func waitForSignal() os.Signal { return <-ch } + +func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string) { + resp.WriteHeader(http.StatusBadRequest) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal(map[string]interface{}{ + "errors": errorsPerField, + }) + _, _ = resp.Write(result) +} + +func apiForbidden(resp http.ResponseWriter, reason string) { + resp.WriteHeader(http.StatusForbidden) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal(map[string]interface{}{ + "error": reason, + }) + _, _ = resp.Write(result) +} + +func apiNotFound(resp http.ResponseWriter, reason string) { + resp.WriteHeader(http.StatusNotFound) + resp.Header().Set("Content-Type", "application/json") + result, _ := json.Marshal([]interface{}{ + reason, + }) + _, _ = resp.Write(result) +} + +func apiServerError(resp http.ResponseWriter) { + resp.WriteHeader(http.StatusInternalServerError) +} diff --git a/http/http_test.go b/http/http_test.go index ec74497..2b6d93d 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -1,101 +1,27 @@ package http import ( + "io/ioutil" + "net/http/httptest" "testing" - "time" - "github.com/stretchr/testify/mock" - - "github.com/elyby/chrly/api/mojang" - - "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" - - "github.com/elyby/chrly/interfaces/mock_interfaces" - "github.com/elyby/chrly/interfaces/mock_wd" ) -func TestParseUsername(t *testing.T) { +func TestConfig_NotFound(t *testing.T) { assert := testify.New(t) - assert.Equal("test", parseUsername("test.png"), "Function should trim .png at end") - assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end") -} - -type mojangTexturesProviderMock struct { - mock.Mock -} - -func (m *mojangTexturesProviderMock) GetForUsername(username string) (*mojang.SignedTexturesResponse, error) { - args := m.Called(username) - var result *mojang.SignedTexturesResponse - if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { - result = casted - } - - return result, args.Error(1) -} - -type mocks struct { - Skins *mock_interfaces.MockSkinsRepository - Capes *mock_interfaces.MockCapesRepository - MojangProvider *mojangTexturesProviderMock - Auth *mock_interfaces.MockAuthChecker - Log *mock_wd.MockWatchdog -} - -func setupMocks(ctrl *gomock.Controller) (*Config, *mocks) { - skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl) - capesRepo := mock_interfaces.NewMockCapesRepository(ctrl) - authChecker := mock_interfaces.NewMockAuthChecker(ctrl) - wd := mock_wd.NewMockWatchdog(ctrl) - texturesProvider := &mojangTexturesProviderMock{} - - return &Config{ - SkinsRepo: skinsRepo, - CapesRepo: capesRepo, - Auth: authChecker, - MojangTexturesProvider: texturesProvider, - Logger: wd, - }, &mocks{ - Skins: skinsRepo, - Capes: capesRepo, - Auth: authChecker, - MojangProvider: texturesProvider, - Log: wd, - } -} - -func createTexturesResponse(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse { - timeZone, _ := time.LoadLocation("Europe/Minsk") - textures := &mojang.TexturesProp{ - Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(), - ProfileID: "00000000000000000000000000000000", - ProfileName: "mock_user", - Textures: &mojang.TexturesResponse{}, - } - - if includeSkin { - textures.Textures.Skin = &mojang.SkinTexturesResponse{ - Url: "http://mojang/skin.png", - } - } - - if includeCape { - textures.Textures.Cape = &mojang.CapeTexturesResponse{ - Url: "http://mojang/cape.png", - } - } - - response := &mojang.SignedTexturesResponse{ - Id: "00000000000000000000000000000000", - Name: "mock_user", - Props: []*mojang.Property{ - { - Name: "textures", - Value: mojang.EncodeTextures(textures), - }, - }, - } - - return response + + req := httptest.NewRequest("GET", "http://example.com", nil) + w := httptest.NewRecorder() + + NotFound(w, req) + + resp := w.Result() + assert.Equal(404, resp.StatusCode) + assert.Equal("application/json", resp.Header.Get("Content-Type")) + response, _ := ioutil.ReadAll(resp.Body) + assert.JSONEq(`{ + "status": "404", + "message": "Not Found" + }`, string(response)) } diff --git a/http/not_found.go b/http/not_found.go deleted file mode 100644 index 33e4705..0000000 --- a/http/not_found.go +++ /dev/null @@ -1,17 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" -) - -func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) { - data, _ := json.Marshal(map[string]string{ - "status": "404", - "message": "Not Found", - }) - - response.Header().Set("Content-Type", "application/json") - response.WriteHeader(http.StatusNotFound) - response.Write(data) -} diff --git a/http/not_found_test.go b/http/not_found_test.go deleted file mode 100644 index dfab394..0000000 --- a/http/not_found_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package http - -import ( - "io/ioutil" - "net/http/httptest" - "testing" - - testify "github.com/stretchr/testify/assert" -) - -func TestConfig_NotFound(t *testing.T) { - assert := testify.New(t) - - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/", nil) - w := httptest.NewRecorder() - - (&Config{}).CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(404, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "status": "404", - "message": "Not Found" - }`, string(response)) -} diff --git a/http/signed_textures.go b/http/signed_textures.go deleted file mode 100644 index a3b1a20..0000000 --- a/http/signed_textures.go +++ /dev/null @@ -1,52 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - "strings" - - "github.com/gorilla/mux" - - "github.com/elyby/chrly/api/mojang" -) - -func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Request) { - cfg.Logger.IncCounter("signed_textures.request", 1) - username := parseUsername(mux.Vars(request)["username"]) - - var responseData *mojang.SignedTexturesResponse - - rec, err := cfg.SkinsRepo.FindByUsername(username) - if err == nil && rec.SkinId != 0 && rec.MojangTextures != "" { - responseData = &mojang.SignedTexturesResponse{ - Id: strings.Replace(rec.Uuid, "-", "", -1), - Name: rec.Username, - Props: []*mojang.Property{ - { - Name: "textures", - Signature: rec.MojangSignature, - Value: rec.MojangTextures, - }, - }, - } - } else if request.URL.Query().Get("proxy") != "" { - mojangTextures, err := cfg.MojangTexturesProvider.GetForUsername(username) - if err == nil && mojangTextures != nil { - responseData = mojangTextures - } - } - - if responseData == nil { - response.WriteHeader(http.StatusNoContent) - return - } - - responseData.Props = append(responseData.Props, &mojang.Property{ - Name: "chrly", - Value: "how do you tame a horse in Minecraft?", - }) - - responseJson, _ := json.Marshal(responseData) - response.Header().Set("Content-Type", "application/json") - response.Write(responseJson) -} diff --git a/http/signed_textures_test.go b/http/signed_textures_test.go deleted file mode 100644 index c07a969..0000000 --- a/http/signed_textures_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package http - -import ( - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "github.com/elyby/chrly/db" -) - -func TestConfig_SignedTextures(t *testing.T) { - t.Run("Obtain signed textures for exists user", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/signed/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "id": "0f657aa8bfbe415db7005750090d3af3", - "name": "mock_user", - "properties": [ - { - "name": "textures", - "signature": "mocked signature", - "value": "mocked textures base64" - }, - { - "name": "chrly", - "value": "how do you tame a horse in Minecraft?" - } - ] - }`, string(response)) - }) - - t.Run("Obtain signed textures for not exists user", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{}) - - req := httptest.NewRequest("GET", "http://chrly/textures/signed/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(204, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Equal("", string(response)) - }) - - t.Run("Obtain signed textures for exists user, but without signed textures", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - skinModel := createSkinModel("mock_user", false) - skinModel.MojangTextures = "" - skinModel.MojangSignature = "" - - mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(skinModel, nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/signed/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(204, resp.StatusCode) - response, _ := ioutil.ReadAll(resp.Body) - assert.Equal("", string(response)) - }) - - t.Run("Obtain signed textures for exists user, but without signed textures", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - skinModel := createSkinModel("mock_user", false) - skinModel.MojangTextures = "" - skinModel.MojangSignature = "" - - mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(skinModel, nil) - mocks.MojangProvider.On("GetForUsername", "mock_user").Once().Return(createTexturesResponse(true, false), nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/signed/mock_user?proxy=true", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "id": "00000000000000000000000000000000", - "name": "mock_user", - "properties": [ - { - "name": "textures", - "value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXIiLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9tb2phbmcvc2tpbi5wbmcifX19" - }, - { - "name": "chrly", - "value": "how do you tame a horse in Minecraft?" - } - ] - }`, string(response)) - }) -} diff --git a/http/skin.go b/http/skin.go deleted file mode 100644 index a7f9397..0000000 --- a/http/skin.go +++ /dev/null @@ -1,49 +0,0 @@ -package http - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -func (cfg *Config) Skin(response http.ResponseWriter, request *http.Request) { - if mux.Vars(request)["converted"] == "" { - cfg.Logger.IncCounter("skins.request", 1) - } - - username := parseUsername(mux.Vars(request)["username"]) - rec, err := cfg.SkinsRepo.FindByUsername(username) - if err == nil && rec.SkinId != 0 { - http.Redirect(response, request, rec.Url, 301) - return - } - - mojangTextures, err := cfg.MojangTexturesProvider.GetForUsername(username) - if err != nil || mojangTextures == nil { - response.WriteHeader(http.StatusNotFound) - return - } - - texturesProp := mojangTextures.DecodeTextures() - skin := texturesProp.Textures.Skin - if skin == nil { - response.WriteHeader(http.StatusNotFound) - return - } - - http.Redirect(response, request, skin.Url, 301) -} - -func (cfg *Config) SkinGET(response http.ResponseWriter, request *http.Request) { - cfg.Logger.IncCounter("skins.get_request", 1) - username := request.URL.Query().Get("name") - if username == "" { - response.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(request)["username"] = username - mux.Vars(request)["converted"] = "1" - - cfg.Skin(response, request) -} diff --git a/http/skin_test.go b/http/skin_test.go deleted file mode 100644 index 23475ab..0000000 --- a/http/skin_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package http - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "github.com/elyby/chrly/db" - "github.com/elyby/chrly/model" -) - -type skinsTestCase struct { - Name string - RequestUrl string - ExpectedLogKey string - ExistsInLocalStorage bool - ExistsInMojang bool - HasSkinInMojangResp bool - AssertResponse func(assert *testify.Assertions, resp *http.Response) -} - -var skinsTestCases = []*skinsTestCase{ - { - Name: "Obtain skin for known username", - ExistsInLocalStorage: true, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(301, resp.StatusCode) - assert.Equal("http://chrly/skin.png", resp.Header.Get("Location")) - }, - }, - { - Name: "Obtain skin for unknown username that exists in Mojang and has a cape", - ExistsInLocalStorage: false, - ExistsInMojang: true, - HasSkinInMojangResp: true, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(301, resp.StatusCode) - assert.Equal("http://mojang/skin.png", resp.Header.Get("Location")) - }, - }, - { - Name: "Obtain skin for unknown username that exists in Mojang, but don't has a cape", - ExistsInLocalStorage: false, - ExistsInMojang: true, - HasSkinInMojangResp: false, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(404, resp.StatusCode) - }, - }, - { - Name: "Obtain skin for unknown username that doesn't exists in Mojang", - ExistsInLocalStorage: false, - ExistsInMojang: false, - AssertResponse: func(assert *testify.Assertions, resp *http.Response) { - assert.Equal(404, resp.StatusCode) - }, - }, -} - -func TestConfig_Skin(t *testing.T) { - performTest := func(t *testing.T, testCase *skinsTestCase) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter(testCase.ExpectedLogKey, int64(1)) - if testCase.ExistsInLocalStorage { - mocks.Skins.EXPECT().FindByUsername("mock_username").Return(createSkinModel("mock_username", false), nil) - } else { - mocks.Skins.EXPECT().FindByUsername("mock_username").Return(nil, &db.SkinNotFoundError{Who: "mock_username"}) - } - - if testCase.ExistsInMojang { - textures := createTexturesResponse(testCase.HasSkinInMojangResp, true) - mocks.MojangProvider.On("GetForUsername", "mock_username").Return(textures, nil) - } else { - mocks.MojangProvider.On("GetForUsername", "mock_username").Return(nil, nil) - } - - req := httptest.NewRequest("GET", testCase.RequestUrl, nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - testCase.AssertResponse(assert, resp) - } - - t.Run("Normal API", func(t *testing.T) { - for _, testCase := range skinsTestCases { - testCase.RequestUrl = "http://chrly/skins/mock_username" - testCase.ExpectedLogKey = "skins.request" - t.Run(testCase.Name, func(t *testing.T) { - performTest(t, testCase) - }) - } - }) - - t.Run("GET fallback API", func(t *testing.T) { - for _, testCase := range skinsTestCases { - testCase.RequestUrl = "http://chrly/skins?name=mock_username" - testCase.ExpectedLogKey = "skins.get_request" - t.Run(testCase.Name, func(t *testing.T) { - performTest(t, testCase) - }) - } - - t.Run("Should trim trailing slash", func(t *testing.T) { - assert := testify.New(t) - - req := httptest.NewRequest("GET", "http://chrly/skins/?name=notch", nil) - w := httptest.NewRecorder() - - (&Config{}).CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://chrly/skins?name=notch", resp.Header.Get("Location")) - }) - - t.Run("Return error when name is not provided", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) - - req := httptest.NewRequest("GET", "http://chrly/skins", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(400, resp.StatusCode) - }) - }) -} - -func createSkinModel(username string, isSlim bool) *model.Skin { - return &model.Skin{ - UserId: 1, - Username: username, - Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3", // Use non nil UUID to pass validation in api tests - SkinId: 1, - Url: "http://chrly/skin.png", - MojangTextures: "mocked textures base64", - MojangSignature: "mocked signature", - IsSlim: isSlim, - } -} diff --git a/http/skinsystem.go b/http/skinsystem.go new file mode 100644 index 0000000..c7fc2a9 --- /dev/null +++ b/http/skinsystem.go @@ -0,0 +1,503 @@ +package http + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/mono83/slf/wd" + "github.com/thedevsaddam/govalidator" + + "github.com/elyby/chrly/api/mojang" + "github.com/elyby/chrly/auth" + "github.com/elyby/chrly/model" +) + +//noinspection GoSnakeCaseUsage +const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + +var regexUuidAny = regexp.MustCompile(UUID_ANY) + +func init() { + govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error { + if message == "" { + message = "Skin uploading is temporary unavailable" + } + + return errors.New(message) + }) + + // Add ability to validate any possible uuid form + govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error { + str := value.(string) + if !regexUuidAny.MatchString(str) { + if message == "" { + message = fmt.Sprintf("The %s field must contain valid UUID", field) + } + + return errors.New(message) + } + + return nil + }) +} + +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 +} + +type CapesRepository interface { + FindByUsername(username string) (*model.Cape, error) +} + +type SkinNotFoundError struct { + Who string +} + +func (e SkinNotFoundError) Error() string { + return "Skin data not found." +} + +type CapeNotFoundError struct { + Who string +} + +func (e CapeNotFoundError) Error() string { + return "Cape file not found." +} + +type MojangTexturesProvider interface { + GetForUsername(username string) (*mojang.SignedTexturesResponse, error) +} + +type AuthChecker interface { + Check(req *http.Request) error +} + +type Skinsystem struct { + ListenSpec string + + SkinsRepo SkinsRepository + CapesRepo CapesRepository + MojangTexturesProvider MojangTexturesProvider + Auth AuthChecker + Logger wd.Watchdog +} + +func (ctx *Skinsystem) Run() error { + ctx.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", ctx.ListenSpec)) + + listener, err := net.Listen("tcp", ctx.ListenSpec) + if err != nil { + return err + } + + server := &http.Server{ + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 16, + Handler: ctx.CreateHandler(), + } + + go server.Serve(listener) + + s := waitForSignal() + ctx.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s)) + + return nil +} + +func (ctx *Skinsystem) CreateHandler() *mux.Router { + router := mux.NewRouter().StrictSlash(true) + + router.HandleFunc("/skins/{username}", ctx.Skin).Methods("GET") + router.HandleFunc("/cloaks/{username}", ctx.Cape).Methods("GET").Name("cloaks") + router.HandleFunc("/textures/{username}", ctx.Textures).Methods("GET") + router.HandleFunc("/textures/signed/{username}", ctx.SignedTextures).Methods("GET") + // Legacy + router.HandleFunc("/skins", ctx.SkinGET).Methods("GET") + router.HandleFunc("/cloaks", ctx.CapeGET).Methods("GET") + // API + apiRouter := router.PathPrefix("/api").Subrouter() + apiRouter.Use(ctx.AuthenticationMiddleware) + apiRouter.Handle("/skins", http.HandlerFunc(ctx.PostSkin)).Methods("POST") + apiRouter.Handle("/skins/id:{id:[0-9]+}", http.HandlerFunc(ctx.DeleteSkinByUserId)).Methods("DELETE") + apiRouter.Handle("/skins/{username}", http.HandlerFunc(ctx.DeleteSkinByUsername)).Methods("DELETE") + // 404 + router.NotFoundHandler = http.HandlerFunc(NotFound) + + return router +} + +func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request) { + if mux.Vars(request)["converted"] == "" { + ctx.Logger.IncCounter("skins.request", 1) + } + + username := parseUsername(mux.Vars(request)["username"]) + rec, err := ctx.SkinsRepo.FindByUsername(username) + if err == nil && rec.SkinId != 0 { + http.Redirect(response, request, rec.Url, 301) + return + } + + mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username) + if err != nil || mojangTextures == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + texturesProp := mojangTextures.DecodeTextures() + skin := texturesProp.Textures.Skin + if skin == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + http.Redirect(response, request, skin.Url, 301) +} + +func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Request) { + username := request.URL.Query().Get("name") + if username == "" { + response.WriteHeader(http.StatusBadRequest) + return + } + + ctx.Logger.IncCounter("skins.get_request", 1) + mux.Vars(request)["username"] = username + mux.Vars(request)["converted"] = "1" + + ctx.Skin(response, request) +} + +func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request) { + if mux.Vars(request)["converted"] == "" { + ctx.Logger.IncCounter("capes.request", 1) + } + + username := parseUsername(mux.Vars(request)["username"]) + rec, err := ctx.CapesRepo.FindByUsername(username) + if err == nil { + request.Header.Set("Content-Type", "image/png") + _, _ = io.Copy(response, rec.File) + return + } + + mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username) + if err != nil || mojangTextures == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + texturesProp := mojangTextures.DecodeTextures() + cape := texturesProp.Textures.Cape + if cape == nil { + response.WriteHeader(http.StatusNotFound) + return + } + + http.Redirect(response, request, cape.Url, 301) +} + +func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Request) { + username := request.URL.Query().Get("name") + if username == "" { + response.WriteHeader(http.StatusBadRequest) + return + } + + ctx.Logger.IncCounter("capes.get_request", 1) + mux.Vars(request)["username"] = username + mux.Vars(request)["converted"] = "1" + + ctx.Cape(response, request) +} + +func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Request) { + ctx.Logger.IncCounter("textures.request", 1) + username := parseUsername(mux.Vars(request)["username"]) + + var textures *mojang.TexturesResponse + skin, skinErr := ctx.SkinsRepo.FindByUsername(username) + _, capeErr := ctx.CapesRepo.FindByUsername(username) + if (skinErr == nil && skin.SkinId != 0) || capeErr == nil { + textures = &mojang.TexturesResponse{} + + if skinErr == nil && skin.SkinId != 0 { + skinTextures := &mojang.SkinTexturesResponse{ + Url: skin.Url, + } + + if skin.IsSlim { + skinTextures.Metadata = &mojang.SkinTexturesMetadata{ + Model: "slim", + } + } + + textures.Skin = skinTextures + } + + if capeErr == nil { + textures.Cape = &mojang.CapeTexturesResponse{ + Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username, + } + } + } else { + mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username) + if err != nil || mojangTextures == nil { + response.WriteHeader(http.StatusNoContent) + return + } + + texturesProp := mojangTextures.DecodeTextures() + if texturesProp == nil { + response.WriteHeader(http.StatusInternalServerError) + ctx.Logger.Error("Unable to find textures property") + return + } + + textures = texturesProp.Textures + } + + responseData, _ := json.Marshal(textures) + response.Header().Set("Content-Type", "application/json") + response.Write(responseData) +} + +func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *http.Request) { + ctx.Logger.IncCounter("signed_textures.request", 1) + username := parseUsername(mux.Vars(request)["username"]) + + var responseData *mojang.SignedTexturesResponse + + rec, err := ctx.SkinsRepo.FindByUsername(username) + if err == nil && rec.SkinId != 0 && rec.MojangTextures != "" { + responseData = &mojang.SignedTexturesResponse{ + Id: strings.Replace(rec.Uuid, "-", "", -1), + Name: rec.Username, + Props: []*mojang.Property{ + { + Name: "textures", + Signature: rec.MojangSignature, + Value: rec.MojangTextures, + }, + }, + } + } else if request.URL.Query().Get("proxy") != "" { + mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username) + if err == nil && mojangTextures != nil { + responseData = mojangTextures + } + } + + if responseData == nil { + response.WriteHeader(http.StatusNoContent) + return + } + + responseData.Props = append(responseData.Props, &mojang.Property{ + Name: "chrly", + Value: "how do you tame a horse in Minecraft?", + }) + + responseJson, _ := json.Marshal(responseData) + response.Header().Set("Content-Type", "application/json") + response.Write(responseJson) +} + +func (ctx *Skinsystem) PostSkin(resp http.ResponseWriter, req *http.Request) { + ctx.Logger.IncCounter("api.skins.post.request", 1) + validationErrors := validatePostSkinRequest(req) + if validationErrors != nil { + ctx.Logger.IncCounter("api.skins.post.validation_failed", 1) + apiBadRequest(resp, validationErrors) + return + } + + identityId, _ := strconv.Atoi(req.Form.Get("identityId")) + username := req.Form.Get("username") + + record, err := findIdentity(ctx.SkinsRepo, identityId, username) + if err != nil { + ctx.Logger.Error("Error on requesting a skin from the repository: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + skinId, _ := strconv.Atoi(req.Form.Get("skinId")) + is18, _ := strconv.ParseBool(req.Form.Get("is1_8")) + isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim")) + + record.Uuid = req.Form.Get("uuid") + record.SkinId = skinId + record.Is1_8 = is18 + record.IsSlim = isSlim + record.Url = req.Form.Get("url") + record.MojangTextures = req.Form.Get("mojangTextures") + record.MojangSignature = req.Form.Get("mojangSignature") + + err = ctx.SkinsRepo.Save(record) + if err != nil { + ctx.Logger.Error("Unable to save record to the repository: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + ctx.Logger.IncCounter("api.skins.post.success", 1) + resp.WriteHeader(http.StatusCreated) +} + +func (ctx *Skinsystem) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) { + ctx.Logger.IncCounter("api.skins.delete.request", 1) + id, _ := strconv.Atoi(mux.Vars(req)["id"]) + skin, err := ctx.SkinsRepo.FindByUserId(id) + if err != nil { + ctx.Logger.IncCounter("api.skins.delete.not_found", 1) + apiNotFound(resp, "Cannot find record for requested user id") + return + } + + ctx.deleteSkin(skin, resp) +} + +func (ctx *Skinsystem) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) { + ctx.Logger.IncCounter("api.skins.delete.request", 1) + username := mux.Vars(req)["username"] + skin, err := ctx.SkinsRepo.FindByUsername(username) + if err != nil { + ctx.Logger.IncCounter("api.skins.delete.not_found", 1) + apiNotFound(resp, "Cannot find record for requested username") + return + } + + ctx.deleteSkin(skin, resp) +} + +func (ctx *Skinsystem) AuthenticationMiddleware(handler http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + ctx.Logger.IncCounter("authentication.challenge", 1) + err := ctx.Auth.Check(req) + if err != nil { + if _, ok := err.(*auth.Unauthorized); ok { + ctx.Logger.IncCounter("authentication.failed", 1) + apiForbidden(resp, err.Error()) + } else { + ctx.Logger.Error("Unknown error on validating api request: :err", wd.ErrParam(err)) + apiServerError(resp) + } + + return + } + + ctx.Logger.IncCounter("authentication.success", 1) + handler.ServeHTTP(resp, req) + }) +} + +func (ctx *Skinsystem) deleteSkin(skin *model.Skin, resp http.ResponseWriter) { + err := ctx.SkinsRepo.RemoveByUserId(skin.UserId) + if err != nil { + ctx.Logger.Error("Cannot delete skin by error: :err", wd.ErrParam(err)) + apiServerError(resp) + return + } + + ctx.Logger.IncCounter("api.skins.delete.success", 1) + resp.WriteHeader(http.StatusNoContent) +} + +func validatePostSkinRequest(request *http.Request) map[string][]string { + const maxMultipartMemory int64 = 32 << 20 + const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both" + + _ = request.ParseMultipartForm(maxMultipartMemory) + + validationRules := govalidator.MapData{ + "identityId": {"required", "numeric", "min:1"}, + "username": {"required"}, + "uuid": {"required", "uuid_any"}, + "skinId": {"required", "numeric", "min:1"}, + "url": {"url"}, + "file:skin": {"ext:png", "size:24576", "mime:image/png"}, + "is1_8": {"bool"}, + "isSlim": {"bool"}, + } + + shouldAppendSkinRequiredError := false + url := request.Form.Get("url") + _, _, skinErr := request.FormFile("skin") + if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) { + shouldAppendSkinRequiredError = true + } else if skinErr == nil { + validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable") + } else if url != "" { + validationRules["is1_8"] = append(validationRules["is1_8"], "required") + validationRules["isSlim"] = append(validationRules["isSlim"], "required") + } + + mojangTextures := request.Form.Get("mojangTextures") + if mojangTextures != "" { + validationRules["mojangSignature"] = []string{"required"} + } + + validator := govalidator.New(govalidator.Options{ + Request: request, + Rules: validationRules, + RequiredDefault: false, + FormSize: maxMultipartMemory, + }) + validationResults := validator.Validate() + if shouldAppendSkinRequiredError { + validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage) + validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage) + } + + if len(validationResults) != 0 { + return validationResults + } + + return nil +} + +func findIdentity(repo SkinsRepository, identityId int, username string) (*model.Skin, error) { + var record *model.Skin + record, err := repo.FindByUserId(identityId) + if err != nil { + if _, isSkinNotFound := err.(*SkinNotFoundError); !isSkinNotFound { + return nil, err + } + + record, err = repo.FindByUsername(username) + if err == nil { + _ = repo.RemoveByUsername(username) + record.UserId = identityId + } else { + record = &model.Skin{ + UserId: identityId, + Username: username, + } + } + } else if record.Username != username { + _ = repo.RemoveByUserId(identityId) + record.Username = username + } + + return record, nil +} + +func parseUsername(username string) string { + return strings.TrimSuffix(username, ".png") +} diff --git a/http/skinsystem_test.go b/http/skinsystem_test.go new file mode 100644 index 0000000..463eeb2 --- /dev/null +++ b/http/skinsystem_test.go @@ -0,0 +1,1092 @@ +package http + +import ( + "bytes" + "encoding/base64" + "github.com/elyby/chrly/auth" + testify "github.com/stretchr/testify/assert" + "image" + "image/png" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/elyby/chrly/api/mojang" + "github.com/elyby/chrly/model" + "github.com/elyby/chrly/tests" +) + +/*************** + * Setup mocks * + ***************/ + +type skinsRepositoryMock struct { + mock.Mock +} + +func (m *skinsRepositoryMock) FindByUsername(username string) (*model.Skin, error) { + args := m.Called(username) + var result *model.Skin + if casted, ok := args.Get(0).(*model.Skin); ok { + result = casted + } + + return result, args.Error(1) +} + +func (m *skinsRepositoryMock) FindByUserId(id int) (*model.Skin, error) { + args := m.Called(id) + var result *model.Skin + if casted, ok := args.Get(0).(*model.Skin); ok { + result = casted + } + + return result, args.Error(1) +} + +func (m *skinsRepositoryMock) Save(skin *model.Skin) error { + args := m.Called(skin) + return args.Error(0) +} + +func (m *skinsRepositoryMock) RemoveByUserId(id int) error { + args := m.Called(id) + return args.Error(0) +} + +func (m *skinsRepositoryMock) RemoveByUsername(username string) error { + args := m.Called(username) + return args.Error(0) +} + +type capesRepositoryMock struct { + mock.Mock +} + +func (m *capesRepositoryMock) FindByUsername(username string) (*model.Cape, error) { + args := m.Called(username) + var result *model.Cape + if casted, ok := args.Get(0).(*model.Cape); ok { + result = casted + } + + return result, args.Error(1) +} + +type mojangTexturesProviderMock struct { + mock.Mock +} + +func (m *mojangTexturesProviderMock) GetForUsername(username string) (*mojang.SignedTexturesResponse, error) { + args := m.Called(username) + var result *mojang.SignedTexturesResponse + if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { + result = casted + } + + return result, args.Error(1) +} + +type authCheckerMock struct { + mock.Mock +} + +func (m *authCheckerMock) Check(req *http.Request) error { + args := m.Called(req) + return args.Error(0) +} + +type skinsystemTestSuite struct { + suite.Suite + + App *Skinsystem + + SkinsRepository *skinsRepositoryMock + CapesRepository *capesRepositoryMock + MojangTexturesProvider *mojangTexturesProviderMock + Auth *authCheckerMock + Logger *tests.WdMock +} + +/******************** + * Setup test suite * + ********************/ + +func (suite *skinsystemTestSuite) SetupTest() { + suite.SkinsRepository = &skinsRepositoryMock{} + suite.CapesRepository = &capesRepositoryMock{} + suite.MojangTexturesProvider = &mojangTexturesProviderMock{} + suite.Auth = &authCheckerMock{} + suite.Logger = &tests.WdMock{} + + suite.App = &Skinsystem{ + SkinsRepo: suite.SkinsRepository, + CapesRepo: suite.CapesRepository, + MojangTexturesProvider: suite.MojangTexturesProvider, + Auth: suite.Auth, + Logger: suite.Logger, + } +} + +func (suite *skinsystemTestSuite) TearDownTest() { + suite.SkinsRepository.AssertExpectations(suite.T()) + suite.CapesRepository.AssertExpectations(suite.T()) + suite.MojangTexturesProvider.AssertExpectations(suite.T()) + suite.Auth.AssertExpectations(suite.T()) + suite.Logger.AssertExpectations(suite.T()) +} + +func (suite *skinsystemTestSuite) RunSubTest(name string, subTest func()) { + suite.SetupTest() + suite.Run(name, subTest) + suite.TearDownTest() +} + +/************* + * Run tests * + *************/ + +func TestSkinsystem(t *testing.T) { + suite.Run(t, new(skinsystemTestSuite)) +} + +type skinsystemTestCase struct { + Name string + BeforeTest func(suite *skinsystemTestSuite) + AfterTest func(suite *skinsystemTestSuite, response *http.Response) +} + +/************************ + * Get skin tests cases * + ************************/ + +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) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(301, response.StatusCode) + suite.Equal("http://chrly/skin.png", response.Header.Get("Location")) + }, + }, + { + 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, &SkinNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(301, response.StatusCode) + suite.Equal("http://mojang/skin.png", response.Header.Get("Location")) + }, + }, + { + 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, &SkinNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(404, response.StatusCode) + }, + }, + { + 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, &SkinNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(404, response.StatusCode) + }, + }, +} + +func (suite *skinsystemTestSuite) TestSkin() { + for _, testCase := range skinsTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "skins.request", int64(1)).Once() + testCase.BeforeTest(suite) + + req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } + + suite.RunSubTest("Pass username with png extension", func() { + suite.Logger.On("IncCounter", "skins.request", int64(1)).Once() + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + + req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + suite.Equal(301, resp.StatusCode) + suite.Equal("http://chrly/skin.png", resp.Header.Get("Location")) + }) +} + +func (suite *skinsystemTestSuite) TestSkinGET() { + for _, testCase := range skinsTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "skins.get_request", int64(1)).Once() + testCase.BeforeTest(suite) + + req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } + + suite.RunSubTest("Do not pass name param", func() { + req := httptest.NewRequest("GET", "http://chrly/skins", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + suite.Equal(400, resp.StatusCode) + }) +} + +/************************ + * Get cape tests cases * + ************************/ + +var capesTestsCases = []*skinsystemTestCase{ + { + Name: "Username exists in the local storage", + BeforeTest: func(suite *skinsystemTestSuite) { + suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + responseData, _ := ioutil.ReadAll(response.Body) + suite.Equal(createCape(), responseData) + suite.Equal("image/png", response.Header.Get("Content-Type")) + }, + }, + { + 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, &CapeNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, true), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(301, response.StatusCode) + suite.Equal("http://mojang/cape.png", response.Header.Get("Location")) + }, + }, + { + 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, &CapeNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(false, false), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(404, response.StatusCode) + }, + }, + { + 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, &CapeNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(404, response.StatusCode) + }, + }, +} + +func (suite *skinsystemTestSuite) TestCape() { + for _, testCase := range capesTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "capes.request", int64(1)).Once() + testCase.BeforeTest(suite) + + req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } + + suite.RunSubTest("Pass username with png extension", func() { + suite.Logger.On("IncCounter", "capes.request", int64(1)).Once() + suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + + req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + suite.Equal(200, resp.StatusCode) + responseData, _ := ioutil.ReadAll(resp.Body) + suite.Equal(createCape(), responseData) + suite.Equal("image/png", resp.Header.Get("Content-Type")) + }) +} + +func (suite *skinsystemTestSuite) TestCapeGET() { + for _, testCase := range capesTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "capes.get_request", int64(1)).Once() + testCase.BeforeTest(suite) + + req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } + + suite.RunSubTest("Do not pass name param", func() { + req := httptest.NewRequest("GET", "http://chrly/cloaks", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + suite.Equal(400, resp.StatusCode) + }) +} + +/**************************** + * Get textures tests cases * + ****************************/ + +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, &CapeNotFoundError{Who: "mock_username"}) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "SKIN": { + "url": "http://chrly/skin.png" + } + }`, string(body)) + }, + }, + { + 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, &CapeNotFoundError{Who: "mock_username"}) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "SKIN": { + "url": "http://chrly/skin.png", + "metadata": { + "model": "slim" + } + } + }`, string(body)) + }, + }, + { + Name: "Username exists and has cape, no skin", + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"}) + suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "CAPE": { + "url": "http://chrly/cloaks/mock_username" + } + }`, string(body)) + }, + }, + { + 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) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "SKIN": { + "url": "http://chrly/skin.png" + }, + "CAPE": { + "url": "http://chrly/cloaks/mock_username" + } + }`, string(body)) + }, + }, + { + Name: "Username not exists, but Mojang profile available", + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{}) + suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponse(true, true), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "SKIN": { + "url": "http://mojang/skin.png" + }, + "CAPE": { + "url": "http://mojang/cape.png" + } + }`, string(body)) + }, + }, + { + Name: "Username not exists and Mojang profile unavailable", + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{}) + suite.CapesRepository.On("FindByUsername", "mock_username").Return(nil, &CapeNotFoundError{}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(204, response.StatusCode) + }, + }, +} + +func (suite *skinsystemTestSuite) TestTextures() { + for _, testCase := range texturesTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "textures.request", int64(1)).Once() + testCase.BeforeTest(suite) + + req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } +} + +/*********************************** + * Get signed textures tests cases * + ***********************************/ + +type signedTexturesTestCase struct { + Name string + AllowProxy bool + BeforeTest func(suite *skinsystemTestSuite) + AfterTest func(suite *skinsystemTestSuite, response *http.Response) +} + +var signedTexturesTestsCases = []*signedTexturesTestCase{ + { + Name: "Username exists", + AllowProxy: false, + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "id": "0f657aa8bfbe415db7005750090d3af3", + "name": "mock_username", + "properties": [ + { + "name": "textures", + "signature": "mocked signature", + "value": "mocked textures base64" + }, + { + "name": "chrly", + "value": "how do you tame a horse in Minecraft?" + } + ] + }`, string(body)) + }, + }, + { + Name: "Username not exists", + AllowProxy: false, + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"}) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(204, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Equal("", string(body)) + }, + }, + { + Name: "Username exists, but has no signed textures", + AllowProxy: false, + BeforeTest: func(suite *skinsystemTestSuite) { + skinModel := createSkinModel("mock_username", true) + skinModel.MojangTextures = "" + skinModel.MojangSignature = "" + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(skinModel, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(204, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Equal("", string(body)) + }, + }, + { + 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, &SkinNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponse(true, false), nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(200, response.StatusCode) + suite.Equal("application/json", response.Header.Get("Content-Type")) + body, _ := ioutil.ReadAll(response.Body) + suite.JSONEq(`{ + "id": "00000000000000000000000000000000", + "name": "mock_username", + "properties": [ + { + "name": "textures", + "value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ==" + }, + { + "name": "chrly", + "value": "how do you tame a horse in Minecraft?" + } + ] + }`, string(body)) + }, + }, + { + 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, &SkinNotFoundError{Who: "mock_username"}) + suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(204, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Equal("", string(body)) + }, + }, +} + +func (suite *skinsystemTestSuite) TestSignedTextures() { + for _, testCase := range signedTexturesTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "signed_textures.request", int64(1)).Once() + testCase.BeforeTest(suite) + + var target string + if testCase.AllowProxy { + target = "http://chrly/textures/signed/mock_username?proxy=true" + } else { + target = "http://chrly/textures/signed/mock_username" + } + + req := httptest.NewRequest("GET", target, nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } +} + +/************************* + * Post skin tests cases * + *************************/ + +type postSkinTestCase struct { + Name string + Form io.Reader + ExpectSuccess bool + BeforeTest func(suite *skinsystemTestSuite) + AfterTest func(suite *skinsystemTestSuite, response *http.Response) +} + +var postSkinTestsCases = []*postSkinTestCase{ + { + Name: "Upload new identity with textures data", + Form: bytes.NewBufferString(url.Values{ + "identityId": {"1"}, + "username": {"mock_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://example.com/skin.png"}, + }.Encode()), + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"}) + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"}) + suite.SkinsRepository.On("Save", 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) + suite.Equal(5, model.SkinId) + suite.False(model.Is1_8) + suite.False(model.IsSlim) + suite.Equal("http://example.com/skin.png", model.Url) + + return true + })).Times(1).Return(nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(201, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Empty(body) + }, + }, + { + Name: "Update exists identity by changing only textures data", + Form: bytes.NewBufferString(url.Values{ + "identityId": {"1"}, + "username": {"mock_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "is1_8": {"1"}, + "isSlim": {"1"}, + "url": {"http://textures-server.com/skin.png"}, + }.Encode()), + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("Save", 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) + suite.Equal(5, model.SkinId) + suite.True(model.Is1_8) + suite.True(model.IsSlim) + suite.Equal("http://textures-server.com/skin.png", model.Url) + + return true + })).Times(1).Return(nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(201, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Empty(body) + }, + }, + { + Name: "Update exists identity by changing its identityId", + Form: bytes.NewBufferString(url.Values{ + "identityId": {"2"}, + "username": {"mock_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://example.com/skin.png"}, + }.Encode()), + BeforeTest: func(suite *skinsystemTestSuite) { + suite.SkinsRepository.On("FindByUserId", 2).Return(nil, &SkinNotFoundError{Who: "unknown"}) + 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.Equal(2, model.UserId) + suite.Equal("mock_username", model.Username) + suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) + + return true + })).Times(1).Return(nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(201, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Empty(body) + }, + }, + { + Name: "Update exists identity by changing its username", + Form: bytes.NewBufferString(url.Values{ + "identityId": {"1"}, + "username": {"changed_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://example.com/skin.png"}, + }.Encode()), + BeforeTest: func(suite *skinsystemTestSuite) { + 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.Equal(1, model.UserId) + suite.Equal("changed_username", model.Username) + suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid) + + return true + })).Times(1).Return(nil) + }, + AfterTest: func(suite *skinsystemTestSuite, response *http.Response) { + suite.Equal(201, response.StatusCode) + body, _ := ioutil.ReadAll(response.Body) + suite.Empty(body) + }, + }, +} + +func (suite *skinsystemTestSuite) TestPostSkin() { + for _, testCase := range postSkinTestsCases { + suite.RunSubTest(testCase.Name, func() { + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.success", int64(1)).Once() + suite.Auth.On("Check", mock.Anything).Return(nil) + testCase.BeforeTest(suite) + + req := httptest.NewRequest("POST", "http://chrly/api/skins", testCase.Form) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + testCase.AfterTest(suite, w.Result()) + }) + } + + suite.RunSubTest("Get errors about required fields", func() { + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.validation_failed", int64(1)).Once() + suite.Auth.On("Check", mock.Anything).Return(nil) + + req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(url.Values{ + "mojangTextures": {"someBase64EncodedString"}, + }.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(400, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.JSONEq(`{ + "errors": { + "identityId": [ + "The identityId field is required", + "The identityId field must be numeric", + "The identityId field must be minimum 1 char" + ], + "skinId": [ + "The skinId field is required", + "The skinId field must be numeric", + "The skinId field must be minimum 1 char" + ], + "username": [ + "The username field is required" + ], + "uuid": [ + "The uuid field is required", + "The uuid field must contain valid UUID" + ], + "url": [ + "One of url or skin should be provided, but not both" + ], + "skin": [ + "One of url or skin should be provided, but not both" + ], + "mojangSignature": [ + "The mojangSignature field is required" + ] + } + }`, string(body)) + }) + + suite.RunSubTest("Send request without authorization", func() { + req := httptest.NewRequest("POST", "http://chrly/api/skins", nil) + req.Header.Add("Authorization", "Bearer invalid.jwt.token") + w := httptest.NewRecorder() + + suite.Auth.On("Check", mock.Anything).Return(&auth.Unauthorized{Reason: "Cannot parse passed JWT token"}) + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.failed", int64(1)).Once() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(403, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.JSONEq(`{ + "error": "Cannot parse passed JWT token" + }`, string(body)) + }) + + suite.RunSubTest("Upload textures with skin as file", func() { + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.post.validation_failed", int64(1)).Once() + suite.Auth.On("Check", mock.Anything).Return(nil) + + inputBody := &bytes.Buffer{} + writer := multipart.NewWriter(inputBody) + + part, _ := writer.CreateFormFile("skin", "char.png") + _, _ = part.Write(loadSkinFile()) + + _ = writer.WriteField("identityId", "1") + _ = writer.WriteField("username", "mock_user") + _ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3") + _ = writer.WriteField("skinId", "5") + + err := writer.Close() + if err != nil { + panic(err) + } + + req := httptest.NewRequest("POST", "http://chrly/api/skins", inputBody) + req.Header.Add("Content-Type", writer.FormDataContentType()) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(400, resp.StatusCode) + responseBody, _ := ioutil.ReadAll(resp.Body) + suite.JSONEq(`{ + "errors": { + "skin": [ + "Skin uploading is temporary unavailable" + ] + } + }`, string(responseBody)) + }) +} + +/************************************** + * Delete skin by user id tests cases * + **************************************/ + +func (suite *skinsystemTestSuite) TestDeleteByUserId() { + suite.RunSubTest("Delete skin by its identity id", func() { + suite.Auth.On("Check", mock.Anything).Return(nil) + suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.success", int64(1)).Once() + + req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(204, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.Empty(body) + }) + + suite.RunSubTest("Try to remove not exists identity id", func() { + suite.Auth.On("Check", mock.Anything).Return(nil) + suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"}) + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.not_found", int64(1)).Once() + + req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(404, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.JSONEq(`[ + "Cannot find record for requested user id" + ]`, string(body)) + }) +} + +/*************************************** + * Delete skin by username tests cases * + ***************************************/ + +func (suite *skinsystemTestSuite) TestDeleteByUsername() { + suite.RunSubTest("Delete skin by its identity username", func() { + suite.Auth.On("Check", mock.Anything).Return(nil) + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil) + suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil) + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.success", int64(1)).Once() + + req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(204, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.Empty(body) + }) + + suite.RunSubTest("Try to remove not exists identity username", func() { + suite.Auth.On("Check", mock.Anything).Return(nil) + suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"}) + suite.Logger.On("IncCounter", "authentication.challenge", int64(1)).Once() + suite.Logger.On("IncCounter", "authentication.success", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.request", int64(1)).Once() + suite.Logger.On("IncCounter", "api.skins.delete.not_found", int64(1)).Once() + + req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil) + w := httptest.NewRecorder() + + suite.App.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + suite.Equal(404, resp.StatusCode) + body, _ := ioutil.ReadAll(resp.Body) + suite.JSONEq(`[ + "Cannot find record for requested username" + ]`, string(body)) + }) +} + +/**************** + * Custom tests * + ****************/ + +func TestParseUsername(t *testing.T) { + assert := testify.New(t) + assert.Equal("test", parseUsername("test.png"), "Function should trim .png at end") + assert.Equal("test", parseUsername("test"), "Function should return string itself, if it not contains .png at end") +} + +/************* + * Utilities * + *************/ + +func createSkinModel(username string, isSlim bool) *model.Skin { + return &model.Skin{ + UserId: 1, + Username: username, + Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3", // Use non nil UUID to pass validation in api tests + SkinId: 1, + Url: "http://chrly/skin.png", + MojangTextures: "mocked textures base64", + MojangSignature: "mocked signature", + IsSlim: isSlim, + } +} + +func createCape() []byte { + img := image.NewAlpha(image.Rect(0, 0, 64, 32)) + writer := &bytes.Buffer{} + _ = png.Encode(writer, img) + pngBytes, _ := ioutil.ReadAll(writer) + + return pngBytes +} + +func createCapeModel() *model.Cape { + return &model.Cape{File: bytes.NewReader(createCape())} +} + +func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse { + timeZone, _ := time.LoadLocation("Europe/Minsk") + textures := &mojang.TexturesProp{ + Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(), + ProfileID: "00000000000000000000000000000000", + ProfileName: "mock_username", + Textures: &mojang.TexturesResponse{}, + } + + if includeSkin { + textures.Textures.Skin = &mojang.SkinTexturesResponse{ + Url: "http://mojang/skin.png", + } + } + + if includeCape { + textures.Textures.Cape = &mojang.CapeTexturesResponse{ + Url: "http://mojang/cape.png", + } + } + + response := &mojang.SignedTexturesResponse{ + Id: "00000000000000000000000000000000", + Name: "mock_username", + Props: []*mojang.Property{ + { + Name: "textures", + Value: mojang.EncodeTextures(textures), + }, + }, + } + + return response +} + +// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png +var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==") + +func loadSkinFile() []byte { + result := make([]byte, 92) + _, err := base64.StdEncoding.Decode(result, OnePxPng) + if err != nil { + panic(err) + } + + return result +} diff --git a/http/textures.go b/http/textures.go deleted file mode 100644 index 244cd25..0000000 --- a/http/textures.go +++ /dev/null @@ -1,61 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - - "github.com/elyby/chrly/api/mojang" -) - -func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) { - cfg.Logger.IncCounter("textures.request", 1) - username := parseUsername(mux.Vars(request)["username"]) - - var textures *mojang.TexturesResponse - skin, skinErr := cfg.SkinsRepo.FindByUsername(username) - _, capeErr := cfg.CapesRepo.FindByUsername(username) - if (skinErr == nil && skin.SkinId != 0) || capeErr == nil { - textures = &mojang.TexturesResponse{} - - if skinErr == nil && skin.SkinId != 0 { - skinTextures := &mojang.SkinTexturesResponse{ - Url: skin.Url, - } - - if skin.IsSlim { - skinTextures.Metadata = &mojang.SkinTexturesMetadata{ - Model: "slim", - } - } - - textures.Skin = skinTextures - } - - if capeErr == nil { - textures.Cape = &mojang.CapeTexturesResponse{ - Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username, - } - } - } else { - mojangTextures, err := cfg.MojangTexturesProvider.GetForUsername(username) - if err != nil || mojangTextures == nil { - response.WriteHeader(http.StatusNoContent) - return - } - - texturesProp := mojangTextures.DecodeTextures() - if texturesProp == nil { - response.WriteHeader(http.StatusInternalServerError) - cfg.Logger.Error("Unable to find textures property") - return - } - - textures = texturesProp.Textures - } - - responseData, _ := json.Marshal(textures) - response.Header().Set("Content-Type", "application/json") - response.Write(responseData) -} diff --git a/http/textures_test.go b/http/textures_test.go deleted file mode 100644 index 9ffa91c..0000000 --- a/http/textures_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package http - -import ( - "bytes" - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/golang/mock/gomock" - testify "github.com/stretchr/testify/assert" - - "github.com/elyby/chrly/db" - "github.com/elyby/chrly/model" -) - -func TestConfig_Textures(t *testing.T) { - t.Run("Obtain textures for exists user with only default skin", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{Who: "mock_user"}) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "SKIN": { - "url": "http://chrly/skin.png" - } - }`, string(response)) - }) - - t.Run("Obtain textures for exists user with only slim skin", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{Who: "mock_user"}) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "SKIN": { - "url": "http://chrly/skin.png", - "metadata": { - "model": "slim" - } - } - }`, string(response)) - }) - - t.Run("Obtain textures for exists user with only cape", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{Who: "mock_user"}) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(&model.Cape{File: bytes.NewReader(createCape())}, nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "CAPE": { - "url": "http://chrly/cloaks/mock_user" - } - }`, string(response)) - }) - - t.Run("Obtain textures for exists user with skin and cape", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(&model.Cape{File: bytes.NewReader(createCape())}, nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_user", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "SKIN": { - "url": "http://chrly/skin.png" - }, - "CAPE": { - "url": "http://chrly/cloaks/mock_user" - } - }`, string(response)) - }) - - t.Run("Obtain textures for not exists user that exists in Mojang", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_username").Return(nil, &db.SkinNotFoundError{}) - mocks.Capes.EXPECT().FindByUsername("mock_username").Return(nil, &db.CapeNotFoundError{}) - mocks.MojangProvider.On("GetForUsername", "mock_username").Once().Return(createTexturesResponse(true, true), nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(200, resp.StatusCode) - assert.Equal("application/json", resp.Header.Get("Content-Type")) - response, _ := ioutil.ReadAll(resp.Body) - assert.JSONEq(`{ - "SKIN": { - "url": "http://mojang/skin.png" - }, - "CAPE": { - "url": "http://mojang/cape.png" - } - }`, string(response)) - }) - - t.Run("Obtain textures for not exists user that not exists in Mojang too", func(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - - mocks.Skins.EXPECT().FindByUsername("mock_username").Return(nil, &db.SkinNotFoundError{}) - mocks.Capes.EXPECT().FindByUsername("mock_username").Return(nil, &db.CapeNotFoundError{}) - mocks.MojangProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil) - - req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil) - w := httptest.NewRecorder() - - config.CreateHandler().ServeHTTP(w, req) - - resp := w.Result() - assert.Equal(204, resp.StatusCode) - }) -} diff --git a/interfaces/auth.go b/interfaces/auth.go deleted file mode 100644 index 3f645f7..0000000 --- a/interfaces/auth.go +++ /dev/null @@ -1,7 +0,0 @@ -package interfaces - -import "net/http" - -type AuthChecker interface { - Check(req *http.Request) error -} diff --git a/interfaces/mock_interfaces/mock_auth.go b/interfaces/mock_interfaces/mock_auth.go deleted file mode 100644 index 6b78454..0000000 --- a/interfaces/mock_interfaces/mock_auth.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: interfaces/auth.go - -package mock_interfaces - -import ( - gomock "github.com/golang/mock/gomock" - http "net/http" - reflect "reflect" -) - -// MockAuthChecker is a mock of AuthChecker interface -type MockAuthChecker struct { - ctrl *gomock.Controller - recorder *MockAuthCheckerMockRecorder -} - -// MockAuthCheckerMockRecorder is the mock recorder for MockAuthChecker -type MockAuthCheckerMockRecorder struct { - mock *MockAuthChecker -} - -// NewMockAuthChecker creates a new mock instance -func NewMockAuthChecker(ctrl *gomock.Controller) *MockAuthChecker { - mock := &MockAuthChecker{ctrl: ctrl} - mock.recorder = &MockAuthCheckerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (_m *MockAuthChecker) EXPECT() *MockAuthCheckerMockRecorder { - return _m.recorder -} - -// Check mocks base method -func (_m *MockAuthChecker) Check(req *http.Request) error { - ret := _m.ctrl.Call(_m, "Check", req) - ret0, _ := ret[0].(error) - return ret0 -} - -// Check indicates an expected call of Check -func (_mr *MockAuthCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Check", reflect.TypeOf((*MockAuthChecker)(nil).Check), arg0) -} diff --git a/interfaces/mock_interfaces/mock_interfaces.go b/interfaces/mock_interfaces/mock_interfaces.go deleted file mode 100644 index 846ec92..0000000 --- a/interfaces/mock_interfaces/mock_interfaces.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: interfaces/repositories.go - -package mock_interfaces - -import ( - model "github.com/elyby/chrly/model" - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockSkinsRepository is a mock of SkinsRepository interface -type MockSkinsRepository struct { - ctrl *gomock.Controller - recorder *MockSkinsRepositoryMockRecorder -} - -// MockSkinsRepositoryMockRecorder is the mock recorder for MockSkinsRepository -type MockSkinsRepositoryMockRecorder struct { - mock *MockSkinsRepository -} - -// NewMockSkinsRepository creates a new mock instance -func NewMockSkinsRepository(ctrl *gomock.Controller) *MockSkinsRepository { - mock := &MockSkinsRepository{ctrl: ctrl} - mock.recorder = &MockSkinsRepositoryMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (_m *MockSkinsRepository) EXPECT() *MockSkinsRepositoryMockRecorder { - return _m.recorder -} - -// FindByUsername mocks base method -func (_m *MockSkinsRepository) FindByUsername(username string) (*model.Skin, error) { - ret := _m.ctrl.Call(_m, "FindByUsername", username) - ret0, _ := ret[0].(*model.Skin) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindByUsername indicates an expected call of FindByUsername -func (_mr *MockSkinsRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUsername), arg0) -} - -// FindByUserId mocks base method -func (_m *MockSkinsRepository) FindByUserId(id int) (*model.Skin, error) { - ret := _m.ctrl.Call(_m, "FindByUserId", id) - ret0, _ := ret[0].(*model.Skin) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindByUserId indicates an expected call of FindByUserId -func (_mr *MockSkinsRepositoryMockRecorder) FindByUserId(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).FindByUserId), arg0) -} - -// Save mocks base method -func (_m *MockSkinsRepository) Save(skin *model.Skin) error { - ret := _m.ctrl.Call(_m, "Save", skin) - ret0, _ := ret[0].(error) - return ret0 -} - -// Save indicates an expected call of Save -func (_mr *MockSkinsRepositoryMockRecorder) Save(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Save", reflect.TypeOf((*MockSkinsRepository)(nil).Save), arg0) -} - -// RemoveByUserId mocks base method -func (_m *MockSkinsRepository) RemoveByUserId(id int) error { - ret := _m.ctrl.Call(_m, "RemoveByUserId", id) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveByUserId indicates an expected call of RemoveByUserId -func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUserId(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUserId", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUserId), arg0) -} - -// RemoveByUsername mocks base method -func (_m *MockSkinsRepository) RemoveByUsername(username string) error { - ret := _m.ctrl.Call(_m, "RemoveByUsername", username) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveByUsername indicates an expected call of RemoveByUsername -func (_mr *MockSkinsRepositoryMockRecorder) RemoveByUsername(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RemoveByUsername", reflect.TypeOf((*MockSkinsRepository)(nil).RemoveByUsername), arg0) -} - -// MockCapesRepository is a mock of CapesRepository interface -type MockCapesRepository struct { - ctrl *gomock.Controller - recorder *MockCapesRepositoryMockRecorder -} - -// MockCapesRepositoryMockRecorder is the mock recorder for MockCapesRepository -type MockCapesRepositoryMockRecorder struct { - mock *MockCapesRepository -} - -// NewMockCapesRepository creates a new mock instance -func NewMockCapesRepository(ctrl *gomock.Controller) *MockCapesRepository { - mock := &MockCapesRepository{ctrl: ctrl} - mock.recorder = &MockCapesRepositoryMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (_m *MockCapesRepository) EXPECT() *MockCapesRepositoryMockRecorder { - return _m.recorder -} - -// FindByUsername mocks base method -func (_m *MockCapesRepository) FindByUsername(username string) (*model.Cape, error) { - ret := _m.ctrl.Call(_m, "FindByUsername", username) - ret0, _ := ret[0].(*model.Cape) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FindByUsername indicates an expected call of FindByUsername -func (_mr *MockCapesRepositoryMockRecorder) FindByUsername(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "FindByUsername", reflect.TypeOf((*MockCapesRepository)(nil).FindByUsername), arg0) -} diff --git a/interfaces/mock_wd/mock_wd.go b/interfaces/mock_wd/mock_wd.go deleted file mode 100644 index 0bdde12..0000000 --- a/interfaces/mock_wd/mock_wd.go +++ /dev/null @@ -1,218 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mono83/slf/wd (interfaces: Watchdog) - -package mock_wd - -import ( - gomock "github.com/golang/mock/gomock" - slf "github.com/mono83/slf" - wd "github.com/mono83/slf/wd" - reflect "reflect" - time "time" -) - -// MockWatchdog is a mock of Watchdog interface -type MockWatchdog struct { - ctrl *gomock.Controller - recorder *MockWatchdogMockRecorder -} - -// MockWatchdogMockRecorder is the mock recorder for MockWatchdog -type MockWatchdogMockRecorder struct { - mock *MockWatchdog -} - -// NewMockWatchdog creates a new mock instance -func NewMockWatchdog(ctrl *gomock.Controller) *MockWatchdog { - mock := &MockWatchdog{ctrl: ctrl} - mock.recorder = &MockWatchdogMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (_m *MockWatchdog) EXPECT() *MockWatchdogMockRecorder { - return _m.recorder -} - -// Alert mocks base method -func (_m *MockWatchdog) Alert(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Alert", _s...) -} - -// Alert indicates an expected call of Alert -func (_mr *MockWatchdogMockRecorder) Alert(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Alert", reflect.TypeOf((*MockWatchdog)(nil).Alert), _s...) -} - -// Debug mocks base method -func (_m *MockWatchdog) Debug(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Debug", _s...) -} - -// Debug indicates an expected call of Debug -func (_mr *MockWatchdogMockRecorder) Debug(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Debug", reflect.TypeOf((*MockWatchdog)(nil).Debug), _s...) -} - -// Emergency mocks base method -func (_m *MockWatchdog) Emergency(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Emergency", _s...) -} - -// Emergency indicates an expected call of Emergency -func (_mr *MockWatchdogMockRecorder) Emergency(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Emergency", reflect.TypeOf((*MockWatchdog)(nil).Emergency), _s...) -} - -// Error mocks base method -func (_m *MockWatchdog) Error(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Error", _s...) -} - -// Error indicates an expected call of Error -func (_mr *MockWatchdogMockRecorder) Error(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Error", reflect.TypeOf((*MockWatchdog)(nil).Error), _s...) -} - -// IncCounter mocks base method -func (_m *MockWatchdog) IncCounter(_param0 string, _param1 int64, _param2 ...slf.Param) { - _s := []interface{}{_param0, _param1} - for _, _x := range _param2 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "IncCounter", _s...) -} - -// IncCounter indicates an expected call of IncCounter -func (_mr *MockWatchdogMockRecorder) IncCounter(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0, arg1}, arg2...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "IncCounter", reflect.TypeOf((*MockWatchdog)(nil).IncCounter), _s...) -} - -// Info mocks base method -func (_m *MockWatchdog) Info(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Info", _s...) -} - -// Info indicates an expected call of Info -func (_mr *MockWatchdogMockRecorder) Info(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Info", reflect.TypeOf((*MockWatchdog)(nil).Info), _s...) -} - -// RecordTimer mocks base method -func (_m *MockWatchdog) RecordTimer(_param0 string, _param1 time.Duration, _param2 ...slf.Param) { - _s := []interface{}{_param0, _param1} - for _, _x := range _param2 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "RecordTimer", _s...) -} - -// RecordTimer indicates an expected call of RecordTimer -func (_mr *MockWatchdogMockRecorder) RecordTimer(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0, arg1}, arg2...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "RecordTimer", reflect.TypeOf((*MockWatchdog)(nil).RecordTimer), _s...) -} - -// Timer mocks base method -func (_m *MockWatchdog) Timer(_param0 string, _param1 ...slf.Param) slf.Timer { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - ret := _m.ctrl.Call(_m, "Timer", _s...) - ret0, _ := ret[0].(slf.Timer) - return ret0 -} - -// Timer indicates an expected call of Timer -func (_mr *MockWatchdogMockRecorder) Timer(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Timer", reflect.TypeOf((*MockWatchdog)(nil).Timer), _s...) -} - -// Trace mocks base method -func (_m *MockWatchdog) Trace(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Trace", _s...) -} - -// Trace indicates an expected call of Trace -func (_mr *MockWatchdogMockRecorder) Trace(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Trace", reflect.TypeOf((*MockWatchdog)(nil).Trace), _s...) -} - -// UpdateGauge mocks base method -func (_m *MockWatchdog) UpdateGauge(_param0 string, _param1 int64, _param2 ...slf.Param) { - _s := []interface{}{_param0, _param1} - for _, _x := range _param2 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "UpdateGauge", _s...) -} - -// UpdateGauge indicates an expected call of UpdateGauge -func (_mr *MockWatchdogMockRecorder) UpdateGauge(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0, arg1}, arg2...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "UpdateGauge", reflect.TypeOf((*MockWatchdog)(nil).UpdateGauge), _s...) -} - -// Warning mocks base method -func (_m *MockWatchdog) Warning(_param0 string, _param1 ...slf.Param) { - _s := []interface{}{_param0} - for _, _x := range _param1 { - _s = append(_s, _x) - } - _m.ctrl.Call(_m, "Warning", _s...) -} - -// Warning indicates an expected call of Warning -func (_mr *MockWatchdogMockRecorder) Warning(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - _s := append([]interface{}{arg0}, arg1...) - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Warning", reflect.TypeOf((*MockWatchdog)(nil).Warning), _s...) -} - -// WithParams mocks base method -func (_m *MockWatchdog) WithParams(_param0 ...slf.Param) wd.Watchdog { - _s := []interface{}{} - for _, _x := range _param0 { - _s = append(_s, _x) - } - ret := _m.ctrl.Call(_m, "WithParams", _s...) - ret0, _ := ret[0].(wd.Watchdog) - return ret0 -} - -// WithParams indicates an expected call of WithParams -func (_mr *MockWatchdogMockRecorder) WithParams(arg0 ...interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "WithParams", reflect.TypeOf((*MockWatchdog)(nil).WithParams), arg0...) -} diff --git a/interfaces/repositories.go b/interfaces/repositories.go deleted file mode 100644 index 848045a..0000000 --- a/interfaces/repositories.go +++ /dev/null @@ -1,22 +0,0 @@ -package interfaces - -import ( - "github.com/elyby/chrly/api/mojang" - "github.com/elyby/chrly/model" -) - -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 -} - -type CapesRepository interface { - FindByUsername(username string) (*model.Cape, error) -} - -type MojangTexturesProvider interface { - GetForUsername(username string) (*mojang.SignedTexturesResponse, error) -}