Compare commits

...

28 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
d36fc77df0 Prepare 4.2.1 release 2019-05-06 17:20:52 +03:00
ErickSkrauch
ab78af33a5 Remove validation rules for a hash field 2019-05-06 17:17:44 +03:00
ErickSkrauch
1f057a27aa Adjust Mojang's queue behavior 2019-05-06 17:12:37 +03:00
ErickSkrauch
9dde5715f5 Adjust Mojang's queue behavior 2019-05-05 23:06:29 +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
19 changed files with 342 additions and 211 deletions

View File

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

View File

@@ -4,7 +4,38 @@ 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/),
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
### Changed
- Improved Keep-Alive settings for HTTP client used to perform requests to Mojang's APIs.
- Mojang's textures queue now has static delay of 1 second after each iteration to prevent strange `429` errors.
- Mojang's textures queue now caches even errored responses for signed textures to avoid `429` errors.
- Mojang's textures queue now caches textures data for 70 seconds to avoid strange `429` errors.
- Mojang's textures queue now doesn't log timeout errors.
### Fixed
- Panic when Redis connection is broken.
- Duplication of Redis connections pool for Mojang's textures queue.
- Removed validation rules for `hash` field.
## [4.2.0] - 2019-05-02
### Added
@@ -44,5 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
from the textures link instead.
- `hash` field from `POST /api/skins` endpoint.
[Unreleased]: https://github.com/elyby/chrly/compare/4.2.0...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.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]
[![Build Status][ico-build]][link-build]
[![Coverage][ico-coverage]][link-coverage]
[![Keep a Changelog][ico-changelog]](CHANGELOG.md)
[![Software License][ico-license]](LICENSE)
@@ -57,10 +58,11 @@ docker-compose up -d app
**Variables to adjust:**
| ENV | Description | Example |
|--------------------|------------------------------------------------------------------------------------|-------------------------------------------|
|--------------------|-------------------------------------------------------------------------------------------------|-------------------------------------------|
| 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` |
| 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`.
@@ -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-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-license]: https://img.shields.io/github/license/elyby/chrly.svg?style=flat-square
[link-go]: https://golang.org
[link-build]: https://travis-ci.org/elyby/chrly
[link-coverage]: https://codecov.io/gh/elyby/chrly

View File

