diff --git a/api/mojang/mojang.go b/api/mojang/mojang.go index db2a7f0..194e20c 100644 --- a/api/mojang/mojang.go +++ b/api/mojang/mojang.go @@ -5,9 +5,12 @@ import ( "encoding/json" "io/ioutil" "net/http" + "time" ) -var HttpClient = &http.Client{} +var HttpClient = &http.Client{ + Timeout: 3 * time.Second, +} type SignedTexturesResponse struct { Id string `json:"id"` @@ -32,10 +35,7 @@ type ProfileInfo struct { // See https://wiki.vg/Mojang_API#Playernames_-.3E_UUIDs func UsernamesToUuids(usernames []string) ([]*ProfileInfo, error) { requestBody, _ := json.Marshal(usernames) - request, err := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody)) - if err != nil { - panic(err) - } + request, _ := http.NewRequest("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(requestBody)) request.Header.Set("Content-Type", "application/json") @@ -65,10 +65,7 @@ func UuidToTextures(uuid string, signed bool) (*SignedTexturesResponse, error) { url += "?unsigned=false" } - request, err := http.NewRequest("GET", url, nil) - if err != nil { - panic(err) - } + request, _ := http.NewRequest("GET", url, nil) response, err := HttpClient.Do(request) if err != nil { @@ -92,15 +89,30 @@ func validateResponse(response *http.Response) error { switch { case response.StatusCode == 204: 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: return &TooManyRequestsError{} case response.StatusCode >= 500: - return &ServerError{response.StatusCode} + return &ServerError{Status: response.StatusCode} } return nil } +type ResponseError interface { + IsMojangError() bool +} + // Mojang API doesn't return a 404 Not Found error for non-existent data identifiers // Instead, they return 204 with an empty body type EmptyResponse struct { @@ -110,19 +122,48 @@ func (*EmptyResponse) Error() string { 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 type TooManyRequestsError struct { + ResponseError } func (*TooManyRequestsError) Error() string { return "Too Many Requests" } +func (*TooManyRequestsError) IsMojangError() bool { + return true +} + // ServerError happens when Mojang's API returns any response with 50* status type ServerError struct { + ResponseError Status int } func (e *ServerError) Error() string { return "Server error" } + +func (*ServerError) IsMojangError() bool { + return true +} diff --git a/api/mojang/mojang_test.go b/api/mojang/mojang_test.go index 633e6f7..fa1aa4f 100644 --- a/api/mojang/mojang_test.go +++ b/api/mojang/mojang_test.go @@ -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) { assert := testify.New(t) @@ -72,6 +96,7 @@ func TestUsernamesToUuids(t *testing.T) { assert.Nil(result) assert.IsType(&TooManyRequestsError{}, err) assert.EqualError(err, "Too Many Requests") + assert.Implements((*ResponseError)(nil), err) }) t.Run("handle server error", func(t *testing.T) { @@ -93,6 +118,7 @@ func TestUsernamesToUuids(t *testing.T) { assert.IsType(&ServerError{}, err) assert.EqualError(err, "Server error") assert.Equal(500, err.(*ServerError).Status) + assert.Implements((*ResponseError)(nil), err) }) } @@ -185,6 +211,7 @@ func TestUuidToTextures(t *testing.T) { assert.Nil(result) assert.IsType(&EmptyResponse{}, err) assert.EqualError(err, "Empty Response") + assert.Implements((*ResponseError)(nil), err) }) t.Run("handle too many requests response", func(t *testing.T) { @@ -208,6 +235,7 @@ func TestUuidToTextures(t *testing.T) { assert.Nil(result) assert.IsType(&TooManyRequestsError{}, err) assert.EqualError(err, "Too Many Requests") + assert.Implements((*ResponseError)(nil), err) }) t.Run("handle server error", func(t *testing.T) { @@ -229,5 +257,6 @@ func TestUuidToTextures(t *testing.T) { assert.IsType(&ServerError{}, err) assert.EqualError(err, "Server error") assert.Equal(500, err.(*ServerError).Status) + assert.Implements((*ResponseError)(nil), err) }) } diff --git a/api/mojang/queue/queue.go b/api/mojang/queue/queue.go index 566b41f..c0a126f 100644 --- a/api/mojang/queue/queue.go +++ b/api/mojang/queue/queue.go @@ -1,9 +1,11 @@ package queue import ( + "net" "regexp" "strings" "sync" + "syscall" "time" "github.com/elyby/chrly/api/mojang" @@ -100,15 +102,15 @@ func (ctx *JobsQueue) queueRound() { } profiles, err := usernamesToUuids(usernames) - switch err.(type) { - case *mojang.TooManyRequestsError, *mojang.ServerError: - for _, job := range jobs { - job.RespondTo <- nil - } + if err != nil { + defer func() { + for _, job := range jobs { + job.RespondTo <- nil + } + }() + maybeShouldPanic(err) return - case error: - panic(err) } var wg sync.WaitGroup @@ -146,11 +148,9 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse { shouldCache := true result, err := uuidToTextures(uuid, true) - switch err.(type) { - case *mojang.EmptyResponse, *mojang.TooManyRequestsError, *mojang.ServerError: + if err != nil { + maybeShouldPanic(err) shouldCache = false - case error: - panic(err) } if shouldCache && result != nil { @@ -159,3 +159,25 @@ func (ctx *JobsQueue) getTextures(uuid string) *mojang.SignedTexturesResponse { 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) +} diff --git a/api/mojang/queue/queue_test.go b/api/mojang/queue/queue_test.go index be7af0e..2005bb3 100644 --- a/api/mojang/queue/queue_test.go +++ b/api/mojang/queue/queue_test.go @@ -6,16 +6,18 @@ import ( "github.com/elyby/chrly/api/mojang" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + "net" "strings" + "syscall" "testing" "time" ) -type MojangApiMocks struct { +type mojangApiMocks struct { mock.Mock } -func (o *MojangApiMocks) UsernameToUuids(usernames []string) ([]*mojang.ProfileInfo, error) { +func (o *mojangApiMocks) UsernamesToUuids(usernames []string) ([]*mojang.ProfileInfo, error) { args := o.Called(usernames) var result []*mojang.ProfileInfo 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) } -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) var result *mojang.SignedTexturesResponse 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) } -type MockStorage struct { +type mockStorage struct { mock.Mock } -func (m *MockStorage) GetUuid(username string) (string, error) { +func (m *mockStorage) GetUuid(username string) (string, error) { args := m.Called(username) 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) } -func (m *MockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) { +func (m *mockStorage) GetTextures(uuid string) (*mojang.SignedTexturesResponse, error) { args := m.Called(uuid) var result *mojang.SignedTexturesResponse 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) } -func (m *MockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) { +func (m *mockStorage) StoreTextures(textures *mojang.SignedTexturesResponse) { m.Called(textures) } -type QueueTestSuite struct { +type queueTestSuite struct { suite.Suite Queue *JobsQueue - Storage *MockStorage - MojangApi *MojangApiMocks + Storage *mockStorage + MojangApi *mojangApiMocks Iterate func() iterateChan chan bool done func() } -func (suite *QueueTestSuite) SetupSuite() { +func (suite *queueTestSuite) SetupSuite() { delay = 0 } -func (suite *QueueTestSuite) SetupTest() { - suite.Storage = &MockStorage{} +func (suite *queueTestSuite) SetupTest() { + suite.Storage = &mockStorage{} suite.Queue = &JobsQueue{Storage: suite.Storage} @@ -95,25 +97,25 @@ func (suite *QueueTestSuite) SetupTest() { suite.iterateChan <- false } - suite.MojangApi = new(MojangApiMocks) - usernamesToUuids = suite.MojangApi.UsernameToUuids + suite.MojangApi = new(mojangApiMocks) + usernamesToUuids = suite.MojangApi.UsernamesToUuids uuidToTextures = suite.MojangApi.UuidToTextures } -func (suite *QueueTestSuite) TearDownTest() { +func (suite *queueTestSuite) TearDownTest() { suite.done() suite.MojangApi.AssertExpectations(suite.T()) suite.Storage.AssertExpectations(suite.T()) } -func (suite *QueueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() { +func (suite *queueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() { expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) 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"}, }, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) @@ -126,7 +128,7 @@ func (suite *QueueTestSuite) TestReceiveTexturesForOneUsernameWithoutAnyCache() suite.Assert().Equal(expectedResult, result) } -func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() { +func (suite *queueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() { expectedResult1 := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} 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("StoreTextures", expectedResult1).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: "4566e69fc90748ee8d71d7ba5aa00d20", Name: "Thinkofdeath"}, }, nil) @@ -154,14 +156,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForFewUsernamesWithoutAnyCache() suite.Assert().Equal(expectedResult2, <-resultChan2) } -func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() { +func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() { expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} 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() - // MojangApi.UsernameToUuids shouldn't be called + // MojangApi.UsernamesToUuids shouldn't be called suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -172,14 +174,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUuid() { suite.Assert().Equal(expectedResult, result) } -func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult() { +func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult() { expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} suite.Storage.On("GetUuid", "maksimkurb").Once().Return("0d252b7218b648bfb86c2ae476954d32", nil) // Storage.StoreUuid shouldn't be called suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(expectedResult, nil) // Storage.StoreTextures shouldn't be called - // MojangApi.UsernameToUuids shouldn't be called + // MojangApi.UsernamesToUuids shouldn't be called // MojangApi.UuidToTextures shouldn't be called resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -190,12 +192,12 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithFullyCachedResult suite.Assert().Equal(expectedResult, result) } -func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid() { +func (suite *queueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid() { suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", nil) // Storage.StoreUuid shouldn't be called // Storage.GetTextures 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 resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -205,7 +207,7 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameWithCachedUnknownUuid suite.Assert().Nil(<-resultChan) } -func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { +func (suite *queueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { usernames := make([]string, 120, 120) for i := 0; i < 120; i++ { 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("StoreUuid", mock.Anything, "").Times(120) // if username is not compared to uuid, then receive "" // Storage.GetTextures and Storage.SetTextures shouldn't be called - suite.MojangApi.On("UsernameToUuids", 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[0:100]).Once().Return([]*mojang.ProfileInfo{}, nil) + suite.MojangApi.On("UsernamesToUuids", usernames[100:120]).Once().Return([]*mojang.ProfileInfo{}, nil) for _, username := range usernames { suite.Queue.GetTexturesForUsername(username) @@ -225,14 +227,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForMoreThan100Usernames() { suite.Iterate() } -func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() { +func (suite *queueTestSuite) TestReceiveTexturesForTheSameUsernames() { expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{}) suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) 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"}, }, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(expectedResult, nil) @@ -246,14 +248,14 @@ func (suite *QueueTestSuite) TestReceiveTexturesForTheSameUsernames() { suite.Assert().Equal(expectedResult, <-resultChan2) } -func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() { +func (suite *queueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing() { expectedResult := &mojang.SignedTexturesResponse{Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"} suite.Storage.On("GetUuid", "maksimkurb").Twice().Return("", &ValueNotFound{}) suite.Storage.On("StoreUuid", "maksimkurb", "0d252b7218b648bfb86c2ae476954d32").Once() suite.Storage.On("GetTextures", "0d252b7218b648bfb86c2ae476954d32").Once().Return(nil, &ValueNotFound{}) 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"}, }, nil) suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true). @@ -275,11 +277,11 @@ func (suite *QueueTestSuite) TestReceiveTexturesForUsernameThatAlreadyProcessing suite.Assert().Equal(expectedResult, <-resultChan2) } -func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { +func (suite *queueTestSuite) TestDoNothingWhenNoTasks() { suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) suite.Storage.On("StoreUuid", "maksimkurb", "").Once() // 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 resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") @@ -293,97 +295,60 @@ func (suite *QueueTestSuite) TestDoNothingWhenNoTasks() { suite.Iterate() } -func (suite *QueueTestSuite) TestHandleTooManyRequestsResponseWhenExchangingUsernamesToUuids() { - 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) +type timeoutError struct { } -func (suite *QueueTestSuite) TestHandleServerErrorWhenExchangingUsernamesToUuids() { - 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.ServerError{Status: 500}) - - resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") - - suite.Iterate() - - suite.Assert().Nil(<-resultChan) -} - -func (suite *QueueTestSuite) TestHandleEmptyResponseWhenRequestingUsersTextures() { - 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.EmptyResponse{}, - ) - - resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") - - suite.Iterate() - - suite.Assert().Nil(<-resultChan) +func (*timeoutError) Error() string { return "timeout error" } +func (*timeoutError) Timeout() bool { return true } +func (*timeoutError) Temporary() bool { return false } + +var expectedErrors = []error{ + &mojang.BadRequestError{}, + &mojang.TooManyRequestsError{}, + &mojang.ServerError{}, + &timeoutError{}, + &net.OpError{Op: "read"}, + &net.OpError{Op: "dial"}, + syscall.ECONNREFUSED, } -func (suite *QueueTestSuite) TestHandleTooManyRequestsResponseWhenRequestingUsersTextures() { - 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.TooManyRequestsError{}, - ) - - resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") - - suite.Iterate() - - suite.Assert().Nil(<-resultChan) +func (suite *queueTestSuite) TestShouldNotPanicWhenExpectedErrorReturnedFromUsernameToUuidRequest() { + for _, err := range expectedErrors { + suite.Storage.On("GetUuid", "maksimkurb").Once().Return("", &ValueNotFound{}) + suite.MojangApi.On("UsernamesToUuids", []string{"maksimkurb"}).Once().Return(nil, err) + resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") + suite.Iterate() + suite.Assert().Nil(<-resultChan) + suite.MojangApi.AssertExpectations(suite.T()) + suite.MojangApi.ExpectedCalls = nil // https://github.com/stretchr/testify/issues/558#issuecomment-372112364 + } } -func (suite *QueueTestSuite) TestHandleServerErrorWhenRequestingUsersTextures() { - 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) TestShouldNotPanicWhenExpectedErrorReturnedFromUuidToTexturesRequest() { + for _, err := range expectedErrors { + 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("UsernamesToUuids", []string{"maksimkurb"}).Once().Return([]*mojang.ProfileInfo{ + {Id: "0d252b7218b648bfb86c2ae476954d32", Name: "maksimkurb"}, + }, nil) + suite.MojangApi.On("UuidToTextures", "0d252b7218b648bfb86c2ae476954d32", true).Once().Return(nil, err) + resultChan := suite.Queue.GetTexturesForUsername("maksimkurb") + suite.Iterate() + suite.Assert().Nil(<-resultChan) + suite.MojangApi.AssertExpectations(suite.T()) + suite.MojangApi.ExpectedCalls = nil // https://github.com/stretchr/testify/issues/558#issuecomment-372112364 + } } -func (suite *QueueTestSuite) TestReceiveTexturesForNotAllowedMojangUsername() { +func (suite *queueTestSuite) TestReceiveTexturesForNotAllowedMojangUsername() { resultChan := suite.Queue.GetTexturesForUsername("Not allowed") suite.Assert().Nil(<-resultChan) } func TestJobsQueueSuite(t *testing.T) { - suite.Run(t, new(QueueTestSuite)) + suite.Run(t, new(queueTestSuite)) } var replacer = strings.NewReplacer("-", "_", "=", "")