From 2c7a1625f33d2660d5b2f7e381e5bd5e399d5e86 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Sun, 28 Apr 2019 00:43:22 +0300 Subject: [PATCH] #1: Tests for http layer are restored --- http/api_test.go | 850 ++++++++++++++-------------- http/cape_test.go | 207 ++++--- http/http_test.go | 51 +- http/signed_textures.go | 9 +- http/signed_textures_test.go | 170 ++++-- http/skin_test.go | 194 ++++--- http/textures.go | 37 +- http/textures_test.go | 308 +++++----- tests/mojang_textures_queue_mock.go | 33 ++ 9 files changed, 1051 insertions(+), 808 deletions(-) create mode 100644 tests/mojang_textures_queue_mock.go diff --git a/http/api_test.go b/http/api_test.go index cc2cf6f..2d44e18 100644 --- a/http/api_test.go +++ b/http/api_test.go @@ -17,474 +17,482 @@ import ( testify "github.com/stretchr/testify/assert" ) -func TestConfig_PostSkin_Valid(t *testing.T) { - assert := testify.New(t) +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() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - resultModel := createSkinModel("mock_user", false) - resultModel.SkinId = 5 - resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" - resultModel.Url = "http://ely.by/minecraft/skins/default.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" + resultModel := createSkinModel("mock_user", false) + resultModel.SkinId = 5 + resultModel.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.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"}, - "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://ely.by/minecraft/skins/default.png"}, - } - - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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) -} - -func TestConfig_PostSkin_ChangedIdentityId(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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" - resultModel.Url = "http://ely.by/minecraft/skins/default.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"2"}, - "username": {"mock_user"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://ely.by/minecraft/skins/default.png"}, - } - - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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{"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) -} - -func TestConfig_PostSkin_ChangedUsername(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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" - resultModel.Url = "http://ely.by/minecraft/skins/default.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"1"}, - "username": {"changed_username"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://ely.by/minecraft/skins/default.png"}, - } - - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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) -} - -func TestConfig_PostSkin_CompletelyNewIdentity(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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" - resultModel.Url = "http://ely.by/minecraft/skins/default.png" - resultModel.MojangTextures = "" - resultModel.MojangSignature = "" - - form := url.Values{ - "identityId": {"1"}, - "username": {"mock_user"}, - "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, - "skinId": {"5"}, - "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, - "is1_8": {"0"}, - "isSlim": {"0"}, - "url": {"http://ely.by/minecraft/skins/default.png"}, - } - - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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{"unknown"}) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{"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) -} - -func TestConfig_PostSkin_UploadSkin(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://skinsystem.ely.by/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" - ] + form := url.Values{ + "identityId": {"1"}, + "username": {"mock_user"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, } - }`, string(response)) -} -func TestConfig_PostSkin_RequiredFields(t *testing.T) { - assert := testify.New(t) + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() - ctrl := gomock.NewController(t) - defer ctrl.Finish() + 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, mocks := setupMocks(ctrl) + config.CreateHandler().ServeHTTP(w, req) - form := url.Values{ - "mojangTextures": {"someBase64EncodedString"}, - } + resp := w.Result() + defer resp.Body.Close() + assert.Equal(201, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(response) + }) - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() + t.Run("Upload new identity with skin file", func(t *testing.T) { + assert := testify.New(t) - 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)) + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config.CreateHandler().ServeHTTP(w, req) + config, mocks := setupMocks(ctrl) - 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" - ] + 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) } - }`, string(response)) + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + 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"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + form := url.Values{ + "identityId": {"2"}, + "username": {"mock_user"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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.Hash = "94a457d92a61460cb9cb5d6f29732d2a" + resultModel.Url = "http://ely.by/minecraft/skins/default.png" + resultModel.MojangTextures = "" + resultModel.MojangSignature = "" + + form := url.Values{ + "identityId": {"1"}, + "username": {"changed_username"}, + "uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"}, + "skinId": {"5"}, + "hash": {"94a457d92a61460cb9cb5d6f29732d2a"}, + "is1_8": {"0"}, + "isSlim": {"0"}, + "url": {"http://ely.by/minecraft/skins/default.png"}, + } + + req := httptest.NewRequest("POST", "http://skinsystem.ely.by/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://skinsystem.ely.by/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://skinsystem.ely.by/api/skins", nil) + req.Header.Add("Authorization", "Bearer invalid.jwt.token") + w := httptest.NewRecorder() + + mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"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_PostSkin_Unauthorized(t *testing.T) { - assert := testify.New(t) +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() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", nil) - req.Header.Add("Authorization", "Bearer invalid.jwt.token") - w := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil) + w := httptest.NewRecorder() - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"Cannot parse passed JWT token"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) + 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) + 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)) + 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://skinsystem.ely.by/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{"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_DeleteSkinByUserId_Success(t *testing.T) { - assert := testify.New(t) +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() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user", 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)) + 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) + 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) -} + resp := w.Result() + defer resp.Body.Close() + assert.Equal(204, resp.StatusCode) + response, _ := ioutil.ReadAll(resp.Body) + assert.Empty(response) + }) -func TestConfig_DeleteSkinByUserId_NotFound(t *testing.T) { - assert := testify.New(t) + t.Run("Try to remove not exists identity username", func(t *testing.T) { + assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:2", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user_2", nil) + w := httptest.NewRecorder() - mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) - mocks.Skins.EXPECT().FindByUserId(2).Return(nil, &db.SkinNotFoundError{"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)) + mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil) + mocks.Skins.EXPECT().FindByUsername("mock_user_2").Return(nil, &db.SkinNotFoundError{"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) + 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_Success(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/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) -} - -func TestConfig_DeleteSkinByUsername_NotFound(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/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{"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(`[ + 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_SignatureKeyNotSet(t *testing.T) { - assert := testify.New(t) +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() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - req := httptest.NewRequest("POST", "http://localhost", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("POST", "http://localhost", nil) + w := httptest.NewRecorder() - mocks.Auth.EXPECT().Check(gomock.Any()).Return(&auth.Unauthorized{"signing key not available"}) - mocks.Log.EXPECT().IncCounter("authentication.challenge", int64(1)) - mocks.Log.EXPECT().IncCounter("authentication.failed", int64(1)) + 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.Authenticate(http.HandlerFunc(func (resp http.ResponseWriter, req *http.Request) {})) - res.ServeHTTP(w, req) + res := config.Authenticate(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)) + 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 diff --git a/http/cape_test.go b/http/cape_test.go index fe20a48..5c0de9a 100644 --- a/http/cape_test.go +++ b/http/cape_test.go @@ -5,6 +5,7 @@ import ( "image" "image/png" "io/ioutil" + "net/http" "net/http/httptest" "testing" @@ -15,123 +16,147 @@ import ( "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) { - assert := testify.New(t) + performTest := func(t *testing.T, testCase *capesTestCase) { + assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - cape := createCape() + 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"}) + } - mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ - File: bytes.NewReader(cape), - }, nil) - mocks.Log.EXPECT().IncCounter("capes.request", int64(1)) + if testCase.ExistsInMojang { + textures := createTexturesResponse(false, testCase.HasCapeInMojangResp) + mocks.Queue.On("GetTexturesForUsername", "mock_username").Return(textures) + } else { + mocks.Queue.On("GetTexturesForUsername", "mock_username").Return(nil) + } - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/mocked_username", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("GET", testCase.RequestUrl, nil) + w := httptest.NewRecorder() - config.CreateHandler().ServeHTTP(w, req) + 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")) -} + resp := w.Result() + testCase.AssertResponse(assert, resp) + } -func TestConfig_Cape2(t *testing.T) { - assert := testify.New(t) + 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) + }) + } + }) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + 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) + }) + } - config, mocks := setupMocks(ctrl) + t.Run("Should trim trailing slash", func(t *testing.T) { + assert := testify.New(t) - mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) - mocks.Log.EXPECT().IncCounter("capes.request", int64(1)) + req := httptest.NewRequest("GET", "http://chrly/cloaks/?name=notch", nil) + w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/cloaks/notch", nil) - w := httptest.NewRecorder() + (&Config{}).CreateHandler().ServeHTTP(w, req) - config.CreateHandler().ServeHTTP(w, req) + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://chrly/cloaks?name=notch", resp.Header.Get("Location")) + }) - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://skins.minecraft.net/MinecraftCloaks/notch.png", resp.Header.Get("Location")) -} + t.Run("Return error when name is not provided", func(t *testing.T) { + assert := testify.New(t) -func TestConfig_CapeGET(t *testing.T) { - assert := testify.New(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() - ctrl := gomock.NewController(t) - defer ctrl.Finish() + config, mocks := setupMocks(ctrl) + mocks.Log.EXPECT().IncCounter("capes.get_request", int64(1)) - config, mocks := setupMocks(ctrl) + req := httptest.NewRequest("GET", "http://chrly/cloaks", nil) + w := httptest.NewRecorder() - cape := createCape() + config.CreateHandler().ServeHTTP(w, req) - mocks.Capes.EXPECT().FindByUsername("mocked_username").Return(&model.Cape{ - File: bytes.NewReader(cape), - }, nil) - mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0) - mocks.Log.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, mocks := setupMocks(ctrl) - - mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{"notch"}) - mocks.Log.EXPECT().IncCounter("capes.request", int64(1)).Times(0) - mocks.Log.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")) + 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) - + _ = png.Encode(writer, img) pngBytes, _ := ioutil.ReadAll(writer) return pngBytes diff --git a/http/http_test.go b/http/http_test.go index 884899a..66b3472 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -2,7 +2,11 @@ package http import ( "testing" + "time" + "github.com/elyby/chrly/api/mojang" + + "github.com/elyby/chrly/tests" "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" @@ -19,6 +23,7 @@ func TestParseUsername(t *testing.T) { type mocks struct { Skins *mock_interfaces.MockSkinsRepository Capes *mock_interfaces.MockCapesRepository + Queue *tests.MojangTexturesQueueMock Auth *mock_interfaces.MockAuthChecker Log *mock_wd.MockWatchdog } @@ -31,16 +36,54 @@ func setupMocks(ctrl *gomock.Controller) ( capesRepo := mock_interfaces.NewMockCapesRepository(ctrl) authChecker := mock_interfaces.NewMockAuthChecker(ctrl) wd := mock_wd.NewMockWatchdog(ctrl) + texturesQueue := &tests.MojangTexturesQueueMock{} return &Config{ - SkinsRepo: skinsRepo, - CapesRepo: capesRepo, - Auth: authChecker, - Logger: wd, + SkinsRepo: skinsRepo, + CapesRepo: capesRepo, + Auth: authChecker, + MojangTexturesQueue: texturesQueue, + Logger: wd, }, &mocks{ Skins: skinsRepo, Capes: capesRepo, Auth: authChecker, + Queue: texturesQueue, 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 +} diff --git a/http/signed_textures.go b/http/signed_textures.go index 7134516..34002a5 100644 --- a/http/signed_textures.go +++ b/http/signed_textures.go @@ -27,10 +27,6 @@ func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Re Signature: rec.MojangSignature, Value: rec.MojangTextures, }, - { - Name: "chrly", - Value: "how do you tame a horse in Minecraft?", - }, }, } } else if request.URL.Query().Get("proxy") != "" { @@ -42,6 +38,11 @@ func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Re 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 index 41934f5..c10fe1d 100644 --- a/http/signed_textures_test.go +++ b/http/signed_textures_test.go @@ -12,60 +12,130 @@ import ( ) func TestConfig_SignedTextures(t *testing.T) { - assert := testify.New(t) + t.Run("Obtain signed textures for exists user", func(t *testing.T) { + assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Log.EXPECT().IncCounter("signed_textures.request", int64(1)) + 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://skinsystem.ely.by/textures/signed/mock_user", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://chrly/textures/signed/mock_user", nil) + w := httptest.NewRecorder() - config.CreateHandler().ServeHTTP(w, req) + 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)) -} - -func TestConfig_SignedTextures2(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(nil, &db.SkinNotFoundError{}) - mocks.Log.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)) + 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.Queue.On("GetTexturesForUsername", "mock_user").Once().Return(createTexturesResponse(true, false)) + + 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_test.go b/http/skin_test.go index 1540171..fbc1da8 100644 --- a/http/skin_test.go +++ b/http/skin_test.go @@ -1,6 +1,7 @@ package http import ( + "net/http" "net/http/httptest" "testing" @@ -11,113 +12,146 @@ import ( "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) { - assert := testify.New(t) + performTest := func(t *testing.T, testCase *skinsTestCase) { + assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Log.EXPECT().IncCounter("skins.request", int64(1)) + 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"}) + } - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/mock_user", nil) - w := httptest.NewRecorder() + if testCase.ExistsInMojang { + textures := createTexturesResponse(testCase.HasSkinInMojangResp, true) + mocks.Queue.On("GetTexturesForUsername", "mock_username").Return(textures) + } else { + mocks.Queue.On("GetTexturesForUsername", "mock_username").Return(nil) + } - config.CreateHandler().ServeHTTP(w, req) + req := httptest.NewRequest("GET", testCase.RequestUrl, nil) + w := httptest.NewRecorder() - resp := w.Result() - assert.Equal(301, resp.StatusCode) - assert.Equal("http://ely.by/minecraft/skins/skin.png", resp.Header.Get("Location")) -} + config.CreateHandler().ServeHTTP(w, req) -func TestConfig_Skin2(t *testing.T) { - assert := testify.New(t) + resp := w.Result() + testCase.AssertResponse(assert, resp) + } - ctrl := gomock.NewController(t) - defer ctrl.Finish() + 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) + }) + } + }) - config, mocks := setupMocks(ctrl) + 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) + }) + } - mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) - mocks.Log.EXPECT().IncCounter("skins.request", int64(1)) + t.Run("Should trim trailing slash", func(t *testing.T) { + assert := testify.New(t) - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins/notch", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://chrly/skins/?name=notch", nil) + w := httptest.NewRecorder() - config.CreateHandler().ServeHTTP(w, req) + (&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")) -} + resp := w.Result() + assert.Equal(301, resp.StatusCode) + assert.Equal("http://chrly/skins?name=notch", resp.Header.Get("Location")) + }) -func TestConfig_SkinGET(t *testing.T) { - assert := testify.New(t) + t.Run("Return error when name is not provided", func(t *testing.T) { + assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) + mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) - mocks.Log.EXPECT().IncCounter("skins.request", int64(1)).Times(0) + req := httptest.NewRequest("GET", "http://chrly/skins", nil) + w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/skins?name=mock_user", nil) - w := httptest.NewRecorder() + config.CreateHandler().ServeHTTP(w, req) - 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, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{"notch"}) - mocks.Log.EXPECT().IncCounter("skins.get_request", int64(1)) - mocks.Log.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")) + 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", + Uuid: "0f657aa8-bfbe-415d-b700-5750090d3af3", // Use non nil UUID to pass validation in api tests SkinId: 1, - Hash: "55d2a8848764f5ff04012cdb093458bd", - Url: "http://ely.by/minecraft/skins/skin.png", + Hash: "00000000000000000000000000000000", + Url: "http://chrly/skin.png", MojangTextures: "mocked textures base64", MojangSignature: "mocked signature", IsSlim: isSlim, diff --git a/http/textures.go b/http/textures.go index 1489eb9..c9f2dec 100644 --- a/http/textures.go +++ b/http/textures.go @@ -14,29 +14,28 @@ func (cfg *Config) Textures(response http.ResponseWriter, request *http.Request) username := parseUsername(mux.Vars(request)["username"]) var textures *mojang.TexturesResponse - skin, err := cfg.SkinsRepo.FindByUsername(username) - if err == nil && skin.SkinId != 0 { - textures = &mojang.TexturesResponse{ - Skin: &mojang.SkinTexturesResponse{ + 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 { - textures.Skin.Metadata = &mojang.SkinTexturesMetadata{ - Model: "slim", - } - } - - _, err = cfg.CapesRepo.FindByUsername(username) - if err == nil { - var scheme = "http://" - if request.TLS != nil { - scheme = "https://" } + if skin.IsSlim { + skinTextures.Metadata = &mojang.SkinTexturesMetadata{ + Model: "slim", + } + } + + textures.Skin = skinTextures + } + + if capeErr == nil { textures.Cape = &mojang.CapeTexturesResponse{ - Url: scheme + request.Host + "/cloaks/" + username, + Url: request.URL.Scheme + "://" + request.Host + "/cloaks/" + username, } } } else { diff --git a/http/textures_test.go b/http/textures_test.go index c4c879f..be6ae3a 100644 --- a/http/textures_test.go +++ b/http/textures_test.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "net/http/httptest" "testing" - "time" "github.com/golang/mock/gomock" testify "github.com/stretchr/testify/assert" @@ -15,152 +14,183 @@ import ( ) func TestConfig_Textures(t *testing.T) { - assert := testify.New(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() + ctrl := gomock.NewController(t) + defer ctrl.Finish() - config, mocks := setupMocks(ctrl) + config, mocks := setupMocks(ctrl) - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", false), nil) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) - mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) + mocks.Log.EXPECT().IncCounter("textures.request", int64(1)) - req := httptest.NewRequest("GET", "http://skinsystem.ely.by/textures/mock_user", nil) - w := httptest.NewRecorder() + 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"}) - config.CreateHandler().ServeHTTP(w, req) + req := httptest.NewRequest("GET", "http://chrly/textures/mock_user", nil) + w := httptest.NewRecorder() - 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)) -} + config.CreateHandler().ServeHTTP(w, req) -func TestConfig_Textures2(t *testing.T) { - assert := testify.New(t) - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("mock_user").Return(createSkinModel("mock_user", true), nil) - mocks.Capes.EXPECT().FindByUsername("mock_user").Return(nil, &db.CapeNotFoundError{"mock_user"}) - mocks.Log.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" + 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)) + }`, 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.Queue.On("GetTexturesForUsername", "mock_username").Once().Return(createTexturesResponse(true, true)) + + 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.Queue.On("GetTexturesForUsername", "mock_username").Once().Return(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) // TODO: this is not confirmed behavior + }) } -func TestConfig_Textures3(t *testing.T) { - assert := testify.New(t) - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - config, mocks := setupMocks(ctrl) - - 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) - mocks.Log.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, mocks := setupMocks(ctrl) - - mocks.Skins.EXPECT().FindByUsername("notch").Return(nil, &db.SkinNotFoundError{}) - mocks.Capes.EXPECT().FindByUsername("notch").Return(nil, &db.CapeNotFoundError{}) - mocks.Log.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/tests/mojang_textures_queue_mock.go b/tests/mojang_textures_queue_mock.go new file mode 100644 index 0000000..0494243 --- /dev/null +++ b/tests/mojang_textures_queue_mock.go @@ -0,0 +1,33 @@ +package tests + +import ( + "github.com/elyby/chrly/api/mojang" + + "github.com/stretchr/testify/mock" +) + +type MojangTexturesQueueMock struct { + mock.Mock +} + +func (m *MojangTexturesQueueMock) GetTexturesForUsername(username string) chan *mojang.SignedTexturesResponse { + args := m.Called(username) + result := make(chan *mojang.SignedTexturesResponse) + arg := args.Get(0) + switch arg.(type) { + case *mojang.SignedTexturesResponse: + go func() { + result <- arg.(*mojang.SignedTexturesResponse) + }() + case chan *mojang.SignedTexturesResponse: + return arg.(chan *mojang.SignedTexturesResponse) + case nil: + go func() { + result <- nil + }() + default: + panic("unsupported return value") + } + + return result +}