mirror of
				https://github.com/elyby/chrly.git
				synced 2025-05-31 14:11:51 +05:30 
			
		
		
		
	Remove worker mode
This commit is contained in:
		| @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||||||
| - StatsD metrics: | - StatsD metrics: | ||||||
|  - Gauges: |  - Gauges: | ||||||
|    - `ely.skinsystem.{hostname}.app.redis.pool.available` |    - `ely.skinsystem.{hostname}.app.redis.pool.available` | ||||||
|  | - Worker mode. Use URL spoofing to load balance outgoing requests. | ||||||
|  |  | ||||||
| ## [4.6.0] - 2021-03-04 | ## [4.6.0] - 2021-03-04 | ||||||
| ### Added | ### Added | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								README.md
									
									
									
									
									
								
							| @@ -134,22 +134,6 @@ docker-compose up -d app | |||||||
|         </td> |         </td> | ||||||
|         <td><code>true</code></td> |         <td><code>true</code></td> | ||||||
|     </tr> |     </tr> | ||||||
|     <tr> |  | ||||||
|         <td id="remote-mojang-uuids-provider">MOJANG_TEXTURES_UUIDS_PROVIDER_DRIVER</td> |  | ||||||
|         <td> |  | ||||||
|             Specifies the preferred provider of the Mojang's UUIDs. Takes <code>remote</code> value. |  | ||||||
|             In any other case, the local queue will be used. |  | ||||||
|         </td> |  | ||||||
|         <td><code>remote</code></td> |  | ||||||
|     </tr> |  | ||||||
|     <tr> |  | ||||||
|         <td>MOJANG_TEXTURES_UUIDS_PROVIDER_URL</td> |  | ||||||
|         <td> |  | ||||||
|             When the UUIDs driver set to <code>remote</code>, sets the remote URL. |  | ||||||
|             The trailing slash won't cause any problems. |  | ||||||
|         </td> |  | ||||||
|         <td><code>http://remote-provider.com/api/worker/mojang-uuid</code></td> |  | ||||||
|     </tr> |  | ||||||
|     <tr> |     <tr> | ||||||
|         <td>MOJANG_API_BASE_URL</td> |         <td>MOJANG_API_BASE_URL</td> | ||||||
|         <td> |         <td> | ||||||
| @@ -399,31 +383,6 @@ response will be: | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Worker mode |  | ||||||
|  |  | ||||||
| The worker mode can be used in cooperation with the [remote server mode](#remote-mojang-uuids-provider) |  | ||||||
| to exchange Mojang usernames to UUIDs. This mode by itself doesn't solve the problem of |  | ||||||
| [extremely strict limits](https://github.com/elyby/chrly/issues/10) on the number of requests to the Mojang's API. |  | ||||||
| But with a proxying load balancer (e.g. HAProxy, Nginx, etc.) it's easy to build a cluster of workers, |  | ||||||
| which will multiply the bandwidth of the exchanging usernames to its UUIDs. |  | ||||||
|  |  | ||||||
| The instructions for setting up a proxy load balancer are outside the context of this documentation, |  | ||||||
| but you get the idea ;) |  | ||||||
|  |  | ||||||
| #### `GET /api/worker/mojang-uuid/{username}` |  | ||||||
|  |  | ||||||
| Performs [batch usernames exchange to UUIDs](https://github.com/elyby/chrly/issues/1) and returns the result in the |  | ||||||
| [same format as it returns from the Mojang's API](https://wiki.vg/Mojang_API#Username_-.3E_UUID_at_time): |  | ||||||
|  |  | ||||||
| ```json |  | ||||||
| { |  | ||||||
|     "id": "3e3ee6c35afa48abb61e8cd8c42fc0d9", |  | ||||||
|     "name": "ErickSkrauch" |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| > **Note**: the results aren't cached. |  | ||||||
|  |  | ||||||
| ### Health check | ### Health check | ||||||
|  |  | ||||||
| #### `GET /healthcheck` | #### `GET /healthcheck` | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/spf13/cobra" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var workerCmd = &cobra.Command{ |  | ||||||
| 	Use:   "worker", |  | ||||||
| 	Short: "Starts HTTP handler for the Mojang usernames to UUIDs worker", |  | ||||||
| 	Run: func(cmd *cobra.Command, args []string) { |  | ||||||
| 		startServer([]string{"worker"}) |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	RootCmd.AddCommand(workerCmd) |  | ||||||
| } |  | ||||||
| @@ -11,14 +11,12 @@ import ( | |||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
|  |  | ||||||
| 	. "github.com/elyby/chrly/http" | 	. "github.com/elyby/chrly/http" | ||||||
| 	"github.com/elyby/chrly/mojangtextures" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var handlers = di.Options( | var handlers = di.Options( | ||||||
| 	di.Provide(newHandlerFactory, di.As(new(http.Handler))), | 	di.Provide(newHandlerFactory, di.As(new(http.Handler))), | ||||||
| 	di.Provide(newSkinsystemHandler, di.WithName("skinsystem")), | 	di.Provide(newSkinsystemHandler, di.WithName("skinsystem")), | ||||||
| 	di.Provide(newApiHandler, di.WithName("api")), | 	di.Provide(newApiHandler, di.WithName("api")), | ||||||
| 	di.Provide(newUUIDsWorkerHandler, di.WithName("worker")), |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func newHandlerFactory( | func newHandlerFactory( | ||||||
| @@ -48,17 +46,6 @@ func newHandlerFactory( | |||||||
| 	// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279 | 	// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279 | ||||||
| 	router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFoundHandler)) | 	router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFoundHandler)) | ||||||
|  |  | ||||||
| 	// Enable the worker module before api to allow gorilla.mux to correctly find the target router |  | ||||||
| 	// as it uses the first matching and /api overrides the more accurate /api/worker |  | ||||||
| 	if hasValue(enabledModules, "worker") { |  | ||||||
| 		var workerRouter *mux.Router |  | ||||||
| 		if err := container.Resolve(&workerRouter, di.Name("worker")); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		mount(router, "/api/worker", workerRouter) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if hasValue(enabledModules, "api") { | 	if hasValue(enabledModules, "api") { | ||||||
| 		var apiRouter *mux.Router | 		var apiRouter *mux.Router | ||||||
| 		if err := container.Resolve(&apiRouter, di.Name("api")); err != nil { | 		if err := container.Resolve(&apiRouter, di.Name("api")); err != nil { | ||||||
| @@ -127,12 +114,6 @@ func newApiHandler(skinsRepository SkinsRepository) *mux.Router { | |||||||
| 	}).Handler() | 	}).Handler() | ||||||
| } | } | ||||||
|  |  | ||||||
| func newUUIDsWorkerHandler(mojangUUIDsProvider *mojangtextures.BatchUuidsProvider) *mux.Router { |  | ||||||
| 	return (&UUIDsWorker{ |  | ||||||
| 		MojangUuidsProvider: mojangUUIDsProvider, |  | ||||||
| 	}).Handler() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func hasValue(slice []string, needle string) bool { | func hasValue(slice []string, needle string) bool { | ||||||
| 	for _, value := range slice { | 	for _, value := range slice { | ||||||
| 		if value == needle { | 		if value == needle { | ||||||
|   | |||||||
| @@ -24,7 +24,6 @@ var mojangTextures = di.Options( | |||||||
| 	di.Provide(newMojangTexturesBatchUUIDsProviderStrategyFactory), | 	di.Provide(newMojangTexturesBatchUUIDsProviderStrategyFactory), | ||||||
| 	di.Provide(newMojangTexturesBatchUUIDsProviderDelayedStrategy), | 	di.Provide(newMojangTexturesBatchUUIDsProviderDelayedStrategy), | ||||||
| 	di.Provide(newMojangTexturesBatchUUIDsProviderFullBusStrategy), | 	di.Provide(newMojangTexturesBatchUUIDsProviderFullBusStrategy), | ||||||
| 	di.Provide(newMojangTexturesRemoteUUIDsProvider), |  | ||||||
| 	di.Provide(newMojangSignedTexturesProvider), | 	di.Provide(newMojangSignedTexturesProvider), | ||||||
| 	di.Provide(newMojangTexturesStorageFactory), | 	di.Provide(newMojangTexturesStorageFactory), | ||||||
| ) | ) | ||||||
| @@ -86,17 +85,8 @@ func newMojangTexturesProvider( | |||||||
| } | } | ||||||
|  |  | ||||||
| func newMojangTexturesUuidsProviderFactory( | func newMojangTexturesUuidsProviderFactory( | ||||||
| 	config *viper.Viper, |  | ||||||
| 	container *di.Container, | 	container *di.Container, | ||||||
| ) (mojangtextures.UUIDsProvider, error) { | ) (mojangtextures.UUIDsProvider, error) { | ||||||
| 	preferredUuidsProvider := config.GetString("mojang_textures.uuids_provider.driver") |  | ||||||
| 	if preferredUuidsProvider == "remote" { |  | ||||||
| 		var provider *mojangtextures.RemoteApiUuidsProvider |  | ||||||
| 		err := container.Resolve(&provider) |  | ||||||
|  |  | ||||||
| 		return provider, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var provider *mojangtextures.BatchUuidsProvider | 	var provider *mojangtextures.BatchUuidsProvider | ||||||
| 	err := container.Resolve(&provider) | 	err := container.Resolve(&provider) | ||||||
|  |  | ||||||
| @@ -188,36 +178,6 @@ func newMojangTexturesBatchUUIDsProviderFullBusStrategy(config *viper.Viper) *mo | |||||||
| 	) | 	) | ||||||
| } | } | ||||||
|  |  | ||||||
| func newMojangTexturesRemoteUUIDsProvider( |  | ||||||
| 	container *di.Container, |  | ||||||
| 	config *viper.Viper, |  | ||||||
| 	emitter mojangtextures.Emitter, |  | ||||||
| ) (*mojangtextures.RemoteApiUuidsProvider, error) { |  | ||||||
| 	remoteUrl, err := url.Parse(config.GetString("mojang_textures.uuids_provider.url")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("unable to parse remote url: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := container.Provide(func(emitter es.Subscriber, config *viper.Viper) *namedHealthChecker { |  | ||||||
| 		config.SetDefault("healthcheck.mojang_api_textures_provider_cool_down_duration", time.Minute+10*time.Second) |  | ||||||
|  |  | ||||||
| 		return &namedHealthChecker{ |  | ||||||
| 			Name: "mojang-api-textures-provider-response-checker", |  | ||||||
| 			Checker: es.MojangApiTexturesProviderResponseChecker( |  | ||||||
| 				emitter, |  | ||||||
| 				config.GetDuration("healthcheck.mojang_api_textures_provider_cool_down_duration"), |  | ||||||
| 			), |  | ||||||
| 		} |  | ||||||
| 	}); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &mojangtextures.RemoteApiUuidsProvider{ |  | ||||||
| 		Emitter: emitter, |  | ||||||
| 		Url:     *remoteUrl, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func newMojangSignedTexturesProvider(emitter mojangtextures.Emitter) mojangtextures.TexturesProvider { | func newMojangSignedTexturesProvider(emitter mojangtextures.Emitter) mojangtextures.TexturesProvider { | ||||||
| 	return &mojangtextures.MojangApiTexturesProvider{ | 	return &mojangtextures.MojangApiTexturesProvider{ | ||||||
| 		Emitter: emitter, | 		Emitter: emitter, | ||||||
|   | |||||||
| @@ -20,15 +20,6 @@ services: | |||||||
|     environment: |     environment: | ||||||
|       CHRLY_SECRET: replace_this_value_in_production |       CHRLY_SECRET: replace_this_value_in_production | ||||||
|  |  | ||||||
|   # Use this configuration in case when you need a remote Mojang UUIDs provider |  | ||||||
|   # worker: |  | ||||||
|   #   image: elyby/chrly |  | ||||||
|   #   hostname: chrly0 |  | ||||||
|   #   restart: always |  | ||||||
|   #   ports: |  | ||||||
|   #     - "8080:80" |  | ||||||
|   #   command: ["worker"] |  | ||||||
|  |  | ||||||
|   redis: |   redis: | ||||||
|     image: redis:4.0-32bit # 32-bit version is recommended to spare some memory |     image: redis:4.0-32bit # 32-bit version is recommended to spare some memory | ||||||
|     restart: always |     restart: always | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ if [ ! -d /data/capes ]; then | |||||||
|     mkdir -p /data/capes |     mkdir -p /data/capes | ||||||
| fi | fi | ||||||
|  |  | ||||||
| if [ "$1" = "serve" ] || [ "$1" = "worker" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then | if [ "$1" = "serve" ] || [ "$1" = "token" ] || [ "$1" = "version" ]; then | ||||||
|     set -- /usr/local/bin/chrly "$@" |     set -- /usr/local/bin/chrly "$@" | ||||||
| fi | fi | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,53 +0,0 @@ | |||||||
| package http |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
|  |  | ||||||
| 	"github.com/gorilla/mux" |  | ||||||
|  |  | ||||||
| 	"github.com/elyby/chrly/api/mojang" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type MojangUuidsProvider interface { |  | ||||||
| 	GetUuid(username string) (*mojang.ProfileInfo, error) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type UUIDsWorker struct { |  | ||||||
| 	MojangUuidsProvider |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (ctx *UUIDsWorker) Handler() *mux.Router { |  | ||||||
| 	router := mux.NewRouter().StrictSlash(true) |  | ||||||
| 	router.Handle("/mojang-uuid/{username}", http.HandlerFunc(ctx.getUUIDHandler)).Methods("GET") |  | ||||||
|  |  | ||||||
| 	return router |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (ctx *UUIDsWorker) getUUIDHandler(response http.ResponseWriter, request *http.Request) { |  | ||||||
| 	username := mux.Vars(request)["username"] |  | ||||||
| 	profile, err := ctx.GetUuid(username) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if _, ok := err.(*mojang.TooManyRequestsError); ok { |  | ||||||
| 			response.WriteHeader(http.StatusTooManyRequests) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		response.Header().Set("Content-Type", "application/json") |  | ||||||
| 		response.WriteHeader(http.StatusInternalServerError) |  | ||||||
| 		result, _ := json.Marshal(map[string]interface{}{ |  | ||||||
| 			"provider": err.Error(), |  | ||||||
| 		}) |  | ||||||
| 		_, _ = response.Write(result) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if profile == nil { |  | ||||||
| 		response.WriteHeader(http.StatusNoContent) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	response.Header().Set("Content-Type", "application/json") |  | ||||||
| 	responseData, _ := json.Marshal(profile) |  | ||||||
| 	_, _ = response.Write(responseData) |  | ||||||
| } |  | ||||||
| @@ -1,154 +0,0 @@ | |||||||
| package http |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| 	"github.com/stretchr/testify/suite" |  | ||||||
|  |  | ||||||
| 	"github.com/elyby/chrly/api/mojang" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| /*************** |  | ||||||
|  * Setup mocks * |  | ||||||
|  ***************/ |  | ||||||
|  |  | ||||||
| type uuidsProviderMock struct { |  | ||||||
| 	mock.Mock |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *uuidsProviderMock) GetUuid(username string) (*mojang.ProfileInfo, error) { |  | ||||||
| 	args := m.Called(username) |  | ||||||
| 	var result *mojang.ProfileInfo |  | ||||||
| 	if casted, ok := args.Get(0).(*mojang.ProfileInfo); ok { |  | ||||||
| 		result = casted |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return result, args.Error(1) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type uuidsWorkerTestSuite struct { |  | ||||||
| 	suite.Suite |  | ||||||
|  |  | ||||||
| 	App *UUIDsWorker |  | ||||||
|  |  | ||||||
| 	UuidsProvider *uuidsProviderMock |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /******************** |  | ||||||
|  * Setup test suite * |  | ||||||
|  ********************/ |  | ||||||
|  |  | ||||||
| func (suite *uuidsWorkerTestSuite) SetupTest() { |  | ||||||
| 	suite.UuidsProvider = &uuidsProviderMock{} |  | ||||||
|  |  | ||||||
| 	suite.App = &UUIDsWorker{ |  | ||||||
| 		MojangUuidsProvider: suite.UuidsProvider, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *uuidsWorkerTestSuite) TearDownTest() { |  | ||||||
| 	suite.UuidsProvider.AssertExpectations(suite.T()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *uuidsWorkerTestSuite) RunSubTest(name string, subTest func()) { |  | ||||||
| 	suite.SetupTest() |  | ||||||
| 	suite.Run(name, subTest) |  | ||||||
| 	suite.TearDownTest() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /************* |  | ||||||
|  * Run tests * |  | ||||||
|  *************/ |  | ||||||
|  |  | ||||||
| func TestUUIDsWorker(t *testing.T) { |  | ||||||
| 	suite.Run(t, new(uuidsWorkerTestSuite)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type uuidsWorkerTestCase struct { |  | ||||||
| 	Name       string |  | ||||||
| 	BeforeTest func(suite *uuidsWorkerTestSuite) |  | ||||||
| 	AfterTest  func(suite *uuidsWorkerTestSuite, response *http.Response) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /************************ |  | ||||||
|  * Get UUID tests cases * |  | ||||||
|  ************************/ |  | ||||||
|  |  | ||||||
| var getUuidTestsCases = []*uuidsWorkerTestCase{ |  | ||||||
| 	{ |  | ||||||
| 		Name: "Success provider response", |  | ||||||
| 		BeforeTest: func(suite *uuidsWorkerTestSuite) { |  | ||||||
| 			suite.UuidsProvider.On("GetUuid", "mock_username").Return(&mojang.ProfileInfo{ |  | ||||||
| 				Id:   "0fcc38620f1845f3a54e1b523c1bd1c7", |  | ||||||
| 				Name: "mock_username", |  | ||||||
| 			}, nil) |  | ||||||
| 		}, |  | ||||||
| 		AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) { |  | ||||||
| 			suite.Equal(200, response.StatusCode) |  | ||||||
| 			suite.Equal("application/json", response.Header.Get("Content-Type")) |  | ||||||
| 			body, _ := ioutil.ReadAll(response.Body) |  | ||||||
| 			suite.JSONEq(`{ |  | ||||||
| 				"id": "0fcc38620f1845f3a54e1b523c1bd1c7", |  | ||||||
| 				"name": "mock_username" |  | ||||||
| 			}`, string(body)) |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		Name: "Receive empty response from UUIDs provider", |  | ||||||
| 		BeforeTest: func(suite *uuidsWorkerTestSuite) { |  | ||||||
| 			suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, nil) |  | ||||||
| 		}, |  | ||||||
| 		AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) { |  | ||||||
| 			suite.Equal(204, response.StatusCode) |  | ||||||
| 			body, _ := ioutil.ReadAll(response.Body) |  | ||||||
| 			suite.Assert().Empty(body) |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		Name: "Receive error from UUIDs provider", |  | ||||||
| 		BeforeTest: func(suite *uuidsWorkerTestSuite) { |  | ||||||
| 			err := errors.New("this is an error") |  | ||||||
| 			suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err) |  | ||||||
| 		}, |  | ||||||
| 		AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) { |  | ||||||
| 			suite.Equal(500, response.StatusCode) |  | ||||||
| 			suite.Equal("application/json", response.Header.Get("Content-Type")) |  | ||||||
| 			body, _ := ioutil.ReadAll(response.Body) |  | ||||||
| 			suite.JSONEq(`{ |  | ||||||
| 				"provider": "this is an error" |  | ||||||
| 			}`, string(body)) |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	{ |  | ||||||
| 		Name: "Receive Too Many Requests from UUIDs provider", |  | ||||||
| 		BeforeTest: func(suite *uuidsWorkerTestSuite) { |  | ||||||
| 			err := &mojang.TooManyRequestsError{} |  | ||||||
| 			suite.UuidsProvider.On("GetUuid", "mock_username").Return(nil, err) |  | ||||||
| 		}, |  | ||||||
| 		AfterTest: func(suite *uuidsWorkerTestSuite, response *http.Response) { |  | ||||||
| 			suite.Equal(429, response.StatusCode) |  | ||||||
| 			body, _ := ioutil.ReadAll(response.Body) |  | ||||||
| 			suite.Empty(body) |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *uuidsWorkerTestSuite) TestGetUUID() { |  | ||||||
| 	for _, testCase := range getUuidTestsCases { |  | ||||||
| 		suite.RunSubTest(testCase.Name, func() { |  | ||||||
| 			testCase.BeforeTest(suite) |  | ||||||
|  |  | ||||||
| 			req := httptest.NewRequest("GET", "http://chrly/mojang-uuid/mock_username", nil) |  | ||||||
| 			w := httptest.NewRecorder() |  | ||||||
|  |  | ||||||
| 			suite.App.Handler().ServeHTTP(w, req) |  | ||||||
|  |  | ||||||
| 			testCase.AfterTest(suite, w.Result()) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,67 +0,0 @@ | |||||||
| package mojangtextures |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" |  | ||||||
| 	. "net/url" |  | ||||||
| 	"path" |  | ||||||
|  |  | ||||||
| 	"github.com/elyby/chrly/api/mojang" |  | ||||||
| 	"github.com/elyby/chrly/version" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var HttpClient = &http.Client{ |  | ||||||
| 	Transport: &http.Transport{ |  | ||||||
| 		MaxIdleConnsPerHost: 1024, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type RemoteApiUuidsProvider struct { |  | ||||||
| 	Emitter |  | ||||||
| 	Url URL |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (ctx *RemoteApiUuidsProvider) GetUuid(username string) (*mojang.ProfileInfo, error) { |  | ||||||
| 	url := ctx.Url |  | ||||||
| 	url.Path = path.Join(url.Path, username) |  | ||||||
| 	urlStr := url.String() |  | ||||||
|  |  | ||||||
| 	request, _ := http.NewRequest("GET", urlStr, nil) |  | ||||||
| 	request.Header.Add("Accept", "application/json") |  | ||||||
| 	// Change default User-Agent to allow specify "Username -> UUID at time" Mojang's api endpoint |  | ||||||
| 	request.Header.Add("User-Agent", "Chrly/"+version.Version()) |  | ||||||
|  |  | ||||||
| 	ctx.Emit("mojang_textures:remote_api_uuids_provider:before_request", urlStr) |  | ||||||
| 	response, err := HttpClient.Do(request) |  | ||||||
| 	ctx.Emit("mojang_textures:remote_api_uuids_provider:after_request", response, err) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	defer response.Body.Close() |  | ||||||
|  |  | ||||||
| 	if response.StatusCode == 204 { |  | ||||||
| 		return nil, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if response.StatusCode != 200 { |  | ||||||
| 		return nil, &UnexpectedRemoteApiResponse{response} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var result *mojang.ProfileInfo |  | ||||||
| 	body, _ := ioutil.ReadAll(response.Body) |  | ||||||
| 	err = json.Unmarshal(body, &result) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return result, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type UnexpectedRemoteApiResponse struct { |  | ||||||
| 	Response *http.Response |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (*UnexpectedRemoteApiResponse) Error() string { |  | ||||||
| 	return "Unexpected remote api response" |  | ||||||
| } |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| package mojangtextures |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"net" |  | ||||||
| 	"net/http" |  | ||||||
| 	. "net/url" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/h2non/gock" |  | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/mock" |  | ||||||
| 	"github.com/stretchr/testify/suite" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type remoteApiUuidsProviderTestSuite struct { |  | ||||||
| 	suite.Suite |  | ||||||
|  |  | ||||||
| 	Provider *RemoteApiUuidsProvider |  | ||||||
| 	Emitter  *mockEmitter |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) SetupSuite() { |  | ||||||
| 	client := &http.Client{} |  | ||||||
| 	gock.InterceptClient(client) |  | ||||||
|  |  | ||||||
| 	HttpClient = client |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) SetupTest() { |  | ||||||
| 	suite.Emitter = &mockEmitter{} |  | ||||||
| 	suite.Provider = &RemoteApiUuidsProvider{ |  | ||||||
| 		Emitter: suite.Emitter, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TearDownTest() { |  | ||||||
| 	suite.Emitter.AssertExpectations(suite.T()) |  | ||||||
| 	gock.Off() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestRemoteApiUuidsProvider(t *testing.T) { |  | ||||||
| 	suite.Run(t, new(remoteApiUuidsProviderTestSuite)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForValidUsername() { |  | ||||||
| 	suite.Emitter.On("Emit", "mojang_textures:remote_api_uuids_provider:before_request", "http://example.com/subpath/username").Once() |  | ||||||
| 	suite.Emitter.On("Emit", |  | ||||||
| 		"mojang_textures:remote_api_uuids_provider:after_request", |  | ||||||
| 		mock.AnythingOfType("*http.Response"), |  | ||||||
| 		nil, |  | ||||||
| 	).Once() |  | ||||||
|  |  | ||||||
| 	gock.New("http://example.com"). |  | ||||||
| 		Get("/subpath/username"). |  | ||||||
| 		Reply(200). |  | ||||||
| 		JSON(map[string]interface{}{ |  | ||||||
| 			"id":   "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", |  | ||||||
| 			"name": "username", |  | ||||||
| 		}) |  | ||||||
|  |  | ||||||
| 	suite.Provider.Url = shouldParseUrl("http://example.com/subpath") |  | ||||||
| 	result, err := suite.Provider.GetUuid("username") |  | ||||||
|  |  | ||||||
| 	assert := suite.Assert() |  | ||||||
| 	if assert.NoError(err) { |  | ||||||
| 		assert.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", result.Id) |  | ||||||
| 		assert.Equal("username", result.Name) |  | ||||||
| 		assert.False(result.IsLegacy) |  | ||||||
| 		assert.False(result.IsDemo) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNotExistsUsername() { |  | ||||||
| 	suite.Emitter.On("Emit", "mojang_textures:remote_api_uuids_provider:before_request", "http://example.com/subpath/username").Once() |  | ||||||
| 	suite.Emitter.On("Emit", |  | ||||||
| 		"mojang_textures:remote_api_uuids_provider:after_request", |  | ||||||
| 		mock.AnythingOfType("*http.Response"), |  | ||||||
| 		nil, |  | ||||||
| 	).Once() |  | ||||||
|  |  | ||||||
| 	gock.New("http://example.com"). |  | ||||||
| 		Get("/subpath/username"). |  | ||||||
| 		Reply(204) |  | ||||||
|  |  | ||||||
| 	suite.Provider.Url = shouldParseUrl("http://example.com/subpath") |  | ||||||
| 	result, err := suite.Provider.GetUuid("username") |  | ||||||
|  |  | ||||||
| 	assert := suite.Assert() |  | ||||||
| 	assert.Nil(result) |  | ||||||
| 	assert.Nil(err) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNon20xResponse() { |  | ||||||
| 	suite.Emitter.On("Emit", "mojang_textures:remote_api_uuids_provider:before_request", "http://example.com/subpath/username").Once() |  | ||||||
| 	suite.Emitter.On("Emit", |  | ||||||
| 		"mojang_textures:remote_api_uuids_provider:after_request", |  | ||||||
| 		mock.AnythingOfType("*http.Response"), |  | ||||||
| 		nil, |  | ||||||
| 	).Once() |  | ||||||
|  |  | ||||||
| 	gock.New("http://example.com"). |  | ||||||
| 		Get("/subpath/username"). |  | ||||||
| 		Reply(504). |  | ||||||
| 		BodyString("504 Gateway Timeout") |  | ||||||
|  |  | ||||||
| 	suite.Provider.Url = shouldParseUrl("http://example.com/subpath") |  | ||||||
| 	result, err := suite.Provider.GetUuid("username") |  | ||||||
|  |  | ||||||
| 	assert := suite.Assert() |  | ||||||
| 	assert.Nil(result) |  | ||||||
| 	assert.EqualError(err, "Unexpected remote api response") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForNotSuccessRequest() { |  | ||||||
| 	suite.Emitter.On("Emit", "mojang_textures:remote_api_uuids_provider:before_request", "http://example.com/subpath/username").Once() |  | ||||||
| 	suite.Emitter.On("Emit", |  | ||||||
| 		"mojang_textures:remote_api_uuids_provider:after_request", |  | ||||||
| 		mock.AnythingOfType("*http.Response"), |  | ||||||
| 		mock.AnythingOfType("*url.Error"), |  | ||||||
| 	).Once() |  | ||||||
|  |  | ||||||
| 	expectedError := &net.OpError{Op: "dial"} |  | ||||||
|  |  | ||||||
| 	gock.New("http://example.com"). |  | ||||||
| 		Get("/subpath/username"). |  | ||||||
| 		ReplyError(expectedError) |  | ||||||
|  |  | ||||||
| 	suite.Provider.Url = shouldParseUrl("http://example.com/subpath") |  | ||||||
| 	result, err := suite.Provider.GetUuid("username") |  | ||||||
|  |  | ||||||
| 	assert := suite.Assert() |  | ||||||
| 	assert.Nil(result) |  | ||||||
| 	if assert.Error(err) { |  | ||||||
| 		assert.IsType(&Error{}, err) |  | ||||||
| 		casterErr, _ := err.(*Error) |  | ||||||
| 		assert.Equal(expectedError, casterErr.Err) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (suite *remoteApiUuidsProviderTestSuite) TestGetUuidForInvalidSuccessResponse() { |  | ||||||
| 	suite.Emitter.On("Emit", "mojang_textures:remote_api_uuids_provider:before_request", "http://example.com/subpath/username").Once() |  | ||||||
| 	suite.Emitter.On("Emit", |  | ||||||
| 		"mojang_textures:remote_api_uuids_provider:after_request", |  | ||||||
| 		mock.AnythingOfType("*http.Response"), |  | ||||||
| 		nil, |  | ||||||
| 	).Once() |  | ||||||
|  |  | ||||||
| 	gock.New("http://example.com"). |  | ||||||
| 		Get("/subpath/username"). |  | ||||||
| 		Reply(200). |  | ||||||
| 		BodyString("completely not json") |  | ||||||
|  |  | ||||||
| 	suite.Provider.Url = shouldParseUrl("http://example.com/subpath") |  | ||||||
| 	result, err := suite.Provider.GetUuid("username") |  | ||||||
|  |  | ||||||
| 	assert := suite.Assert() |  | ||||||
| 	assert.Nil(result) |  | ||||||
| 	assert.Error(err) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func shouldParseUrl(rawUrl string) URL { |  | ||||||
| 	url, err := Parse(rawUrl) |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return *url |  | ||||||
| } |  | ||||||
		Reference in New Issue
	
	Block a user