From 04714543b8bec1f2fe3484787be7bf0699a27206 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 20 Aug 2017 01:22:42 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=BE=D1=80=D0=B3=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=20daemon=20=D0=B2=20http.=20=D0=A3=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B4=D0=BD=D1=91=D0=BD=20=D0=BF=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=20utils.=20=D0=A3=D0=B4=D0=B0=D0=BB=D1=91=D0=BD=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA=20minecr?= =?UTF-8?q?aft.php=20(legacy=20=D1=81=20=D1=81=D0=B0=D0=BC=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE-=D1=81=D0=B0=D0=BC=D0=BE=D0=B3=D0=BE=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B0=D0=BB=D0=B0=20Ely.by)=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20api-=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/amqpWorker.go | 2 +- cmd/serve.go | 8 +- daemon/http.go | 52 ----- db/filesystem.go | 15 +- {ui => http}/cape.go | 19 +- http/cape_test.go | 138 +++++++++++ http/face.go | 27 +++ http/face_test.go | 53 +++++ http/http.go | 91 ++++++++ http/http_test.go | 40 ++++ http/mock_wd/mock_wd.go | 218 ++++++++++++++++++ {ui => http}/not_found.go | 8 +- http/not_found_test.go | 28 +++ {ui => http}/signed_textures.go | 16 +- http/signed_textures_test.go | 71 ++++++ http/skin.go | 36 +++ http/skin_test.go | 124 ++++++++++ {ui => http}/textures.go | 59 +++-- http/textures_test.go | 166 +++++++++++++ interfaces/mock_interfaces/mock_interfaces.go | 107 +++++++++ interfaces/repositories.go | 2 +- model/cape.go | 6 +- ui/face.go | 28 --- ui/minecraft_php.go | 33 --- ui/service.go | 24 -- ui/skin.go | 38 --- ui/ui.go | 39 ---- utils/utils.go | 43 ---- utils/utils_test.go | 60 ----- 29 files changed, 1170 insertions(+), 381 deletions(-) delete mode 100644 daemon/http.go rename {ui => http}/cape.go (54%) create mode 100644 http/cape_test.go create mode 100644 http/face.go create mode 100644 http/face_test.go create mode 100644 http/http.go create mode 100644 http/http_test.go create mode 100644 http/mock_wd/mock_wd.go rename {ui => http}/not_found.go (60%) create mode 100644 http/not_found_test.go rename {ui => http}/signed_textures.go (70%) create mode 100644 http/signed_textures_test.go create mode 100644 http/skin.go create mode 100644 http/skin_test.go rename {ui => http}/textures.go (54%) create mode 100644 http/textures_test.go create mode 100644 interfaces/mock_interfaces/mock_interfaces.go delete mode 100644 ui/face.go delete mode 100644 ui/minecraft_php.go delete mode 100644 ui/service.go delete mode 100644 ui/skin.go delete mode 100644 ui/ui.go delete mode 100644 utils/utils.go delete mode 100644 utils/utils_test.go diff --git a/cmd/amqpWorker.go b/cmd/amqpWorker.go index 1053d18..c2baea7 100644 --- a/cmd/amqpWorker.go +++ b/cmd/amqpWorker.go @@ -57,7 +57,7 @@ var amqpWorkerCmd = &cobra.Command{ services := &worker.Services{ Logger: logger, Channel: amqpChannel, - SkinsRepo: skinsRepo, + SkinsRepo: skinsRepo, AccountsAPI: accountsApi, } diff --git a/cmd/serve.go b/cmd/serve.go index 77f1bed..acc57a4 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -8,9 +8,8 @@ import ( "github.com/spf13/viper" "elyby/minecraft-skinsystem/bootstrap" - "elyby/minecraft-skinsystem/daemon" "elyby/minecraft-skinsystem/db" - "elyby/minecraft-skinsystem/ui" + "elyby/minecraft-skinsystem/http" ) var serveCmd = &cobra.Command{ @@ -41,15 +40,14 @@ var serveCmd = &cobra.Command{ } logger.Info("Capes repository successfully initialized") - cfg := &daemon.Config{ + cfg := &http.Config{ ListenSpec: fmt.Sprintf("%s:%d", viper.GetString("server.host"), viper.GetInt("server.port")), SkinsRepo: skinsRepo, CapesRepo: capesRepo, Logger: logger, - UI: ui.Config{}, } - if err := daemon.Run(cfg); err != nil { + if err := cfg.Run(); err != nil { logger.Error(fmt.Sprintf("Error in main(): %v", err)) } }, diff --git a/daemon/http.go b/daemon/http.go deleted file mode 100644 index bee888c..0000000 --- a/daemon/http.go +++ /dev/null @@ -1,52 +0,0 @@ -package daemon - -import ( - "fmt" - "net" - "os" - "os/signal" - "syscall" - - "github.com/mono83/slf/wd" - - "elyby/minecraft-skinsystem/interfaces" - "elyby/minecraft-skinsystem/ui" -) - -type Config struct { - ListenSpec string - - SkinsRepo interfaces.SkinsRepository - CapesRepo interfaces.CapesRepository - Logger wd.Watchdog - UI ui.Config -} - -func Run(cfg *Config) error { - cfg.Logger.Info(fmt.Sprintf("Starting, HTTP on: %s\n", cfg.ListenSpec)) - - uiService, err := ui.NewUiService(cfg.Logger, cfg.SkinsRepo, cfg.CapesRepo) - if err != nil { - cfg.Logger.Error(fmt.Sprintf("Error creating ui services: %v\n", err)) - return err - } - - listener, err := net.Listen("tcp", cfg.ListenSpec) - if err != nil { - cfg.Logger.Error(fmt.Sprintf("Error creating listener: %v\n", err)) - return err - } - - ui.Start(cfg.UI, uiService, listener) - - waitForSignal(cfg) - - return nil -} - -func waitForSignal(cfg *Config) { - ch := make(chan os.Signal) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - s := <-ch - cfg.Logger.Info(fmt.Sprintf("Got signal: %v, exiting.", s)) -} diff --git a/db/filesystem.go b/db/filesystem.go index d8dfc01..376c167 100644 --- a/db/filesystem.go +++ b/db/filesystem.go @@ -5,8 +5,8 @@ import ( "path" "strings" - "elyby/minecraft-skinsystem/model" "elyby/minecraft-skinsystem/interfaces" + "elyby/minecraft-skinsystem/model" ) type FilesystemFactory struct { @@ -42,19 +42,18 @@ type filesStorage struct { path string } -func (repository *filesStorage) FindByUsername(username string) (model.Cape, error) { - var record model.Cape +func (repository *filesStorage) FindByUsername(username string) (*model.Cape, error) { if username == "" { - return record, &CapeNotFoundError{username} + return nil, &CapeNotFoundError{username} } capePath := path.Join(repository.path, strings.ToLower(username) + ".png") file, err := os.Open(capePath) if err != nil { - return record, &CapeNotFoundError{username} + return nil, &CapeNotFoundError{username} } - record.File = file - - return record, nil + return &model.Cape{ + File: file, + }, nil } diff --git a/ui/cape.go b/http/cape.go similarity index 54% rename from ui/cape.go rename to http/cape.go index 22ae151..9614ebd 100644 --- a/ui/cape.go +++ b/http/cape.go @@ -1,31 +1,30 @@ -package ui +package http import ( "io" "net/http" "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/utils" ) -func (s *uiService) Cape(response http.ResponseWriter, request *http.Request) { +func (cfg *Config) Cape(response http.ResponseWriter, request *http.Request) { if mux.Vars(request)["converted"] == "" { - s.logger.IncCounter("capes.request", 1) + cfg.Logger.IncCounter("capes.request", 1) } - username := utils.ParseUsername(mux.Vars(request)["username"]) - rec, err := s.capesRepo.FindByUsername(username) + username := parseUsername(mux.Vars(request)["username"]) + rec, err := cfg.CapesRepo.FindByUsername(username) if err != nil { http.Redirect(response, request, "http://skins.minecraft.net/MinecraftCloaks/" + username + ".png", 301) + return } request.Header.Set("Content-Type", "image/png") io.Copy(response, rec.File) } -func (s *uiService) CapeGET(response http.ResponseWriter, request *http.Request) { - s.logger.IncCounter("capes.get_request", 1) +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) @@ -35,5 +34,5 @@ func (s *uiService) CapeGET(response http.ResponseWriter, request *http.Request) mux.Vars(request)["username"] = username mux.Vars(request)["converted"] = "1" - s.Cape(response, request) + cfg.Cape(response, request) } diff --git a/http/cape_test.go b/http/cape_test.go new file mode 100644 index 0000000..ed50c1e --- /dev/null +++ b/http/cape_test.go @@ -0,0 +1,138 @@ +package http + +import ( + "bytes" + "image" + "image/png" + "io/ioutil" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/db" + "elyby/minecraft-skinsystem/model" +) + +func TestConfig_Cape(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, _, capesRepo, wd := setupMocks(ctrl) + + cape := createCape() + + capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ + File: bytes.NewReader(cape), + }, nil) + wd.EXPECT().IncCounter("capes.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(200, resp.StatusCode) + responseData, _ := ioutil.ReadAll(resp.Body) + assert.Equal(cape, responseData) + assert.Equal("image/png", resp.Header.Get("Content-Type")) +} + +func TestConfig_Cape2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, _, capesRepo, wd := setupMocks(ctrl) + + capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) + wd.EXPECT().IncCounter("capes.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location")) +} + +func TestConfig_CapeGET(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, _, capesRepo, wd := setupMocks(ctrl) + + cape := createCape() + + capesRepo.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ + File: bytes.NewReader(cape), + }, nil) + wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0) + wd.EXPECT().IncCounter("capes.get_request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=mocked_username", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(200, resp.StatusCode) + responseData, _ := ioutil.ReadAll(resp.Body) + assert.Equal(cape, responseData) + assert.Equal("image/png", resp.Header.Get("Content-Type")) +} + +func TestConfig_CapeGET2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, _, capesRepo, wd := setupMocks(ctrl) + + capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) + wd.EXPECT().IncCounter("capes.request", int64(1)).Times(0) + wd.EXPECT().IncCounter("capes.get_request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks?name=notch", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location")) +} + +func TestConfig_CapeGET3(t *testing.T) { + assert := testify.New(t) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/?name=notch", nil) + w := httptest.NewRecorder() + + (&Config{}).CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skinsystem.ely.by/cloaks?name=notch", resp.Header.Get("Location")) +} + +// 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/face.go b/http/face.go new file mode 100644 index 0000000..2032f39 --- /dev/null +++ b/http/face.go @@ -0,0 +1,27 @@ +package http + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +const defaultHash = "default" + +func (cfg *Config) Face(response http.ResponseWriter, request *http.Request) { + cfg.Logger.IncCounter("faces.request", 1) + username := parseUsername(mux.Vars(request)["username"]) + rec, err := cfg.SkinsRepo.FindByUsername(username) + var hash string + if err != nil || rec.SkinId == 0 { + hash = defaultHash + } else { + hash = rec.Hash + } + + http.Redirect(response, request, buildElyUrl(buildFaceUrl(hash)), 301) +} + +func buildFaceUrl(hash string) string { + return "/minecraft/skin_buffer/faces/" + hash + ".png" +} diff --git a/http/face_test.go b/http/face_test.go new file mode 100644 index 0000000..f61daff --- /dev/null +++ b/http/face_test.go @@ -0,0 +1,53 @@ +package http + +import ( + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/db" +) + +func TestConfig_Face(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + wd.EXPECT().IncCounter("faces.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://ely.by/minecraft/skin_buffer/faces/55d2a8848764f5ff04012cdb093458bd.png", resp.Header.Get("Location")) +} + +func TestConfig_Face2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"mock_user"}) + wd.EXPECT().IncCounter("faces.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user/face.png", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://ely.by/minecraft/skin_buffer/faces/default.png", resp.Header.Get("Location")) +} diff --git a/http/http.go b/http/http.go new file mode 100644 index 0000000..b07fdd0 --- /dev/null +++ b/http/http.go @@ -0,0 +1,91 @@ +package http + +import ( + "fmt" + "net" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/gorilla/mux" + "github.com/mono83/slf/wd" + + "elyby/minecraft-skinsystem/interfaces" +) + +type Config struct { + ListenSpec string + + SkinsRepo interfaces.SkinsRepository + CapesRepo interfaces.CapesRepository + Logger wd.Watchdog +} + +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") + router.HandleFunc("/skins/{username}/face", cfg.Face).Methods("GET") + router.HandleFunc("/skins/{username}/face.png", cfg.Face).Methods("GET") + // Legacy + router.HandleFunc("/skins", cfg.SkinGET).Methods("GET") + router.HandleFunc("/cloaks", cfg.CapeGET).Methods("GET") + // 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 +} + +func buildElyUrl(route string) string { + prefix := "http://ely.by" + if !strings.HasPrefix(route, prefix) { + route = prefix + route + } + + return route +} + +func waitForSignal() os.Signal { + ch := make(chan os.Signal) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + + return <-ch +} diff --git a/http/http_test.go b/http/http_test.go new file mode 100644 index 0000000..172b9a5 --- /dev/null +++ b/http/http_test.go @@ -0,0 +1,40 @@ +package http + +import ( + "testing" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/http/mock_wd" + "elyby/minecraft-skinsystem/interfaces/mock_interfaces" +) + +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") +} + +func TestBuildElyUrl(t *testing.T) { + assert := testify.New(t) + assert.Equal("http://ely.by/route", buildElyUrl("/route"), "Function should add prefix to the provided relative url.") + assert.Equal("http://ely.by/test/route", buildElyUrl("http://ely.by/test/route"), "Function should do not add prefix to the provided prefixed url.") +} + +func setupMocks(ctrl *gomock.Controller) ( + *Config, + *mock_interfaces.MockSkinsRepository, + *mock_interfaces.MockCapesRepository, + *mock_wd.MockWatchdog, +) { + skinsRepo := mock_interfaces.NewMockSkinsRepository(ctrl) + capesRepo := mock_interfaces.NewMockCapesRepository(ctrl) + wd := mock_wd.NewMockWatchdog(ctrl) + + return &Config{ + SkinsRepo: skinsRepo, + CapesRepo: capesRepo, + Logger: wd, + }, skinsRepo, capesRepo, wd +} diff --git a/http/mock_wd/mock_wd.go b/http/mock_wd/mock_wd.go new file mode 100644 index 0000000..0bdde12 --- /dev/null +++ b/http/mock_wd/mock_wd.go @@ -0,0 +1,218 @@ +// 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/ui/not_found.go b/http/not_found.go similarity index 60% rename from ui/not_found.go rename to http/not_found.go index 7723146..3328634 100644 --- a/ui/not_found.go +++ b/http/not_found.go @@ -1,12 +1,12 @@ -package ui +package http import ( "encoding/json" "net/http" ) -func NotFound(response http.ResponseWriter, request *http.Request) { - json, _ := json.Marshal(map[string]string{ +func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request) { + data, _ := json.Marshal(map[string]string{ "status": "404", "message": "Not Found", "link": "http://docs.ely.by/skin-system.html", @@ -14,5 +14,5 @@ func NotFound(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") response.WriteHeader(http.StatusNotFound) - response.Write(json) + response.Write(data) } diff --git a/http/not_found_test.go b/http/not_found_test.go new file mode 100644 index 0000000..44c8a81 --- /dev/null +++ b/http/not_found_test.go @@ -0,0 +1,28 @@ +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", + "link": "http://docs.ely.by/skin-system.html" + }`, string(response)) +} diff --git a/ui/signed_textures.go b/http/signed_textures.go similarity index 70% rename from ui/signed_textures.go rename to http/signed_textures.go index aff0086..49950b1 100644 --- a/ui/signed_textures.go +++ b/http/signed_textures.go @@ -1,4 +1,4 @@ -package ui +package http import ( "encoding/json" @@ -6,8 +6,6 @@ import ( "strings" "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/utils" ) type signedTexturesResponse struct { @@ -18,16 +16,16 @@ type signedTexturesResponse struct { } type property struct { - Name string `json:"name"` + Name string `json:"name"` Signature string `json:"signature,omitempty"` - Value string `json:"value"` + Value string `json:"value"` } -func (s *uiService) SignedTextures(response http.ResponseWriter, request *http.Request) { - s.logger.IncCounter("signed_textures.request", 1) - username := utils.ParseUsername(mux.Vars(request)["username"]) +func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Request) { + cfg.Logger.IncCounter("signed_textures.request", 1) + username := parseUsername(mux.Vars(request)["username"]) - rec, err := s.skinsRepo.FindByUsername(username) + rec, err := cfg.SkinsRepo.FindByUsername(username) if err != nil || rec.SkinId == 0 || rec.MojangTextures == "" { response.WriteHeader(http.StatusNoContent) return diff --git a/http/signed_textures_test.go b/http/signed_textures_test.go new file mode 100644 index 0000000..48d728a --- /dev/null +++ b/http/signed_textures_test.go @@ -0,0 +1,71 @@ +package http + +import ( + "io/ioutil" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/db" +) + +func TestConfig_SignedTextures(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + wd.EXPECT().IncCounter("signed_textures.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/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": "ely", + "value": "but why are you asking?" + } + ] + }`, string(response)) +} + +func TestConfig_SignedTextures2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{}) + wd.EXPECT().IncCounter("signed_textures.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/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)) +} diff --git a/http/skin.go b/http/skin.go new file mode 100644 index 0000000..0c8e0eb --- /dev/null +++ b/http/skin.go @@ -0,0 +1,36 @@ +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, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301) + return + } + + http.Redirect(response, request, buildElyUrl(rec.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 new file mode 100644 index 0000000..0f55cb7 --- /dev/null +++ b/http/skin_test.go @@ -0,0 +1,124 @@ +package http + +import ( + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/db" + "elyby/minecraft-skinsystem/model" +) + +func TestConfig_Skin(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + wd.EXPECT().IncCounter("skins.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location")) +} + +func TestConfig_Skin2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) + wd.EXPECT().IncCounter("skins.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location")) +} + +func TestConfig_SkinGET(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + wd.EXPECT().IncCounter("skins.get_request", int64(1)) + wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location")) +} + +func TestConfig_SkinGET2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, _, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) + wd.EXPECT().IncCounter("skins.get_request", int64(1)) + wd.EXPECT().IncCounter("skins.request", int64(1)).Times(0) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=notch", nil) + w := httptest.NewRecorder() + + config.CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skins.minecraft.net/MinecraftSkins/notch.png", resp.Header.Get("Location")) +} + +func TestConfig_SkinGET3(t *testing.T) { + assert := testify.New(t) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/?name=notch", nil) + w := httptest.NewRecorder() + + (&Config{}).CreateHandler().ServeHTTP(w, req) + + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://skinsystem.ely.by/skins?name=notch", resp.Header.Get("Location")) +} + +func createSkinModel(username string, isSlim bool) *model.Skin { + return &model.Skin{ + Username: username, + Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3", + SkinId: 1, + Hash: "55d2a8848764f5ff04012cdb093458bd", + Url: "http://ely.by/minecraft/skins/skin.png", + MojangTextures: "mocked textures base64", + MojangSignature: "mocked signature", + IsSlim: isSlim, + } +} diff --git a/ui/textures.go b/http/textures.go similarity index 54% rename from ui/textures.go rename to http/textures.go index 1afb8b5..4001b29 100644 --- a/ui/textures.go +++ b/http/textures.go @@ -1,17 +1,17 @@ -package ui +package http import ( + "crypto/md5" + "encoding/hex" "encoding/json" + "io" "net/http" + "strconv" + "time" "github.com/gorilla/mux" - "crypto/md5" - "encoding/hex" - "io" - "elyby/minecraft-skinsystem/model" - "elyby/minecraft-skinsystem/utils" ) type texturesResponse struct { @@ -34,16 +34,20 @@ type Cape struct { Hash string `json:"hash"` } -func (s *uiService) Textures(response http.ResponseWriter, request *http.Request) { - s.logger.IncCounter("textures.request", 1) - username := utils.ParseUsername(mux.Vars(request)["username"]) +func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) { + cfg.Logger.IncCounter("textures.request", 1) + username := parseUsername(mux.Vars(request)["username"]) - skin, err := s.skinsRepo.FindByUsername(username) + skin, err := cfg.SkinsRepo.FindByUsername(username) if err != nil || skin.SkinId == 0 { + if skin == nil { + skin = &model.Skin{} + } + skin.Url = "http://skins.minecraft.net/MinecraftSkins/" + username + ".png" - skin.Hash = string(utils.BuildNonElyTexturesHash(username)) + skin.Hash = string(buildNonElyTexturesHash(username)) } else { - skin.Url = utils.BuildElyUrl(skin.Url) + skin.Url = buildElyUrl(skin.Url) } textures := texturesResponse{ @@ -59,35 +63,42 @@ func (s *uiService) Textures(response http.ResponseWriter, request *http.Request } } - cape, err := s.capesRepo.FindByUsername(username) + cape, err := cfg.CapesRepo.FindByUsername(username) if err == nil { - // TODO: восстановить функционал получения ссылки на плащ - // capeUrl, err := services.Router.Get("cloaks").URL("username", username) - capeUrl := "/capes/" + username - if err != nil { - s.logger.Error(err.Error()) - } - var scheme string = "http://" if request.TLS != nil { scheme = "https://" } textures.Cape = &Cape{ - // Url: scheme + request.Host + capeUrl.String(), - Url: scheme + request.Host + capeUrl, + Url: scheme + request.Host + "/cloaks/" + username, Hash: calculateCapeHash(cape), } } - responseData,_ := json.Marshal(textures) + responseData, _ := json.Marshal(textures) response.Header().Set("Content-Type", "application/json") response.Write(responseData) } -func calculateCapeHash(cape model.Cape) string { +func calculateCapeHash(cape *model.Cape) string { hasher := md5.New() io.Copy(hasher, cape.File) return hex.EncodeToString(hasher.Sum(nil)) } + +func buildNonElyTexturesHash(username string) string { + hour := getCurrentHour() + hasher := md5.New() + hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username)) + + return hex.EncodeToString(hasher.Sum(nil)) +} + +var timeNow = time.Now + +func getCurrentHour() int64 { + n := timeNow() + return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix() +} diff --git a/http/textures_test.go b/http/textures_test.go new file mode 100644 index 0000000..97f6ac2 --- /dev/null +++ b/http/textures_test.go @@ -0,0 +1,166 @@ +package http + +import ( + "bytes" + "io/ioutil" + "net/http/httptest" + "testing" + "time" + + "github.com/golang/mock/gomock" + testify "github.com/stretchr/testify/assert" + + "elyby/minecraft-skinsystem/db" + "elyby/minecraft-skinsystem/model" +) + +func TestConfig_Textures(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) + wd.EXPECT().IncCounter("textures.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/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://ely.by/minecraft/skins/skin.png", + "hash": "55d2a8848764f5ff04012cdb093458bd" + } + }`, string(response)) +} + +func TestConfig_Textures2(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil) + capesRepo.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) + wd.EXPECT().IncCounter("textures.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/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://ely.by/minecraft/skins/skin.png", + "hash": "55d2a8848764f5ff04012cdb093458bd", + "metadata": { + "model": "slim" + } + } + }`, string(response)) +} + +func TestConfig_Textures3(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) + capesRepo.EXPECT().FindByUsername("mock_user").Return(&model.Cape{ + File: bytes.NewReader(createCape()), + }, nil) + wd.EXPECT().IncCounter("textures.request", int64(1)) + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/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://ely.by/minecraft/skins/skin.png", + "hash": "55d2a8848764f5ff04012cdb093458bd" + }, + "CAPE": { + "url": "http://skinsystem.ely.by/cloaks/mock_user", + "hash": "424ff79dce9940af89c28ad80de8aaad" + } + }`, string(response)) +} + +func TestConfig_Textures4(t *testing.T) { + assert := testify.New(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config, skinsRepo, capesRepo, wd := setupMocks(ctrl) + + skinsRepo.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{}) + capesRepo.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{}) + wd.EXPECT().IncCounter("textures.request", int64(1)) + timeNow = func() time.Time { + return time.Date(2017, time.August, 20, 0, 15, 54, 0, time.UTC) + } + + req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/notch", 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://skins.minecraft.net/MinecraftSkins/notch.png", + "hash": "5923cf3f7fa170a279e4d7a9483cfc52" + } + }`, string(response)) +} + +func TestBuildNonElyTexturesHash(t *testing.T) { + assert := testify.New(t) + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 16, 15, 34, 0, time.UTC) + } + + assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should return fixed hash by username-time pair") + assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair") + + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 16, 20, 12, 0, time.UTC) + } + + assert.Equal("686d788a5353cb636e8fdff727634d88", buildNonElyTexturesHash("username"), "Function should do not change it's value if hour the same") + assert.Equal("fb876f761683a10accdb17d403cef64c", buildNonElyTexturesHash("another-username"), "Function should return fixed hash by username-time pair") + + timeNow = func() time.Time { + return time.Date(2017, time.November, 30, 17, 1, 3, 0, time.UTC) + } + + assert.Equal("42277892fd24bc0ed86285b3bb8b8fad", buildNonElyTexturesHash("username"), "Function should change it's value if hour changed") +} diff --git a/interfaces/mock_interfaces/mock_interfaces.go b/interfaces/mock_interfaces/mock_interfaces.go new file mode 100644 index 0000000..72b1a52 --- /dev/null +++ b/interfaces/mock_interfaces/mock_interfaces.go @@ -0,0 +1,107 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces/repositories.go + +package mock_interfaces + +import ( + model "elyby/minecraft-skinsystem/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) +} + +// 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/repositories.go b/interfaces/repositories.go index e8b5695..94164e9 100644 --- a/interfaces/repositories.go +++ b/interfaces/repositories.go @@ -11,5 +11,5 @@ type SkinsRepository interface { } type CapesRepository interface { - FindByUsername(username string) (model.Cape, error) + FindByUsername(username string) (*model.Cape, error) } diff --git a/model/cape.go b/model/cape.go index 355aef7..26485f9 100644 --- a/model/cape.go +++ b/model/cape.go @@ -1,7 +1,9 @@ package model -import "os" +import ( + "io" +) type Cape struct { - File *os.File // TODO: нужно абстрагироваться в отдельный файл с инфой о скине + File io.Reader } diff --git a/ui/face.go b/ui/face.go deleted file mode 100644 index ea7e4e3..0000000 --- a/ui/face.go +++ /dev/null @@ -1,28 +0,0 @@ -package ui - -import ( - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/utils" -) - -const defaultHash = "default" - -func (s *uiService) Face(response http.ResponseWriter, request *http.Request) { - username := utils.ParseUsername(mux.Vars(request)["username"]) - rec, err := s.skinsRepo.FindByUsername(username) - var hash string - if err != nil || rec.SkinId == 0 { - hash = defaultHash - } else { - hash = rec.Hash - } - - http.Redirect(response, request, utils.BuildElyUrl(buildFaceUrl(hash)), 301) -} - -func buildFaceUrl(hash string) string { - return "/minecraft/skin_buffer/faces/" + hash + ".png" -} diff --git a/ui/minecraft_php.go b/ui/minecraft_php.go deleted file mode 100644 index 9878a82..0000000 --- a/ui/minecraft_php.go +++ /dev/null @@ -1,33 +0,0 @@ -package ui - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -// Метод-наследие от первой версии системы скинов. -// Всё ещё иногда используется -// Просто конвертируем данные и отправляем их в основной обработчик -func (s *uiService) MinecraftPHP(response http.ResponseWriter, request *http.Request) { - username := request.URL.Query().Get("name") - required := request.URL.Query().Get("type") - if username == "" || required == "" { - response.WriteHeader(http.StatusBadRequest) - return - } - - mux.Vars(request)["username"] = username - mux.Vars(request)["converted"] = "1" - - switch required { - case "skin": - s.logger.IncCounter("skins.minecraft-php-request", 1) - s.Skin(response, request) - case "cloack": - s.logger.IncCounter("capes.minecraft-php-request", 1) - s.Cape(response, request) - default: - response.WriteHeader(http.StatusNotFound) - } -} diff --git a/ui/service.go b/ui/service.go deleted file mode 100644 index d622b2c..0000000 --- a/ui/service.go +++ /dev/null @@ -1,24 +0,0 @@ -package ui - -import ( - "github.com/mono83/slf/wd" - "elyby/minecraft-skinsystem/interfaces" -) - -type uiService struct { - logger wd.Watchdog - skinsRepo interfaces.SkinsRepository - capesRepo interfaces.CapesRepository -} - -func NewUiService( - logger wd.Watchdog, - skinsRepo interfaces.SkinsRepository, - capesRepo interfaces.CapesRepository, -) (*uiService, error) { - return &uiService{ - logger: logger, - skinsRepo: skinsRepo, - capesRepo: capesRepo, - }, nil -} diff --git a/ui/skin.go b/ui/skin.go deleted file mode 100644 index 8d67f35..0000000 --- a/ui/skin.go +++ /dev/null @@ -1,38 +0,0 @@ -package ui - -import ( - "net/http" - - "github.com/gorilla/mux" - - "elyby/minecraft-skinsystem/utils" -) - -func (s *uiService) Skin(response http.ResponseWriter, request *http.Request) { - if mux.Vars(request)["converted"] == "" { - s.logger.IncCounter("skins.request", 1) - } - - username := utils.ParseUsername(mux.Vars(request)["username"]) - rec, err := s.skinsRepo.FindByUsername(username) - if err != nil { - http.Redirect(response, request, "http://skins.minecraft.net/MinecraftSkins/" + username + ".png", 301) - return - } - - http.Redirect(response, request, utils.BuildElyUrl(rec.Url), 301) -} - -func (s *uiService) SkinGET(response http.ResponseWriter, request *http.Request) { - s.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" - - s.Skin(response, request) -} diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index 77719a0..0000000 --- a/ui/ui.go +++ /dev/null @@ -1,39 +0,0 @@ -package ui - -import ( - "net" - "net/http" - "time" - - "github.com/gorilla/mux" -) - -type Config struct { - -} - -func Start(cfg Config, s *uiService, lst net.Listener) { - router := mux.NewRouter().StrictSlash(true) - - router.HandleFunc("/skins/{username}", s.Skin).Methods("GET") - router.HandleFunc("/cloaks/{username}", s.Cape).Methods("GET") - router.HandleFunc("/textures/{username}", s.Textures).Methods("GET") - router.HandleFunc("/textures/signed/{username}", s.SignedTextures).Methods("GET") - router.HandleFunc("/skins/{username}/face", s.Face).Methods("GET") - router.HandleFunc("/skins/{username}/face.png", s.Face).Methods("GET") - // Legacy - router.HandleFunc("/minecraft.php", s.MinecraftPHP).Methods("GET") - router.HandleFunc("/skins/", s.SkinGET).Methods("GET") - router.HandleFunc("/cloaks/", s.CapeGET).Methods("GET") - // 404 - router.NotFoundHandler = http.HandlerFunc(NotFound) - - server := &http.Server{ - ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, - MaxHeaderBytes: 1 << 16, - Handler: router, - } - - go server.Serve(lst) -} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index a550022..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,43 +0,0 @@ -package utils - -import ( - "crypto/md5" - "encoding/hex" - "strconv" - "strings" - "time" -) - -func ParseUsername(username string) string { - const suffix = ".png" - if strings.HasSuffix(username, suffix) { - username = strings.TrimSuffix(username, suffix) - } - - return username -} - -func BuildNonElyTexturesHash(username string) string { - hour := getCurrentHour() - hasher := md5.New() - hasher.Write([]byte("non-ely-" + strconv.FormatInt(hour, 10) + "-" + username)) - - return hex.EncodeToString(hasher.Sum(nil)) -} - -func BuildElyUrl(route string) string { - prefix := "http://ely.by" - if !strings.HasPrefix(route, prefix) { - route = prefix + route - } - - return route -} - -var timeNow = time.Now - -func getCurrentHour() int64 { - n := timeNow() - return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), 0, 0, 0, time.UTC).Unix() -} - diff --git a/utils/utils_test.go b/utils/utils_test.go deleted file mode 100644 index 98f7625..0000000 --- a/utils/utils_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package utils - -import ( - "testing" - "time" -) - -func TestParseUsername(t *testing.T) { - if ParseUsername("test.png") != "test" { - t.Error("Function should trim .png at end") - } - - if ParseUsername("test") != "test" { - t.Error("Function should return string itself, if it not contains .png at end") - } -} - -func TestBuildNonElyTexturesHash(t *testing.T) { - timeNow = func() time.Time { - return time.Date(2017, time.November, 30, 16, 15, 34, 0, time.UTC) - } - - if BuildNonElyTexturesHash("username") != "686d788a5353cb636e8fdff727634d88" { - t.Error("Function should return fixed hash by username-time pair") - } - - if BuildNonElyTexturesHash("another-username") != "fb876f761683a10accdb17d403cef64c" { - t.Error("Function should return fixed hash by username-time pair") - } - - timeNow = func() time.Time { - return time.Date(2017, time.November, 30, 16, 20, 12, 0, time.UTC) - } - - if BuildNonElyTexturesHash("username") != "686d788a5353cb636e8fdff727634d88" { - t.Error("Function should do not change it's value if hour the same") - } - - if BuildNonElyTexturesHash("another-username") != "fb876f761683a10accdb17d403cef64c" { - t.Error("Function should return fixed hash by username-time pair") - } - - timeNow = func() time.Time { - return time.Date(2017, time.November, 30, 17, 1, 3, 0, time.UTC) - } - - if BuildNonElyTexturesHash("username") != "42277892fd24bc0ed86285b3bb8b8fad" { - t.Error("Function should change it's value if hour changed") - } -} - -func TestBuildElyUrl(t *testing.T) { - if BuildElyUrl("/route") != "http://ely.by/route" { - t.Error("Function should add prefix to the provided relative url.") - } - - if BuildElyUrl("http://ely.by/test/route") != "http://ely.by/test/route" { - t.Error("Function should do not add prefix to the provided prefixed url.") - } -}