From ec461efe34b04b733cab136d0fdf7081bb4ac49c Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 18 Aug 2017 17:48:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D1=84=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B0=20API=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=B5=D0=B3=D0=BE=20=D0=B8=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/accounts/auto-refresh-token.go | 56 ++++++ api/accounts/auto-refresh-token_test.go | 242 ++++++++++++++++++++++++ cmd/amqpWorker.go | 15 +- worker/worker.go | 34 ++-- 4 files changed, 326 insertions(+), 21 deletions(-) create mode 100644 api/accounts/auto-refresh-token.go create mode 100644 api/accounts/auto-refresh-token_test.go diff --git a/api/accounts/auto-refresh-token.go b/api/accounts/auto-refresh-token.go new file mode 100644 index 0000000..9df3ba5 --- /dev/null +++ b/api/accounts/auto-refresh-token.go @@ -0,0 +1,56 @@ +package accounts + +type AutoRefresh struct { + token *Token + config *Config + repeatsCount int +} + +const repeatsLimit = 3 + +func (config *Config) GetTokenWithAutoRefresh() *AutoRefresh { + return &AutoRefresh{ + config: config, + } +} + +func (refresher *AutoRefresh) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) { + defer refresher.resetRepeatsCount() + + apiToken, err := refresher.getToken() + if err != nil { + return nil, err + } + + result, err := apiToken.AccountInfo(attribute, value) + if err != nil { + _, isTokenExpire := err.(*UnauthorizedResponse) + if !isTokenExpire || refresher.repeatsCount >= repeatsLimit - 1 { + return nil, err + } + + refresher.repeatsCount++ + refresher.token = nil + + return refresher.AccountInfo(attribute, value) + } + + return result, nil +} + +func (refresher *AutoRefresh) getToken() (*Token, error) { + if refresher.token == nil { + newToken, err := refresher.config.GetToken() + if err != nil { + return nil, err + } + + refresher.token = newToken + } + + return refresher.token, nil +} + +func (refresher *AutoRefresh) resetRepeatsCount() { + refresher.repeatsCount = 0 +} diff --git a/api/accounts/auto-refresh-token_test.go b/api/accounts/auto-refresh-token_test.go new file mode 100644 index 0000000..4af6b88 --- /dev/null +++ b/api/accounts/auto-refresh-token_test.go @@ -0,0 +1,242 @@ +package accounts + +import ( + "net/http" + "strings" + "testing" + + testify "github.com/stretchr/testify/assert" + "gopkg.in/h2non/gock.v1" +) + +var config = &Config{ + Addr: "https://account.ely.by", + Id: "mock-id", + Secret: "mock-secret", + Scopes: []string{"scope1", "scope2"}, +} + +func TestConfig_GetTokenWithAutoRefresh(t *testing.T) { + assert := testify.New(t) + + testConfig := &Config{} + *testConfig = *config + + result := testConfig.GetTokenWithAutoRefresh() + assert.Equal(testConfig, result.config) +} + +func TestAutoRefresh_AccountInfo(t *testing.T) { + assert := testify.New(t) + + defer gock.Off() + gock.New("https://account.ely.by"). + Post("/api/oauth2/v1/token"). + Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). + Reply(200). + JSON(map[string]interface{}{ + "access_token": "mocked-token", + "token_type": "Bearer", + "expires_in": 86400, + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + Times(2). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token"). + Reply(200). + JSON(map[string]interface{}{ + "id": 1, + "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", + "username": "dummy", + "email": "dummy@ely.by", + }) + + client := &http.Client{} + gock.InterceptClient(client) + + testConfig := &Config{} + *testConfig = *config + testConfig.Client = client + + autoRefresher := testConfig.GetTokenWithAutoRefresh() + result, err := autoRefresher.AccountInfo("id", "1") + if assert.NoError(err) { + assert.Equal(1, result.Id) + assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid) + assert.Equal("dummy", result.Username) + assert.Equal("dummy@ely.by", result.Email) + } + + result2, err2 := autoRefresher.AccountInfo("id", "1") + if assert.NoError(err2) { + assert.Equal(result, result2, "Results should still be same without token refreshing") + } +} + +func TestAutoRefresh_AccountInfo2(t *testing.T) { + assert := testify.New(t) + + defer gock.Off() + gock.New("https://account.ely.by"). + Post("/api/oauth2/v1/token"). + Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). + Reply(200). + JSON(map[string]interface{}{ + "access_token": "mocked-token-1", + "token_type": "Bearer", + "expires_in": 86400, + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token-1"). + Reply(200). + JSON(map[string]interface{}{ + "id": 1, + "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", + "username": "dummy", + "email": "dummy@ely.by", + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token-1"). + Reply(401). + JSON(map[string]interface{}{ + "name": "Unauthorized", + "message": "Incorrect token", + "code": 0, + "status": 401, + }) + + gock.New("https://account.ely.by"). + Post("/api/oauth2/v1/token"). + Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). + Reply(200). + JSON(map[string]interface{}{ + "access_token": "mocked-token-2", + "token_type": "Bearer", + "expires_in": 86400, + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token-2"). + Reply(200). + JSON(map[string]interface{}{ + "id": 1, + "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", + "username": "dummy", + "email": "dummy@ely.by", + }) + + client := &http.Client{} + gock.InterceptClient(client) + + testConfig := &Config{} + *testConfig = *config + testConfig.Client = client + + autoRefresher := testConfig.GetTokenWithAutoRefresh() + result, err := autoRefresher.AccountInfo("id", "1") + if assert.NoError(err) { + assert.Equal(1, result.Id) + assert.Equal("0f657aa8-bfbe-415d-b700-5750090d3af3", result.Uuid) + assert.Equal("dummy", result.Username) + assert.Equal("dummy@ely.by", result.Email) + } + + result2, err2 := autoRefresher.AccountInfo("id", "1") + if assert.NoError(err2) { + assert.Equal(result, result2, "Results should still be same with refreshed token") + } +} + +func TestAutoRefresh_AccountInfo3(t *testing.T) { + assert := testify.New(t) + + defer gock.Off() + gock.New("https://account.ely.by"). + Post("/api/oauth2/v1/token"). + Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). + Reply(200). + JSON(map[string]interface{}{ + "access_token": "mocked-token-1", + "token_type": "Bearer", + "expires_in": 86400, + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token-1"). + Reply(404). + JSON(map[string]interface{}{ + "name": "Not Found", + "message": "Page not found.", + "code": 0, + "status": 404, + }) + + client := &http.Client{} + gock.InterceptClient(client) + + testConfig := &Config{} + *testConfig = *config + testConfig.Client = client + + autoRefresher := testConfig.GetTokenWithAutoRefresh() + result, err := autoRefresher.AccountInfo("id", "1") + assert.Nil(result) + assert.Error(err) + assert.IsType(&NotFoundResponse{}, err) +} + +func TestAutoRefresh_AccountInfo4(t *testing.T) { + assert := testify.New(t) + + defer gock.Off() + gock.New("https://account.ely.by"). + Post("/api/oauth2/v1/token"). + Times(3). + Body(strings.NewReader("client_id=mock-id&client_secret=mock-secret&grant_type=client_credentials&scope=scope1%2Cscope2")). + Reply(200). + JSON(map[string]interface{}{ + "access_token": "mocked-token-1", + "token_type": "Bearer", + "expires_in": 86400, + }) + + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + Times(3). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mocked-token-1"). + Reply(401). + JSON(map[string]interface{}{ + "name": "Unauthorized", + "message": "Incorrect token", + "code": 0, + "status": 401, + }) + + client := &http.Client{} + gock.InterceptClient(client) + + testConfig := &Config{} + *testConfig = *config + testConfig.Client = client + + autoRefresher := testConfig.GetTokenWithAutoRefresh() + result, err := autoRefresher.AccountInfo("id", "1") + assert.Nil(result) + assert.Error(err) + if !assert.IsType(&UnauthorizedResponse{}, err) { + t.Fatal(err) + } +} diff --git a/cmd/amqpWorker.go b/cmd/amqpWorker.go index 8cc1d7b..1053d18 100644 --- a/cmd/amqpWorker.go +++ b/cmd/amqpWorker.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "elyby/minecraft-skinsystem/api/accounts" "elyby/minecraft-skinsystem/bootstrap" "elyby/minecraft-skinsystem/db" "elyby/minecraft-skinsystem/worker" @@ -46,10 +47,18 @@ var amqpWorkerCmd = &cobra.Command{ } logger.Info("AMQP connection successfully initialized") + accountsApi := (&accounts.Config{ + Addr: viper.GetString("api.accounts.host"), + Id: viper.GetString("api.accounts.id"), + Secret: viper.GetString("api.accounts.secret"), + Scopes: viper.GetStringSlice("api.accounts.scopes"), + }).GetTokenWithAutoRefresh() + services := &worker.Services{ - Logger: logger, - Channel: amqpChannel, - SkinsRepo: skinsRepo, + Logger: logger, + Channel: amqpChannel, + SkinsRepo: skinsRepo, + AccountsAPI: accountsApi, } if err := services.Run(); err != nil { diff --git a/worker/worker.go b/worker/worker.go index 142f3af..fe3c010 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -2,18 +2,21 @@ package worker import ( "encoding/json" + "fmt" + "strconv" "github.com/mono83/slf/wd" "github.com/streadway/amqp" - "elyby/minecraft-skinsystem/model" "elyby/minecraft-skinsystem/interfaces" + "elyby/minecraft-skinsystem/model" ) type Services struct { - Channel *amqp.Channel - SkinsRepo interfaces.SkinsRepository - Logger wd.Watchdog + Channel *amqp.Channel + SkinsRepo interfaces.SkinsRepository + AccountsAPI interfaces.AccountsAPI + Logger wd.Watchdog } const exchangeName string = "events" @@ -68,24 +71,21 @@ func (service *Services) HandleChangeUsername(event *model.UsernameChanged) bool record, err := service.SkinsRepo.FindByUserId(event.AccountId) if err != nil { - /* - // TODO: вернуть логику восстановления информации об аккаунте service.Logger.IncCounter("worker.change_username.id_not_found", 1) service.Logger.Warning("Cannot find user id. Trying to search.") - response, err := getById(event.AccountId) + response, err := service.AccountsAPI.AccountInfo("id", strconv.Itoa(event.AccountId)) if err != nil { service.Logger.IncCounter("worker.change_username.id_not_restored", 1) - service.Logger.Error("Cannot restore user info. %T\n", err) + service.Logger.Error(fmt.Sprintf("Cannot restore user info. %+v\n", err)) // TODO: логгировать в какой-нибудь Sentry, если там не 404 return true } service.Logger.IncCounter("worker.change_username.id_restored", 1) fmt.Println("User info successfully restored.") - record = &event.Skin{ + record = &model.Skin{ UserId: response.Id, } - */ } record.Username = event.NewUsername @@ -101,21 +101,19 @@ func (service *Services) HandleSkinChanged(event *model.SkinChanged) bool { if err != nil { service.Logger.IncCounter("worker.skin_changed.id_not_found", 1) service.Logger.Warning("Cannot find user id. Trying to search.") - /* - // TODO: вернуть логику восстановления информации об аккаунте - response, err := getById(event.AccountId) + response, err := service.AccountsAPI.AccountInfo("id", strconv.Itoa(event.AccountId)) if err != nil { - services.Logger.IncCounter("worker.skin_changed.id_not_restored", 1) - fmt.Printf("Cannot restore user info. %T\n", err) + service.Logger.IncCounter("worker.skin_changed.id_not_restored", 1) + service.Logger.Error(fmt.Sprintf("Cannot restore user info. %+v\n", err)) // TODO: логгировать в какой-нибудь Sentry, если там не 404 return true } - services.Logger.IncCounter("worker.skin_changed.id_restored", 1) - fmt.Println("User info successfully restored.") + service.Logger.IncCounter("worker.skin_changed.id_restored", 1) + service.Logger.Info("User info successfully restored.") + record.UserId = response.Id record.Username = response.Username - */ } record.Uuid = event.Uuid