Compare commits

...

24 Commits

Author SHA1 Message Date
ErickSkrauch
ab847589ba Merge branch 'master' into ely 2019-11-08 02:13:05 +03:00
ErickSkrauch
6fd88e077e Prepare 4.3.0 release 2019-11-08 02:12:31 +03:00
ErickSkrauch
ae185e1daa Bump Go version to 1.13 2019-11-08 02:08:10 +03:00
ErickSkrauch
7353047467 Increase queue loop delay from 1 to 2.5 seconds. Add configuration param to adjust its value 2019-11-08 01:54:16 +03:00
ErickSkrauch
b2a1fd450b Handle 403 Forbidden error from Mojang's API 2019-11-08 01:32:26 +03:00
ErickSkrauch
e573e6146d Merge branch 'master' into ely 2019-10-03 01:40:30 +03:00
ErickSkrauch
334e60ff2f Prepare 4.2.3 release 2019-10-03 01:26:34 +03:00
ErickSkrauch
6d6d0e4b79 Decrease queue batch size. Log all 400 response from the Mojang's API. Resolves #10. 2019-10-03 01:24:25 +03:00
ErickSkrauch
2775b14e78 Merge branch 'master' into ely 2019-06-19 01:08:32 +03:00
ErickSkrauch
0cfed45b64 Prepare 4.2.2 release 2019-06-19 01:02:41 +03:00
ErickSkrauch
f872fe4698 Fix race condition, introduced in the previous commit 2019-06-19 00:56:09 +03:00
ErickSkrauch
5b4761e4e5 Fixes #9. Start GC loop for in-memory textures cache. 2019-06-18 23:34:16 +03:00
ErickSkrauch
c25a2f2360 Merge branch 'master' into ely 2019-05-06 17:27:28 +03:00
ErickSkrauch
e81ca1520d Add codecov shield [skip ci] 2019-05-06 17:26:55 +03:00
ErickSkrauch
edc368aa81 Merge branch 'master' into ely 2019-05-02 21:56:04 +03:00
ErickSkrauch
4097e61a02 Merge branch 'master' into ely 2019-05-02 21:08:21 +03:00
ErickSkrauch
26a8628070 Remove faces endpoint 2019-05-02 20:55:50 +03:00
ErickSkrauch
ae0ff91a64 Fix test for signed textures 2019-05-02 20:54:18 +03:00
ErickSkrauch
ab6410ff4a Merge branch 'master' into ely 2019-05-02 20:53:45 +03:00
ErickSkrauch
c7ac890812 Merge branch 'master' into ely 2018-03-19 02:19:14 +03:00
ErickSkrauch
7734f2cbd5 Merge branch 'master' into ely 2018-02-17 01:52:47 +03:00
ErickSkrauch
55b8c12955 Restore Ely documentation link on the 404 page 2018-02-16 21:01:01 +03:00
ErickSkrauch
10ff6f34fb Restore Ely signed textures ely property 2018-02-16 21:01:01 +03:00
ErickSkrauch
31cd75ffa7 Restore Ely faces API 2018-02-16 21:01:01 +03:00
13 changed files with 120 additions and 37 deletions

View File

@@ -2,7 +2,7 @@ sudo: required
language: go language: go
go: go:
- 1.12 - 1.13
services: services:
- docker - docker

View File

