Implemented remote api mojang uuids provider

This commit is contained in:
ErickSkrauch 2019-11-24 04:07:56 +03:00
parent d27caa4922
commit 1033069211
4 changed files with 247 additions and 6 deletions

View File

@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] - xxxx-xx-xx
### Added
- Added remote mode for Mojang's textures queue.
- New StatsD metrics:
- Counters:
- `ely.skinsystem.{hostname}.app.mojang_textures.usernames.textures_hit`

View File

@ -3,8 +3,10 @@ package cmd
import (
"fmt"
"log"
"net/url"
"time"
"github.com/mono83/slf/wd"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -19,6 +21,7 @@ var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts http handler for the skins system",
Run: func(cmd *cobra.Command, args []string) {
// TODO: this is a mess, need to organize this code somehow to make services initialization more compact
logger, err := bootstrap.CreateLogger(viper.GetString("statsd.addr"), viper.GetString("sentry.dsn"))
if err != nil {
log.Fatal(fmt.Printf("Cannot initialize logger: %v", err))
@ -52,15 +55,32 @@ var serveCmd = &cobra.Command{
return
}
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
texturesStorage.Start()
mojangTexturesProvider := &mojangtextures.Provider{
Logger: logger,
UuidsProvider: &mojangtextures.BatchUuidsProvider{
var uuidsProvider mojangtextures.UuidsProvider
preferredUuidsProvider := viper.GetString("mojang_textures.uuids_provider.driver")
if preferredUuidsProvider == "remote" {
remoteUrl, err := url.Parse(viper.GetString("mojang_textures.uuids_provider.url"))
if err != nil {
logger.Emergency("Unable to parse remote url :err", wd.ErrParam(err))
return
}
uuidsProvider = &mojangtextures.RemoteApiUuidsProvider{
Url: *remoteUrl,
Logger: logger,
}
} else {
uuidsProvider = &mojangtextures.BatchUuidsProvider{
IterationDelay: time.Duration(viper.GetInt("queue.loop_delay")) * time.Millisecond,
IterationSize: viper.GetInt("queue.batch_size"),
Logger: logger,
},
}
}
texturesStorage := mojangtextures.NewInMemoryTexturesStorage()
texturesStorage.Start()
mojangTexturesProvider := &mojangtextures.Provider{
Logger: logger,
UuidsProvider: uuidsProvider,
TexturesProvider: &mojangtextures.MojangApiTexturesProvider{
Logger: logger,
},

View File

@ -0,0 +1,71 @@
package mojangtextures
import (
"encoding/json"
"io/ioutil"
"net/http"
. "net/url"
"path"
"time"
"github.com/mono83/slf/wd"
"github.com/elyby/chrly/api/mojang"
"github.com/elyby/chrly/bootstrap"
)
var HttpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 1024,
},
}
type RemoteApiUuidsProvider struct {
Url URL
Logger wd.Watchdog
}
func (ctx *RemoteApiUuidsProvider) GetUuid(username string) (*mojang.ProfileInfo, error) {
ctx.Logger.IncCounter("mojang_textures.usernames.request", 1)
url := ctx.Url
url.Path = path.Join(url.Path, username)
request, _ := http.NewRequest("GET", url.String(), nil)
request.Header.Add("Accept", "application/json")
// Change default User-Agent to allow specify "Username -> UUID at time" Mojang's api endpoint
request.Header.Add("User-Agent", "Chrly/"+bootstrap.GetVersion())
start := time.Now()
response, err := HttpClient.Do(request)
ctx.Logger.RecordTimer("mojang_textures.usernames.request_time", time.Since(start))
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == 204 {
return nil, nil
}
if response.StatusCode != 200 {
return nil, &UnexpectedRemoteApiResponse{response}
}
var result *mojang.ProfileInfo
body, _ := ioutil.ReadAll(response.Body)
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
return result, nil
}
type UnexpectedRemoteApiResponse struct {
Response *http.Response
}
func (*UnexpectedRemoteApiResponse) Error() string {
return "Unexpected remote api response"
}

View File

@ -0,0 +1,149 @@
package mojangtextures
import (
"net"
"net/http"
"net/url"
"testing"
"github.com/h2non/gock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
mocks "github.com/elyby/chrly/tests"
)
type remoteApiUuidsProviderTestSuite struct {
suite.Suite
Provider *RemoteApiUuidsProvider
Logger *mocks.WdMock
}
func (suite *remoteApiUuidsProviderTestSuite) SetupSuite() {
client := &http.Client{}
gock.InterceptClient(client)
HttpClient = client
}
func (suite *remoteApiUuidsProviderTestSuite) SetupTest() {
suite.Logger = &mocks.WdMock{}
suite.Provider = &RemoteApiUuidsProvider{
Logger: suite.Logger,
}
}
func (suite *remoteApiUuidsProviderTestSuite) TearDownTest() {
suite.Logger.AssertExpectations(suite.T())
gock.Off()
}
func TestRemoteApiUuidsProvider(t *testing.T) {
suite.Run(t, new(remoteApiUuidsProviderTestSuite))
}
func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForValidUsername() {
suite.Logger.On("IncCounter", "mojang_textures.usernames.request", int64(1)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.request_time", mock.Anything).Once()
gock.New("http://example.com").
Get("/subpath/username").
Reply(200).
JSON(map[string]interface{}{
"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"name": "username",
})
suite.Provider.Url = shouldParseUrl("http://example.com/subpath")
result, err := suite.Provider.GetUuid("username")
assert := suite.Assert()
if assert.NoError(err) {
assert.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", result.Id)
assert.Equal("username", result.Name)
assert.False(result.IsLegacy)
assert.False(result.IsDemo)
}
}
func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNotExistsUsername() {
suite.Logger.On("IncCounter", "mojang_textures.usernames.request", int64(1)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.request_time", mock.Anything).Once()
gock.New("http://example.com").
Get("/subpath/username").
Reply(204)
suite.Provider.Url = shouldParseUrl("http://example.com/subpath")
result, err := suite.Provider.GetUuid("username")
assert := suite.Assert()
assert.Nil(result)
assert.Nil(err)
}
func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNon20xResponse() {
suite.Logger.On("IncCounter", "mojang_textures.usernames.request", int64(1)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.request_time", mock.Anything).Once()
gock.New("http://example.com").
Get("/subpath/username").
Reply(504).
BodyString("504 Gateway Timeout")
suite.Provider.Url = shouldParseUrl("http://example.com/subpath")
result, err := suite.Provider.GetUuid("username")
assert := suite.Assert()
assert.Nil(result)
assert.EqualError(err, "Unexpected remote api response")
}
func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNotSuccessRequest() {
suite.Logger.On("IncCounter", "mojang_textures.usernames.request", int64(1)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.request_time", mock.Anything).Once()
expectedError := &net.OpError{Op: "dial"}
gock.New("http://example.com").
Get("/subpath/username").
ReplyError(expectedError)
suite.Provider.Url = shouldParseUrl("http://example.com/subpath")
result, err := suite.Provider.GetUuid("username")
assert := suite.Assert()
assert.Nil(result)
if assert.Error(err) {
assert.IsType(&url.Error{}, err)
casterErr, _ := err.(*url.Error)
assert.Equal(expectedError, casterErr.Err)
}
}
func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForInvalidSuccessResponse() {
suite.Logger.On("IncCounter", "mojang_textures.usernames.request", int64(1)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.request_time", mock.Anything).Once()
gock.New("http://example.com").
Get("/subpath/username").
Reply(200).
BodyString("completely not json")
suite.Provider.Url = shouldParseUrl("http://example.com/subpath")
result, err := suite.Provider.GetUuid("username")
assert := suite.Assert()
assert.Nil(result)
assert.Error(err)
}
func shouldParseUrl(rawUrl string) url.URL {
url, err := url.Parse(rawUrl)
if err != nil {
panic(err)
}
return *url
}