mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Completely move app configuration from cmd to di container
Implemented graceful server shutdown Extract records manipulating API into separate handlers group
This commit is contained in:
209
http/api.go
Normal file
209
http/api.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/thedevsaddam/govalidator"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
//noinspection GoSnakeCaseUsage
|
||||
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
|
||||
var regexUuidAny = regexp.MustCompile(UUID_ANY)
|
||||
|
||||
func init() {
|
||||
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||
if message == "" {
|
||||
message = "Skin uploading is temporary unavailable"
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
})
|
||||
|
||||
// Add ability to validate any possible uuid form
|
||||
govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
|
||||
str := value.(string)
|
||||
if !regexUuidAny.MatchString(str) {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("The %s field must contain valid UUID", field)
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type Api struct {
|
||||
Emitter
|
||||
SkinsRepo SkinsRepository
|
||||
}
|
||||
|
||||
func (ctx *Api) Handler() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
router.HandleFunc("/skins", ctx.postSkinHandler).Methods(http.MethodPost)
|
||||
router.HandleFunc("/skins/id:{id:[0-9]+}", ctx.deleteSkinByUserIdHandler).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/skins/{username}", ctx.deleteSkinByUsernameHandler).Methods(http.MethodDelete)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
validationErrors := validatePostSkinRequest(req)
|
||||
if validationErrors != nil {
|
||||
apiBadRequest(resp, validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
||||
username := req.Form.Get("username")
|
||||
|
||||
record, err := ctx.findIdentityOrCleanup(identityId, username)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
||||
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
||||
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
||||
|
||||
record.Uuid = req.Form.Get("uuid")
|
||||
record.SkinId = skinId
|
||||
record.Is1_8 = is18
|
||||
record.IsSlim = isSlim
|
||||
record.Url = req.Form.Get("url")
|
||||
record.MojangTextures = req.Form.Get("mojangTextures")
|
||||
record.MojangSignature = req.Form.Get("mojangSignature")
|
||||
|
||||
err = ctx.SkinsRepo.Save(record)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (ctx *Api) deleteSkinByUserIdHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
||||
skin, err := ctx.SkinsRepo.FindByUserId(id)
|
||||
ctx.deleteSkin(skin, err, resp)
|
||||
}
|
||||
|
||||
func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.Request) {
|
||||
username := mux.Vars(req)["username"]
|
||||
skin, err := ctx.SkinsRepo.FindByUsername(username)
|
||||
ctx.deleteSkin(skin, err, resp)
|
||||
}
|
||||
|
||||
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
||||
if err != nil {
|
||||
if _, ok := err.(*SkinNotFoundError); ok {
|
||||
apiNotFound(resp, "Cannot find record for the requested identifier")
|
||||
} else {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.SkinsRepo.RemoveByUserId(skin.UserId)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (ctx *Api) findIdentityOrCleanup(identityId int, username string) (*model.Skin, error) {
|
||||
var record *model.Skin
|
||||
record, err := ctx.SkinsRepo.FindByUserId(identityId)
|
||||
if err != nil {
|
||||
if _, isSkinNotFound := err.(*SkinNotFoundError); !isSkinNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err = ctx.SkinsRepo.FindByUsername(username)
|
||||
if err == nil {
|
||||
_ = ctx.SkinsRepo.RemoveByUsername(username)
|
||||
record.UserId = identityId
|
||||
} else {
|
||||
record = &model.Skin{
|
||||
UserId: identityId,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
} else if record.Username != username {
|
||||
_ = ctx.SkinsRepo.RemoveByUserId(identityId)
|
||||
record.Username = username
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||
const maxMultipartMemory int64 = 32 << 20
|
||||
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||
|
||||
_ = request.ParseMultipartForm(maxMultipartMemory)
|
||||
|
||||
validationRules := govalidator.MapData{
|
||||
"identityId": {"required", "numeric", "min:1"},
|
||||
"username": {"required"},
|
||||
"uuid": {"required", "uuid_any"},
|
||||
"skinId": {"required", "numeric", "min:1"},
|
||||
"url": {"url"},
|
||||
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||
"is1_8": {"bool"},
|
||||
"isSlim": {"bool"},
|
||||
}
|
||||
|
||||
shouldAppendSkinRequiredError := false
|
||||
url := request.Form.Get("url")
|
||||
_, _, skinErr := request.FormFile("skin")
|
||||
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||
shouldAppendSkinRequiredError = true
|
||||
} else if skinErr == nil {
|
||||
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||
} else if url != "" {
|
||||
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||
}
|
||||
|
||||
mojangTextures := request.Form.Get("mojangTextures")
|
||||
if mojangTextures != "" {
|
||||
validationRules["mojangSignature"] = []string{"required"}
|
||||
}
|
||||
|
||||
validator := govalidator.New(govalidator.Options{
|
||||
Request: request,
|
||||
Rules: validationRules,
|
||||
RequiredDefault: false,
|
||||
FormSize: maxMultipartMemory,
|
||||
})
|
||||
validationResults := validator.Validate()
|
||||
if shouldAppendSkinRequiredError {
|
||||
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||
}
|
||||
|
||||
if len(validationResults) != 0 {
|
||||
return validationResults
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
442
http/api_test.go
Normal file
442
http/api_test.go
Normal file
@@ -0,0 +1,442 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
/***************
|
||||
* Setup mocks *
|
||||
***************/
|
||||
|
||||
type apiTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
App *Api
|
||||
|
||||
SkinsRepository *skinsRepositoryMock
|
||||
Emitter *emitterMock
|
||||
}
|
||||
|
||||
/********************
|
||||
* Setup test suite *
|
||||
********************/
|
||||
|
||||
func (suite *apiTestSuite) SetupTest() {
|
||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||
suite.Emitter = &emitterMock{}
|
||||
|
||||
suite.App = &Api{
|
||||
SkinsRepo: suite.SkinsRepository,
|
||||
Emitter: suite.Emitter,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *apiTestSuite) TearDownTest() {
|
||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
|
||||
suite.SetupTest()
|
||||
suite.Run(name, subTest)
|
||||
suite.TearDownTest()
|
||||
}
|
||||
|
||||
/*************
|
||||
* Run tests *
|
||||
*************/
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
suite.Run(t, new(apiTestSuite))
|
||||
}
|
||||
|
||||
/*************************
|
||||
* Post skin tests cases *
|
||||
*************************/
|
||||
|
||||
type postSkinTestCase struct {
|
||||
Name string
|
||||
Form io.Reader
|
||||
BeforeTest func(suite *apiTestSuite)
|
||||
AfterTest func(suite *apiTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
var postSkinTestsCases = []*postSkinTestCase{
|
||||
{
|
||||
Name: "Upload new identity with textures data",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
suite.Equal(5, model.SkinId)
|
||||
suite.False(model.Is1_8)
|
||||
suite.False(model.IsSlim)
|
||||
suite.Equal("http://example.com/skin.png", model.Url)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing only textures data",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"1"},
|
||||
"isSlim": {"1"},
|
||||
"url": {"http://textures-server.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
suite.Equal(5, model.SkinId)
|
||||
suite.True(model.Is1_8)
|
||||
suite.True(model.IsSlim)
|
||||
suite.Equal("http://textures-server.com/skin.png", model.Url)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing its identityId",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"2"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 2).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUsername", "mock_username").Times(1).Return(nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(2, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing its username",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Times(1).Return(nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("changed_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when loading the data from the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"1"},
|
||||
"isSlim": {"1"},
|
||||
"url": {"http://textures-server.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("Save", mock.Anything).Return(err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "unable to save record to the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when saving the data into the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *apiTestSuite) {
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "error on requesting a skin from the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *apiTestSuite) TestPostSkin() {
|
||||
for _, testCase := range postSkinTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/skins", testCase.Form)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Get errors about required fields", func() {
|
||||
req := httptest.NewRequest("POST", "http://chrly/skins", bytes.NewBufferString(url.Values{
|
||||
"mojangTextures": {"someBase64EncodedString"},
|
||||
}.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"errors": {
|
||||
"identityId": [
|
||||
"The identityId field is required",
|
||||
"The identityId field must be numeric",
|
||||
"The identityId field must be minimum 1 char"
|
||||
],
|
||||
"skinId": [
|
||||
"The skinId field is required",
|
||||
"The skinId field must be numeric",
|
||||
"The skinId field must be minimum 1 char"
|
||||
],
|
||||
"username": [
|
||||
"The username field is required"
|
||||
],
|
||||
"uuid": [
|
||||
"The uuid field is required",
|
||||
"The uuid field must contain valid UUID"
|
||||
],
|
||||
"url": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"skin": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"mojangSignature": [
|
||||
"The mojangSignature field is required"
|
||||
]
|
||||
}
|
||||
}`, string(body))
|
||||
})
|
||||
|
||||
suite.RunSubTest("Upload textures with skin as file", func() {
|
||||
inputBody := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(inputBody)
|
||||
|
||||
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||
_, _ = part.Write(loadSkinFile())
|
||||
|
||||
_ = writer.WriteField("identityId", "1")
|
||||
_ = writer.WriteField("username", "mock_user")
|
||||
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||
_ = writer.WriteField("skinId", "5")
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/skins", inputBody)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
responseBody, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"errors": {
|
||||
"skin": [
|
||||
"Skin uploading is temporary unavailable"
|
||||
]
|
||||
}
|
||||
}`, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
/**************************************
|
||||
* Delete skin by user id tests cases *
|
||||
**************************************/
|
||||
|
||||
func (suite *apiTestSuite) TestDeleteByUserId() {
|
||||
suite.RunSubTest("Delete skin by its identity id", func() {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(204, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.Empty(body)
|
||||
})
|
||||
|
||||
suite.RunSubTest("Try to remove not exists identity id", func() {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/skins/id:1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(404, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`[
|
||||
"Cannot find record for the requested identifier"
|
||||
]`, string(body))
|
||||
})
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* Delete skin by username tests cases *
|
||||
***************************************/
|
||||
|
||||
func (suite *apiTestSuite) TestDeleteByUsername() {
|
||||
suite.RunSubTest("Delete skin by its identity username", func() {
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(204, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.Empty(body)
|
||||
})
|
||||
|
||||
suite.RunSubTest("Try to remove not exists identity username", func() {
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(404, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`[
|
||||
"Cannot find record for the requested identifier"
|
||||
]`, string(body))
|
||||
})
|
||||
}
|
||||
|
||||
/*************
|
||||
* Utilities *
|
||||
*************/
|
||||
|
||||
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
||||
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
||||
|
||||
func loadSkinFile() []byte {
|
||||
result := make([]byte, 92)
|
||||
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
35
http/http.go
35
http/http.go
@@ -1,13 +1,18 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mono83/slf"
|
||||
"github.com/mono83/slf/wd"
|
||||
)
|
||||
|
||||
type Emitter interface {
|
||||
@@ -31,6 +36,34 @@ func Serve(address string, handler http.Handler) error {
|
||||
return server.Serve(listener)
|
||||
}
|
||||
|
||||
func StartServer(server *http.Server, logger slf.Logger) {
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
logger.Info("Starting the server, HTTP on: :addr", wd.StringParam("addr", server.Addr))
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
logger.Emergency("Error in main(): :err", wd.ErrParam(err))
|
||||
close(done)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
s := waitForExitSignal()
|
||||
logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
|
||||
server.Shutdown(context.Background())
|
||||
logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
|
||||
close(done)
|
||||
}()
|
||||
|
||||
<-done
|
||||
}
|
||||
|
||||
func waitForExitSignal() os.Signal {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill)
|
||||
|
||||
return <-ch
|
||||
}
|
||||
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
@@ -78,7 +111,7 @@ func CreateAuthenticationMiddleware(checker Authenticator) mux.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func NotFound(response http.ResponseWriter, _ *http.Request) {
|
||||
func NotFoundHandler(response http.ResponseWriter, _ *http.Request) {
|
||||
data, _ := json.Marshal(map[string]string{
|
||||
"status": "404",
|
||||
"message": "Not Found",
|
||||
|
@@ -93,13 +93,13 @@ func TestCreateAuthenticationMiddleware(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotFound(t *testing.T) {
|
||||
func TestNotFoundHandler(t *testing.T) {
|
||||
assert := testify.New(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
NotFound(w, req)
|
||||
NotFoundHandler(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
assert.Equal(404, resp.StatusCode)
|
||||
|
@@ -3,49 +3,16 @@ package http
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/thedevsaddam/govalidator"
|
||||
|
||||
"github.com/elyby/chrly/api/mojang"
|
||||
"github.com/elyby/chrly/model"
|
||||
)
|
||||
|
||||
//noinspection GoSnakeCaseUsage
|
||||
const UUID_ANY = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
|
||||
var regexUuidAny = regexp.MustCompile(UUID_ANY)
|
||||
|
||||
func init() {
|
||||
govalidator.AddCustomRule("skinUploadingNotAvailable", func(field string, rule string, message string, value interface{}) error {
|
||||
if message == "" {
|
||||
message = "Skin uploading is temporary unavailable"
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
})
|
||||
|
||||
// Add ability to validate any possible uuid form
|
||||
govalidator.AddCustomRule("uuid_any", func(field string, rule string, message string, value interface{}) error {
|
||||
str := value.(string)
|
||||
if !regexUuidAny.MatchString(str) {
|
||||
if message == "" {
|
||||
message = fmt.Sprintf("The %s field must contain valid UUID", field)
|
||||
}
|
||||
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type SkinsRepository interface {
|
||||
FindByUsername(username string) (*model.Skin, error)
|
||||
FindByUserId(id int) (*model.Skin, error)
|
||||
@@ -58,6 +25,7 @@ type CapesRepository interface {
|
||||
FindByUsername(username string) (*model.Cape, error)
|
||||
}
|
||||
|
||||
// TODO: can I get rid of this?
|
||||
type SkinNotFoundError struct {
|
||||
Who string
|
||||
}
|
||||
@@ -70,6 +38,7 @@ type CapeNotFoundError struct {
|
||||
Who string
|
||||
}
|
||||
|
||||
// TODO: can I get rid of this?
|
||||
func (e CapeNotFoundError) Error() string {
|
||||
return "cape file not found"
|
||||
}
|
||||
@@ -80,42 +49,28 @@ type MojangTexturesProvider interface {
|
||||
|
||||
type Skinsystem struct {
|
||||
Emitter
|
||||
TexturesExtraParamName string
|
||||
TexturesExtraParamValue string
|
||||
SkinsRepo SkinsRepository
|
||||
CapesRepo CapesRepository
|
||||
MojangTexturesProvider MojangTexturesProvider
|
||||
Authenticator Authenticator
|
||||
TexturesExtraParamName string
|
||||
TexturesExtraParamValue string
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) CreateHandler() *mux.Router {
|
||||
requestEventsMiddleware := CreateRequestEventsMiddleware(ctx.Emitter, "skinsystem")
|
||||
|
||||
func (ctx *Skinsystem) Handler() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
router.Use(requestEventsMiddleware)
|
||||
|
||||
router.HandleFunc("/skins/{username}", ctx.Skin).Methods(http.MethodGet)
|
||||
router.HandleFunc("/cloaks/{username}", ctx.Cape).Methods(http.MethodGet).Name("cloaks")
|
||||
router.HandleFunc("/textures/{username}", ctx.Textures).Methods(http.MethodGet)
|
||||
router.HandleFunc("/textures/signed/{username}", ctx.SignedTextures).Methods(http.MethodGet)
|
||||
router.HandleFunc("/skins/{username}", ctx.skinHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
|
||||
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
||||
// Legacy
|
||||
router.HandleFunc("/skins", ctx.SkinGET).Methods(http.MethodGet)
|
||||
router.HandleFunc("/cloaks", ctx.CapeGET).Methods(http.MethodGet)
|
||||
// API
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
apiRouter.Use(CreateAuthenticationMiddleware(ctx.Authenticator))
|
||||
apiRouter.HandleFunc("/skins", ctx.PostSkin).Methods(http.MethodPost)
|
||||
apiRouter.HandleFunc("/skins/id:{id:[0-9]+}", ctx.DeleteSkinByUserId).Methods(http.MethodDelete)
|
||||
apiRouter.HandleFunc("/skins/{username}", ctx.DeleteSkinByUsername).Methods(http.MethodDelete)
|
||||
// 404
|
||||
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
|
||||
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
|
||||
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFound))
|
||||
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
||||
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := ctx.SkinsRepo.FindByUsername(username)
|
||||
if err == nil && rec.SkinId != 0 {
|
||||
@@ -139,7 +94,7 @@ func (ctx *Skinsystem) Skin(response http.ResponseWriter, request *http.Request)
|
||||
http.Redirect(response, request, skin.Url, 301)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := request.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
@@ -149,10 +104,10 @@ func (ctx *Skinsystem) SkinGET(response http.ResponseWriter, request *http.Reque
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
ctx.Skin(response, request)
|
||||
ctx.skinHandler(response, request)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
rec, err := ctx.CapesRepo.FindByUsername(username)
|
||||
if err == nil {
|
||||
@@ -177,7 +132,7 @@ func (ctx *Skinsystem) Cape(response http.ResponseWriter, request *http.Request)
|
||||
http.Redirect(response, request, cape.Url, 301)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := request.URL.Query().Get("name")
|
||||
if username == "" {
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
@@ -187,10 +142,10 @@ func (ctx *Skinsystem) CapeGET(response http.ResponseWriter, request *http.Reque
|
||||
mux.Vars(request)["username"] = username
|
||||
mux.Vars(request)["converted"] = "1"
|
||||
|
||||
ctx.Cape(response, request)
|
||||
ctx.capeHandler(response, request)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
var textures *mojang.TexturesResponse
|
||||
@@ -233,6 +188,7 @@ func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Requ
|
||||
}
|
||||
|
||||
textures = texturesProp.Textures
|
||||
// TODO: return 204 in case when there is no skin and cape on mojang textures
|
||||
}
|
||||
|
||||
responseData, _ := json.Marshal(textures)
|
||||
@@ -240,7 +196,7 @@ func (ctx *Skinsystem) Textures(response http.ResponseWriter, request *http.Requ
|
||||
_, _ = response.Write(responseData)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *http.Request) {
|
||||
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
|
||||
var responseData *mojang.SignedTexturesResponse
|
||||
@@ -280,158 +236,6 @@ func (ctx *Skinsystem) SignedTextures(response http.ResponseWriter, request *htt
|
||||
_, _ = response.Write(responseJson)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) PostSkin(resp http.ResponseWriter, req *http.Request) {
|
||||
validationErrors := validatePostSkinRequest(req)
|
||||
if validationErrors != nil {
|
||||
apiBadRequest(resp, validationErrors)
|
||||
return
|
||||
}
|
||||
|
||||
identityId, _ := strconv.Atoi(req.Form.Get("identityId"))
|
||||
username := req.Form.Get("username")
|
||||
|
||||
record, err := findIdentity(ctx.SkinsRepo, identityId, username)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
skinId, _ := strconv.Atoi(req.Form.Get("skinId"))
|
||||
is18, _ := strconv.ParseBool(req.Form.Get("is1_8"))
|
||||
isSlim, _ := strconv.ParseBool(req.Form.Get("isSlim"))
|
||||
|
||||
record.Uuid = req.Form.Get("uuid")
|
||||
record.SkinId = skinId
|
||||
record.Is1_8 = is18
|
||||
record.IsSlim = isSlim
|
||||
record.Url = req.Form.Get("url")
|
||||
record.MojangTextures = req.Form.Get("mojangTextures")
|
||||
record.MojangSignature = req.Form.Get("mojangSignature")
|
||||
|
||||
err = ctx.SkinsRepo.Save(record)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) DeleteSkinByUserId(resp http.ResponseWriter, req *http.Request) {
|
||||
id, _ := strconv.Atoi(mux.Vars(req)["id"])
|
||||
skin, err := ctx.SkinsRepo.FindByUserId(id)
|
||||
ctx.deleteSkin(skin, err, resp)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) DeleteSkinByUsername(resp http.ResponseWriter, req *http.Request) {
|
||||
username := mux.Vars(req)["username"]
|
||||
skin, err := ctx.SkinsRepo.FindByUsername(username)
|
||||
ctx.deleteSkin(skin, err, resp)
|
||||
}
|
||||
|
||||
func (ctx *Skinsystem) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
||||
if err != nil {
|
||||
if _, ok := err.(*SkinNotFoundError); ok {
|
||||
apiNotFound(resp, "Cannot find record for the requested identifier")
|
||||
} else {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
||||
apiServerError(resp)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.SkinsRepo.RemoveByUserId(skin.UserId)
|
||||
if err != nil {
|
||||
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
||||
apiServerError(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func validatePostSkinRequest(request *http.Request) map[string][]string {
|
||||
const maxMultipartMemory int64 = 32 << 20
|
||||
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
|
||||
|
||||
_ = request.ParseMultipartForm(maxMultipartMemory)
|
||||
|
||||
validationRules := govalidator.MapData{
|
||||
"identityId": {"required", "numeric", "min:1"},
|
||||
"username": {"required"},
|
||||
"uuid": {"required", "uuid_any"},
|
||||
"skinId": {"required", "numeric", "min:1"},
|
||||
"url": {"url"},
|
||||
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
|
||||
"is1_8": {"bool"},
|
||||
"isSlim": {"bool"},
|
||||
}
|
||||
|
||||
shouldAppendSkinRequiredError := false
|
||||
url := request.Form.Get("url")
|
||||
_, _, skinErr := request.FormFile("skin")
|
||||
if (url != "" && skinErr == nil) || (url == "" && skinErr != nil) {
|
||||
shouldAppendSkinRequiredError = true
|
||||
} else if skinErr == nil {
|
||||
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
|
||||
} else if url != "" {
|
||||
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
|
||||
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
|
||||
}
|
||||
|
||||
mojangTextures := request.Form.Get("mojangTextures")
|
||||
if mojangTextures != "" {
|
||||
validationRules["mojangSignature"] = []string{"required"}
|
||||
}
|
||||
|
||||
validator := govalidator.New(govalidator.Options{
|
||||
Request: request,
|
||||
Rules: validationRules,
|
||||
RequiredDefault: false,
|
||||
FormSize: maxMultipartMemory,
|
||||
})
|
||||
validationResults := validator.Validate()
|
||||
if shouldAppendSkinRequiredError {
|
||||
validationResults["url"] = append(validationResults["url"], oneOfSkinOrUrlMessage)
|
||||
validationResults["skin"] = append(validationResults["skin"], oneOfSkinOrUrlMessage)
|
||||
}
|
||||
|
||||
if len(validationResults) != 0 {
|
||||
return validationResults
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findIdentity(repo SkinsRepository, identityId int, username string) (*model.Skin, error) {
|
||||
var record *model.Skin
|
||||
record, err := repo.FindByUserId(identityId)
|
||||
if err != nil {
|
||||
if _, isSkinNotFound := err.(*SkinNotFoundError); !isSkinNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err = repo.FindByUsername(username)
|
||||
if err == nil {
|
||||
_ = repo.RemoveByUsername(username)
|
||||
record.UserId = identityId
|
||||
} else {
|
||||
record = &model.Skin{
|
||||
UserId: identityId,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
} else if record.Username != username {
|
||||
_ = repo.RemoveByUserId(identityId)
|
||||
record.Username = username
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func parseUsername(username string) string {
|
||||
return strings.TrimSuffix(username, ".png")
|
||||
}
|
||||
|
@@ -2,16 +2,11 @@ package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -102,7 +97,6 @@ type skinsystemTestSuite struct {
|
||||
SkinsRepository *skinsRepositoryMock
|
||||
CapesRepository *capesRepositoryMock
|
||||
MojangTexturesProvider *mojangTexturesProviderMock
|
||||
Auth *authCheckerMock
|
||||
Emitter *emitterMock
|
||||
}
|
||||
|
||||
@@ -114,14 +108,12 @@ func (suite *skinsystemTestSuite) SetupTest() {
|
||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||
suite.CapesRepository = &capesRepositoryMock{}
|
||||
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
||||
suite.Auth = &authCheckerMock{}
|
||||
suite.Emitter = &emitterMock{}
|
||||
|
||||
suite.App = &Skinsystem{
|
||||
SkinsRepo: suite.SkinsRepository,
|
||||
CapesRepo: suite.CapesRepository,
|
||||
MojangTexturesProvider: suite.MojangTexturesProvider,
|
||||
Authenticator: suite.Auth,
|
||||
Emitter: suite.Emitter,
|
||||
}
|
||||
}
|
||||
@@ -130,7 +122,6 @@ func (suite *skinsystemTestSuite) TearDownTest() {
|
||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||
suite.CapesRepository.AssertExpectations(suite.T())
|
||||
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
||||
suite.Auth.AssertExpectations(suite.T())
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
@@ -205,28 +196,24 @@ var skinsTestsCases = []*skinsystemTestCase{
|
||||
func (suite *skinsystemTestSuite) TestSkin() {
|
||||
for _, testCase := range skinsTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Pass username with png extension", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
suite.Equal(301, resp.StatusCode)
|
||||
@@ -237,27 +224,23 @@ func (suite *skinsystemTestSuite) TestSkin() {
|
||||
func (suite *skinsystemTestSuite) TestSkinGET() {
|
||||
for _, testCase := range skinsTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Do not pass name param", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
@@ -317,28 +300,24 @@ var capesTestsCases = []*skinsystemTestCase{
|
||||
func (suite *skinsystemTestSuite) TestCape() {
|
||||
for _, testCase := range capesTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Pass username with png extension", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.CapesRepository.On("FindByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
suite.Equal(200, resp.StatusCode)
|
||||
@@ -351,27 +330,23 @@ func (suite *skinsystemTestSuite) TestCape() {
|
||||
func (suite *skinsystemTestSuite) TestCapeGET() {
|
||||
for _, testCase := range capesTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Do not pass name param", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
@@ -494,14 +469,12 @@ var texturesTestsCases = []*skinsystemTestCase{
|
||||
func (suite *skinsystemTestSuite) TestTextures() {
|
||||
for _, testCase := range texturesTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
@@ -619,8 +592,6 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
||||
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||
for _, testCase := range signedTexturesTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
var target string
|
||||
@@ -633,417 +604,13 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||
req := httptest.NewRequest("GET", target, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*************************
|
||||
* Post skin tests cases *
|
||||
*************************/
|
||||
|
||||
type postSkinTestCase struct {
|
||||
Name string
|
||||
Form io.Reader
|
||||
ExpectSuccess bool
|
||||
BeforeTest func(suite *skinsystemTestSuite)
|
||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||
}
|
||||
|
||||
var postSkinTestsCases = []*postSkinTestCase{
|
||||
{
|
||||
Name: "Upload new identity with textures data",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
suite.Equal(5, model.SkinId)
|
||||
suite.False(model.Is1_8)
|
||||
suite.False(model.IsSlim)
|
||||
suite.Equal("http://example.com/skin.png", model.Url)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing only textures data",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"1"},
|
||||
"isSlim": {"1"},
|
||||
"url": {"http://textures-server.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
suite.Equal(5, model.SkinId)
|
||||
suite.True(model.Is1_8)
|
||||
suite.True(model.IsSlim)
|
||||
suite.Equal("http://textures-server.com/skin.png", model.Url)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing its identityId",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"2"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 2).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUsername", "mock_username").Times(1).Return(nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(2, model.UserId)
|
||||
suite.Equal("mock_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Update exists identity by changing its username",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Times(1).Return(nil)
|
||||
suite.SkinsRepository.On("Save", mock.MatchedBy(func(model *model.Skin) bool {
|
||||
suite.Equal(1, model.UserId)
|
||||
suite.Equal("changed_username", model.Username)
|
||||
suite.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", model.Uuid)
|
||||
|
||||
return true
|
||||
})).Times(1).Return(nil)
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(201, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when loading the data from the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"mock_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"1"},
|
||||
"isSlim": {"1"},
|
||||
"url": {"http://textures-server.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("Save", mock.Anything).Return(err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "unable to save record to the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Handle an error when saving the data into the repository",
|
||||
Form: bytes.NewBufferString(url.Values{
|
||||
"identityId": {"1"},
|
||||
"username": {"changed_username"},
|
||||
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
|
||||
"skinId": {"5"},
|
||||
"is1_8": {"0"},
|
||||
"isSlim": {"0"},
|
||||
"url": {"http://example.com/skin.png"},
|
||||
}.Encode()),
|
||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||
err := errors.New("mock error")
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, err)
|
||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
||||
return cErr.Error() == "error on requesting a skin from the repository: mock error" &&
|
||||
errors.Is(cErr, err)
|
||||
})).Once()
|
||||
},
|
||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||
suite.Equal(500, response.StatusCode)
|
||||
body, _ := ioutil.ReadAll(response.Body)
|
||||
suite.Empty(body)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (suite *skinsystemTestSuite) TestPostSkin() {
|
||||
for _, testCase := range postSkinTestsCases {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", testCase.Form)
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
}
|
||||
|
||||
suite.RunSubTest("Get errors about required fields", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(url.Values{
|
||||
"mojangTextures": {"someBase64EncodedString"},
|
||||
}.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"errors": {
|
||||
"identityId": [
|
||||
"The identityId field is required",
|
||||
"The identityId field must be numeric",
|
||||
"The identityId field must be minimum 1 char"
|
||||
],
|
||||
"skinId": [
|
||||
"The skinId field is required",
|
||||
"The skinId field must be numeric",
|
||||
"The skinId field must be minimum 1 char"
|
||||
],
|
||||
"username": [
|
||||
"The username field is required"
|
||||
],
|
||||
"uuid": [
|
||||
"The uuid field is required",
|
||||
"The uuid field must contain valid UUID"
|
||||
],
|
||||
"url": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"skin": [
|
||||
"One of url or skin should be provided, but not both"
|
||||
],
|
||||
"mojangSignature": [
|
||||
"The mojangSignature field is required"
|
||||
]
|
||||
}
|
||||
}`, string(body))
|
||||
})
|
||||
|
||||
suite.RunSubTest("Send request without authorization", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", nil)
|
||||
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(errors.New("Cannot parse passed JWT token"))
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(403, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"error": "Cannot parse passed JWT token"
|
||||
}`, string(body))
|
||||
})
|
||||
|
||||
suite.RunSubTest("Upload textures with skin as file", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
|
||||
inputBody := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(inputBody)
|
||||
|
||||
part, _ := writer.CreateFormFile("skin", "char.png")
|
||||
_, _ = part.Write(loadSkinFile())
|
||||
|
||||
_ = writer.WriteField("identityId", "1")
|
||||
_ = writer.WriteField("username", "mock_user")
|
||||
_ = writer.WriteField("uuid", "0f657aa8-bfbe-415d-b700-5750090d3af3")
|
||||
_ = writer.WriteField("skinId", "5")
|
||||
|
||||
err := writer.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "http://chrly/api/skins", inputBody)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(400, resp.StatusCode)
|
||||
responseBody, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`{
|
||||
"errors": {
|
||||
"skin": [
|
||||
"Skin uploading is temporary unavailable"
|
||||
]
|
||||
}
|
||||
}`, string(responseBody))
|
||||
})
|
||||
}
|
||||
|
||||
/**************************************
|
||||
* Delete skin by user id tests cases *
|
||||
**************************************/
|
||||
|
||||
func (suite *skinsystemTestSuite) TestDeleteByUserId() {
|
||||
suite.RunSubTest("Delete skin by its identity id", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(204, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.Empty(body)
|
||||
})
|
||||
|
||||
suite.RunSubTest("Try to remove not exists identity id", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
suite.SkinsRepository.On("FindByUserId", 1).Return(nil, &SkinNotFoundError{Who: "unknown"})
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(404, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`[
|
||||
"Cannot find record for the requested identifier"
|
||||
]`, string(body))
|
||||
})
|
||||
}
|
||||
|
||||
/***************************************
|
||||
* Delete skin by username tests cases *
|
||||
***************************************/
|
||||
|
||||
func (suite *skinsystemTestSuite) TestDeleteByUsername() {
|
||||
suite.RunSubTest("Delete skin by its identity username", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||
suite.SkinsRepository.On("RemoveByUserId", 1).Once().Return(nil)
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(204, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.Empty(body)
|
||||
})
|
||||
|
||||
suite.RunSubTest("Try to remove not exists identity username", func() {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, mock.Anything)
|
||||
suite.Auth.On("Authenticate", mock.Anything).Return(nil)
|
||||
suite.SkinsRepository.On("FindByUsername", "mock_username").Return(nil, &SkinNotFoundError{Who: "mock_username"})
|
||||
|
||||
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
suite.Equal(404, resp.StatusCode)
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
suite.JSONEq(`[
|
||||
"Cannot find record for the requested identifier"
|
||||
]`, string(body))
|
||||
})
|
||||
}
|
||||
|
||||
/****************
|
||||
* Custom tests *
|
||||
****************/
|
||||
@@ -1118,16 +685,3 @@ func createMojangResponse(includeSkin bool, includeCape bool) *mojang.SignedText
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// base64 https://github.com/mathiasbynens/small/blob/0ca3c51/png-transparent.png
|
||||
var OnePxPng = []byte("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")
|
||||
|
||||
func loadSkinFile() []byte {
|
||||
result := make([]byte, 92)
|
||||
_, err := base64.StdEncoding.Decode(result, OnePxPng)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@@ -14,29 +14,19 @@ type MojangUuidsProvider interface {
|
||||
}
|
||||
|
||||
type UUIDsWorker struct {
|
||||
Emitter
|
||||
UUIDsProvider MojangUuidsProvider
|
||||
MojangUuidsProvider
|
||||
}
|
||||
|
||||
func (ctx *UUIDsWorker) CreateHandler() *mux.Router {
|
||||
requestEventsMiddleware := CreateRequestEventsMiddleware(ctx.Emitter, "skinsystem") // This prefix should be unified
|
||||
|
||||
func (ctx *UUIDsWorker) Handler() *mux.Router {
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
router.Use(requestEventsMiddleware)
|
||||
|
||||
router.Handle("/api/worker/mojang-uuid/{username}", http.HandlerFunc(ctx.GetUUID)).Methods("GET")
|
||||
|
||||
// 404
|
||||
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
|
||||
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
|
||||
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFound))
|
||||
router.Handle("/mojang-uuid/{username}", http.HandlerFunc(ctx.getUUIDHandler)).Methods("GET")
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (ctx *UUIDsWorker) GetUUID(response http.ResponseWriter, request *http.Request) {
|
||||
username := parseUsername(mux.Vars(request)["username"])
|
||||
profile, err := ctx.UUIDsProvider.GetUuid(username)
|
||||
func (ctx *UUIDsWorker) getUUIDHandler(response http.ResponseWriter, request *http.Request) {
|
||||
username := mux.Vars(request)["username"]
|
||||
profile, err := ctx.GetUuid(username)
|
||||
if err != nil {
|
||||
if _, ok := err.(*mojang.TooManyRequestsError); ok {
|
||||
response.WriteHeader(http.StatusTooManyRequests)
|
||||
|
@@ -37,7 +37,6 @@ type uuidsWorkerTestSuite struct {
|
||||
App *UUIDsWorker
|
||||
|
||||
UuidsProvider *uuidsProviderMock
|
||||
Emitter *emitterMock
|
||||
}
|
||||
|
||||
/********************
|
||||
@@ -46,17 +45,14 @@ type uuidsWorkerTestSuite struct {
|
||||
|
||||
func (suite *uuidsWorkerTestSuite) SetupTest() {
|
||||
suite.UuidsProvider = &uuidsProviderMock{}
|
||||
suite.Emitter = &emitterMock{}
|
||||
|
||||
suite.App = &UUIDsWorker{
|
||||
UUIDsProvider: suite.UuidsProvider,
|
||||
Emitter: suite.Emitter,
|
||||
MojangUuidsProvider: suite.UuidsProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *uuidsWorkerTestSuite) TearDownTest() {
|
||||
suite.UuidsProvider.AssertExpectations(suite.T())
|
||||
suite.Emitter.AssertExpectations(suite.T())
|
||||
}
|
||||
|
||||
func (suite *uuidsWorkerTestSuite) RunSubTest(name string, subTest func()) {
|
||||
@@ -87,8 +83,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
||||
{
|
||||
Name: "Success provider response",
|
||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 200)
|
||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(&mojang.ProfileInfo{
|
||||
Id: "0fcc38620f1845f3a54e1b523c1bd1c7",
|
||||
Name: "mock_username",
|
||||
@@ -107,8 +101,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
||||
{
|
||||
Name: "Receive empty response from UUIDs provider",
|
||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 204)
|
||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, nil)
|
||||
},
|
||||
AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) {
|
||||
@@ -120,8 +112,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
||||
{
|
||||
Name: "Receive error from UUIDs provider",
|
||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 500)
|
||||
err := errors.New("this is an error")
|
||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
||||
},
|
||||
@@ -137,8 +127,6 @@ var getUuidTestsCases = []*uuidsWorkerTestCase{
|
||||
{
|
||||
Name: "Receive Too Many Requests from UUIDs provider",
|
||||
BeforeTest: func(suite *uuidsWorkerTestSuite) {
|
||||
suite.Emitter.On("Emit", "skinsystem:before_request", mock.Anything)
|
||||
suite.Emitter.On("Emit", "skinsystem:after_request", mock.Anything, 429)
|
||||
err := &mojang.TooManyRequestsError{}
|
||||
suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err)
|
||||
},
|
||||
@@ -155,10 +143,10 @@ func (suite *uuidsWorkerTestSuite) TestGetUUID() {
|
||||
suite.RunSubTest(testCase.Name, func() {
|
||||
testCase.BeforeTest(suite)
|
||||
|
||||
req := httptest.NewRequest("GET", "http://chrly/api/worker/mojang-uuid/mock_username", nil)
|
||||
req := httptest.NewRequest("GET", "http://chrly/mojang-uuid/mock_username", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
suite.App.CreateHandler().ServeHTTP(w, req)
|
||||
suite.App.Handler().ServeHTTP(w, req)
|
||||
|
||||
testCase.AfterTest(suite, w.Result())
|
||||
})
|
||||
|
Reference in New Issue
Block a user