@@ -4,7 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 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] ## [Unreleased] - xxxx-xx-xx
## [4.3.0] - 2019-11-08
### Added
- 403 Forbidden errors from the Mojang's API are now logged
- `QUEUE_LOOP_DELAY` configuration param to adjust Mojang's textures queue performance
### Changed
- Mojang's textures queue loop is now has an iteration delay of 2.5 seconds (was 1)
- Bumped Go version to 1.13.
## [4.2.3] - 2019-10-03
### Changed
- Mojang's textures queue batch size [reduced to 10](https://wiki.vg/index.php?title=Mojang_API&type=revision&diff=14964&oldid=14954).
- 400 BadRequest errors from the Mojang's API are now logged.
## [4.2.2] - 2019-06-19
### Fixed
- GC for in-memory textures cache has not been initialized.
## [4.2.1] - 2019-05-06 ## [4.2.1] - 2019-05-06
### Changed ### Changed
@@ -57,6 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
from the textures link instead. from the textures link instead.
- `hash` field from `POST /api/skins` endpoint. - `hash` field from `POST /api/skins` endpoint.
[Unreleased]: https://github.com/elyby/chrly/compare/4.2.1...HEAD [Unreleased]: https://github.com/elyby/chrly/compare/4.3.0...HEAD
[4.3.0]: https://github.com/elyby/chrly/compare/4.2.3...4.3.0
[4.2.3]: https://github.com/elyby/chrly/compare/4.2.2...4.2.3
[4.2.2]: https://github.com/elyby/chrly/compare/4.2.1...4.2.2
[4.2.1]: https://github.com/elyby/chrly/compare/4.2.0...4.2.1 [4.2.1]: https://github.com/elyby/chrly/compare/4.2.0...4.2.1
[4.2.0]: https://github.com/elyby/chrly/compare/4.1.1...4.2.0 [4.2.0]: https://github.com/elyby/chrly/compare/4.1.1...4.2.0

View File

@@ -2,6 +2,7 @@
[![Written in Go][ico-lang]][link-go] [![Written in Go][ico-lang]][link-go]
[![Build Status][ico-build]][link-build] [![Build Status][ico-build]][link-build]
[![Coverage][ico-coverage]][link-coverage]
[![Keep a Changelog][ico-changelog]](CHANGELOG.md) [![Keep a Changelog][ico-changelog]](CHANGELOG.md)
[![Software License][ico-license]](LICENSE) [![Software License][ico-license]](LICENSE)
@@ -56,11 +57,12 @@ docker-compose up -d app
**Variables to adjust:** **Variables to adjust:**
| ENV | Description | Example | | ENV | Description | Example |
|--------------------|------------------------------------------------------------------------------------|-------------------------------------------| |--------------------|-------------------------------------------------------------------------------------------------|-------------------------------------------|
| STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` | | STORAGE_REDIS_POOL | By default, Chrly creates pool with 10 connection, but you may want to increase it | `20` |
| STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` | | STATSD_ADDR | StatsD can be used to collect metrics | `localhost:8125` |
| SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` | | SENTRY_DSN | Sentry can be used to collect app errors | `https://public:private@your.sentry.io/1` |
| QUEUE_LOOP_DELAY | Parameter is sets the delay before each iteration of the Mojang's textures queue (milliseconds) | `3200` |
If something goes wrong, you can always access logs by executing `docker-compose logs -f app`. If something goes wrong, you can always access logs by executing `docker-compose logs -f app`.
@@ -260,8 +262,10 @@ To run tests execute `go test ./...`. If your Go version is older than 1.9, then
[ico-lang]: https://img.shields.io/badge/lang-go%201.12-blue.svg?style=flat-square [ico-lang]: https://img.shields.io/badge/lang-go%201.12-blue.svg?style=flat-square
[ico-build]: https://img.shields.io/travis/elyby/chrly.svg?style=flat-square [ico-build]: https://img.shields.io/travis/elyby/chrly.svg?style=flat-square
[ico-coverage]: https://img.shields.io/codecov/c/github/elyby/chrly.svg?style=flat-square
[ico-changelog]: https://img.shields.io/badge/keep%20a-changelog-orange.svg?style=flat-square [ico-changelog]: https://img.shields.io/badge/keep%20a-changelog-orange.svg?style=flat-square
[ico-license]: https://img.shields.io/github/license/elyby/chrly.svg?style=flat-square [ico-license]: https://img.shields.io/github/license/elyby/chrly.svg?style=flat-square
[link-go]: https://golang.org [link-go]: https://golang.org
[link-build]: https://travis-ci.org/elyby/chrly [link-build]: https://travis-ci.org/elyby/chrly
[link-coverage]: https://codecov.io/gh/elyby/chrly

View File

