mirror of
https://github.com/elyby/chrly.git
synced 2024-11-26 08:42:14 +05:30
Implemented /profile/{username} endpoint to get complete profile with signed by the current server textures.
Implemented /signing-key endpoint to get public key in der format, used to sign the textures. Improved logging of errors from http package. Changed behavior of the /cloaks endpoint
This commit is contained in:
parent
247499df6a
commit
6f148a8791
@ -5,11 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased] - xxxx-xx-xx
|
## [Unreleased] - xxxx-xx-xx
|
||||||
|
### Added
|
||||||
|
- `/profile/{username}` endpoint.
|
||||||
|
- `/signing-key` endpoint.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- [#29](https://github.com/elyby/chrly/issues/29) If a previously cached UUID no longer exists,
|
- [#29](https://github.com/elyby/chrly/issues/29) If a previously cached UUID no longer exists,
|
||||||
it will be invalidated and re-requested.
|
it will be invalidated and re-requested.
|
||||||
- Use correct status code for error about empty response from Mojang's API.
|
- Use correct status code for error about empty response from Mojang's API.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- All skinsystem's endpoints are now returns `500` status code when an error occurred during request processing.
|
||||||
|
|
||||||
## [4.5.0] - 2020-05-01
|
## [4.5.0] - 2020-05-01
|
||||||
### Added
|
### Added
|
||||||
- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
|
- [#24](https://github.com/elyby/chrly/issues/24): Implemented a new strategy for the queue in the batch provider of
|
||||||
|
1
di/di.go
1
di/di.go
@ -11,6 +11,7 @@ func New() (*di.Container, error) {
|
|||||||
mojangTextures,
|
mojangTextures,
|
||||||
handlers,
|
handlers,
|
||||||
server,
|
server,
|
||||||
|
signer,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -104,6 +104,7 @@ func newSkinsystemHandler(
|
|||||||
skinsRepository SkinsRepository,
|
skinsRepository SkinsRepository,
|
||||||
capesRepository CapesRepository,
|
capesRepository CapesRepository,
|
||||||
mojangTexturesProvider MojangTexturesProvider,
|
mojangTexturesProvider MojangTexturesProvider,
|
||||||
|
texturesSigner TexturesSigner,
|
||||||
) *mux.Router {
|
) *mux.Router {
|
||||||
config.SetDefault("textures.extra_param_name", "chrly")
|
config.SetDefault("textures.extra_param_name", "chrly")
|
||||||
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
||||||
@ -113,14 +114,14 @@ func newSkinsystemHandler(
|
|||||||
SkinsRepo: skinsRepository,
|
SkinsRepo: skinsRepository,
|
||||||
CapesRepo: capesRepository,
|
CapesRepo: capesRepository,
|
||||||
MojangTexturesProvider: mojangTexturesProvider,
|
MojangTexturesProvider: mojangTexturesProvider,
|
||||||
|
TexturesSigner: texturesSigner,
|
||||||
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
|
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
|
||||||
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
|
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
|
||||||
}).Handler()
|
}).Handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newApiHandler(emitter Emitter, skinsRepository SkinsRepository) *mux.Router {
|
func newApiHandler(skinsRepository SkinsRepository) *mux.Router {
|
||||||
return (&Api{
|
return (&Api{
|
||||||
Emitter: emitter,
|
|
||||||
SkinsRepo: skinsRepository,
|
SkinsRepo: skinsRepository,
|
||||||
}).Handler()
|
}).Handler()
|
||||||
}
|
}
|
||||||
|
18
di/server.go
18
di/server.go
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/raven-go"
|
||||||
@ -42,13 +43,26 @@ func newServer(params serverParams) *http.Server {
|
|||||||
params.Config.SetDefault("server.host", "")
|
params.Config.SetDefault("server.host", "")
|
||||||
params.Config.SetDefault("server.port", 80)
|
params.Config.SetDefault("server.port", 80)
|
||||||
|
|
||||||
handler := params.Handler
|
var handler http.Handler
|
||||||
if params.Sentry != nil {
|
if params.Sentry != nil {
|
||||||
// raven.Recoverer uses DefaultClient and nothing can be done about it
|
// raven.Recoverer uses DefaultClient and nothing can be done about it
|
||||||
// To avoid code duplication, if the Sentry service is successfully initiated,
|
// To avoid code duplication, if the Sentry service is successfully initiated,
|
||||||
// it will also replace DefaultClient, so raven.Recoverer will work with the instance
|
// it will also replace DefaultClient, so raven.Recoverer will work with the instance
|
||||||
// created in the application constructor
|
// created in the application constructor
|
||||||
handler = raven.Recoverer(handler)
|
handler = raven.Recoverer(params.Handler)
|
||||||
|
} else {
|
||||||
|
// Raven's Recoverer is prints the stacktrace and sets the corresponding status itself.
|
||||||
|
// But there is no magic and if you don't define a panic handler, Mux will just reset the connection
|
||||||
|
handler = http.HandlerFunc(func(request http.ResponseWriter, response *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if recovered := recover(); recovered != nil {
|
||||||
|
debug.PrintStack() // TODO: colorize output
|
||||||
|
request.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
params.Handler.ServeHTTP(request, response)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
|
address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
|
||||||
|
50
di/signer.go
Normal file
50
di/signer.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package di
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"github.com/elyby/chrly/http"
|
||||||
|
. "github.com/elyby/chrly/signer"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goava/di"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var signer = di.Options(
|
||||||
|
di.Provide(newTexturesSigner,
|
||||||
|
di.As(new(http.TexturesSigner)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTexturesSigner(config *viper.Viper) (*Signer, error) {
|
||||||
|
// TODO: add CHANGELOG and README entries about this variable
|
||||||
|
// TODO: rename param variable
|
||||||
|
keyStr := config.GetString("textures.signer.pem")
|
||||||
|
if keyStr == "" {
|
||||||
|
return nil, errors.New("texturesSigner.pem must be set in order to sign textures")
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyBytes []byte
|
||||||
|
if strings.HasPrefix(keyStr, "base64:") {
|
||||||
|
base64Value := keyStr[7:]
|
||||||
|
decodedKey, err := base64.URLEncoding.DecodeString(base64Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes = decodedKey
|
||||||
|
} else {
|
||||||
|
keyBytes = []byte(keyStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPem, _ := pem.Decode(keyBytes)
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Signer{Key: key}, nil
|
||||||
|
}
|
17
http/api.go
17
http/api.go
@ -43,7 +43,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Api struct {
|
type Api struct {
|
||||||
Emitter
|
|
||||||
SkinsRepo SkinsRepository
|
SkinsRepo SkinsRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +67,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
record, err := ctx.findIdentityOrCleanup(identityId, username)
|
record, err := ctx.findIdentityOrCleanup(identityId, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("error on requesting a skin from the repository: %w", err))
|
panic(err)
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if record == nil {
|
if record == nil {
|
||||||
@ -94,9 +91,7 @@ func (ctx *Api) postSkinHandler(resp http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
err = ctx.SkinsRepo.SaveSkin(record)
|
err = ctx.SkinsRepo.SaveSkin(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to save record to the repository: %w", err))
|
panic(err)
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.WriteHeader(http.StatusCreated)
|
resp.WriteHeader(http.StatusCreated)
|
||||||
@ -116,9 +111,7 @@ func (ctx *Api) deleteSkinByUsernameHandler(resp http.ResponseWriter, req *http.
|
|||||||
|
|
||||||
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("unable to find skin info from the repository: %w", err))
|
panic(err)
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if skin == nil {
|
if skin == nil {
|
||||||
@ -128,9 +121,7 @@ func (ctx *Api) deleteSkin(skin *model.Skin, err error, resp http.ResponseWriter
|
|||||||
|
|
||||||
err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId)
|
err = ctx.SkinsRepo.RemoveSkinByUserId(skin.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Emit("skinsystem:error", fmt.Errorf("cannot delete skin by error: %w", err))
|
panic(err)
|
||||||
apiServerError(resp)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.WriteHeader(http.StatusNoContent)
|
resp.WriteHeader(http.StatusNoContent)
|
||||||
|
@ -28,7 +28,6 @@ type apiTestSuite struct {
|
|||||||
App *Api
|
App *Api
|
||||||
|
|
||||||
SkinsRepository *skinsRepositoryMock
|
SkinsRepository *skinsRepositoryMock
|
||||||
Emitter *emitterMock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************
|
/********************
|
||||||
@ -37,17 +36,14 @@ type apiTestSuite struct {
|
|||||||
|
|
||||||
func (suite *apiTestSuite) SetupTest() {
|
func (suite *apiTestSuite) SetupTest() {
|
||||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||||
suite.Emitter = &emitterMock{}
|
|
||||||
|
|
||||||
suite.App = &Api{
|
suite.App = &Api{
|
||||||
SkinsRepo: suite.SkinsRepository,
|
SkinsRepo: suite.SkinsRepository,
|
||||||
Emitter: suite.Emitter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *apiTestSuite) TearDownTest() {
|
func (suite *apiTestSuite) TearDownTest() {
|
||||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||||
suite.Emitter.AssertExpectations(suite.T())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
|
func (suite *apiTestSuite) RunSubTest(name string, subTest func()) {
|
||||||
@ -72,6 +68,7 @@ type postSkinTestCase struct {
|
|||||||
Name string
|
Name string
|
||||||
Form io.Reader
|
Form io.Reader
|
||||||
BeforeTest func(suite *apiTestSuite)
|
BeforeTest func(suite *apiTestSuite)
|
||||||
|
PanicErr string
|
||||||
AfterTest func(suite *apiTestSuite, response *http.Response)
|
AfterTest func(suite *apiTestSuite, response *http.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +195,22 @@ var postSkinTestsCases = []*postSkinTestCase{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Handle an error when loading the data from the repository",
|
Name: "Handle an error when loading the data from 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) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(nil, errors.New("can't find skin by user id"))
|
||||||
|
},
|
||||||
|
PanicErr: "can't find skin by user id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Handle an error when saving the data into the repository",
|
||||||
Form: bytes.NewBufferString(url.Values{
|
Form: bytes.NewBufferString(url.Values{
|
||||||
"identityId": {"1"},
|
"identityId": {"1"},
|
||||||
"username": {"mock_username"},
|
"username": {"mock_username"},
|
||||||
@ -209,43 +222,9 @@ var postSkinTestsCases = []*postSkinTestCase{
|
|||||||
}.Encode()),
|
}.Encode()),
|
||||||
BeforeTest: func(suite *apiTestSuite) {
|
BeforeTest: func(suite *apiTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
suite.SkinsRepository.On("FindSkinByUserId", 1).Return(createSkinModel("mock_username", false), nil)
|
||||||
err := errors.New("mock error")
|
suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(errors.New("can't save textures"))
|
||||||
suite.SkinsRepository.On("SaveSkin", mock.Anything).Return(err)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
|
||||||
return cErr.Error() == "unable to save record to the repository: mock error" &&
|
|
||||||
errors.Is(cErr, err)
|
|
||||||
})).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("FindSkinByUserId", 1).Return(nil, err)
|
|
||||||
suite.Emitter.On("Emit", "skinsystem:error", mock.MatchedBy(func(cErr error) bool {
|
|
||||||
return cErr.Error() == "error on requesting a skin from the repository: mock error" &&
|
|
||||||
errors.Is(cErr, err)
|
|
||||||
})).Once()
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *apiTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(500, response.StatusCode)
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.Empty(body)
|
|
||||||
},
|
},
|
||||||
|
PanicErr: "can't save textures",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,9 +237,14 @@ func (suite *apiTestSuite) TestPostSkin() {
|
|||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ func StartServer(server *http.Server, logger slf.Logger) {
|
|||||||
go func() {
|
go func() {
|
||||||
s := waitForExitSignal()
|
s := waitForExitSignal()
|
||||||
logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
|
logger.Info("Got signal: :signal, starting graceful shutdown", wd.StringParam("signal", s.String()))
|
||||||
server.Shutdown(context.Background())
|
_ = server.Shutdown(context.Background())
|
||||||
logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
|
logger.Info("Graceful shutdown succeed, exiting", wd.StringParam("signal", s.String()))
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
@ -135,7 +135,3 @@ func apiNotFound(resp http.ResponseWriter, reason string) {
|
|||||||
})
|
})
|
||||||
_, _ = resp.Write(result)
|
_, _ = resp.Write(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiServerError(resp http.ResponseWriter) {
|
|
||||||
resp.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/elyby/chrly/utils"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
@ -12,6 +17,8 @@ import (
|
|||||||
"github.com/elyby/chrly/model"
|
"github.com/elyby/chrly/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var timeNow = time.Now
|
||||||
|
|
||||||
type SkinsRepository interface {
|
type SkinsRepository interface {
|
||||||
FindSkinByUsername(username string) (*model.Skin, error)
|
FindSkinByUsername(username string) (*model.Skin, error)
|
||||||
FindSkinByUserId(id int) (*model.Skin, error)
|
FindSkinByUserId(id int) (*model.Skin, error)
|
||||||
@ -28,15 +35,30 @@ type MojangTexturesProvider interface {
|
|||||||
GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
|
GetForUsername(username string) (*mojang.SignedTexturesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TexturesSigner interface {
|
||||||
|
SignTextures(textures string) (string, error)
|
||||||
|
GetPublicKey() (*rsa.PublicKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Skinsystem struct {
|
type Skinsystem struct {
|
||||||
Emitter
|
Emitter
|
||||||
SkinsRepo SkinsRepository
|
SkinsRepo SkinsRepository
|
||||||
CapesRepo CapesRepository
|
CapesRepo CapesRepository
|
||||||
MojangTexturesProvider MojangTexturesProvider
|
MojangTexturesProvider MojangTexturesProvider
|
||||||
|
TexturesSigner TexturesSigner
|
||||||
TexturesExtraParamName string
|
TexturesExtraParamName string
|
||||||
TexturesExtraParamValue string
|
TexturesExtraParamValue string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type profile struct {
|
||||||
|
Id string
|
||||||
|
Username string
|
||||||
|
Textures *mojang.TexturesResponse
|
||||||
|
CapeFile io.Reader
|
||||||
|
MojangTextures string
|
||||||
|
MojangSignature string
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) Handler() *mux.Router {
|
func (ctx *Skinsystem) Handler() *mux.Router {
|
||||||
router := mux.NewRouter().StrictSlash(true)
|
router := mux.NewRouter().StrictSlash(true)
|
||||||
|
|
||||||
@ -44,40 +66,28 @@ func (ctx *Skinsystem) Handler() *mux.Router {
|
|||||||
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
|
router.HandleFunc("/cloaks/{username}", ctx.capeHandler).Methods(http.MethodGet).Name("cloaks")
|
||||||
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
router.HandleFunc("/textures/{username}", ctx.texturesHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
router.HandleFunc("/textures/signed/{username}", ctx.signedTexturesHandler).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/profile/{username}", ctx.profileHandler).Methods(http.MethodGet)
|
||||||
// Legacy
|
// Legacy
|
||||||
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
router.HandleFunc("/skins", ctx.skinGetHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
router.HandleFunc("/cloaks", ctx.capeGetHandler).Methods(http.MethodGet)
|
||||||
|
// Utils
|
||||||
|
router.HandleFunc("/signing-key", ctx.signingKeyHandler).Methods(http.MethodGet)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) skinHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
profile, err := ctx.getProfile(request, true)
|
||||||
rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
if err != nil {
|
||||||
if err == nil && rec != nil && rec.SkinId != 0 {
|
panic(err)
|
||||||
http.Redirect(response, request, rec.Url, 301)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
if profile == nil || profile.Textures == nil || profile.Textures.Skin == nil {
|
||||||
if err != nil || mojangTextures == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
response.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
http.Redirect(response, request, profile.Textures.Skin.Url, 301)
|
||||||
if texturesProp == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
skin := texturesProp.Textures.Skin
|
|
||||||
if skin == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(response, request, skin.Url, 301)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
@ -88,39 +98,28 @@ func (ctx *Skinsystem) skinGetHandler(response http.ResponseWriter, request *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux.Vars(request)["username"] = username
|
mux.Vars(request)["username"] = username
|
||||||
mux.Vars(request)["converted"] = "1"
|
|
||||||
|
|
||||||
ctx.skinHandler(response, request)
|
ctx.skinHandler(response, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: write CHANGELOG about breaking change in this method
|
||||||
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) capeHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
profile, err := ctx.getProfile(request, true)
|
||||||
rec, err := ctx.CapesRepo.FindCapeByUsername(username)
|
if err != nil {
|
||||||
if err == nil && rec != nil {
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile == nil || profile.Textures == nil || (profile.CapeFile == nil && profile.Textures.Cape == nil) {
|
||||||
|
response.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile.CapeFile == nil {
|
||||||
|
http.Redirect(response, request, profile.Textures.Cape.Url, 301)
|
||||||
|
} else {
|
||||||
request.Header.Set("Content-Type", "image/png")
|
request.Header.Set("Content-Type", "image/png")
|
||||||
_, _ = io.Copy(response, rec.File)
|
_, _ = io.Copy(response, profile.CapeFile)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
|
||||||
if err != nil || mojangTextures == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
|
||||||
if texturesProp == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cape := texturesProp.Textures.Cape
|
|
||||||
if cape == nil {
|
|
||||||
response.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(response, request, cape.Url, 301)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
@ -131,104 +130,221 @@ func (ctx *Skinsystem) capeGetHandler(response http.ResponseWriter, request *htt
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux.Vars(request)["username"] = username
|
mux.Vars(request)["username"] = username
|
||||||
mux.Vars(request)["converted"] = "1"
|
|
||||||
|
|
||||||
ctx.capeHandler(response, request)
|
ctx.capeHandler(response, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) texturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
profile, err := ctx.getProfile(request, true)
|
||||||
|
if err != nil {
|
||||||
var textures *mojang.TexturesResponse
|
panic(err)
|
||||||
skin, skinErr := ctx.SkinsRepo.FindSkinByUsername(username)
|
|
||||||
cape, capeErr := ctx.CapesRepo.FindCapeByUsername(username)
|
|
||||||
if (skinErr == nil && skin != nil && skin.SkinId != 0) || (capeErr == nil && cape != nil) {
|
|
||||||
textures = &mojang.TexturesResponse{}
|
|
||||||
if skinErr == nil && skin != nil && skin.SkinId != 0 {
|
|
||||||
skinTextures := &mojang.SkinTexturesResponse{
|
|
||||||
Url: skin.Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
if skin.IsSlim {
|
|
||||||
skinTextures.Metadata = &mojang.SkinTexturesMetadata{
|
|
||||||
Model: "slim",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textures.Skin = skinTextures
|
|
||||||
}
|
|
||||||
|
|
||||||
if capeErr == nil && cape != nil {
|
|
||||||
textures.Cape = &mojang.CapeTexturesResponse{
|
|
||||||
// Use statically http since the application doesn't support TLS
|
|
||||||
Url: "http://" + request.Host + "/cloaks/" + username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
|
||||||
if err != nil || mojangTextures == nil {
|
|
||||||
response.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
texturesProp, _ := mojangTextures.DecodeTextures()
|
|
||||||
if texturesProp == nil {
|
|
||||||
response.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
textures = texturesProp.Textures
|
|
||||||
if textures.Skin == nil && textures.Cape == nil {
|
|
||||||
response.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData, _ := json.Marshal(textures)
|
if profile == nil || profile.Textures == nil || (profile.Textures.Skin == nil && profile.Textures.Cape == nil) {
|
||||||
|
response.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData, _ := json.Marshal(profile.Textures)
|
||||||
response.Header().Set("Content-Type", "application/json")
|
response.Header().Set("Content-Type", "application/json")
|
||||||
_, _ = response.Write(responseData)
|
_, _ = response.Write(responseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
func (ctx *Skinsystem) signedTexturesHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
username := parseUsername(mux.Vars(request)["username"])
|
profile, err := ctx.getProfile(request, request.URL.Query().Get("proxy") != "")
|
||||||
|
if err != nil {
|
||||||
var responseData *mojang.SignedTexturesResponse
|
panic(err)
|
||||||
|
|
||||||
rec, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
|
||||||
if err == nil && rec != nil && rec.SkinId != 0 && rec.MojangTextures != "" {
|
|
||||||
responseData = &mojang.SignedTexturesResponse{
|
|
||||||
Id: strings.Replace(rec.Uuid, "-", "", -1),
|
|
||||||
Name: rec.Username,
|
|
||||||
Props: []*mojang.Property{
|
|
||||||
{
|
|
||||||
Name: "textures",
|
|
||||||
Signature: rec.MojangSignature,
|
|
||||||
Value: rec.MojangTextures,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else if request.URL.Query().Get("proxy") != "" {
|
|
||||||
mojangTextures, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
|
||||||
if err == nil && mojangTextures != nil {
|
|
||||||
responseData = mojangTextures
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if responseData == nil {
|
if profile == nil || profile.MojangTextures == "" {
|
||||||
response.WriteHeader(http.StatusNoContent)
|
response.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData.Props = append(responseData.Props, &mojang.Property{
|
profileResponse := &mojang.SignedTexturesResponse{
|
||||||
Name: ctx.TexturesExtraParamName,
|
Id: profile.Id,
|
||||||
Value: ctx.TexturesExtraParamValue,
|
Name: profile.Username,
|
||||||
})
|
Props: []*mojang.Property{
|
||||||
|
{
|
||||||
|
Name: "textures",
|
||||||
|
Signature: profile.MojangSignature,
|
||||||
|
Value: profile.MojangTextures,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: ctx.TexturesExtraParamName,
|
||||||
|
Value: ctx.TexturesExtraParamValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
responseJson, _ := json.Marshal(responseData)
|
responseJson, _ := json.Marshal(profileResponse)
|
||||||
response.Header().Set("Content-Type", "application/json")
|
response.Header().Set("Content-Type", "application/json")
|
||||||
_, _ = response.Write(responseJson)
|
_, _ = response.Write(responseJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add README entry about this method
|
||||||
|
func (ctx *Skinsystem) profileHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
profile, err := ctx.getProfile(request, true)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile == nil {
|
||||||
|
response.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
texturesPropContent := &mojang.TexturesProp{
|
||||||
|
Timestamp: utils.UnixMillisecond(timeNow()),
|
||||||
|
ProfileID: profile.Id,
|
||||||
|
ProfileName: profile.Username,
|
||||||
|
Textures: profile.Textures,
|
||||||
|
}
|
||||||
|
|
||||||
|
texturesPropValueJson, _ := json.Marshal(texturesPropContent)
|
||||||
|
texturesPropEncodedValue := base64.StdEncoding.EncodeToString(texturesPropValueJson)
|
||||||
|
|
||||||
|
texturesProp := &mojang.Property{
|
||||||
|
Name: "textures",
|
||||||
|
Value: texturesPropEncodedValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.URL.Query().Get("unsigned") == "false" {
|
||||||
|
signature, err := ctx.TexturesSigner.SignTextures(texturesProp.Value)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
texturesProp.Signature = signature
|
||||||
|
}
|
||||||
|
|
||||||
|
profileResponse := &mojang.SignedTexturesResponse{
|
||||||
|
Id: profile.Id,
|
||||||
|
Name: profile.Username,
|
||||||
|
Props: []*mojang.Property{
|
||||||
|
texturesProp,
|
||||||
|
{
|
||||||
|
Name: ctx.TexturesExtraParamName,
|
||||||
|
Value: ctx.TexturesExtraParamValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
responseJson, _ := json.Marshal(profileResponse)
|
||||||
|
response.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = response.Write(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add README entry about this method
|
||||||
|
func (ctx *Skinsystem) signingKeyHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
|
publicKey, err := ctx.TexturesSigner.GetPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
asn1Bytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = response.Write(asn1Bytes)
|
||||||
|
response.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
response.Header().Set("Content-Disposition", "attachment; filename=\"yggdrasil_session_pubkey.der\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: in v5 should be extracted into some ProfileProvider interface,
|
||||||
|
// which will encapsulate all logics, declared in this method
|
||||||
|
func (ctx *Skinsystem) getProfile(request *http.Request, proxy bool) (*profile, error) {
|
||||||
|
username := parseUsername(mux.Vars(request)["username"])
|
||||||
|
|
||||||
|
skin, err := ctx.SkinsRepo.FindSkinByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := &profile{
|
||||||
|
Id: "",
|
||||||
|
Username: "",
|
||||||
|
Textures: &mojang.TexturesResponse{}, // Field must be initialized to avoid "null" after json encoding
|
||||||
|
CapeFile: nil,
|
||||||
|
MojangTextures: "",
|
||||||
|
MojangSignature: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if skin != nil {
|
||||||
|
profile.Id = strings.Replace(skin.Uuid, "-", "", -1)
|
||||||
|
profile.Username = skin.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if skin != nil && skin.SkinId != 0 {
|
||||||
|
profile.Textures.Skin = &mojang.SkinTexturesResponse{
|
||||||
|
Url: skin.Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
if skin.IsSlim {
|
||||||
|
profile.Textures.Skin.Metadata = &mojang.SkinTexturesMetadata{
|
||||||
|
Model: "slim",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cape, _ := ctx.CapesRepo.FindCapeByUsername(username)
|
||||||
|
if cape != nil {
|
||||||
|
profile.CapeFile = cape.File
|
||||||
|
profile.Textures.Cape = &mojang.CapeTexturesResponse{
|
||||||
|
// Use statically http since the application doesn't support TLS
|
||||||
|
Url: "http://" + request.Host + "/cloaks/" + username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.MojangTextures = skin.MojangTextures
|
||||||
|
profile.MojangSignature = skin.MojangSignature
|
||||||
|
} else if proxy {
|
||||||
|
mojangProfile, err := ctx.MojangTexturesProvider.GetForUsername(username)
|
||||||
|
// If we at least know something about a user,
|
||||||
|
// than we can ignore an error and return profile without textures
|
||||||
|
if err != nil && profile.Id != "" {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || mojangProfile == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedTextures, err := mojangProfile.DecodeTextures()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// There might be no textures property
|
||||||
|
if decodedTextures != nil {
|
||||||
|
profile.Textures = decodedTextures.Textures
|
||||||
|
}
|
||||||
|
|
||||||
|
var texturesProp *mojang.Property
|
||||||
|
for _, prop := range mojangProfile.Props {
|
||||||
|
if prop.Name == "textures" {
|
||||||
|
texturesProp = prop
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if texturesProp != nil {
|
||||||
|
profile.MojangTextures = texturesProp.Value
|
||||||
|
profile.MojangSignature = texturesProp.Signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user id is unknown at this point, then use values from Mojang profile
|
||||||
|
if profile.Id == "" {
|
||||||
|
profile.Id = mojangProfile.Id
|
||||||
|
profile.Username = mojangProfile.Name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseUsername(username string) string {
|
func parseUsername(username string) string {
|
||||||
return strings.TrimSuffix(username, ".png")
|
return strings.TrimSuffix(username, ".png")
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -89,6 +93,25 @@ func (m *mojangTexturesProviderMock) GetForUsername(username string) (*mojang.Si
|
|||||||
return result, args.Error(1)
|
return result, args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type texturesSignerMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *texturesSignerMock) SignTextures(textures string) (string, error) {
|
||||||
|
args := m.Called(textures)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *texturesSignerMock) GetPublicKey() (*rsa.PublicKey, error) {
|
||||||
|
args := m.Called()
|
||||||
|
var publicKey *rsa.PublicKey
|
||||||
|
if casted, ok := args.Get(0).(*rsa.PublicKey); ok {
|
||||||
|
publicKey = casted
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicKey, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
type skinsystemTestSuite struct {
|
type skinsystemTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
@ -97,6 +120,7 @@ type skinsystemTestSuite struct {
|
|||||||
SkinsRepository *skinsRepositoryMock
|
SkinsRepository *skinsRepositoryMock
|
||||||
CapesRepository *capesRepositoryMock
|
CapesRepository *capesRepositoryMock
|
||||||
MojangTexturesProvider *mojangTexturesProviderMock
|
MojangTexturesProvider *mojangTexturesProviderMock
|
||||||
|
TexturesSigner *texturesSignerMock
|
||||||
Emitter *emitterMock
|
Emitter *emitterMock
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,15 +129,22 @@ type skinsystemTestSuite struct {
|
|||||||
********************/
|
********************/
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) SetupTest() {
|
func (suite *skinsystemTestSuite) SetupTest() {
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
CET, _ := time.LoadLocation("CET")
|
||||||
|
return time.Date(2021, 02, 25, 01, 50, 23, 0, CET)
|
||||||
|
}
|
||||||
|
|
||||||
suite.SkinsRepository = &skinsRepositoryMock{}
|
suite.SkinsRepository = &skinsRepositoryMock{}
|
||||||
suite.CapesRepository = &capesRepositoryMock{}
|
suite.CapesRepository = &capesRepositoryMock{}
|
||||||
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
suite.MojangTexturesProvider = &mojangTexturesProviderMock{}
|
||||||
|
suite.TexturesSigner = &texturesSignerMock{}
|
||||||
suite.Emitter = &emitterMock{}
|
suite.Emitter = &emitterMock{}
|
||||||
|
|
||||||
suite.App = &Skinsystem{
|
suite.App = &Skinsystem{
|
||||||
SkinsRepo: suite.SkinsRepository,
|
SkinsRepo: suite.SkinsRepository,
|
||||||
CapesRepo: suite.CapesRepository,
|
CapesRepo: suite.CapesRepository,
|
||||||
MojangTexturesProvider: suite.MojangTexturesProvider,
|
MojangTexturesProvider: suite.MojangTexturesProvider,
|
||||||
|
TexturesSigner: suite.TexturesSigner,
|
||||||
Emitter: suite.Emitter,
|
Emitter: suite.Emitter,
|
||||||
TexturesExtraParamName: "texturesParamName",
|
TexturesExtraParamName: "texturesParamName",
|
||||||
TexturesExtraParamValue: "texturesParamValue",
|
TexturesExtraParamValue: "texturesParamValue",
|
||||||
@ -124,6 +155,7 @@ func (suite *skinsystemTestSuite) TearDownTest() {
|
|||||||
suite.SkinsRepository.AssertExpectations(suite.T())
|
suite.SkinsRepository.AssertExpectations(suite.T())
|
||||||
suite.CapesRepository.AssertExpectations(suite.T())
|
suite.CapesRepository.AssertExpectations(suite.T())
|
||||||
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
suite.MojangTexturesProvider.AssertExpectations(suite.T())
|
||||||
|
suite.TexturesSigner.AssertExpectations(suite.T())
|
||||||
suite.Emitter.AssertExpectations(suite.T())
|
suite.Emitter.AssertExpectations(suite.T())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,6 +176,7 @@ func TestSkinsystem(t *testing.T) {
|
|||||||
type skinsystemTestCase struct {
|
type skinsystemTestCase struct {
|
||||||
Name string
|
Name string
|
||||||
BeforeTest func(suite *skinsystemTestSuite)
|
BeforeTest func(suite *skinsystemTestSuite)
|
||||||
|
PanicErr string
|
||||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +189,7 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username exists in the local storage",
|
Name: "Username exists in the local storage",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(301, response.StatusCode)
|
suite.Equal(301, response.StatusCode)
|
||||||
@ -203,6 +237,13 @@ var skinsTestsCases = []*skinsystemTestCase{
|
|||||||
suite.Equal(404, response.StatusCode)
|
suite.Equal(404, response.StatusCode)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the SkinsRepository",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||||
|
},
|
||||||
|
PanicErr: "skins repository error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestSkin() {
|
func (suite *skinsystemTestSuite) TestSkin() {
|
||||||
@ -213,14 +254,20 @@ func (suite *skinsystemTestSuite) TestSkin() {
|
|||||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Pass username with png extension", func() {
|
suite.RunSubTest("Pass username with png extension", func() {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins/mock_username.png", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@ -241,14 +288,18 @@ func (suite *skinsystemTestSuite) TestSkinGET() {
|
|||||||
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins?name=mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Do not pass name param", func() {
|
suite.RunSubTest("Do not pass name param", func() {
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
req := httptest.NewRequest("GET", "http://chrly/skins", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -267,6 +318,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username exists in the local storage",
|
Name: "Username exists in the local storage",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -279,7 +331,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(true, true), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -290,7 +342,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has no cape texture",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createMojangResponseWithTextures(false, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -300,7 +352,7 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
Name: "Username doesn't exists on the local storage, but exists on Mojang and has an empty properties",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(createEmptyMojangResponse(), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -310,13 +362,20 @@ var capesTestsCases = []*skinsystemTestCase{
|
|||||||
{
|
{
|
||||||
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
Name: "Username doesn't exists on the local storage and doesn't exists on Mojang",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(404, response.StatusCode)
|
suite.Equal(404, response.StatusCode)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the SkinsRepository",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||||
|
},
|
||||||
|
PanicErr: "skins repository error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestCape() {
|
func (suite *skinsystemTestSuite) TestCape() {
|
||||||
@ -327,13 +386,19 @@ func (suite *skinsystemTestSuite) TestCape() {
|
|||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Pass username with png extension", func() {
|
suite.RunSubTest("Pass username with png extension", func() {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks/mock_username.png", nil)
|
||||||
@ -357,14 +422,18 @@ func (suite *skinsystemTestSuite) TestCapeGET() {
|
|||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks?name=mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.RunSubTest("Do not pass name param", func() {
|
suite.RunSubTest("Do not pass name param", func() {
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
req := httptest.NewRequest("GET", "http://chrly/cloaks", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -417,23 +486,9 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
}`, string(body))
|
}`, string(body))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
// There is no case when the user has cape, but has no skin.
|
||||||
Name: "Username exists and has cape, no skin",
|
// In v5 we will rework textures repositories to be more generic about source of textures,
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
// but right now it's not possible to return profile entity with a cape only.
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
|
||||||
},
|
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
|
||||||
suite.Equal(200, response.StatusCode)
|
|
||||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
|
||||||
suite.JSONEq(`{
|
|
||||||
"CAPE": {
|
|
||||||
"url": "http://chrly/cloaks/mock_username"
|
|
||||||
}
|
|
||||||
}`, string(body))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "Username exists and has both skin and cape",
|
Name: "Username exists and has both skin and cape",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
@ -458,7 +513,6 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username not exists, but Mojang profile available",
|
Name: "Username not exists, but Mojang profile available",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -479,7 +533,6 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -490,7 +543,6 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -501,7 +553,6 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
Name: "Username not exists and Mojang profile unavailable",
|
Name: "Username not exists and Mojang profile unavailable",
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
|
||||||
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
@ -510,6 +561,13 @@ var texturesTestsCases = []*skinsystemTestCase{
|
|||||||
suite.Equal("", string(body))
|
suite.Equal("", string(body))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the SkinsRepository",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||||
|
},
|
||||||
|
PanicErr: "skins repository error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestTextures() {
|
func (suite *skinsystemTestSuite) TestTextures() {
|
||||||
@ -520,9 +578,14 @@ func (suite *skinsystemTestSuite) TestTextures() {
|
|||||||
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
req := httptest.NewRequest("GET", "http://chrly/textures/mock_username", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
testCase.AfterTest(suite, w.Result())
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,6 +598,7 @@ type signedTexturesTestCase struct {
|
|||||||
Name string
|
Name string
|
||||||
AllowProxy bool
|
AllowProxy bool
|
||||||
BeforeTest func(suite *skinsystemTestSuite)
|
BeforeTest func(suite *skinsystemTestSuite)
|
||||||
|
PanicErr string
|
||||||
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,6 +608,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
AllowProxy: false,
|
AllowProxy: false,
|
||||||
BeforeTest: func(suite *skinsystemTestSuite) {
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(200, response.StatusCode)
|
suite.Equal(200, response.StatusCode)
|
||||||
@ -586,6 +651,7 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
skinModel.MojangTextures = ""
|
skinModel.MojangTextures = ""
|
||||||
skinModel.MojangSignature = ""
|
skinModel.MojangSignature = ""
|
||||||
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil)
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skinModel, nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
},
|
},
|
||||||
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
suite.Equal(204, response.StatusCode)
|
suite.Equal(204, response.StatusCode)
|
||||||
@ -605,12 +671,13 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
body, _ := ioutil.ReadAll(response.Body)
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
suite.JSONEq(`{
|
suite.JSONEq(`{
|
||||||
"id": "00000000000000000000000000000000",
|
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||||
"name": "mock_username",
|
"name": "mock_username",
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "textures",
|
"name": "textures",
|
||||||
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ=="
|
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn19fQ==",
|
||||||
|
"signature": "mojang signature"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "texturesParamName",
|
"name": "texturesParamName",
|
||||||
@ -633,6 +700,13 @@ var signedTexturesTestsCases = []*signedTexturesTestCase{
|
|||||||
suite.Equal("", string(body))
|
suite.Equal("", string(body))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the SkinsRepository",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||||
|
},
|
||||||
|
PanicErr: "skins repository error",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
func (suite *skinsystemTestSuite) TestSignedTextures() {
|
||||||
@ -650,9 +724,406 @@ func (suite *skinsystemTestSuite) TestSignedTextures() {
|
|||||||
req := httptest.NewRequest("GET", target, nil)
|
req := httptest.NewRequest("GET", target, nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
suite.App.Handler().ServeHTTP(w, req)
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
testCase.AfterTest(suite, w.Result())
|
/***************************
|
||||||
|
* Get profile tests cases *
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
type profileTestCase struct {
|
||||||
|
Name string
|
||||||
|
Signed bool
|
||||||
|
BeforeTest func(suite *skinsystemTestSuite)
|
||||||
|
PanicErr string
|
||||||
|
AfterTest func(suite *skinsystemTestSuite, response *http.Response)
|
||||||
|
}
|
||||||
|
|
||||||
|
var profileTestsCases = []*profileTestCase{
|
||||||
|
{
|
||||||
|
Name: "Username exists and has both skin and cape, don't sign",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username exists and has both skin and cape",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(createCapeModel(), nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19").Return("textures signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "textures signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifSwiQ0FQRSI6eyJ1cmwiOiJodHRwOi8vY2hybHkvY2xvYWtzL21vY2tfdXNlcm5hbWUifX19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username exists and has skin, no cape",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "textures signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmcifX19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username exists and has slim skin, no cape",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", true), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("textures signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "textures signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vY2hybHkvc2tpbi5wbmciLCJtZXRhZGF0YSI6eyJtb2RlbCI6InNsaW0ifX19fQ=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username exists, but has no skin and Mojang profile with textures available",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
skin := createSkinModel("mock_username", false)
|
||||||
|
skin.SkinId = 0
|
||||||
|
skin.Url = ""
|
||||||
|
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "chrly signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username exists, but has no skin and Mojang textures proxy returned an error",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
skin := createSkinModel("mock_username", false)
|
||||||
|
skin.SkinId = 0
|
||||||
|
skin.Url = ""
|
||||||
|
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(skin, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("shit happened"))
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "0f657aa8bfbe415db7005750090d3af3",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "chrly signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjBmNjU3YWE4YmZiZTQxNWRiNzAwNTc1MDA5MGQzYWYzIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username not exists, but Mojang profile with textures available",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(true, true), nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "chrly signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vbW9qYW5nL3NraW4ucG5nIn0sIkNBUEUiOnsidXJsIjoiaHR0cDovL21vamFuZy9jYXBlLnBuZyJ9fX0="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username not exists, but Mojang profile available, but there is an empty skin and cape textures",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createMojangResponseWithTextures(false, false), nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "chrly signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username not exists, but Mojang profile available, but there is an empty properties",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(createEmptyMojangResponse(), nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("chrly signature", nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/json", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.JSONEq(`{
|
||||||
|
"id": "292a1db7353d476ca99cab8f57mojang",
|
||||||
|
"name": "mock_username",
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"signature": "chrly signature",
|
||||||
|
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6IjI5MmExZGI3MzUzZDQ3NmNhOTljYWI4ZjU3bW9qYW5nIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXJuYW1lIiwidGV4dHVyZXMiOnt9fQ=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texturesParamName",
|
||||||
|
"value": "texturesParamValue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username not exists and Mojang profile unavailable",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(204, response.StatusCode)
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Equal("", string(body))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Username not exists and Mojang textures proxy returned an error",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.MojangTexturesProvider.On("GetForUsername", "mock_username").Once().Return(nil, errors.New("mojang textures provider error"))
|
||||||
|
},
|
||||||
|
PanicErr: "mojang textures provider error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the SkinsRepository",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(nil, errors.New("skins repository error"))
|
||||||
|
},
|
||||||
|
PanicErr: "skins repository error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Receive an error from the TexturesSigner",
|
||||||
|
Signed: true,
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.SkinsRepository.On("FindSkinByUsername", "mock_username").Return(createSkinModel("mock_username", false), nil)
|
||||||
|
suite.CapesRepository.On("FindCapeByUsername", "mock_username").Return(nil, nil)
|
||||||
|
suite.TexturesSigner.On("SignTextures", mock.Anything).Return("", errors.New("textures signer error"))
|
||||||
|
},
|
||||||
|
PanicErr: "textures signer error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *skinsystemTestSuite) TestProfile() {
|
||||||
|
for _, testCase := range profileTestsCases {
|
||||||
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
|
url := "http://chrly/profile/mock_username"
|
||||||
|
if testCase.Signed {
|
||||||
|
url += "?unsigned=false"
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", url, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************
|
||||||
|
* Get profile tests cases *
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
var signingKeyTestsCases = []*skinsystemTestCase{
|
||||||
|
{
|
||||||
|
Name: "Get public key",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
pubPem, _ := pem.Decode([]byte("-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----"))
|
||||||
|
publicKey, _ := x509.ParsePKIXPublicKey(pubPem.Bytes)
|
||||||
|
|
||||||
|
suite.TexturesSigner.On("GetPublicKey").Return(publicKey, nil)
|
||||||
|
},
|
||||||
|
AfterTest: func(suite *skinsystemTestSuite, response *http.Response) {
|
||||||
|
suite.Equal(200, response.StatusCode)
|
||||||
|
suite.Equal("application/octet-stream", response.Header.Get("Content-Type"))
|
||||||
|
body, _ := ioutil.ReadAll(response.Body)
|
||||||
|
suite.Equal([]byte{48, 92, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 75, 0, 48, 72, 2, 65, 0, 214, 212, 165, 80, 153, 144, 194, 169, 126, 246, 25, 211, 197, 183, 150, 233, 157, 1, 166, 49, 44, 25, 230, 80, 57, 115, 28, 20, 7, 220, 58, 88, 121, 254, 86, 8, 237, 246, 76, 53, 58, 125, 226, 9, 231, 192, 52, 148, 12, 176, 130, 214, 120, 195, 8, 182, 116, 97, 206, 207, 253, 97, 2, 247, 2, 3, 1, 0, 1}, body)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Error while obtaining public key",
|
||||||
|
BeforeTest: func(suite *skinsystemTestSuite) {
|
||||||
|
suite.TexturesSigner.On("GetPublicKey").Return(nil, errors.New("textures signer error"))
|
||||||
|
},
|
||||||
|
PanicErr: "textures signer error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *skinsystemTestSuite) TestSigningKey() {
|
||||||
|
for _, testCase := range signingKeyTestsCases {
|
||||||
|
suite.RunSubTest(testCase.Name, func() {
|
||||||
|
testCase.BeforeTest(suite)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "http://chrly/signing-key", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
if testCase.PanicErr != "" {
|
||||||
|
suite.PanicsWithError(testCase.PanicErr, func() {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
suite.App.Handler().ServeHTTP(w, req)
|
||||||
|
testCase.AfterTest(suite, w.Result())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -699,7 +1170,7 @@ func createCapeModel() *model.Cape {
|
|||||||
|
|
||||||
func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
|
func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
|
||||||
return &mojang.SignedTexturesResponse{
|
return &mojang.SignedTexturesResponse{
|
||||||
Id: "00000000000000000000000000000000",
|
Id: "292a1db7353d476ca99cab8f57mojang",
|
||||||
Name: "mock_username",
|
Name: "mock_username",
|
||||||
Props: []*mojang.Property{},
|
Props: []*mojang.Property{},
|
||||||
}
|
}
|
||||||
@ -708,8 +1179,8 @@ func createEmptyMojangResponse() *mojang.SignedTexturesResponse {
|
|||||||
func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojang.SignedTexturesResponse {
|
||||||
timeZone, _ := time.LoadLocation("Europe/Minsk")
|
timeZone, _ := time.LoadLocation("Europe/Minsk")
|
||||||
textures := &mojang.TexturesProp{
|
textures := &mojang.TexturesProp{
|
||||||
Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).Unix(),
|
Timestamp: time.Date(2019, 4, 27, 23, 56, 12, 0, timeZone).UnixNano() / int64(time.Millisecond),
|
||||||
ProfileID: "00000000000000000000000000000000",
|
ProfileID: "292a1db7353d476ca99cab8f57mojang",
|
||||||
ProfileName: "mock_username",
|
ProfileName: "mock_username",
|
||||||
Textures: &mojang.TexturesResponse{},
|
Textures: &mojang.TexturesResponse{},
|
||||||
}
|
}
|
||||||
@ -728,8 +1199,9 @@ func createMojangResponseWithTextures(includeSkin bool, includeCape bool) *mojan
|
|||||||
|
|
||||||
response := createEmptyMojangResponse()
|
response := createEmptyMojangResponse()
|
||||||
response.Props = append(response.Props, &mojang.Property{
|
response.Props = append(response.Props, &mojang.Property{
|
||||||
Name: "textures",
|
Name: "textures",
|
||||||
Value: mojang.EncodeTextures(textures),
|
Value: mojang.EncodeTextures(textures),
|
||||||
|
Signature: "mojang signature",
|
||||||
})
|
})
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/elyby/chrly/api/mojang"
|
"github.com/elyby/chrly/api/mojang"
|
||||||
|
"github.com/elyby/chrly/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type inMemoryItem struct {
|
type inMemoryItem struct {
|
||||||
@ -53,7 +54,7 @@ func (s *InMemoryTexturesStorage) StoreTextures(uuid string, textures *mojang.Si
|
|||||||
|
|
||||||
s.data[uuid] = &inMemoryItem{
|
s.data[uuid] = &inMemoryItem{
|
||||||
textures: textures,
|
textures: textures,
|
||||||
timestamp: unixNanoToUnixMicro(time.Now().UnixNano()),
|
timestamp: utils.UnixMillisecond(time.Now()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,9 +90,5 @@ func (s *InMemoryTexturesStorage) gc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
|
func (s *InMemoryTexturesStorage) getMinimalNotExpiredTimestamp() int64 {
|
||||||
return unixNanoToUnixMicro(time.Now().Add(s.Duration * time.Duration(-1)).UnixNano())
|
return utils.UnixMillisecond(time.Now().Add(s.Duration * time.Duration(-1)))
|
||||||
}
|
|
||||||
|
|
||||||
func unixNanoToUnixMicro(unixNano int64) int64 {
|
|
||||||
return unixNano / 10e5
|
|
||||||
}
|
}
|
||||||
|
42
signer/signer.go
Normal file
42
signer/signer.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package signer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randomReader = rand.Reader
|
||||||
|
|
||||||
|
type Signer struct {
|
||||||
|
Key *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signer) SignTextures(textures string) (string, error) {
|
||||||
|
if s.Key == nil {
|
||||||
|
return "", errors.New("Key is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
message := []byte(textures)
|
||||||
|
messageHash := sha1.New()
|
||||||
|
_, _ = messageHash.Write(message)
|
||||||
|
messageHashSum := messageHash.Sum(nil)
|
||||||
|
|
||||||
|
signature, err := rsa.SignPKCS1v15(randomReader, s.Key, crypto.SHA1, messageHashSum)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Signer) GetPublicKey() (*rsa.PublicKey, error) {
|
||||||
|
if s.Key == nil {
|
||||||
|
return nil, errors.New("Key is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s.Key.PublicKey, nil
|
||||||
|
}
|
64
signer/signer_test.go
Normal file
64
signer/signer_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package signer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstantReader struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConstantReader) Read(p []byte) (int, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSigner_SignTextures(t *testing.T) {
|
||||||
|
randomReader = &ConstantReader{}
|
||||||
|
|
||||||
|
t.Run("sign textures", func(t *testing.T) {
|
||||||
|
rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
|
||||||
|
key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
|
||||||
|
|
||||||
|
signer := &Signer{key}
|
||||||
|
|
||||||
|
signature, err := signer.SignTextures("eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty key", func(t *testing.T) {
|
||||||
|
signer := &Signer{}
|
||||||
|
|
||||||
|
signature, err := signer.SignTextures("hello world")
|
||||||
|
assert.Error(t, err, "Key is empty")
|
||||||
|
assert.Empty(t, signature)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSigner_GetPublicKey(t *testing.T) {
|
||||||
|
randomReader = &ConstantReader{}
|
||||||
|
|
||||||
|
t.Run("get public key", func(t *testing.T) {
|
||||||
|
rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
|
||||||
|
key, _ := x509.ParsePKCS1PrivateKey(rawKey.Bytes)
|
||||||
|
|
||||||
|
signer := &Signer{key}
|
||||||
|
|
||||||
|
publicKey, err := signer.GetPublicKey()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.IsType(t, &rsa.PublicKey{}, publicKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty key", func(t *testing.T) {
|
||||||
|
signer := &Signer{}
|
||||||
|
|
||||||
|
publicKey, err := signer.GetPublicKey()
|
||||||
|
assert.Error(t, err, "Key is empty")
|
||||||
|
assert.Nil(t, publicKey)
|
||||||
|
})
|
||||||
|
}
|
7
utils/time.go
Normal file
7
utils/time.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func UnixMillisecond(t time.Time) int64 {
|
||||||
|
return t.UnixNano() / int64(time.Millisecond)
|
||||||
|
}
|
16
utils/time_test.go
Normal file
16
utils/time_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnixMillisecond(t *testing.T) {
|
||||||
|
loc, _ := time.LoadLocation("CET")
|
||||||
|
d := time.Date(2021, 02, 26, 00, 43, 57, 987654321, loc)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1614296637987), UnixMillisecond(d))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user