From 4734bfd93cb3e012de3f35f077ccfa382e735de6 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Fri, 18 Aug 2017 01:08:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B4=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=BF=D0=BD=D0=B0=20=D0=BA=20internal=20API=20Accounts?= =?UTF-8?q?=20Ely.by?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Gopkg.lock | 81 +++++++++++++---- Gopkg.toml | 14 +++ api/accounts/accounts.go | 166 ++++++++++++++++++++++++++++++++++ api/accounts/accounts_test.go | 98 ++++++++++++++++++++ interfaces/api.go | 9 ++ 5 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 api/accounts/accounts.go create mode 100644 api/accounts/accounts_test.go create mode 100644 interfaces/api.go diff --git a/Gopkg.lock b/Gopkg.lock index 5d3cd6f..9a3c33c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,16 +2,28 @@ [[projects]] - branch = "master" - name = "github.com/fsnotify/fsnotify" - packages = ["."] - revision = "4da3e2cfbabc9f751898f250b49f2439785783a1" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "629574ca2a5df945712d3079857300b5e4da0236" + version = "v1.4.2" + +[[projects]] + name = "github.com/golang/mock" + packages = ["gomock"] + revision = "13f360950a79f5864a972c786a10a50e44b69541" + version = "v1.0.0" [[projects]] - branch = "master" name = "github.com/gorilla/context" packages = ["."] - revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" + revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a" + version = "v1.1" [[projects]] name = "github.com/gorilla/mux" @@ -34,12 +46,14 @@ [[projects]] name = "github.com/magiconair/properties" packages = ["."] - revision = "51463bfca2576e06c62a8504b5c0f06d61312647" + revision = "be5ece7dd465ab0765a9682137865547526d1dfb" + version = "v1.7.3" [[projects]] + branch = "master" name = "github.com/mediocregopher/radix.v2" packages = ["cluster","pool","redis","util"] - revision = "dbcfd490034f823788edc555737247e9ba628b6c" + revision = "ae7309086d191442b36bf69f7f5eeca5fdbd329e" [[projects]] branch = "master" @@ -60,10 +74,22 @@ revision = "a064bd7e3acfda563ea680b913b9ef24b7a73e15" [[projects]] - branch = "master" + name = "github.com/pelletier/go-buffruneio" + packages = ["."] + revision = "c37440a7cf42ac63b919c752ca73a85067e05992" + version = "v0.2.0" + +[[projects]] name = "github.com/pelletier/go-toml" packages = ["."] - revision = "69d355db5304c0f7f809a2edc054553e7142f016" + revision = "5ccdfb18c776b740aecaf085c4d9a2779199c279" + version = "v1.0.0" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" [[projects]] branch = "master" @@ -78,9 +104,10 @@ version = "v1.1.0" [[projects]] + branch = "master" name = "github.com/spf13/cobra" packages = ["."] - revision = "4d647c8944eb42504a714e57e97f244ed6344722" + revision = "cb731b898346822cc0c225c28550a8a29d93c732" [[projects]] branch = "master" @@ -95,28 +122,50 @@ version = "v1.0.0" [[projects]] + branch = "master" name = "github.com/spf13/viper" packages = ["."] - revision = "c1de95864d73a5465492829d7cb2dd422b19ac96" + revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" [[projects]] + branch = "master" + name = "github.com/streadway/amqp" + packages = ["."] + revision = "2cbfe40c9341ad63ba23e53013b3ddc7989d801c" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" + version = "v1.1.4" + +[[projects]] + branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "90796e5a05ce440b41c768bd9af257005e470461" + revision = "9f7170bcd8e9f4d3691c06401119c46a769a1e03" [[projects]] + branch = "master" name = "golang.org/x/text" packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] - revision = "2bf8f2a19ec09c670e931282edfe6567f6be21c9" + revision = "e56139fd9c5bc7244c76116c68e500765bb6db6b" [[projects]] + name = "gopkg.in/h2non/gock.v1" + packages = ["."] + revision = "84d599244901620fb3eb96473eb9e50619f69b47" + version = "v1.0.6" + +[[projects]] + branch = "v2" name = "gopkg.in/yaml.v2" packages = ["."] - revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b" + revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ec0031edfe5ff25a05e871c72a7ae46c52cefad9f1a9fe5bb54c1b293f965c89" + inputs-digest = "a224232f222d77ae5bbdce4248f82a031590b863a6f34f2dd7350ffd330278e6" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 41a9f42..f23d8b2 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -18,3 +18,17 @@ ignored = ["elyby/minecraft-skinsystem"] [[constraint]] name = "github.com/streadway/amqp" + +# Testing dependencies + +[[constraint]] + name = "github.com/stretchr/testify" + version = "^1.1.4" + +[[constraint]] + name = "github.com/golang/mock" + version = "^1.0.0" + +[[constraint]] + name = "gopkg.in/h2non/gock.v1" + version = "^1.0.6" diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go new file mode 100644 index 0000000..a92890d --- /dev/null +++ b/api/accounts/accounts.go @@ -0,0 +1,166 @@ +package accounts + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "strings" +) + +type Config struct { + Addr string + Id string + Secret string + Scopes []string + + Client *http.Client +} + +type Token struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + config *Config +} + +func (config *Config) GetToken() (*Token, error) { + form := url.Values{} + form.Add("client_id", config.Id) + form.Add("client_secret", config.Secret) + form.Add("grant_type", "client_credentials") + form.Add("scope", strings.Join(config.Scopes, ",")) + + response, err := config.getHttpClient().Post(config.getTokenUrl(), "application/x-www-form-urlencoded", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + defer response.Body.Close() + + var result *Token + responseError := handleResponse(response) + if responseError != nil { + return nil, responseError + } + + body, _ := ioutil.ReadAll(response.Body) + unmarshalError := json.Unmarshal(body, &result) + if unmarshalError != nil { + return nil, err + } + + result.config = config + + return result, nil +} + +func (config *Config) getTokenUrl() string { + return concatenateHostAndPath(config.Addr, "/api/oauth2/v1/token") +} + +func (config *Config) getHttpClient() *http.Client { + if config.Client == nil { + config.Client = &http.Client{} + } + + return config.Client +} + +type AccountInfoResponse struct { + Id int `json:"id"` + Uuid string `json:"uuid"` + Username string `json:"username"` + Email string `json:"email"` +} + +func (token *Token) AccountInfo(attribute string, value string) (*AccountInfoResponse, error) { + request := token.newRequest("GET", token.accountInfoUrl(), nil) + + query := request.URL.Query() + query.Add(attribute, value) + request.URL.RawQuery = query.Encode() + + response, err := token.config.Client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + var info *AccountInfoResponse + + responseError := handleResponse(response) + if responseError != nil { + return nil, responseError + } + + body, _ := ioutil.ReadAll(response.Body) + json.Unmarshal(body, &info) + + return info, nil +} + +func (token *Token) accountInfoUrl() string { + return concatenateHostAndPath(token.config.Addr, "/api/internal/accounts/info") +} + +func (token *Token) newRequest(method string, urlStr string, body io.Reader) *http.Request { + request, err := http.NewRequest(method, urlStr, body) + if err != nil { + panic(err) + } + + request.Header.Add("Authorization", "Bearer " + token.AccessToken) + + return request +} + +func concatenateHostAndPath(host string, pathToJoin string) string { + u, _ := url.Parse(host) + u.Path = path.Join(u.Path, pathToJoin) + + return u.String() +} + +type UnauthorizedResponse struct {} + +func (err UnauthorizedResponse) Error() string { + return "Unauthorized response" +} + +type ForbiddenResponse struct {} + +func (err ForbiddenResponse) Error() string { + return "Forbidden response" +} + +type NotFoundResponse struct {} + +func (err NotFoundResponse) Error() string { + return "Not found" +} + +type NotSuccessResponse struct { + StatusCode int +} + +func (err NotSuccessResponse) Error() string { + return fmt.Sprintf("Response code is \"%d\"", err.StatusCode) +} + +func handleResponse(response *http.Response) error { + switch status := response.StatusCode; status { + case 200: + return nil + case 401: + return &UnauthorizedResponse{} + case 403: + return &ForbiddenResponse{} + case 404: + return &NotFoundResponse{} + default: + return &NotSuccessResponse{status} + } +} diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go new file mode 100644 index 0000000..99be4d2 --- /dev/null +++ b/api/accounts/accounts_test.go @@ -0,0 +1,98 @@ +package accounts + +import ( + "net/http" + "strings" + "testing" + + testify "github.com/stretchr/testify/assert" + "gopkg.in/h2non/gock.v1" +) + +func TestConfig_GetToken(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, + }) + + client := &http.Client{} + gock.InterceptClient(client) + + config := &Config{ + Addr: "https://account.ely.by", + Id: "mock-id", + Secret: "mock-secret", + Scopes: []string{"scope1", "scope2"}, + Client: client, + } + + result, err := config.GetToken() + if assert.NoError(err) { + assert.Equal("mocked-token", result.AccessToken) + assert.Equal("Bearer", result.TokenType) + assert.Equal(86400, result.ExpiresIn) + } +} + +func TestToken_AccountInfo(t *testing.T) { + assert := testify.New(t) + + defer gock.Off() + // To test valid behavior + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mock-token"). + Reply(200). + JSON(map[string]interface{}{ + "id": 1, + "uuid": "0f657aa8-bfbe-415d-b700-5750090d3af3", + "username": "dummy", + "email": "dummy@ely.by", + }) + + // To test behavior on invalid or expired token + gock.New("https://account.ely.by"). + Get("/api/internal/accounts/info"). + MatchParam("id", "1"). + MatchHeader("Authorization", "Bearer mock-token"). + Reply(401). + JSON(map[string]interface{}{ + "name": "Unauthorized", + "message": "Incorrect token", + "code": 0, + "status": 401, + }) + + client := &http.Client{} + gock.InterceptClient(client) + + token := &Token{ + AccessToken: "mock-token", + config: &Config{ + Addr: "https://account.ely.by", + Client: client, + }, + } + + result, err := token.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 := token.AccountInfo("id", "1") + assert.Nil(result2) + assert.Error(err2) + assert.IsType(&UnauthorizedResponse{}, err2) +} diff --git a/interfaces/api.go b/interfaces/api.go new file mode 100644 index 0000000..7b061ec --- /dev/null +++ b/interfaces/api.go @@ -0,0 +1,9 @@ +package interfaces + +import ( + "elyby/minecraft-skinsystem/api/accounts" +) + +type AccountsAPI interface { + AccountInfo(attribute string, value string) (*accounts.AccountInfoResponse, error) +}