@@ -125,6 +125,8 @@ func validateResponse(response *http.Response) error {
_ = json.Unmarshal(body, &decodedError) _ = json.Unmarshal(body, &decodedError)
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message} return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
case response.StatusCode == 403:
return &ForbiddenError{}
case response.StatusCode == 429: case response.StatusCode == 429:
return &TooManyRequestsError{} return &TooManyRequestsError{}
case response.StatusCode >= 500: case response.StatusCode >= 500:
@@ -166,6 +168,15 @@ func (*BadRequestError) IsMojangError() bool {
return true return true
} }
// When Mojang decides you're such a bad guy, this error appears (even if the request has no authorization)
type ForbiddenError struct {
ResponseError
}
func (*ForbiddenError) Error() string {
return "Forbidden"
}
// When you exceed the set limit of requests, this error will be returned // When you exceed the set limit of requests, this error will be returned
type TooManyRequestsError struct { type TooManyRequestsError struct {
ResponseError ResponseError

View File

@@ -102,6 +102,27 @@ func TestUsernamesToUuids(t *testing.T) {
assert.Implements((*ResponseError)(nil), err) assert.Implements((*ResponseError)(nil), err)
}) })
t.Run("handle forbidden response", func(t *testing.T) {
assert := testify.New(t)
defer gock.Off()
gock.New("https://api.mojang.com").
Post("/profiles/minecraft").
Reply(403).
BodyString("just because")
client := &http.Client{}
gock.InterceptClient(client)
HttpClient = client
result, err := UsernamesToUuids([]string{"Thinkofdeath", "maksimkurb"})
assert.Nil(result)
assert.IsType(&ForbiddenError{}, err)
assert.EqualError(err, "Forbidden")
assert.Implements((*ResponseError)(nil), err)
})
t.Run("handle too many requests response", func(t *testing.T) { t.Run("handle too many requests response", func(t *testing.T) {
assert := testify.New(t) assert := testify.New(t)

View File

@@ -25,9 +25,11 @@ type inMemoryTexturesStorage struct {
} }
func CreateInMemoryTexturesStorage() *inMemoryTexturesStorage { func CreateInMemoryTexturesStorage() *inMemoryTexturesStorage {
return &inMemoryTexturesStorage{ storage := &inMemoryTexturesStorage{
data: make(map[string]*inMemoryItem), data: make(map[string]*inMemoryItem),
} }
return storage
} }
func (s *inMemoryTexturesStorage) Start() { func (s *inMemoryTexturesStorage) Start() {

View File

@@ -14,9 +14,10 @@ import (
"github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/api/mojang"
) )
var UuidsQueueIterationDelay = 2*time.Second + 500*time.Millisecond
var usernamesToUuids = mojang.UsernamesToUuids var usernamesToUuids = mojang.UsernamesToUuids
var uuidToTextures = mojang.UuidToTextures var uuidToTextures = mojang.UuidToTextures
var uuidsQueueIterationDelay = time.Second
var forever = func() bool { var forever = func() bool {
return true return true
} }
@@ -97,13 +98,13 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe
func (ctx *JobsQueue) startQueue() { func (ctx *JobsQueue) startQueue() {
go func() { go func() {
time.Sleep(uuidsQueueIterationDelay) time.Sleep(UuidsQueueIterationDelay)
for forever() { for forever() {
start := time.Now() start := time.Now()
ctx.queueRound() ctx.queueRound()
elapsed := time.Since(start) elapsed := time.Since(start)
ctx.Logger.RecordTimer("mojang_textures.usernames.round_time", elapsed) ctx.Logger.RecordTimer("mojang_textures.usernames.round_time", elapsed)
time.Sleep(uuidsQueueIterationDelay) time.Sleep(UuidsQueueIterationDelay)
} }
}() }()
} }
@@ -114,7 +115,7 @@ func (ctx *JobsQueue) queueRound() {
} }
queueSize := ctx.queue.Size() queueSize := ctx.queue.Size()
jobs := ctx.queue.Dequeue(100) jobs := ctx.queue.Dequeue(10)
ctx.Logger.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(jobs))) ctx.Logger.UpdateGauge("mojang_textures.usernames.iteration_size", int64(len(jobs)))
ctx.Logger.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize-len(jobs))) ctx.Logger.UpdateGauge("mojang_textures.usernames.queue_size", int64(queueSize-len(jobs)))
var usernames []string var usernames []string
@@ -182,8 +183,19 @@ func (ctx *JobsQueue) handleResponseError(err error, threadName string) {
switch err.(type) { switch err.(type) {
case mojang.ResponseError: case mojang.ResponseError:
if _, ok := err.(*mojang.BadRequestError); ok {
ctx.Logger.Warning(":name: Got 400 Bad Request :err", wd.NameParam(threadName), wd.ErrParam(err))
return
}
if _, ok := err.(*mojang.ForbiddenError); ok {
ctx.Logger.Warning(":name: Got 403 Forbidden :err", wd.NameParam(threadName), wd.ErrParam(err))
return
}
if _, ok := err.(*mojang.TooManyRequestsError); ok { if _, ok := err.(*mojang.TooManyRequestsError); ok {
ctx.Logger.Warning(":name: Got 429 Too Many Requests :err", wd.NameParam(threadName), wd.ErrParam(err)) ctx.Logger.Warning(":name: Got 429 Too Many Requests :err", wd.NameParam(threadName), wd.ErrParam(err))
return
} }
return return

View File

@@ -85,7 +85,7 @@ type queueTestSuite struct {
} }
func (suite *queueTestSuite) SetupSuite() { func (suite *queueTestSuite) SetupSuite() {
uuidsQueueIterationDelay = 0 UuidsQueueIterationDelay = 0
} }
func (suite *queueTestSuite) SetupTest() { func (suite *queueTestSuite) SetupTest() {
@@ -258,30 +258,30 @@ func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid
suite.Assert().Nil(<-resultChan) suite.Assert().Nil(<-resultChan)
} }
func (suite *queueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { func (suite *queueTestSuite) TestReceiveTexturesForMoreThan10Usernames() {
usernames := make([]string, 120) usernames := make([]string, 12)
for i := 0; i < 120; i++ { for i := 0; i < cap(usernames); i++ {
usernames[i] = randStr(8) usernames[i] = randStr(8)
} }
suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(120) suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(12)
suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(120) suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(12)
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(100)).Once() suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(10)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(20)).Once() suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(2)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(20)).Once() suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(2)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)).Once() suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(0)).Once()
suite.Logger.On("RecordTimer", "mojang_textures.usernames.round_time", mock.Anything).Twice() suite.Logger.On("RecordTimer", "mojang_textures.usernames.round_time", mock.Anything).Twice()
suite.Logger.On("IncCounter", "mojang_textures.usernames.uuid_miss", int64(1)).Times(120) suite.Logger.On("IncCounter", "mojang_textures.usernames.uuid_miss", int64(1)).Times(12)
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Times(120) suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Times(12)
suite.Storage.On("GetUuid", mock.Anything).Times(120).Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", mock.Anything).Times(12).Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", mock.Anything, "").Times(120).Return(nil) // should be called with "" if username is not compared to uuid suite.Storage.On("StoreUuid", mock.Anything, "").Times(12).Return(nil) // should be called with "" if username is not compared to uuid
// Storage.GetTextures and Storage.SetTextures shouldn't be called // Storage.GetTextures and Storage.SetTextures shouldn't be called
suite.MojangApi.On("UsernamesToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).Once().Return([]*mojang.ProfileInfo{}, nil)
suite.MojangApi.On("UsernamesToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernamesToUuids", usernames[10:12]).Once().Return([]*mojang.ProfileInfo{}, nil)
channels := make([]chan *mojang.SignedTexturesResponse, 120) channels := make([]chan *mojang.SignedTexturesResponse, 12)
for i, username := range usernames { for i, username := range usernames {
channels[i] = suite.Queue.GetTexturesForUsername(username) channels[i] = suite.Queue.GetTexturesForUsername(username)
} }
@@ -403,6 +403,7 @@ func (*timeoutError) Temporary() bool { return false }
var expectedErrors = []error{ var expectedErrors = []error{
&mojang.BadRequestError{}, &mojang.BadRequestError{},
&mojang.ForbiddenError{},
&mojang.TooManyRequestsError{}, &mojang.TooManyRequestsError{},
&mojang.ServerError{}, &mojang.ServerError{},
&timeoutError{}, &timeoutError{},
@@ -417,6 +418,8 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything) suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything) suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors)) suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
suite.Logger.On("Warning", ":name: Got 400 Bad Request :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Warning", ":name: Got 403 Forbidden :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once() suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
@@ -453,6 +456,8 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything) suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything) suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors)) suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Times(len(expectedErrors))
suite.Logger.On("Warning", ":name: Got 400 Bad Request :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Warning", ":name: Got 403 Forbidden :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once() suite.Logger.On("Warning", ":name: Got 429 Too Many Requests :err", mock.Anything, mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})

View File

@@ -3,6 +3,7 @@ package cmd
import ( import (
"fmt" "fmt"
"log" "log"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -51,11 +52,14 @@ var serveCmd = &cobra.Command{
return return
} }
queue.UuidsQueueIterationDelay = time.Duration(viper.GetInt("queue.loop_delay")) * time.Millisecond
texturesStorage := queue.CreateInMemoryTexturesStorage()
texturesStorage.Start()
mojangTexturesQueue := &queue.JobsQueue{ mojangTexturesQueue := &queue.JobsQueue{
Logger: logger, Logger: logger,
Storage: &queue.SplittedStorage{ Storage: &queue.SplittedStorage{
UuidsStorage: mojangUuidsRepository, UuidsStorage: mojangUuidsRepository,
TexturesStorage: queue.CreateInMemoryTexturesStorage(), TexturesStorage: texturesStorage,
}, },
} }
logger.Info("Mojang's textures queue is successfully initialized") logger.Info("Mojang's textures queue is successfully initialized")
@@ -84,4 +88,5 @@ func init() {
viper.SetDefault("storage.redis.poll", 10) viper.SetDefault("storage.redis.poll", 10)
viper.SetDefault("storage.filesystem.basePath", "data") viper.SetDefault("storage.filesystem.basePath", "data")
viper.SetDefault("storage.filesystem.capesDirName", "capes") viper.SetDefault("storage.filesystem.capesDirName", "capes")
viper.SetDefault("queue.loop_delay", 2_500)
} }

View File

@@ -9,6 +9,7 @@ func (cfg *Config) NotFound(response http.ResponseWriter, request *http.Request)
data, _ := json.Marshal(map[string]string{ data, _ := json.Marshal(map[string]string{
"status": "404", "status": "404",
"message": "Not Found", "message": "Not Found",
"link": "http://docs.ely.by/skin-system.html",
}) })
response.Header().Set("Content-Type", "application/json") response.Header().Set("Content-Type", "application/json")

View File

@@ -22,6 +22,7 @@ func TestConfig_NotFound(t *testing.T) {
response, _ := ioutil.ReadAll(resp.Body) response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{ assert.JSONEq(`{
"status": "404", "status": "404",
"message": "Not Found" "message": "Not Found",
"link": "http://docs.ely.by/skin-system.html"
}`, string(response)) }`, string(response))
} }

View File

@@ -39,8 +39,8 @@ func (cfg *Config) SignedTextures(response http.ResponseWriter, request *http.Re
} }
responseData.Props = append(responseData.Props, &mojang.Property{ responseData.Props = append(responseData.Props, &mojang.Property{
Name: "chrly", Name: "ely",
Value: "how do you tame a horse in Minecraft?", Value: "but why are you asking?",
}) })
responseJson, _ := json.Marshal(responseData) responseJson, _ := json.Marshal(responseData)

View File

@@ -42,8 +42,8 @@ func TestConfig_SignedTextures(t *testing.T) {
"value": "mocked textures base64" "value": "mocked textures base64"
}, },
{ {
"name": "chrly", "name": "ely",
"value": "how do you tame a horse in Minecraft?" "value": "but why are you asking?"
} }
] ]
}`, string(response)) }`, string(response))
@@ -132,8 +132,8 @@ func TestConfig_SignedTextures(t *testing.T) {
"value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXIiLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9tb2phbmcvc2tpbi5wbmcifX19" "value": "eyJ0aW1lc3RhbXAiOjE1NTYzOTg1NzIsInByb2ZpbGVJZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwicHJvZmlsZU5hbWUiOiJtb2NrX3VzZXIiLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9tb2phbmcvc2tpbi5wbmcifX19"
}, },
{ {
"name": "chrly", "name": "ely",
"value": "how do you tame a horse in Minecraft?" "value": "but why are you asking?"
} }
] ]
}`, string(response)) }`, string(response))