From 15b5d9746f3d394e459dc1d55987d3ef4a0b7ed7 Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 10 Aug 2021 10:57:45 +0800 Subject: [PATCH] [GH-436] Add integration tests for missing User API endpoints (#810) * server/client: support register and login * server/client: support user related apis * integrationtests: Add SetupTestHelperWithoutToken * Add api integration tests for (User APIs) * rename GetUserMe method to GetMe * check GetMe data is match the registered data after login * Add integration test for workspace upload file api * make ci happy --- server/api/api.go | 14 +- server/api/auth.go | 9 + server/client/client.go | 143 +++++++++++++- server/integrationtests/clienttestlib.go | 45 +++-- server/integrationtests/user_test.go | 239 +++++++++++++++++++++++ server/model/user.go | 13 ++ 6 files changed, 443 insertions(+), 20 deletions(-) create mode 100644 server/integrationtests/user_test.go diff --git a/server/api/api.go b/server/api/api.go index 7b338ba8c..cef525652 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "path/filepath" @@ -23,6 +24,7 @@ const ( HeaderRequestedWith = "X-Requested-With" HeaderRequestedWithXML = "XMLHttpRequest" SingleUser = "single-user" + UploadFormFileKey = "file" ) const ( @@ -1245,6 +1247,15 @@ type FileUploadResponse struct { FileID string `json:"fileId"` } +func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) { + var fileUploadResponse FileUploadResponse + + if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil { + return nil, err + } + return &fileUploadResponse, nil +} + func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { // swagger:operation POST /api/v1/workspaces/{workspaceID}/{rootID}/files uploadFile // @@ -1293,10 +1304,9 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { return } - file, handle, err := r.FormFile("file") + file, handle, err := r.FormFile(UploadFormFileKey) if err != nil { fmt.Fprintf(w, "%v", err) - return } defer file.Close() diff --git a/server/api/auth.go b/server/api/auth.go index 1869be855..5b5f1ff08 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -62,6 +63,14 @@ type LoginResponse struct { Token string `json:"token"` } +func LoginResponseFromJSON(data io.Reader) (*LoginResponse, error) { + var resp LoginResponse + if err := json.NewDecoder(data).Decode(&resp); err != nil { + return nil, err + } + return &resp, nil +} + // RegisterRequest is a user registration request // swagger:model type RegisterRequest struct { diff --git a/server/client/client.go b/server/client/client.go index c27b20a73..44e54b5f7 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -1,13 +1,16 @@ package client import ( + "bytes" "encoding/json" "fmt" "io" "io/ioutil" + "mime/multipart" "net/http" "strings" + "github.com/mattermost/focalboard/server/api" "github.com/mattermost/focalboard/server/model" ) @@ -68,15 +71,18 @@ type Client struct { APIURL string HTTPClient *http.Client HTTPHeader map[string]string + // Token if token is empty indicate client is not login yet + Token string } func NewClient(url, sessionToken string) *Client { url = strings.TrimRight(url, "/") + headers := map[string]string{ "X-Requested-With": "XMLHttpRequest", - "Authorization": "Bearer " + sessionToken, } - return &Client{url, url + APIURLSuffix, &http.Client{}, headers} + + return &Client{url, url + APIURLSuffix, &http.Client{}, headers, sessionToken} } func (c *Client) DoAPIGet(url, etag string) (*http.Response, error) { @@ -103,18 +109,28 @@ func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, e return c.doAPIRequestReader(method, url, strings.NewReader(data), etag) } -func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* etag */ string) (*http.Response, error) { +type requestOption func(r *http.Request) + +func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* etag */ string, opts ...requestOption) (*http.Response, error) { rq, err := http.NewRequest(method, url, data) if err != nil { return nil, err } + for _, opt := range opts { + opt(rq) + } + if c.HTTPHeader != nil && len(c.HTTPHeader) > 0 { for k, v := range c.HTTPHeader { rq.Header.Set(k, v) } } + if c.Token != "" { + rq.Header.Set("Authorization", "Bearer "+c.Token) + } + rp, err := c.HTTPClient.Do(rq) if err != nil || rp == nil { return nil, err @@ -224,3 +240,124 @@ func (c *Client) PostSharing(sharing model.Sharing) (bool, *Response) { return true, BuildResponse(r) } + +func (c *Client) GetRegisterRoute() string { + return "/register" +} + +func (c *Client) Register(request *api.RegisterRequest) (bool, *Response) { + r, err := c.DoAPIPost(c.GetRegisterRoute(), toJSON(&request)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) GetLoginRoute() string { + return "/login" +} + +func (c *Client) Login(request *api.LoginRequest) (*api.LoginResponse, *Response) { + r, err := c.DoAPIPost(c.GetLoginRoute(), toJSON(&request)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + data, err := api.LoginResponseFromJSON(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + + if data.Token != "" { + c.Token = data.Token + } + + return data, BuildResponse(r) +} + +func (c *Client) GetMeRoute() string { + return "/users/me" +} + +func (c *Client) GetMe() (*model.User, *Response) { + r, err := c.DoAPIGet(c.GetMeRoute(), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + me, err := model.UserFromJSON(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + return me, BuildResponse(r) +} + +func (c *Client) GetUserRoute(id string) string { + return fmt.Sprintf("/users/%s", id) +} + +func (c *Client) GetUser(id string) (*model.User, *Response) { + r, err := c.DoAPIGet(c.GetUserRoute(id), "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + user, err := model.UserFromJSON(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + return user, BuildResponse(r) +} + +func (c *Client) GetUserChangePasswordRoute(id string) string { + return fmt.Sprintf("/users/%s/changepassword", id) +} + +func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest) (bool, *Response) { + r, err := c.DoAPIPost(c.GetUserChangePasswordRoute(id), toJSON(&data)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + + return true, BuildResponse(r) +} + +func (c *Client) GetWorkspaceUploadFileRoute(workspaceID, rootID string) string { + return fmt.Sprintf("/workspaces/%s/%s/files", workspaceID, rootID) +} + +func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) (*api.FileUploadResponse, *Response) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile(api.UploadFormFileKey, "file") + if err != nil { + return nil, &Response{Error: err} + } + if _, err = io.Copy(part, data); err != nil { + return nil, &Response{Error: err} + } + writer.Close() + + opt := func(r *http.Request) { + r.Header.Add("Content-Type", writer.FormDataContentType()) + } + + r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetWorkspaceUploadFileRoute(workspaceID, rootID), body, "", opt) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + fileUploadResponse, err := api.FileUploadResponseFromJSON(r.Body) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + + return fileUploadResponse, BuildResponse(r) +} diff --git a/server/integrationtests/clienttestlib.go b/server/integrationtests/clienttestlib.go index bce35e778..3bee6fcee 100644 --- a/server/integrationtests/clienttestlib.go +++ b/server/integrationtests/clienttestlib.go @@ -50,21 +50,21 @@ func getTestConfig() *config.Configuration { }` return &config.Configuration{ - ServerRoot: "http://localhost:8888", - Port: 8888, - DBType: dbType, - DBConfigString: connectionString, - DBTablePrefix: "test_", - WebPath: "./pack", - FilesDriver: "local", - FilesPath: "./files", - LoggingCfgJSON: logging, + ServerRoot: "http://localhost:8888", + Port: 8888, + DBType: dbType, + DBConfigString: connectionString, + DBTablePrefix: "test_", + WebPath: "./pack", + FilesDriver: "local", + FilesPath: "./files", + LoggingCfgJSON: logging, + SessionExpireTime: int64(30 * time.Second), + AuthMode: "native", } } -func SetupTestHelper() *TestHelper { - sessionToken := "TESTTOKEN" - th := &TestHelper{} +func newTestServer(singleUserToken string) *server.Server { logger := mlog.NewLogger() if err := logger.Configure("", getTestConfig().LoggingCfgJSON); err != nil { panic(err) @@ -74,13 +74,26 @@ func SetupTestHelper() *TestHelper { if err != nil { panic(err) } - srv, err := server.New(cfg, sessionToken, db, logger, "") + srv, err := server.New(cfg, singleUserToken, db, logger, "") if err != nil { panic(err) } - th.Server = srv - th.Client = client.NewClient(srv.Config().ServerRoot, sessionToken) + return srv +} + +func SetupTestHelper() *TestHelper { + sessionToken := "TESTTOKEN" + th := &TestHelper{} + th.Server = newTestServer(sessionToken) + th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken) + return th +} + +func SetupTestHelperWithoutToken() *TestHelper { + th := &TestHelper{} + th.Server = newTestServer("") + th.Client = client.NewClient(th.Server.Config().ServerRoot, "") return th } @@ -124,4 +137,6 @@ func (th *TestHelper) TearDown() { if err != nil { panic(err) } + + os.RemoveAll(th.Server.Config().FilesPath) } diff --git a/server/integrationtests/user_test.go b/server/integrationtests/user_test.go new file mode 100644 index 000000000..4e04fdfe1 --- /dev/null +++ b/server/integrationtests/user_test.go @@ -0,0 +1,239 @@ +package integrationtests + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/mattermost/focalboard/server/api" + "github.com/mattermost/focalboard/server/utils" + "github.com/stretchr/testify/require" +) + +const ( + fakeUsername = "fakeUsername" + fakeEmail = "mock@test.com" +) + +func TestUserRegister(t *testing.T) { + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + // register + registerRequest := &api.RegisterRequest{ + Username: fakeUsername, + Email: fakeEmail, + Password: utils.CreateGUID(), + } + success, resp := th.Client.Register(registerRequest) + require.NoError(t, resp.Error) + require.True(t, success) + + // register again will failed + success, resp = th.Client.Register(registerRequest) + require.Error(t, resp.Error) + require.False(t, success) +} + +func TestUserLogin(t *testing.T) { + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + t.Run("with nonexist user", func(t *testing.T) { + loginRequest := &api.LoginRequest{ + Type: "normal", + Username: "nonexistuser", + Email: "", + Password: utils.CreateGUID(), + } + data, resp := th.Client.Login(loginRequest) + require.Error(t, resp.Error) + require.Nil(t, data) + }) + + t.Run("with registered user", func(t *testing.T) { + password := utils.CreateGUID() + // register + registerRequest := &api.RegisterRequest{ + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + success, resp := th.Client.Register(registerRequest) + require.NoError(t, resp.Error) + require.True(t, success) + + // login + loginRequest := &api.LoginRequest{ + Type: "normal", + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + data, resp := th.Client.Login(loginRequest) + require.NoError(t, resp.Error) + require.NotNil(t, data) + require.NotNil(t, data.Token) + }) +} + +func TestGetMe(t *testing.T) { + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + t.Run("not login yet", func(t *testing.T) { + me, resp := th.Client.GetMe() + require.Error(t, resp.Error) + require.Nil(t, me) + }) + + t.Run("logged in", func(t *testing.T) { + // register + password := utils.CreateGUID() + registerRequest := &api.RegisterRequest{ + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + success, resp := th.Client.Register(registerRequest) + require.NoError(t, resp.Error) + require.True(t, success) + // login + loginRequest := &api.LoginRequest{ + Type: "normal", + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + data, resp := th.Client.Login(loginRequest) + require.NoError(t, resp.Error) + require.NotNil(t, data) + require.NotNil(t, data.Token) + + // get user me + me, resp := th.Client.GetMe() + require.NoError(t, resp.Error) + require.NotNil(t, me) + require.Equal(t, registerRequest.Email, me.Email) + require.Equal(t, registerRequest.Username, me.Username) + }) +} + +func TestGetUser(t *testing.T) { + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + // register + password := utils.CreateGUID() + registerRequest := &api.RegisterRequest{ + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + success, resp := th.Client.Register(registerRequest) + require.NoError(t, resp.Error) + require.True(t, success) + // login + loginRequest := &api.LoginRequest{ + Type: "normal", + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + data, resp := th.Client.Login(loginRequest) + require.NoError(t, resp.Error) + require.NotNil(t, data) + require.NotNil(t, data.Token) + + me, resp := th.Client.GetMe() + require.NoError(t, resp.Error) + require.NotNil(t, me) + + t.Run("me's id", func(t *testing.T) { + user, resp := th.Client.GetUser(me.ID) + require.NoError(t, resp.Error) + require.NotNil(t, user) + require.Equal(t, me.ID, user.ID) + require.Equal(t, me.Username, user.Username) + }) + + t.Run("nonexist user", func(t *testing.T) { + user, resp := th.Client.GetUser("nonexistid") + require.Error(t, resp.Error) + require.Nil(t, user) + }) +} + +func TestUserChangePassword(t *testing.T) { + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + // register + password := utils.CreateGUID() + registerRequest := &api.RegisterRequest{ + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + success, resp := th.Client.Register(registerRequest) + require.NoError(t, resp.Error) + require.True(t, success) + // login + loginRequest := &api.LoginRequest{ + Type: "normal", + Username: fakeUsername, + Email: fakeEmail, + Password: password, + } + data, resp := th.Client.Login(loginRequest) + require.NoError(t, resp.Error) + require.NotNil(t, data) + require.NotNil(t, data.Token) + + originalMe, resp := th.Client.GetMe() + require.NoError(t, resp.Error) + require.NotNil(t, originalMe) + + // change password + success, resp = th.Client.UserChangePassword(originalMe.ID, &api.ChangePasswordRequest{ + OldPassword: password, + NewPassword: utils.CreateGUID(), + }) + require.NoError(t, resp.Error) + require.True(t, success) +} + +func randomBytes(t *testing.T, n int) []byte { + bb := make([]byte, n) + _, err := rand.Read(bb) + require.NoError(t, err) + return bb +} + +func TestWorkspaceUploadFile(t *testing.T) { + t.Run("no permission", func(t *testing.T) { // native auth, but not login + th := SetupTestHelperWithoutToken().InitBasic() + defer th.TearDown() + + workspaceID := "0" + rootID := utils.CreateGUID() + data := randomBytes(t, 1024) + result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) + require.Error(t, resp.Error) + require.Nil(t, result) + }) + + t.Run("success", func(t *testing.T) { // single token auth + th := SetupTestHelper().InitBasic() + defer th.TearDown() + + workspaceID := "0" + rootID := utils.CreateGUID() + data := randomBytes(t, 1024) + result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) + require.NoError(t, resp.Error) + require.NotNil(t, result) + require.NotEmpty(t, result.FileID) + // TODO get the uploaded file + }) +} diff --git a/server/model/user.go b/server/model/user.go index ded8bface..352e48c6a 100644 --- a/server/model/user.go +++ b/server/model/user.go @@ -1,5 +1,10 @@ package model +import ( + "encoding/json" + "io" +) + // User is a user // swagger:model type User struct { @@ -53,3 +58,11 @@ type Session struct { CreateAt int64 `json:"create_at,omitempty"` UpdateAt int64 `json:"update_at,omitempty"` } + +func UserFromJSON(data io.Reader) (*User, error) { + var user User + if err := json.NewDecoder(data).Decode(&user); err != nil { + return nil, err + } + return &user, nil +}