[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
This commit is contained in:
dave 2021-08-10 10:57:45 +08:00 committed by GitHub
parent d698932c32
commit 15b5d9746f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 443 additions and 20 deletions

View file

@ -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()

View file

@ -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 {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
})
}

View file

@ -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
}