#1: Fix Mojang's API HTTPClient default configuration, make mojang.ResponseError interface not applicable to any type, add handling of some possible network errors

This commit is contained in:
ErickSkrauch
2019-04-21 03:04:03 +03:00
parent a8bbacf8b1
commit 7d1506d0d9
4 changed files with 191 additions and 134 deletions

View File

@@ -5,9 +5,12 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time"
) )
var HttpClient = &http.Client{} var HttpClient = &http.Client{
Timeout: 3 * time.Second,
}
type SignedTexturesResponse struct { type SignedTexturesResponse struct {
Id string `json:"id"` Id string `json:"id"`
@@ -32,10 +35,7 @@ type ProfileInfo struct {
// See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs // See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs
func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) { func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) {
requestBody, _ := json.Marshal(usernames) requestBody, _ := json.Marshal(usernames)
request, err := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody)) request, _ := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody))
if err != nil {
panic(err)
}
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")
@@ -65,10 +65,7 @@ func UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) {
url += "?unsigned=false" url += "?unsigned=false"
} }
request, err := http.NewRequest("GET", url, nil) request, _ := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
response, err := HttpClient.Do(request) response, err := HttpClient.Do(request)
if err != nil { if err != nil {
@@ -92,15 +89,30 @@ func validateResponse(response *http.Response) error {
switch { switch {
case response.StatusCode == 204: case response.StatusCode == 204:
return &EmptyResponse{} return &EmptyResponse{}
case response.StatusCode == 400:
type errorResponse struct {
Error string `json:"error"`
Message string `json:"errorMessage"`
}
var decodedError *errorResponse
body, _ := ioutil.ReadAll(response.Body)
_ = json.Unmarshal(body, &decodedError)
return &BadRequestError{ErrorType: decodedError.Error, Message: decodedError.Message}
case response.StatusCode == 429: case response.StatusCode == 429:
return &TooManyRequestsError{} return &TooManyRequestsError{}
case response.StatusCode >= 500: case response.StatusCode >= 500:
return &ServerError{response.StatusCode} return &ServerError{Status: response.StatusCode}
} }
return nil return nil
} }
type ResponseError interface {
IsMojangError() bool
}
// Mojang API doesn't return a 404 Not Found error for non-existent data identifiers // Mojang API doesn't return a 404 Not Found error for non-existent data identifiers
// Instead, they return 204 with an empty body // Instead, they return 204 with an empty body
type EmptyResponse struct { type EmptyResponse struct {
@@ -110,19 +122,48 @@ func (*EmptyResponse) Error() string {
return "Empty Response" return "Empty Response"
} }
func (*EmptyResponse) IsMojangError() bool {
return true
}
// When passed request params are invalid, Mojang returns 400 Bad Request error
type BadRequestError struct {
ResponseError
ErrorType string
Message string
}
func (e *BadRequestError) Error() string {
return e.Message
}
func (*BadRequestError) IsMojangError() bool {
return true
}
// 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
} }
func (*TooManyRequestsError) Error() string { func (*TooManyRequestsError) Error() string {
return "Too Many Requests" return "Too Many Requests"
} }
func (*TooManyRequestsError) IsMojangError() bool {
return true
}
// ServerError happens when Mojang's API returns any response with 50* status // ServerError happens when Mojang's API returns any response with 50* status
type ServerError struct { type ServerError struct {
ResponseError
Status int Status int
} }
func (e *ServerError) Error() string { func (e *ServerError) Error() string {
return "Server error" return "Server error"
} }
func (*ServerError) IsMojangError() bool {
return true
}

View File