@@ -10,6 +10,9 @@ import (
var HttpClient = &http.Client{
Timeout: 3 * time.Second,
Transport: &http.Transport{
MaxIdleConnsPerHost: 1024,
},
}
type SignedTexturesResponse struct {
@@ -122,6 +125,8 @@ func validateResponse(response *http.Response) error {
_ = json.Unmarshal(body, &decodedError)
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
case response.StatusCode == 403:
return &ForbiddenError{}
case response.StatusCode == 429:
return &TooManyRequestsError{}
case response.StatusCode >= 500:
@@ -163,6 +168,15 @@ func (*BadRequestError) IsMojangError() bool {
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
type TooManyRequestsError struct {
ResponseError

View File

@@ -102,6 +102,27 @@ func TestUsernamesToUuids(t *testing.T) {
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) {
assert := testify.New(t)

View File

@@ -9,8 +9,8 @@ import (
"github.com/tevino/abool"
)
var inMemoryStorageGCPeriod = time.Second
var inMemoryStoragePersistPeriod = time.Second * 60
var inMemoryStorageGCPeriod = 10 * time.Second
var inMemoryStoragePersistPeriod = time.Minute + 10*time.Second
var now = time.Now
type inMemoryItem struct {
@@ -25,9 +25,11 @@ type inMemoryTexturesStorage struct {
}
func CreateInMemoryTexturesStorage() *inMemoryTexturesStorage {
return &inMemoryTexturesStorage{
storage := &inMemoryTexturesStorage{
data: make(map[string]*inMemoryItem),
}
return storage
}
func (s *inMemoryTexturesStorage) Start() {
@@ -59,25 +61,33 @@ func (s *inMemoryTexturesStorage) GetTextures(uuid string) (*mojang.SignedTextur
defer s.lock.Unlock()
item, exists := s.data[uuid]
if !exists || now().Add(inMemoryStoragePersistPeriod*time.Duration(-1)).UnixNano()/10e5 > item.timestamp {
validRange := getMinimalNotExpiredTimestamp()
if !exists || validRange > item.timestamp {
return nil, &ValueNotFound{}
}
return item.textures, nil
}
func (s *inMemoryTexturesStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
s.lock.Lock()
defer s.lock.Unlock()
func (s *inMemoryTexturesStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
var timestamp int64
if textures != nil {
decoded := textures.DecodeTextures()
if decoded == nil {
panic("unable to decode textures")
}
s.data[textures.Id] = &inMemoryItem{
timestamp = decoded.Timestamp
} else {
timestamp = unixNanoToUnixMicro(now().UnixNano())
}
s.lock.Lock()
defer s.lock.Unlock()
s.data[uuid] = &inMemoryItem{
textures: textures,
timestamp: decoded.Timestamp,
timestamp: timestamp,
}
}
@@ -85,10 +95,18 @@ func (s *inMemoryTexturesStorage) gc() {
s.lock.Lock()
defer s.lock.Unlock()
maxTime := now().Add(inMemoryStoragePersistPeriod*time.Duration(-1)).UnixNano() / 10e5
maxTime := getMinimalNotExpiredTimestamp()
for uuid, value := range s.data {
if maxTime > value.timestamp {
delete(s.data, uuid)
}
}
}
func getMinimalNotExpiredTimestamp() int64 {
return unixNanoToUnixMicro(now().Add(inMemoryStoragePersistPeriod * time.Duration(-1)).UnixNano())
}
func unixNanoToUnixMicro(unixNano int64) int64 {
return unixNano / 10e5
}

View File

@@ -59,7 +59,7 @@ func TestInMemoryTexturesStorage_GetTextures(t *testing.T) {
assert := testify.New(t)
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
assert.Equal(texturesWithSkin, result)
@@ -70,7 +70,7 @@ func TestInMemoryTexturesStorage_GetTextures(t *testing.T) {
assert := testify.New(t)
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
now = func() time.Time {
return time.Now().Add(time.Minute * 2)
@@ -90,7 +90,7 @@ func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
assert := testify.New(t)
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithSkin)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
assert.Equal(texturesWithSkin, result)
@@ -101,8 +101,8 @@ func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
assert := testify.New(t)
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(texturesWithoutSkin)
storage.StoreTextures(texturesWithSkin)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithoutSkin)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", texturesWithSkin)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
assert.NotEqual(texturesWithoutSkin, result)
@@ -110,6 +110,17 @@ func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
assert.Nil(err)
})
t.Run("store nil textures", func(t *testing.T) {
assert := testify.New(t)
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", nil)
result, err := storage.GetTextures("dead24f9a4fa4877b7b04c8c6c72bb46")
assert.Nil(result)
assert.Nil(err)
})
t.Run("should panic if textures prop is not decoded", func(t *testing.T) {
assert := testify.New(t)
@@ -121,7 +132,7 @@ func TestInMemoryTexturesStorage_StoreTextures(t *testing.T) {
assert.PanicsWithValue("unable to decode textures", func() {
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(toStore)
storage.StoreTextures("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", toStore)
})
})
}
@@ -164,8 +175,8 @@ func TestInMemoryTexturesStorage_GarbageCollection(t *testing.T) {
}
storage := CreateInMemoryTexturesStorage()
storage.StoreTextures(textures1)
storage.StoreTextures(textures2)
storage.StoreTextures("dead24f9a4fa4877b7b04c8c6c72bb46", textures1)
storage.StoreTextures("b5d58475007d4f9e9ddd1403e2497579", textures2)
storage.Start()

View File

@@ -2,6 +2,7 @@ package queue
import (
"net"
"net/url"
"regexp"
"strings"
"sync"
@@ -13,9 +14,10 @@ import (
"github.com/elyby/chrly/api/mojang"
)
var UuidsQueueIterationDelay = 2*time.Second + 500*time.Millisecond
var usernamesToUuids = mojang.UsernamesToUuids
var uuidToTextures = mojang.UuidToTextures
var uuidsQueuePeriod = time.Second
var forever = func() bool {
return true
}
@@ -96,13 +98,13 @@ func (ctx *JobsQueue) GetTexturesForUsername(username string) chan *mojang.Signe
func (ctx *JobsQueue) startQueue() {
go func() {
time.Sleep(uuidsQueuePeriod)
time.Sleep(UuidsQueueIterationDelay)
for forever() {
start := time.Now()
ctx.queueRound()
elapsed := time.Since(start)
ctx.Logger.RecordTimer("mojang_textures.usernames.round_time", elapsed)
time.Sleep(uuidsQueuePeriod - elapsed)
time.Sleep(UuidsQueueIterationDelay)
}
}()
}
@@ -113,7 +115,7 @@ func (ctx *JobsQueue) queueRound() {
}
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.queue_size", int64(queueSize-len(jobs)))
var usernames []string
@@ -123,7 +125,7 @@ func (ctx *JobsQueue) queueRound() {
profiles, err := usernamesToUuids(usernames)
if err != nil {
ctx.handleResponseError(err)
ctx.handleResponseError(err, "usernames")
for _, job := range jobs {
job.RespondTo <- nil
}
@@ -134,7 +136,7 @@ func (ctx *JobsQueue) queueRound() {
for _, job := range jobs {
go func(job *jobItem) {
var uuid string
// Profiles in response not ordered, so we must search each username over full array
// The profiles in the response are not ordered, so we must search each username over full array
for _, profile := range profiles {
if strings.EqualFold(job.Username, profile.Name) {
uuid = profile.Id
@@ -142,7 +144,7 @@ func (ctx *JobsQueue) queueRound() {
}
}
ctx.Storage.StoreUuid(job.Username, uuid)
_ = ctx.Storage.StoreUuid(job.Username, uuid)
if uuid == "" {
job.RespondTo <- nil
ctx.Logger.IncCounter("mojang_textures.usernames.uuid_miss", 1)
@@ -166,26 +168,34 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {
start := time.Now()
result, err := uuidToTextures(uuid, true)
ctx.Logger.RecordTimer("mojang_textures.textures.request_time", time.Since(start))
shouldCache := true
if err != nil {
ctx.handleResponseError(err)
shouldCache = false
ctx.handleResponseError(err, "textures")
}
if shouldCache && result != nil {
ctx.Storage.StoreTextures(result)
}
// Mojang can respond with an error, but count it as a hit, so store result even if the textures is nil
ctx.Storage.StoreTextures(uuid, result)
return result
}
func (ctx *JobsQueue) handleResponseError(err error) {
ctx.Logger.Debug("Got response error :err", wd.ErrParam(err))
func (ctx *JobsQueue) handleResponseError(err error, threadName string) {
ctx.Logger.Debug(":name: Got response error :err", wd.NameParam(threadName), wd.ErrParam(err))
switch err.(type) {
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 {
ctx.Logger.Warning("Got 429 Too Many Requests :err", wd.ErrParam(err))
ctx.Logger.Warning(":name: Got 429 Too Many Requests :err", wd.NameParam(threadName), wd.ErrParam(err))
return
}
return
@@ -194,6 +204,10 @@ func (ctx *JobsQueue) handleResponseError(err error) {
return
}
if _, ok := err.(*url.Error); ok {
return
}
if opErr, ok := err.(*net.OpError); ok && (opErr.Op == "dial" || opErr.Op == "read") {
return
}
@@ -203,5 +217,5 @@ func (ctx *JobsQueue) handleResponseError(err error) {
}
}
ctx.Logger.Emergency("Unknown Mojang response error: :err", wd.ErrParam(err))
ctx.Logger.Emergency(":name: Unknown Mojang response error: :err", wd.NameParam(threadName), wd.ErrParam(err))
}

View File

@@ -5,16 +5,19 @@ import (
"encoding/base64"
"errors"
"net"
"net/url"
"strings"
"syscall"
"time"
"github.com/elyby/chrly/api/mojang"
mocks "github.com/elyby/chrly/tests"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"testing"
mocks "github.com/elyby/chrly/tests"
)
type mojangApiMocks struct {
@@ -50,8 +53,9 @@ func (m *mockStorage) GetUuid(username string) (string, error) {
return args.String(0), args.Error(1)
}
func (m *mockStorage) StoreUuid(username string, uuid string) {
m.Called(username, uuid)
func (m *mockStorage) StoreUuid(username string, uuid string) error {
args := m.Called(username, uuid)
return args.Error(0)
}
func (m *mockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
@@ -64,8 +68,8 @@ func (m *mockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse,
return result, args.Error(1)
}
func (m *mockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
m.Called(textures)
func (m *mockStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
m.Called(uuid, textures)
}
type queueTestSuite struct {
@@ -81,7 +85,7 @@ type queueTestSuite struct {
}
func (suite *queueTestSuite) SetupSuite() {
uuidsQueuePeriod = 0
UuidsQueueIterationDelay = 0
}
func (suite *queueTestSuite) SetupTest() {
@@ -130,9 +134,9 @@ func (suite *queueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache()
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", expectedResult).Once()
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
@@ -163,12 +167,12 @@ func (suite *queueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache()
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("GetUuid", "Thinkofdeath").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("StoreUuid", "Thinkofdeath", "4566e69fc90748ee8d71d7ba5aa00d20").Once()
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil)
suite.Storage.On("StoreUuid", "Thinkofdeath", "4566e69fc90748ee8d71d7ba5aa00d20").Once().Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("GetTextures", "4566e69fc90748ee8d71d7ba5aa00d20").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult1).Once()
suite.Storage.On("StoreTextures", expectedResult2).Once()
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", expectedResult1).Once()
suite.Storage.On("StoreTextures", "4566e69fc90748ee8d71d7ba5aa00d20", expectedResult2).Once()
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
@@ -198,7 +202,7 @@ func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil)
// Storage.StoreUuid shouldn't be called
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", expectedResult).Once()
// MojangApi.UsernamesToUuids shouldn't be called
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil)
@@ -254,30 +258,30 @@ func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid
suite.Assert().Nil(<-resultChan)
}
func (suite *queueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
usernames := make([]string, 120)
for i := 0; i < 120; i++ {
func (suite *queueTestSuite) TestReceiveTexturesForMoreThan10Usernames() {
usernames := make([]string, 12)
for i := 0; i < cap(usernames); i++ {
usernames[i] = randStr(8)
}
suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(120)
suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(120)
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(100)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(20)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.queue_size", int64(20)).Once()
suite.Logger.On("IncCounter", "mojang_textures.request", int64(1)).Times(12)
suite.Logger.On("IncCounter", "mojang_textures.usernames.queued", int64(1)).Times(12)
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(10)).Once()
suite.Logger.On("UpdateGauge", "mojang_textures.usernames.iteration_size", int64(2)).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("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("RecordTimer", "mojang_textures.result_time", mock.Anything).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(12)
suite.Storage.On("GetUuid", mock.Anything).Times(120).Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", mock.Anything, "").Times(120) // if username is not compared to uuid, then receive ""
suite.Storage.On("GetUuid", mock.Anything).Times(12).Return("", &ValueNotFound{})
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
suite.MojangApi.On("UsernamesToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil)
suite.MojangApi.On("UsernamesToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil)
suite.MojangApi.On("UsernamesToUuids", usernames[0:10]).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 {
channels[i] = suite.Queue.GetTexturesForUsername(username)
}
@@ -305,9 +309,9 @@ func (suite *queueTestSuite) TestReceiveTexturesForTheSameUsernames() {
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", expectedResult).Once()
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil)
@@ -337,9 +341,9 @@ func (suite *queueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once()
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", expectedResult).Once()
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
@@ -373,7 +377,7 @@ func (suite *queueTestSuite) TestDoNothingWhenNoTasks() {
suite.Logger.On("RecordTimer", "mojang_textures.result_time", mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "").Once()
suite.Storage.On("StoreUuid", "maksimkurb", "").Once().Return(nil)
// Storage.GetTextures and Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil)
@@ -399,9 +403,11 @@ func (*timeoutError) Temporary() bool { return false }
var expectedErrors = []error{
&mojang.BadRequestError{},
&mojang.ForbiddenError{},
&mojang.TooManyRequestsError{},
&mojang.ServerError{},
&timeoutError{},
&url.Error{Op: "GET", URL: "http://localhost"},
&net.OpError{Op: "read"},
&net.OpError{Op: "dial"},
syscall.ECONNREFUSED,
@@ -411,8 +417,10 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
suite.Logger.On("IncCounter", mock.Anything, mock.Anything)
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", "Got response error :err", mock.Anything).Times(len(expectedErrors))
suite.Logger.On("Warning", "Got 429 Too Many Requests :err", mock.Anything).Once()
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.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
@@ -431,8 +439,8 @@ func (suite *queueTestSuite) TestShouldLogEmergencyOnUnexpectedErrorReturnedFrom
suite.Logger.On("IncCounter", mock.Anything, mock.Anything)
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", "Got response error :err", mock.Anything).Once()
suite.Logger.On("Emergency", "Unknown Mojang response error: :err", mock.Anything).Once()
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Emergency", ":name: Unknown Mojang response error: :err", mock.Anything, mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
@@ -447,13 +455,15 @@ func (suite *queueTestSuite) TestShouldNotLogErrorWhenExpectedErrorReturnedFromU
suite.Logger.On("IncCounter", mock.Anything, mock.Anything)
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", "Got response error :err", mock.Anything).Times(len(expectedErrors))
suite.Logger.On("Warning", "Got 429 Too Many Requests :err", mock.Anything).Once()
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.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32")
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Return(nil, &ValueNotFound{})
// Storage.StoreTextures shouldn't be called
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", (*mojang.SignedTexturesResponse)(nil))
for _, err := range expectedErrors {
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
@@ -473,13 +483,13 @@ func (suite *queueTestSuite) TestShouldLogEmergencyOnUnexpectedErrorReturnedFrom
suite.Logger.On("IncCounter", mock.Anything, mock.Anything)
suite.Logger.On("UpdateGauge", mock.Anything, mock.Anything)
suite.Logger.On("RecordTimer", mock.Anything, mock.Anything)
suite.Logger.On("Debug", "Got response error :err", mock.Anything).Once()
suite.Logger.On("Emergency", "Unknown Mojang response error: :err", mock.Anything).Once()
suite.Logger.On("Debug", ":name: Got response error :err", mock.Anything, mock.Anything).Once()
suite.Logger.On("Emergency", ":name: Unknown Mojang response error: :err", mock.Anything, mock.Anything).Once()
suite.Storage.On("GetUuid", "maksimkurb").Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32")
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Return(nil)
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Return(nil, &ValueNotFound{})
// Storage.StoreTextures shouldn't be called
suite.Storage.On("StoreTextures", "0d252b7218b648bfb86c2ae476954d32", (*mojang.SignedTexturesResponse)(nil))
suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},

View File

@@ -4,15 +4,15 @@ import "github.com/elyby/chrly/api/mojang"
type UuidsStorage interface {
GetUuid(username string) (string, error)
StoreUuid(username string, uuid string)
StoreUuid(username string, uuid string) error
}
// nil value can be passed to the storage to indicate that there is no textures
// for uuid and we know about it. Return err only in case, when storage completely
// unable to load any information about textures
type TexturesStorage interface {
// nil can be returned to indicate that there is no textures for uuid
// and we know about it. Return err only in case, when storage completely
// don't know anything about uuid
GetTextures(uuid string) (*mojang.SignedTexturesResponse, error)
StoreTextures(textures *mojang.SignedTexturesResponse)
StoreTextures(uuid string, textures *mojang.SignedTexturesResponse)
}
type Storage interface {
@@ -31,16 +31,16 @@ func (s *SplittedStorage) GetUuid(username string) (string, error) {
return s.UuidsStorage.GetUuid(username)
}
func (s *SplittedStorage) StoreUuid(username string, uuid string) {
s.UuidsStorage.StoreUuid(username, uuid)
func (s *SplittedStorage) StoreUuid(username string, uuid string) error {
return s.UuidsStorage.StoreUuid(username, uuid)
}
func (s *SplittedStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
return s.TexturesStorage.GetTextures(uuid)
}
func (s *SplittedStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
s.TexturesStorage.StoreTextures(textures)
func (s *SplittedStorage) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
s.TexturesStorage.StoreTextures(uuid, textures)
}
// This error can be used to indicate, that requested

View File

@@ -17,8 +17,9 @@ func (m *uuidsStorageMock) GetUuid(username string) (string, error) {
return args.String(0), args.Error(1)
}
func (m *uuidsStorageMock) StoreUuid(username string, uuid string) {
func (m *uuidsStorageMock) StoreUuid(username string, uuid string) error {
m.Called(username, uuid)
return nil
}
type texturesStorageMock struct {
@@ -35,8 +36,8 @@ func (m *texturesStorageMock) GetTextures(uuid string) (*mojang.SignedTexturesRe
return result, args.Error(1)
}
func (m *texturesStorageMock) StoreTextures(textures *mojang.SignedTexturesResponse) {
m.Called(textures)
func (m *texturesStorageMock) StoreTextures(uuid string, textures *mojang.SignedTexturesResponse) {
m.Called(uuid, textures)
}
func TestSplittedStorage(t *testing.T) {
@@ -59,7 +60,7 @@ func TestSplittedStorage(t *testing.T) {
t.Run("StoreUuid", func(t *testing.T) {
storage, uuidsMock, _ := createMockedStorage()
uuidsMock.On("StoreUuid", "username", "result").Once()
storage.StoreUuid("username", "result")
_ = storage.StoreUuid("username", "result")
uuidsMock.AssertExpectations(t)
})
@@ -74,10 +75,10 @@ func TestSplittedStorage(t *testing.T) {
})
t.Run("StoreTextures", func(t *testing.T) {
toStore := &mojang.SignedTexturesResponse{Id: "mock id"}
toStore := &mojang.SignedTexturesResponse{}
storage, _, texturesMock := createMockedStorage()
texturesMock.On("StoreTextures", toStore).Once()
storage.StoreTextures(toStore)
texturesMock.On("StoreTextures", "mock id", toStore).Once()
storage.StoreTextures("mock id", toStore)
texturesMock.AssertExpectations(t)
})
}

View File

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

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"strconv"
"strings"
"time"
@@ -24,33 +23,32 @@ type RedisFactory struct {
Host string
Port int
PoolSize int
connection *pool.Pool
pool *pool.Pool
}
// TODO: Why isn't a pointer used here?
func (f RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
func (f *RedisFactory) CreateSkinsRepository() (interfaces.SkinsRepository, error) {
return f.createInstance()
}
func (f RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
func (f *RedisFactory) CreateCapesRepository() (interfaces.CapesRepository, error) {
panic("capes repository not supported for this storage type")
}
func (f RedisFactory) CreateMojangUuidsRepository() (queue.UuidsStorage, error) {
func (f *RedisFactory) CreateMojangUuidsRepository() (queue.UuidsStorage, error) {
return f.createInstance()
}
func (f RedisFactory) createInstance() (*redisDb, error) {
connection, err := f.getConnection()
func (f *RedisFactory) createInstance() (*redisDb, error) {
p, err := f.getPool()
if err != nil {
return nil, err
}
return &redisDb{connection}, nil
return &redisDb{p}, nil
}
func (f RedisFactory) getConnection() (*pool.Pool, error) {
if f.connection == nil {
func (f *RedisFactory) getPool() (*pool.Pool, error) {
if f.pool == nil {
if f.Host == "" {
return nil, &ParamRequired{"host"}
}
@@ -65,88 +63,87 @@ func (f RedisFactory) getConnection() (*pool.Pool, error) {
return nil, err
}
f.connection = conn
go func() {
period := 5
for {
time.Sleep(time.Duration(period) * time.Second)
resp := f.connection.Cmd("PING")
if resp.Err == nil {
continue
f.pool = conn
}
log.Println("Redis not pinged. Try to reconnect")
conn, err := pool.New("tcp", addr, f.PoolSize)
if err != nil {
log.Printf("Cannot reconnect to redis: %v\n", err)
log.Printf("Waiting %d seconds to retry\n", period)
continue
}
f.connection = conn
log.Println("Reconnected")
}
}()
}
return f.connection, nil
return f.pool, nil
}
type redisDb struct {
conn *pool.Pool
pool *pool.Pool
}
const accountIdToUsernameKey = "hash:username-to-account-id"
const mojangUsernameToUuidKey = "hash:mojang-username-to-uuid"
func (db *redisDb) FindByUsername(username string) (*model.Skin, error) {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return nil, err
}
defer db.pool.Put(conn)
return findByUsername(username, conn)
}
func (db *redisDb) FindByUserId(id int) (*model.Skin, error) {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return nil, err
}
defer db.pool.Put(conn)
return findByUserId(id, conn)
}
func (db *redisDb) Save(skin *model.Skin) error {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return err
}
defer db.pool.Put(conn)
return save(skin, conn)
}
func (db *redisDb) RemoveByUserId(id int) error {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return err
}
defer db.pool.Put(conn)
return removeByUserId(id, conn)
}
func (db *redisDb) RemoveByUsername(username string) error {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return err
}
defer db.pool.Put(conn)
return removeByUsername(username, conn)
}
func (db *redisDb) GetUuid(username string) (string, error) {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
conn, err := db.pool.Get()
if err != nil {
return "", err
}
defer db.pool.Put(conn)
return findMojangUuidByUsername(username, conn)
}
func (db *redisDb) StoreUuid(username string, uuid string) {
conn, _ := db.conn.Get()
defer db.conn.Put(conn)
func (db *redisDb) StoreUuid(username string, uuid string) error {
conn, err := db.pool.Get()
if err != nil {
return err
}
defer db.pool.Put(conn)
storeMojangUuid(username, uuid, conn)
return storeMojangUuid(username, uuid, conn)
}
func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
@@ -156,7 +153,7 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
redisKey := buildUsernameKey(username)
response := conn.Cmd("GET", redisKey)
if response.IsType(redis.Nil) {
if !response.IsType(redis.Str) {
return nil, &SkinNotFoundError{username}
}
@@ -183,7 +180,7 @@ func findByUsername(username string, conn util.Cmder) (*model.Skin, error) {
func findByUserId(id int, conn util.Cmder) (*model.Skin, error) {
response := conn.Cmd("HGET", accountIdToUsernameKey, id)
if response.IsType(redis.Nil) {
if !response.IsType(redis.Str) {
return nil, &SkinNotFoundError{"unknown"}
}
@@ -215,17 +212,17 @@ func removeByUserId(id int, conn util.Cmder) error {
func removeByUsername(username string, conn util.Cmder) error {
record, err := findByUsername(username, conn)
if err != nil {
if _, ok := err.(*SkinNotFoundError); !ok {
return err
if _, ok := err.(*SkinNotFoundError); ok {
return nil
}
return err
}
conn.Cmd("MULTI")
conn.Cmd("DEL", buildUsernameKey(record.Username))
if record != nil {
conn.Cmd("HDEL", accountIdToUsernameKey, record.UserId)
}
conn.Cmd("EXEC")
@@ -272,9 +269,14 @@ func findMojangUuidByUsername(username string, conn util.Cmder) (string, error)
return parts[0], nil
}
func storeMojangUuid(username string, uuid string, conn util.Cmder) {
func storeMojangUuid(username string, uuid string, conn util.Cmder) error {
value := uuid + ":" + strconv.FormatInt(time.Now().Unix(), 10)
conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value)
res := conn.Cmd("HSET", mojangUsernameToUuidKey, strings.ToLower(username), value)
if res.IsType(redis.Err) {
return res.Err
}
return nil
}
func buildUsernameKey(username string) string {
@@ -284,8 +286,8 @@ func buildUsernameKey(username string) string {
func zlibEncode(str []byte) []byte {
var buff bytes.Buffer
writer := zlib.NewWriter(&buff)
writer.Write(str)
writer.Close()
_, _ = writer.Write(str)
_ = writer.Close()
return buff.Bytes()
}
@@ -298,7 +300,7 @@ func zlibDecode(bts []byte) ([]byte, error) {
}
resultBuffer := new(bytes.Buffer)
io.Copy(resultBuffer, reader)
_, _ = io.Copy(resultBuffer, reader)
reader.Close()
return resultBuffer.Bytes(), nil

View File

@@ -152,7 +152,7 @@ func validatePostSkinRequest(request *http.Request) map[string][]string {
const maxMultipartMemory int64 = 32 << 20
const oneOfSkinOrUrlMessage = "One of url or skin should be provided, but not both"
request.ParseMultipartForm(maxMultipartMemory)
_ = request.ParseMultipartForm(maxMultipartMemory)
validationRules := govalidator.MapData{
"identityId": {"required", "numeric", "min:1"},
@@ -161,7 +161,6 @@ func validatePostSkinRequest(request *http.Request) map[string][]string {
"skinId": {"required", "numeric", "min:1"},
"url": {"url"},
"file:skin": {"ext:png", "size:24576", "mime:image/png"},
"hash": {},
"is1_8": {"bool"},
"isSlim": {"bool"},
}
@@ -174,7 +173,6 @@ func validatePostSkinRequest(request *http.Request) map[string][]string {
} else if skinErr == nil {
validationRules["file:skin"] = append(validationRules["file:skin"], "skinUploadingNotAvailable")
} else if url != "" {
validationRules["hash"] = append(validationRules["hash"], "required")
validationRules["is1_8"] = append(validationRules["is1_8"], "required")
validationRules["isSlim"] = append(validationRules["isSlim"], "required")
}
@@ -213,7 +211,7 @@ func findIdentity(repo interfaces.SkinsRepository, identityId int, username stri
record, err = repo.FindByUsername(username)
if err == nil {
repo.RemoveByUsername(username)
_ = repo.RemoveByUsername(username)
record.UserId = identityId
} else {
record = &model.Skin{
@@ -222,7 +220,7 @@ func findIdentity(repo interfaces.SkinsRepository, identityId int, username stri
}
}
} else if record.Username != username {
repo.RemoveByUserId(identityId)
_ = repo.RemoveByUserId(identityId)
record.Username = username
}
@@ -235,7 +233,7 @@ func apiBadRequest(resp http.ResponseWriter, errorsPerField map[string][]string)
result, _ := json.Marshal(map[string]interface{}{
"errors": errorsPerField,
})
resp.Write(result)
_, _ = resp.Write(result)
}
func apiForbidden(resp http.ResponseWriter, reason string) {
@@ -244,7 +242,7 @@ func apiForbidden(resp http.ResponseWriter, reason string) {
result, _ := json.Marshal(map[string]interface{}{
"error": reason,
})
resp.Write(result)
_, _ = resp.Write(result)
}
func apiNotFound(resp http.ResponseWriter, reason string) {
@@ -253,7 +251,7 @@ func apiNotFound(resp http.ResponseWriter, reason string) {
result, _ := json.Marshal([]interface{}{
reason,
})
resp.Write(result)
_, _ = resp.Write(result)
}
func apiServerError(resp http.ResponseWriter) {

View File

@@ -28,7 +28,7 @@ func TestConfig_PostSkin(t *testing.T) {
resultModel := createSkinModel("mock_user", false)
resultModel.SkinId = 5
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
@@ -37,13 +37,12 @@ func TestConfig_PostSkin(t *testing.T) {
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://ely.by/minecraft/skins/default.png"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
@@ -89,7 +88,7 @@ func TestConfig_PostSkin(t *testing.T) {
panic(err)
}
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", body)
req := httptest.NewRequest("POST", "http://chrly/api/skins", body)
req.Header.Add("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
@@ -141,7 +140,6 @@ func TestConfig_PostSkin(t *testing.T) {
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://textures-server.com/skin.png"},
@@ -171,7 +169,7 @@ func TestConfig_PostSkin(t *testing.T) {
resultModel := createSkinModel("mock_user", false)
resultModel.UserId = 2
resultModel.SkinId = 5
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
@@ -180,13 +178,12 @@ func TestConfig_PostSkin(t *testing.T) {
"username": {"mock_user"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://ely.by/minecraft/skins/default.png"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
@@ -219,7 +216,7 @@ func TestConfig_PostSkin(t *testing.T) {
resultModel := createSkinModel("changed_username", false)
resultModel.SkinId = 5
resultModel.Url = "http://ely.by/minecraft/skins/default.png"
resultModel.Url = "http://example.com/skin.png"
resultModel.MojangTextures = ""
resultModel.MojangSignature = ""
@@ -228,13 +225,12 @@ func TestConfig_PostSkin(t *testing.T) {
"username": {"changed_username"},
"uuid": {"0f657aa8-bfbe-415d-b700-5750090d3af3"},
"skinId": {"5"},
"hash": {"94a457d92a61460cb9cb5d6f29732d2a"},
"is1_8": {"0"},
"isSlim": {"0"},
"url": {"http://ely.by/minecraft/skins/default.png"},
"url": {"http://example.com/skin.png"},
}
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
@@ -268,7 +264,7 @@ func TestConfig_PostSkin(t *testing.T) {
"mojangTextures": {"someBase64EncodedString"},
}
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", bytes.NewBufferString(form.Encode()))
req := httptest.NewRequest("POST", "http://chrly/api/skins", bytes.NewBufferString(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
@@ -324,7 +320,7 @@ func TestConfig_PostSkin(t *testing.T) {
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("POST", "http://skinsystem.ely.by/api/skins", nil)
req := httptest.NewRequest("POST", "http://chrly/api/skins", nil)
req.Header.Add("Authorization", "Bearer invalid.jwt.token")
w := httptest.NewRecorder()
@@ -353,7 +349,7 @@ func TestConfig_DeleteSkinByUserId(t *testing.T) {
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:1", nil)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:1", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
@@ -381,7 +377,7 @@ func TestConfig_DeleteSkinByUserId(t *testing.T) {
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/id:2", nil)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/id:2", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
@@ -412,7 +408,7 @@ func TestConfig_DeleteSkinByUsername(t *testing.T) {
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user", nil)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)
@@ -440,7 +436,7 @@ func TestConfig_DeleteSkinByUsername(t *testing.T) {
config, mocks := setupMocks(ctrl)
req := httptest.NewRequest("DELETE", "http://skinsystem.ely.by/api/skins/mock_user_2", nil)
req := httptest.NewRequest("DELETE", "http://chrly/api/skins/mock_user_2", nil)
w := httptest.NewRecorder()
mocks.Auth.EXPECT().Check(gomock.Any()).Return(nil)

View File

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

View File

@@ -22,6 +22,7 @@ func TestConfig_NotFound(t *testing.T) {
response, _ := ioutil.ReadAll(resp.Body)
assert.JSONEq(`{
"status": "404",
"message": "Not Found"
"message": "Not Found",
"link": "http://docs.ely.by/skin-system.html"
}`, 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{
Name: "chrly",
Value: "how do you tame a horse in Minecraft?",
Name: "ely",
Value: "but why are you asking?",
})
responseJson, _ := json.Marshal(responseData)

View File

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