@@ -51,6 +51,30 @@ func TestUsernamesToUuids(t *testing.T) {
} }
}) })
t.Run("handle bad request response", func(t *testing.T) {
assert := testify.New(t)
defer gock.Off()
gock.New("https://api.mojang.com").
Post("/profiles/minecraft").
Reply(400).
JSON(map[string]interface{}{
"error": "IllegalArgumentException",
"errorMessage": "profileName can not be null or empty.",
})
client := &http.Client{}
gock.InterceptClient(client)
HttpClient = client
result, err := UsernamesToUuids([]string{""})
assert.Nil(result)
assert.IsType(&BadRequestError{}, err)
assert.EqualError(err, "profileName can not be null or empty.")
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)
@@ -72,6 +96,7 @@ func TestUsernamesToUuids(t *testing.T) {
assert.Nil(result) assert.Nil(result)
assert.IsType(&TooManyRequestsError{}, err) assert.IsType(&TooManyRequestsError{}, err)
assert.EqualError(err, "Too Many Requests") assert.EqualError(err, "Too Many Requests")
assert.Implements((*ResponseError)(nil), err)
}) })
t.Run("handle server error", func(t *testing.T) { t.Run("handle server error", func(t *testing.T) {
@@ -93,6 +118,7 @@ func TestUsernamesToUuids(t *testing.T) {
assert.IsType(&ServerError{}, err) assert.IsType(&ServerError{}, err)
assert.EqualError(err, "Server error") assert.EqualError(err, "Server error")
assert.Equal(500, err.(*ServerError).Status) assert.Equal(500, err.(*ServerError).Status)
assert.Implements((*ResponseError)(nil), err)
}) })
} }
@@ -185,6 +211,7 @@ func TestUuidToTextures(t *testing.T) {
assert.Nil(result) assert.Nil(result)
assert.IsType(&EmptyResponse{}, err) assert.IsType(&EmptyResponse{}, err)
assert.EqualError(err, "Empty Response") assert.EqualError(err, "Empty Response")
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) {
@@ -208,6 +235,7 @@ func TestUuidToTextures(t *testing.T) {
assert.Nil(result) assert.Nil(result)
assert.IsType(&TooManyRequestsError{}, err) assert.IsType(&TooManyRequestsError{}, err)
assert.EqualError(err, "Too Many Requests") assert.EqualError(err, "Too Many Requests")
assert.Implements((*ResponseError)(nil), err)
}) })
t.Run("handle server error", func(t *testing.T) { t.Run("handle server error", func(t *testing.T) {
@@ -229,5 +257,6 @@ func TestUuidToTextures(t *testing.T) {
assert.IsType(&ServerError{}, err) assert.IsType(&ServerError{}, err)
assert.EqualError(err, "Server error") assert.EqualError(err, "Server error")
assert.Equal(500, err.(*ServerError).Status) assert.Equal(500, err.(*ServerError).Status)
assert.Implements((*ResponseError)(nil), err)
}) })
} }

View File

@@ -1,9 +1,11 @@
package queue package queue
import ( import (
"net"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/api/mojang"
@@ -100,15 +102,15 @@ func (ctx *JobsQueue) queueRound() {
} }
profiles, err := usernamesToUuids(usernames) profiles, err := usernamesToUuids(usernames)
switch err.(type) { if err != nil {
case *mojang.TooManyRequestsError, *mojang.ServerError: defer func() {
for _, job := range jobs { for _, job := range jobs {
job.RespondTo <- nil job.RespondTo <- nil
} }
}()
maybeShouldPanic(err)
return return
case error:
panic(err)
} }
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -146,11 +148,9 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {
shouldCache := true shouldCache := true
result, err := uuidToTextures(uuid, true) result, err := uuidToTextures(uuid, true)
switch err.(type) { if err != nil {
case *mojang.EmptyResponse, *mojang.TooManyRequestsError, *mojang.ServerError: maybeShouldPanic(err)
shouldCache = false shouldCache = false
case error:
panic(err)
} }
if shouldCache && result != nil { if shouldCache && result != nil {
@@ -159,3 +159,25 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse {
return result return result
} }
// Starts to panic if there's an unexpected error
func maybeShouldPanic(err error) {
switch err.(type) {
case mojang.ResponseError:
return
case net.Error:
if err.(net.Error).Timeout() {
return
}
if opErr, ok := err.(*net.OpError); ok && (opErr.Op == "dial" || opErr.Op == "read") {
return
}
if err == syscall.ECONNREFUSED {
return
}
}
panic(err)
}

View File

@@ -6,16 +6,18 @@ import (
"github.com/elyby/chrly/api/mojang" "github.com/elyby/chrly/api/mojang"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"net"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
) )
type MojangApiMocks struct { type mojangApiMocks struct {
mock.Mock mock.Mock
} }
func (o *MojangApiMocks) UsernameToUuids(usernames []string) ([]*mojang.ProfileInfo, error) { func (o *mojangApiMocks) UsernamesToUuids(usernames []string) ([]*mojang.ProfileInfo, error) {
args := o.Called(usernames) args := o.Called(usernames)
var result []*mojang.ProfileInfo var result []*mojang.ProfileInfo
if casted, ok := args.Get(0).([]*mojang.ProfileInfo); ok { if casted, ok := args.Get(0).([]*mojang.ProfileInfo); ok {
@@ -25,7 +27,7 @@ func (o *MojangApiMocks) UsernameToUuids(usernames []string) ([]*mojang.ProfileI
return result, args.Error(1) return result, args.Error(1)
} }
func (o *MojangApiMocks) UuidToTextures(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) { func (o *mojangApiMocks) UuidToTextures(uuid string, signed bool) (*mojang.SignedTexturesResponse, error) {
args := o.Called(uuid, signed) args := o.Called(uuid, signed)
var result *mojang.SignedTexturesResponse var result *mojang.SignedTexturesResponse
if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok {
@@ -35,20 +37,20 @@ func (o *MojangApiMocks) UuidToTextures(uuid string, signed bool) (*mojang.Signe
return result, args.Error(1) return result, args.Error(1)
} }
type MockStorage struct { type mockStorage struct {
mock.Mock mock.Mock
} }
func (m *MockStorage) GetUuid(username string) (string, error) { func (m *mockStorage) GetUuid(username string) (string, error) {
args := m.Called(username) args := m.Called(username)
return args.String(0), args.Error(1) return args.String(0), args.Error(1)
} }
func (m *MockStorage) StoreUuid(username string, uuid string) { func (m *mockStorage) StoreUuid(username string, uuid string) {
m.Called(username, uuid) m.Called(username, uuid)
} }
func (m *MockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) { func (m *mockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) {
args := m.Called(uuid) args := m.Called(uuid)
var result *mojang.SignedTexturesResponse var result *mojang.SignedTexturesResponse
if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok { if casted, ok := args.Get(0).(*mojang.SignedTexturesResponse); ok {
@@ -58,27 +60,27 @@ func (m *MockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse,
return result, args.Error(1) return result, args.Error(1)
} }
func (m *MockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) { func (m *mockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) {
m.Called(textures) m.Called(textures)
} }
type QueueTestSuite struct { type queueTestSuite struct {
suite.Suite suite.Suite
Queue *JobsQueue Queue *JobsQueue
Storage *MockStorage Storage *mockStorage
MojangApi *MojangApiMocks MojangApi *mojangApiMocks
Iterate func() Iterate func()
iterateChan chan bool iterateChan chan bool
done func() done func()
} }
func (suite *QueueTestSuite) SetupSuite() { func (suite *queueTestSuite) SetupSuite() {
delay = 0 delay = 0
} }
func (suite *QueueTestSuite) SetupTest() { func (suite *queueTestSuite) SetupTest() {
suite.Storage = &MockStorage{} suite.Storage = &mockStorage{}
suite.Queue = &JobsQueue{Storage: suite.Storage} suite.Queue = &JobsQueue{Storage: suite.Storage}
@@ -95,25 +97,25 @@ func (suite *QueueTestSuite) SetupTest() {
suite.iterateChan <- false suite.iterateChan <- false
} }
suite.MojangApi = new(MojangApiMocks) suite.MojangApi = new(mojangApiMocks)
usernamesToUuids = suite.MojangApi.UsernameToUuids usernamesToUuids = suite.MojangApi.UsernamesToUuids
uuidToTextures = suite.MojangApi.UuidToTextures uuidToTextures = suite.MojangApi.UuidToTextures
} }
func (suite *QueueTestSuite) TearDownTest() { func (suite *queueTestSuite) TearDownTest() {
suite.done() suite.done()
suite.MojangApi.AssertExpectations(suite.T()) suite.MojangApi.AssertExpectations(suite.T())
suite.Storage.AssertExpectations(suite.T()) suite.Storage.AssertExpectations(suite.T())
} }
func (suite *QueueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() { func (suite *queueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once() suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil)
@@ -126,7 +128,7 @@ func (suite *QueueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache()
suite.Assert().Equal(expectedResult, result) suite.Assert().Equal(expectedResult, result)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() { func (suite *queueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() {
expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
expectedResult2 := &mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"} expectedResult2 := &mojang.SignedTexturesResponse{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}
@@ -138,7 +140,7 @@ func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache()
suite.Storage.On("GetTextures", "4566e69fc90748ee8d71d7ba5aa00d20").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("GetTextures", "4566e69fc90748ee8d71d7ba5aa00d20").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult1).Once() suite.Storage.On("StoreTextures", expectedResult1).Once()
suite.Storage.On("StoreTextures", expectedResult2).Once() suite.Storage.On("StoreTextures", expectedResult2).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb", "Thinkofdeath"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
{Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}, {Id: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"},
}, nil) }, nil)
@@ -154,14 +156,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache()
suite.Assert().Equal(expectedResult2, <-resultChan2) suite.Assert().Equal(expectedResult2, <-resultChan2)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() { func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil)
// Storage.StoreUuid shouldn't be called // Storage.StoreUuid shouldn't be called
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once() suite.Storage.On("StoreTextures", expectedResult).Once()
// MojangApi.UsernameToUuids shouldn't be called // MojangApi.UsernamesToUuids shouldn't be called
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil)
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
@@ -172,14 +174,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() {
suite.Assert().Equal(expectedResult, result) suite.Assert().Equal(expectedResult, result)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult() { func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil)
// Storage.StoreUuid shouldn't be called // Storage.StoreUuid shouldn't be called
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(expectedResult, nil) suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(expectedResult, nil)
// Storage.StoreTextures shouldn't be called // Storage.StoreTextures shouldn't be called
// MojangApi.UsernameToUuids shouldn't be called // MojangApi.UsernamesToUuids shouldn't be called
// MojangApi.UuidToTextures shouldn't be called // MojangApi.UuidToTextures shouldn't be called
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
@@ -190,12 +192,12 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult
suite.Assert().Equal(expectedResult, result) suite.Assert().Equal(expectedResult, result)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid() { func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", nil) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", nil)
// Storage.StoreUuid shouldn't be called // Storage.StoreUuid shouldn't be called
// Storage.GetTextures shouldn't be called // Storage.GetTextures shouldn't be called
// Storage.StoreTextures shouldn't be called // Storage.StoreTextures shouldn't be called
// MojangApi.UsernameToUuids shouldn't be called // MojangApi.UsernamesToUuids shouldn't be called
// MojangApi.UuidToTextures shouldn't be called // MojangApi.UuidToTextures shouldn't be called
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
@@ -205,7 +207,7 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid
suite.Assert().Nil(<-resultChan) suite.Assert().Nil(<-resultChan)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { func (suite *queueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
usernames := make([]string, 120, 120) usernames := make([]string, 120, 120)
for i := 0; i < 120; i++ { for i := 0; i < 120; i++ {
usernames[i] = randStr(8) usernames[i] = randStr(8)
@@ -214,8 +216,8 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
suite.Storage.On("GetUuid", mock.Anything).Times(120).Return("", &ValueNotFound{}) 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("StoreUuid", mock.Anything, "").Times(120) // if username is not compared to uuid, then receive ""
// Storage.GetTextures and Storage.SetTextures shouldn't be called // Storage.GetTextures and Storage.SetTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernamesToUuids", usernames[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil)
suite.MojangApi.On("UsernameToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernamesToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil)
for _, username := range usernames { for _, username := range usernames {
suite.Queue.GetTexturesForUsername(username) suite.Queue.GetTexturesForUsername(username)
@@ -225,14 +227,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() {
suite.Iterate() suite.Iterate()
} }
func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() { func (suite *queueTestSuite) TestReceiveTexturesForTheSameUsernames() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once() suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil)
@@ -246,14 +248,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() {
suite.Assert().Equal(expectedResult, <-resultChan2) suite.Assert().Equal(expectedResult, <-resultChan2)
} }
func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() { func (suite *queueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() {
expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}
suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.Storage.On("StoreTextures", expectedResult).Once() suite.Storage.On("StoreTextures", expectedResult).Once()
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil) }, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true). suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).
@@ -275,11 +277,11 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing
suite.Assert().Equal(expectedResult, <-resultChan2) suite.Assert().Equal(expectedResult, <-resultChan2)
} }
func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { func (suite *queueTestSuite) TestDoNothingWhenNoTasks() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "").Once() suite.Storage.On("StoreUuid", "maksimkurb", "").Once()
// Storage.GetTextures and Storage.StoreTextures shouldn't be called // Storage.GetTextures and Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil) suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{}, nil)
// Perform first iteration and await it finish // Perform first iteration and await it finish
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
@@ -293,97 +295,60 @@ func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() {
suite.Iterate() suite.Iterate()
} }
func (suite *QueueTestSuite) TestHandleTooManyRequestsResponseWhenExchangingUsernamesToUuids() { type timeoutError struct {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
// Storage.StoreUuid, Storage.GetTextures and Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.TooManyRequestsError{})
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
suite.Iterate()
suite.Assert().Nil(<-resultChan)
} }
func (suite *QueueTestSuite) TestHandleServerErrorWhenExchangingUsernamesToUuids() { func (*timeoutError) Error() string { return "timeout error" }
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) func (*timeoutError) Timeout() bool { return true }
// Storage.StoreUuid, Storage.GetTextures and Storage.StoreTextures shouldn't be called func (*timeoutError) Temporary() bool { return false }
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return(nil, &mojang.ServerError{Status: 500})
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") var expectedErrors = []error{
&mojang.BadRequestError{},
suite.Iterate() &mojang.TooManyRequestsError{},
&mojang.ServerError{},
suite.Assert().Nil(<-resultChan) &timeoutError{},
&net.OpError{Op: "read"},
&net.OpError{Op: "dial"},
syscall.ECONNREFUSED,
} }
func (suite *QueueTestSuite) TestHandleEmptyResponseWhenRequestingUsersTextures() { func (suite *queueTestSuite) TestShouldNotPanicWhenExpectedErrorReturnedFromUsernameToUuidRequest() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) for _, err := range expectedErrors {
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return(nil, err)
// Storage.StoreTextures shouldn't be called resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ suite.Iterate()
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, suite.Assert().Nil(<-resultChan)
}, nil) suite.MojangApi.AssertExpectations(suite.T())
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return( suite.MojangApi.ExpectedCalls = nil // https://github.com/stretchr/testify/issues/558#issuecomment-372112364
nil, }
&mojang.EmptyResponse{},
)
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
suite.Iterate()
suite.Assert().Nil(<-resultChan)
} }
func (suite *QueueTestSuite) TestHandleTooManyRequestsResponseWhenRequestingUsersTextures() { func (suite *queueTestSuite) TestShouldNotPanicWhenExpectedErrorReturnedFromUuidToTexturesRequest() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) for _, err := range expectedErrors {
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
// Storage.StoreTextures shouldn't be called suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ // Storage.StoreTextures shouldn't be called
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
}, nil) {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return( }, nil)
nil, suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(nil, err)
&mojang.TooManyRequestsError{}, resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
) suite.Iterate()
suite.Assert().Nil(<-resultChan)
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") suite.MojangApi.AssertExpectations(suite.T())
suite.MojangApi.ExpectedCalls = nil // https://github.com/stretchr/testify/issues/558#issuecomment-372112364
suite.Iterate() }
suite.Assert().Nil(<-resultChan)
} }
func (suite *QueueTestSuite) TestHandleServerErrorWhenRequestingUsersTextures() { func (suite *queueTestSuite) TestReceiveTexturesForNotAllowedMojangUsername() {
suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{})
suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once()
suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{})
// Storage.StoreTextures shouldn't be called
suite.MojangApi.On("UsernameToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{
{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"},
}, nil)
suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(
nil,
&mojang.ServerError{Status: 500},
)
resultChan := suite.Queue.GetTexturesForUsername("maksimkurb")
suite.Iterate()
suite.Assert().Nil(<-resultChan)
}
func (suite *QueueTestSuite) TestReceiveTexturesForNotAllowedMojangUsername() {
resultChan := suite.Queue.GetTexturesForUsername("Not allowed") resultChan := suite.Queue.GetTexturesForUsername("Not allowed")
suite.Assert().Nil(<-resultChan) suite.Assert().Nil(<-resultChan)
} }
func TestJobsQueueSuite(t *testing.T) { func TestJobsQueueSuite(t *testing.T) {
suite.Run(t, new(QueueTestSuite)) suite.Run(t, new(queueTestSuite))
} }
var replacer = strings.NewReplacer("-", "_", "=", "") var replacer = strings.NewReplacer("-", "_", "=", "")