GraphQL (part 2): Model changes and resolvers (#19486)
This PR adds the necessary resolvers to support the graphQL front-end. Some additional methods need to be added to the model structs to support this. Comments have been made to clarify their purpose. There is a slight duplication of code in the api layer. But eventually that's going to go away when we move away from the REST API entirely. The schema is in api4/schema.graphqls and gets compiled into the binary as an asset. The GraphiQL editor url is at GET /api/v5/graphql. Everything is behind the feature flag MM_FEATUREFLAGS_GRAPHQL. ```release-note NONE ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
88968f9e17
Коммит
3f52bd197a
15
api4/api.go
15
api4/api.go
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
_ "github.com/mattermost/go-i18n/i18n"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
@@ -15,8 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type Routes struct {
|
||||
Root *mux.Router // ''
|
||||
APIRoot *mux.Router // 'api/v4'
|
||||
Root *mux.Router // ''
|
||||
APIRoot *mux.Router // 'api/v4'
|
||||
APIRoot5 *mux.Router // 'api/v5'
|
||||
|
||||
Users *mux.Router // 'api/v4/users'
|
||||
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
|
||||
@@ -136,10 +138,11 @@ type Routes struct {
|
||||
|
||||
type API struct {
|
||||
srv *app.Server
|
||||
schema *graphql.Schema
|
||||
BaseRoutes *Routes
|
||||
}
|
||||
|
||||
func Init(srv *app.Server) *API {
|
||||
func Init(srv *app.Server) (*API, error) {
|
||||
api := &API{
|
||||
srv: srv,
|
||||
BaseRoutes: &Routes{},
|
||||
@@ -147,6 +150,7 @@ func Init(srv *app.Server) *API {
|
||||
|
||||
api.BaseRoutes.Root = srv.Router
|
||||
api.BaseRoutes.APIRoot = srv.Router.PathPrefix(model.APIURLSuffix).Subrouter()
|
||||
api.BaseRoutes.APIRoot5 = srv.Router.PathPrefix(model.APIURLSuffixV5).Subrouter()
|
||||
|
||||
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter()
|
||||
api.BaseRoutes.User = api.BaseRoutes.APIRoot.PathPrefix("/users/{user_id:[A-Za-z0-9]+}").Subrouter()
|
||||
@@ -292,12 +296,15 @@ func Init(srv *app.Server) *API {
|
||||
api.InitSharedChannels()
|
||||
api.InitPermissions()
|
||||
api.InitExport()
|
||||
if err := api.InitGraphQL(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
|
||||
|
||||
InitLocal(srv)
|
||||
|
||||
return api
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func InitLocal(srv *app.Server) *API {
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
package api4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
s3 "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -45,6 +49,7 @@ type TestHelper struct {
|
||||
|
||||
Context *request.Context
|
||||
Client *model.Client4
|
||||
GraphQLClient *graphQLClient
|
||||
BasicUser *model.User
|
||||
BasicUser2 *model.User
|
||||
TeamAdminUser *model.User
|
||||
@@ -191,6 +196,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
|
||||
}
|
||||
|
||||
th.Client = th.CreateClient()
|
||||
th.GraphQLClient = newGraphQLClient(fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port))
|
||||
th.SystemAdminClient = th.CreateClient()
|
||||
th.SystemManagerClient = th.CreateClient()
|
||||
|
||||
@@ -374,6 +380,13 @@ func (th *TestHelper) TearDown() {
|
||||
th.ShutdownApp()
|
||||
}
|
||||
|
||||
func closeBody(r *http.Response) {
|
||||
if r.Body != nil {
|
||||
_, _ = io.Copy(ioutil.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
var initBasicOnce sync.Once
|
||||
var userCache struct {
|
||||
SystemAdminUser *model.User
|
||||
@@ -768,10 +781,16 @@ func (th *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
|
||||
|
||||
func (th *TestHelper) LoginBasic() {
|
||||
th.LoginBasicWithClient(th.Client)
|
||||
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
|
||||
th.LoginBasicWithGraphQL()
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) LoginBasic2() {
|
||||
th.LoginBasic2WithClient(th.Client)
|
||||
if os.Getenv("MM_FEATUREFLAGS_GRAPHQL") == "true" {
|
||||
th.LoginBasicWithGraphQL()
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) LoginTeamAdmin() {
|
||||
@@ -793,6 +812,13 @@ func (th *TestHelper) LoginBasicWithClient(client *model.Client4) {
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) LoginBasicWithGraphQL() {
|
||||
_, _, err := th.GraphQLClient.login(th.BasicUser.Email, th.BasicUser.Password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) LoginBasic2WithClient(client *model.Client4) {
|
||||
_, _, err := client.Login(th.BasicUser2.Email, th.BasicUser2.Password)
|
||||
if err != nil {
|
||||
@@ -1251,3 +1277,22 @@ func (th *TestHelper) SetupScheme(scope string) *model.Scheme {
|
||||
}
|
||||
return scheme
|
||||
}
|
||||
|
||||
func (th *TestHelper) MakeGraphQLRequest(input *graphQLInput) (*graphql.Response, error) {
|
||||
url := fmt.Sprintf("http://localhost:%v", th.App.Srv().ListenAddr.Port) + model.APIURLSuffixV5 + "/graphql"
|
||||
|
||||
buf, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := th.GraphQLClient.doAPIRequest("POST", url, bytes.NewReader(buf), map[string]string{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer closeBody(resp)
|
||||
|
||||
var gqlResp *graphql.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&gqlResp)
|
||||
return gqlResp, err
|
||||
}
|
||||
|
||||
165
api4/graphql.go
Обычный файл
165
api4/graphql.go
Обычный файл
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
graphql "github.com/graph-gophers/graphql-go"
|
||||
gqlerrors "github.com/graph-gophers/graphql-go/errors"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
type graphQLInput struct {
|
||||
Query string `json:"query"`
|
||||
OperationName string `json:"operationName"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
//go:embed schema.graphqls
|
||||
var schemaRaw string
|
||||
|
||||
func (api *API) InitGraphQL() error {
|
||||
// Guard with a feature flag.
|
||||
if !api.srv.Config().FeatureFlags.GraphQL {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
opts := []graphql.SchemaOpt{
|
||||
graphql.UseFieldResolvers(),
|
||||
graphql.Logger(mlog.NewGraphQLLogger(api.srv.Log)),
|
||||
graphql.MaxParallelism(5),
|
||||
}
|
||||
|
||||
if isProd() {
|
||||
opts = append(opts,
|
||||
// MaxDepth cannot be moved as a general param
|
||||
// because otherwise introspection also doesn't work
|
||||
// with just a depth of 4.
|
||||
graphql.MaxDepth(4),
|
||||
graphql.DisableIntrospection(),
|
||||
)
|
||||
}
|
||||
|
||||
api.schema, err = graphql.ParseSchema(schemaRaw, &resolver{}, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APIHandlerTrustRequester(graphiQL)).Methods("GET")
|
||||
api.BaseRoutes.APIRoot5.Handle("/graphql", api.APISessionRequired(api.graphQL)).Methods("POST")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unique type to hold our context.
|
||||
type ctxKey struct{}
|
||||
|
||||
func (api *API) graphQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var response *graphql.Response
|
||||
defer func() {
|
||||
if response != nil {
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
mlog.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Limit bodies to 100KiB.
|
||||
// We need to enforce a lower limit than the file upload size,
|
||||
// to prevent the library doing unnecessary parsing.
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 102400)
|
||||
|
||||
var params graphQLInput
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
err2 := gqlerrors.Errorf("invalid request body: %v", err)
|
||||
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
|
||||
return
|
||||
}
|
||||
|
||||
if isProd() && params.OperationName == "" {
|
||||
err2 := gqlerrors.Errorf("operation name not passed")
|
||||
response = &graphql.Response{Errors: []*gqlerrors.QueryError{err2}}
|
||||
return
|
||||
}
|
||||
|
||||
// Populate the context with required info.
|
||||
reqCtx := r.Context()
|
||||
reqCtx = context.WithValue(reqCtx, ctxKey{}, c)
|
||||
|
||||
response = api.schema.Exec(reqCtx,
|
||||
params.Query,
|
||||
params.OperationName,
|
||||
params.Variables)
|
||||
|
||||
if len(response.Errors) > 0 {
|
||||
logFunc := mlog.Error
|
||||
for _, gqlErr := range response.Errors {
|
||||
if gqlErr.Err != nil {
|
||||
if appErr, ok := gqlErr.Err.(*model.AppError); ok && appErr.StatusCode < http.StatusInternalServerError {
|
||||
logFunc = mlog.Debug
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
logFunc("Error executing request", mlog.String("operation", params.OperationName),
|
||||
mlog.Array("errors", response.Errors))
|
||||
}
|
||||
}
|
||||
|
||||
func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(graphiqlPage)
|
||||
}
|
||||
|
||||
var graphiqlPage = []byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GraphiQL editor | Mattermost</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
|
||||
<div id="graphiql" style="height: 100vh;">Loading...</div>
|
||||
<script>
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch("/api/v5/graphql", {
|
||||
method: "post",
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
|
||||
document.getElementById("graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
// isProd is a helper function to apply prod-specific graphQL validations.
|
||||
func isProd() bool {
|
||||
return model.BuildNumber != "dev"
|
||||
}
|
||||
87
api4/graphql_client.go
Обычный файл
87
api4/graphql_client.go
Обычный файл
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
// graphQLClient is an internal test client to run the tests.
|
||||
// When the API matures, we will expose it to the model package.
|
||||
type graphQLClient struct {
|
||||
URL string // The location of the server, for example "http://localhost:8065"
|
||||
APIURL string // The api location of the server, for example "http://localhost:8065/api/v4"
|
||||
httpClient *http.Client // The http client
|
||||
authToken string
|
||||
authType string
|
||||
httpHeader map[string]string // Headers to be copied over for each request
|
||||
}
|
||||
|
||||
func newGraphQLClient(url string) *graphQLClient {
|
||||
url = strings.TrimRight(url, "/")
|
||||
return &graphQLClient{url, url + model.APIURLSuffix, &http.Client{}, "", "", map[string]string{}}
|
||||
}
|
||||
|
||||
func (c *graphQLClient) login(loginId string, password string) (*model.User, *model.Response, error) {
|
||||
m := make(map[string]string)
|
||||
m["login_id"] = loginId
|
||||
m["password"] = password
|
||||
|
||||
r, err := c.doAPIRequest(http.MethodPost, c.APIURL+"/users/login", strings.NewReader(model.MapToJSON(m)), map[string]string{model.HeaderEtagClient: ""})
|
||||
|
||||
if err != nil {
|
||||
return nil, model.BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
c.authToken = r.Header.Get(model.HeaderToken)
|
||||
c.authType = model.HeaderBearer
|
||||
|
||||
var user model.User
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&user); jsonErr != nil {
|
||||
return nil, nil, model.NewAppError("login", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return &user, model.BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *graphQLClient) doAPIRequest(method, url string, data io.Reader, headers map[string]string) (*http.Response, error) {
|
||||
rq, err := c.prepareRequest(method, url, data, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rp, err := c.httpClient.Do(rq)
|
||||
if err != nil {
|
||||
return rp, err
|
||||
}
|
||||
|
||||
return rp, nil
|
||||
}
|
||||
|
||||
func (c *graphQLClient) prepareRequest(method, url string, data io.Reader, headers map[string]string) (*http.Request, error) {
|
||||
rq, err := http.NewRequest(method, url, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
rq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
if c.authToken != "" {
|
||||
rq.Header.Set(model.HeaderAuth, c.authType+" "+c.authToken)
|
||||
}
|
||||
|
||||
if c.httpHeader != nil && len(c.httpHeader) > 0 {
|
||||
for k, v := range c.httpHeader {
|
||||
rq.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return rq, nil
|
||||
}
|
||||
34
api4/graphql_test.go
Обычный файл
34
api4/graphql_test.go
Обычный файл
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLPayload(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
largeString := strings.Repeat("hello", 204800)
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "config",
|
||||
Query: largeString,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1)
|
||||
// The actual error isn't exposed. We compare the string
|
||||
// to not confuse with other errors.
|
||||
require.Contains(t, resp.Errors[0].Message, "request body too large")
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
@@ -68,7 +69,8 @@ func TestAPIHandlersWithGzip(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
api := Init(th.Server)
|
||||
api, err := Init(th.Server)
|
||||
require.NoError(t, err)
|
||||
session, _ := th.App.GetSession(th.Client.AuthToken)
|
||||
|
||||
t.Run("with WebserverMode == \"gzip\"", func(t *testing.T) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package api4
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -608,13 +607,6 @@ func TestGetAuthorizedOAuthAppsForUser(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func closeBody(r *http.Response) {
|
||||
if r != nil && r.Body != nil {
|
||||
ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilAuthorizeOAuthApp(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -602,11 +602,12 @@ func TestCreatePostCheckOnlineStatus(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
api := Init(th.Server)
|
||||
api, err := Init(th.Server)
|
||||
require.NoError(t, err)
|
||||
session, _ := th.App.GetSession(th.Client.AuthToken)
|
||||
|
||||
cli := th.CreateClient()
|
||||
_, _, err := cli.Login(th.BasicUser2.Username, th.BasicUser2.Password)
|
||||
_, _, err = cli.Login(th.BasicUser2.Username, th.BasicUser2.Password)
|
||||
require.NoError(t, err)
|
||||
|
||||
wsClient, err := th.CreateWebSocketClientWithClient(cli)
|
||||
|
||||
286
api4/resolver.go
Обычный файл
286
api4/resolver.go
Обычный файл
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
// cursorPrefix is used to categorize objects
|
||||
// sent in a cursor. The type is prepended
|
||||
// to the string with a - to find which
|
||||
// object the id belongs to.
|
||||
//
|
||||
// And after the type is extracted, object
|
||||
// specific logic can be applied to extract the id.
|
||||
type cursorPrefix string
|
||||
|
||||
const (
|
||||
channelMemberCursorPrefix cursorPrefix = "channelMember"
|
||||
channelCursorPrefix cursorPrefix = "channel"
|
||||
)
|
||||
|
||||
type resolver struct {
|
||||
}
|
||||
|
||||
// match with api4.getChannelsForTeamForUser
|
||||
func (r *resolver) Channels(ctx context.Context, args struct {
|
||||
TeamID string
|
||||
UserID string
|
||||
IncludeDeleted bool
|
||||
LastDeleteAt float64
|
||||
LastUpdateAt float64
|
||||
First int32
|
||||
After string
|
||||
}) ([]*channel, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if args.UserID == model.Me {
|
||||
args.UserID = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
if args.TeamID != "" && !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
limit := int(args.First)
|
||||
// ensure args.First limit
|
||||
if limit == 0 {
|
||||
limit = web.PerPageDefault
|
||||
} else if limit > web.PerPageMaximum {
|
||||
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
|
||||
}
|
||||
|
||||
// ensure args.After format
|
||||
var afterChannel string
|
||||
var ok bool
|
||||
if args.After != "" {
|
||||
afterChannel, ok = parseChannelCursor(args.After)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: convert this to a streaming API.
|
||||
channels, appErr := c.App.GetChannelsForTeamForUserWithCursor(args.TeamID, args.UserID, &model.ChannelSearchOpts{
|
||||
IncludeDeleted: args.IncludeDeleted,
|
||||
LastDeleteAt: int(args.LastDeleteAt),
|
||||
LastUpdateAt: int(args.LastUpdateAt),
|
||||
PerPage: model.NewInt(limit),
|
||||
}, afterChannel)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
appErr = c.App.FillInChannelsProps(channels)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return postProcessChannels(c, channels)
|
||||
}
|
||||
|
||||
// match with api4.getUser
|
||||
func (r *resolver) User(ctx context.Context, args struct{ ID string }) (*user, error) {
|
||||
return getGraphQLUser(ctx, args.ID)
|
||||
}
|
||||
|
||||
// match with api4.getClientConfig
|
||||
func (r *resolver) Config(ctx context.Context) (model.StringMap, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId == "" {
|
||||
return c.App.LimitedClientConfigWithComputed(), nil
|
||||
}
|
||||
return c.App.ClientConfigWithComputed(), nil
|
||||
}
|
||||
|
||||
// match with api4.getClientLicense
|
||||
func (r *resolver) License(ctx context.Context) (model.StringMap, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
|
||||
return c.App.Srv().ClientLicense(), nil
|
||||
}
|
||||
return c.App.Srv().GetSanitizedClientLicense(), nil
|
||||
}
|
||||
|
||||
// match with api4.getTeamMembersForUser for teamID=""
|
||||
// and api4.getTeamMember for teamID != ""
|
||||
func (r *resolver) TeamMembers(ctx context.Context, args struct {
|
||||
UserID string
|
||||
TeamID string
|
||||
}) ([]*teamMember, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if args.UserID == model.Me {
|
||||
args.UserID = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadOtherUsersTeams) {
|
||||
c.SetPermissionError(model.PermissionReadOtherUsersTeams)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, args.UserID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
if args.TeamID != "" {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
tm, appErr2 := c.App.GetTeamMember(args.TeamID, args.UserID)
|
||||
if appErr2 != nil {
|
||||
return nil, appErr2
|
||||
}
|
||||
|
||||
return []*teamMember{{*tm}}, nil
|
||||
}
|
||||
|
||||
members, appErr := c.App.GetTeamMembersForUser(args.UserID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// Convert to the wrapper format.
|
||||
res := make([]*teamMember, 0, len(members))
|
||||
for _, tm := range members {
|
||||
res = append(res, &teamMember{*tm})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (*resolver) ChannelsLeft(ctx context.Context, args struct {
|
||||
UserID string
|
||||
Since float64
|
||||
}) ([]string, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if args.UserID == model.Me {
|
||||
args.UserID = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
return c.App.Srv().Store.ChannelMemberHistory().GetChannelsLeftSince(args.UserID, int64(args.Since))
|
||||
}
|
||||
|
||||
// match with api4.getChannelMember
|
||||
func (*resolver) ChannelMembers(ctx context.Context, args struct {
|
||||
UserID string
|
||||
ChannelID string
|
||||
First int32
|
||||
After string
|
||||
LastUpdateAt float64
|
||||
}) ([]*channelMember, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if args.UserID == model.Me {
|
||||
args.UserID = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
// If it's a single channel
|
||||
if args.ChannelID != "" {
|
||||
if !c.App.SessionHasPermissionToChannel(*c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
member, appErr := c.App.GetChannelMember(app.WithMaster(context.Background()), args.ChannelID, args.UserID)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return []*channelMember{{*member}}, nil
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), args.UserID) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
limit := int(args.First)
|
||||
// ensure args.First limit
|
||||
if limit == 0 {
|
||||
limit = web.PerPageDefault
|
||||
} else if limit > web.PerPageMaximum {
|
||||
return nil, fmt.Errorf("first parameter %d higher than allowed maximum of %d", limit, web.PerPageMaximum)
|
||||
}
|
||||
|
||||
// ensure args.After format
|
||||
var afterChannel, afterUser string
|
||||
var ok bool
|
||||
if args.After != "" {
|
||||
afterChannel, afterUser, ok = parseChannelMemberCursor(args.After)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("after cursor not in the correct format: %s", args.After)
|
||||
}
|
||||
}
|
||||
|
||||
members, err := c.App.Srv().Store.Channel().GetMembersForUserWithCursor(args.UserID, afterChannel, afterUser, limit, int(args.LastUpdateAt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]*channelMember, 0, len(members))
|
||||
for _, cm := range members {
|
||||
res = append(res, &channelMember{cm})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// getCtx extracts web.Context out of the usual request context.
|
||||
// Kind of an anti-pattern, but there are lots of methods attached to *web.Context
|
||||
// so we use it for now.
|
||||
func getCtx(ctx context.Context) (*web.Context, error) {
|
||||
c, ok := ctx.Value(ctxKey{}).(*web.Context)
|
||||
if !ok {
|
||||
return nil, errors.New("no web.Context found in context")
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
140
api4/resolver_channel.go
Обычный файл
140
api4/resolver_channel.go
Обычный файл
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
// channel is an internal graphQL wrapper struct to add resolver methods.
|
||||
type channel struct {
|
||||
model.Channel
|
||||
PrettyDisplayName string
|
||||
}
|
||||
|
||||
// match with api4.getTeam
|
||||
func (ch *channel) Team(ctx context.Context) (*model.Team, error) {
|
||||
if ch.TeamId == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return getGraphQLTeam(ctx, ch.TeamId)
|
||||
}
|
||||
|
||||
func (ch *channel) Cursor() *string {
|
||||
cursor := string(channelCursorPrefix) + "-" + ch.Id
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
|
||||
return model.NewString(encoded)
|
||||
}
|
||||
|
||||
func parseChannelCursor(cursor string) (channelID string, ok bool) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
parts := strings.Split(string(decoded), "-")
|
||||
if len(parts) != 2 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if cursorPrefix(parts[0]) != channelCursorPrefix {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return parts[1], true
|
||||
}
|
||||
|
||||
func postProcessChannels(c *web.Context, channels []*model.Channel) ([]*channel, error) {
|
||||
// This approach becomes effectively similar to a dataloader if the displayName computation
|
||||
// were to be done at the field level per channel.
|
||||
|
||||
// Get DM/GM channelIDs
|
||||
var channelIDs []string
|
||||
for _, ch := range channels {
|
||||
if ch.IsGroupOrDirect() {
|
||||
channelIDs = append(channelIDs, ch.Id)
|
||||
}
|
||||
}
|
||||
|
||||
var pref *model.Preference
|
||||
var userInfo map[string][]*model.User
|
||||
var err error
|
||||
var appErr *model.AppError
|
||||
|
||||
// Avoiding unnecessary queries unless necessary.
|
||||
if len(channelIDs) > 0 {
|
||||
userInfo, err = c.App.Srv().Store.Channel().GetMembersInfoByChannelIds(channelIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pref, appErr = c.App.GetPreferenceByCategoryAndNameForUser(c.AppContext.Session().UserId, "display_settings", "name_format")
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the wrapper format.
|
||||
res := make([]*channel, 0, len(channels))
|
||||
for _, ch := range channels {
|
||||
prettyName := ch.DisplayName
|
||||
|
||||
if ch.IsGroupOrDirect() {
|
||||
// get users slice for channel id
|
||||
users := userInfo[ch.Id]
|
||||
if users == nil {
|
||||
return nil, fmt.Errorf("user info not found for channel id: %s", ch.Id)
|
||||
}
|
||||
prettyName = getPrettyDNForUsers(pref.Value, users)
|
||||
}
|
||||
|
||||
res = append(res, &channel{Channel: *ch, PrettyDisplayName: prettyName})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getPrettyDNForUsers(displaySetting string, users []*model.User) string {
|
||||
displayNames := make([]string, 0, len(users))
|
||||
// TODO: optimize this logic.
|
||||
// Name computation happens repeatedly for the same user from
|
||||
// multiple channels.
|
||||
for _, u := range users {
|
||||
displayNames = append(displayNames, getPrettyDNForUser(displaySetting, u))
|
||||
}
|
||||
|
||||
sort.Strings(displayNames)
|
||||
return strings.Join(displayNames, ", ")
|
||||
}
|
||||
|
||||
func getPrettyDNForUser(displaySetting string, user *model.User) string {
|
||||
var displayName string
|
||||
switch displaySetting {
|
||||
case "nickname_full_name":
|
||||
displayName = user.Nickname
|
||||
if displayName == "" {
|
||||
displayName = user.GetFullName()
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
case "full_name":
|
||||
displayName = user.GetFullName()
|
||||
if displayName == "" {
|
||||
displayName = user.Username
|
||||
}
|
||||
default: // the "username" case also falls under this one.
|
||||
displayName = user.Username
|
||||
}
|
||||
|
||||
return displayName
|
||||
}
|
||||
113
api4/resolver_channel_member.go
Обычный файл
113
api4/resolver_channel_member.go
Обычный файл
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
// channelMember is an internal graphQL wrapper struct to add resolver methods.
|
||||
type channelMember struct {
|
||||
model.ChannelMember
|
||||
}
|
||||
|
||||
// match with api4.getUser
|
||||
func (cm *channelMember) User(ctx context.Context) (*user, error) {
|
||||
return getGraphQLUser(ctx, cm.UserId)
|
||||
}
|
||||
|
||||
// match with api4.Channel
|
||||
func (cm *channelMember) Channel(ctx context.Context) (*channel, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(cm.ChannelId)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) &&
|
||||
!c.App.SessionHasPermissionToChannel(*c.AppContext.Session(), cm.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadPublicChannel)
|
||||
return nil, c.Err
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannel(*c.AppContext.Session(), cm.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return nil, c.Err
|
||||
}
|
||||
}
|
||||
|
||||
appErr = c.App.FillInChannelProps(channel)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
res, err := postProcessChannels(c, []*model.Channel{channel})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// A bit of defence-in-depth; can probably be removed after a deeper look.
|
||||
if len(res) != 1 {
|
||||
return nil, fmt.Errorf("postProcessChannels: incorrect number of channels returned %d", len(res))
|
||||
}
|
||||
return res[0], nil
|
||||
}
|
||||
|
||||
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, strings.Fields(cm.Roles))
|
||||
}
|
||||
|
||||
func (cm *channelMember) Cursor() *string {
|
||||
cursor := string(channelMemberCursorPrefix) + "-" + cm.ChannelId + "-" + cm.UserId
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
|
||||
return model.NewString(encoded)
|
||||
}
|
||||
|
||||
func getGraphQLRoles(c *web.Context, roleNames []string) ([]*model.Role, error) {
|
||||
cleanedRoleNames, valid := model.CleanRoleNames(roleNames)
|
||||
if !valid {
|
||||
c.SetInvalidParam("rolename")
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
roles, appErr := c.App.GetRolesByNames(cleanedRoleNames)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func parseChannelMemberCursor(cursor string) (channelID, userID string, ok bool) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cursor)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
parts := strings.Split(string(decoded), "-")
|
||||
if len(parts) != 3 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
if cursorPrefix(parts[0]) != channelMemberCursorPrefix {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return parts[1], parts[2], true
|
||||
}
|
||||
355
api4/resolver_channel_member_test.go
Обычный файл
355
api4/resolver_channel_member_test.go
Обычный файл
@@ -0,0 +1,355 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLChannelMembers(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Adding another team with more channels (public and private)
|
||||
myTeam := th.CreateTeam()
|
||||
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
|
||||
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
|
||||
th.LinkUserToTeam(th.BasicUser, myTeam)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch1, false)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch2, false)
|
||||
|
||||
// Creating some msgcount
|
||||
th.CreateMessagePostWithClient(th.Client, th.BasicChannel, "basic post")
|
||||
th.CreateMessagePostWithClient(th.Client, ch1, "ch1 post")
|
||||
|
||||
var q struct {
|
||||
ChannelMembers []struct {
|
||||
Channel struct {
|
||||
ID string `json:"id"`
|
||||
CreateAt float64 `json:"createAt"`
|
||||
UpdateAt float64 `json:"updateAt"`
|
||||
Type model.ChannelType `json:"type"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Name string `json:"name"`
|
||||
Header string `json:"header"`
|
||||
Purpose string `json:"purpose"`
|
||||
Team struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"team"`
|
||||
} `json:"channel"`
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
NickName string `json:"nickname"`
|
||||
} `json:"user"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
SchemeManaged bool `json:"schemeManaged"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
} `json:"roles"`
|
||||
LastViewedAt float64 `json:"lastViewedAt"`
|
||||
LastUpdateAt float64 `json:"lastUpdateAt"`
|
||||
MsgCount float64 `json:"msgCount"`
|
||||
MentionCount float64 `json:"mentionCount"`
|
||||
MentionCountRoot float64 `json:"mentionCountRoot"`
|
||||
NotifyProps model.StringMap `json:"notifyProps"`
|
||||
SchemeGuest bool `json:"schemeGuest"`
|
||||
SchemeUser bool `json:"schemeUser"`
|
||||
SchemeAdmin bool `json:"schemeAdmin"`
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"channelMembers"`
|
||||
}
|
||||
|
||||
t.Run("all", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: `
|
||||
query channelMembers {
|
||||
channelMembers(userId: "me") {
|
||||
channel {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
type
|
||||
displayName
|
||||
name
|
||||
header
|
||||
team {
|
||||
id
|
||||
}
|
||||
}
|
||||
user {
|
||||
id
|
||||
username
|
||||
email
|
||||
}
|
||||
msgCount
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
schemeGuest
|
||||
schemeUser
|
||||
schemeAdmin
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelMembers, 9)
|
||||
|
||||
numPrivate := 0
|
||||
numPublic := 0
|
||||
numOffTopic := 0
|
||||
numTownSquare := 0
|
||||
for _, ch := range q.ChannelMembers {
|
||||
assert.NotEmpty(t, ch.Channel.ID)
|
||||
assert.NotEmpty(t, ch.Channel.Name)
|
||||
assert.NotEmpty(t, ch.Channel.CreateAt)
|
||||
assert.NotEmpty(t, ch.Channel.UpdateAt)
|
||||
if ch.Channel.Type == model.ChannelTypeOpen {
|
||||
numPublic++
|
||||
} else if ch.Channel.Type == model.ChannelTypePrivate {
|
||||
numPrivate++
|
||||
}
|
||||
|
||||
if ch.Channel.DisplayName == "Off-Topic" {
|
||||
numOffTopic++
|
||||
} else if ch.Channel.DisplayName == "Town Square" {
|
||||
numTownSquare++
|
||||
}
|
||||
|
||||
assert.Equal(t, th.BasicUser.Id, ch.User.ID)
|
||||
assert.Equal(t, th.BasicUser.Username, ch.User.Username)
|
||||
assert.Equal(t, th.BasicUser.Email, ch.User.Email)
|
||||
|
||||
assert.False(t, ch.SchemeGuest)
|
||||
|
||||
if ch.Channel.Team.ID == myTeam.Id {
|
||||
assert.True(t, ch.SchemeAdmin)
|
||||
} else {
|
||||
assert.False(t, ch.SchemeAdmin)
|
||||
}
|
||||
assert.True(t, ch.SchemeUser)
|
||||
|
||||
assert.NotEmpty(t, ch.Cursor)
|
||||
|
||||
switch ch.Channel.ID {
|
||||
case th.BasicChannel.Id:
|
||||
assert.Equal(t, float64(2), ch.MsgCount)
|
||||
case ch1.Id:
|
||||
assert.Equal(t, float64(1), ch.MsgCount)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, numPrivate)
|
||||
assert.Equal(t, 7, numPublic)
|
||||
assert.Equal(t, 2, numOffTopic)
|
||||
assert.Equal(t, 2, numTownSquare)
|
||||
})
|
||||
|
||||
t.Run("user_perms", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: `
|
||||
query channelMembers($user: String!) {
|
||||
channelMembers(userId: $user) {
|
||||
channel {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
}
|
||||
msgCount
|
||||
mentionCount
|
||||
mentionCountRoot
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"user": model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1)
|
||||
})
|
||||
|
||||
t.Run("pagination", func(t *testing.T) {
|
||||
query := `query channelMembers($first: Int, $after: String = "") {
|
||||
channelMembers(userId: "me", first: $first, after: $after) {
|
||||
channel {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
type
|
||||
displayName
|
||||
name
|
||||
header
|
||||
}
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelMembers, 4)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"after": q.ChannelMembers[3].Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelMembers, 4)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"after": q.ChannelMembers[3].Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelMembers, 1)
|
||||
})
|
||||
|
||||
t.Run("channel_filter", func(t *testing.T) {
|
||||
query := `query channelMembers($channelId: String, $first: Int, $after: String = "") {
|
||||
channelMembers(userId: "me", channelId: $channelId, first: $first, after: $after) {
|
||||
channel {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"channelId": ch1.Id,
|
||||
"first": 4,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelMembers, 1)
|
||||
assert.Equal(t, q.ChannelMembers[0].Channel.ID, ch1.Id)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"channelId": model.NewId(),
|
||||
"first": 3,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1)
|
||||
})
|
||||
|
||||
t.Run("UpdateAt", func(t *testing.T) {
|
||||
query := `query channelMembers($first: Int, $after: String = "", $lastUpdateAt: Float) {
|
||||
channelMembers(userId: "me", first: $first, after: $after, lastUpdateAt: $lastUpdateAt) {
|
||||
channel {
|
||||
id
|
||||
}
|
||||
lastUpdateAt
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
now := model.GetMillis()
|
||||
input := graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"lastUpdateAt": float64(now),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
require.Len(t, q.ChannelMembers, 0)
|
||||
|
||||
// Create post to update the lastUpdateAt for the channel member.
|
||||
th.CreateMessagePostWithClient(th.Client, th.BasicChannel, "another post")
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channelMembers",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"lastUpdateAt": float64(now),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
require.Len(t, q.ChannelMembers, 1)
|
||||
assert.Equal(t, th.BasicChannel.Id, q.ChannelMembers[0].Channel.ID)
|
||||
assert.GreaterOrEqual(t, q.ChannelMembers[0].LastUpdateAt, float64(now))
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelMemberCursor(t *testing.T) {
|
||||
ch := channelMember{
|
||||
ChannelMember: model.ChannelMember{ChannelId: "testid", UserId: "userid"},
|
||||
}
|
||||
cur := ch.Cursor()
|
||||
|
||||
chId, userId, ok := parseChannelMemberCursor(*cur)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ch.ChannelId, chId)
|
||||
assert.Equal(t, ch.UserId, userId)
|
||||
}
|
||||
462
api4/resolver_channel_test.go
Обычный файл
462
api4/resolver_channel_test.go
Обычный файл
@@ -0,0 +1,462 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLChannels(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// Adding another team with more channels (public and private)
|
||||
myTeam := th.CreateTeam()
|
||||
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
|
||||
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
|
||||
th.LinkUserToTeam(th.BasicUser, myTeam)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch1, false)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch2, false)
|
||||
|
||||
var q struct {
|
||||
Channels []struct {
|
||||
ID string `json:"id"`
|
||||
CreateAt float64 `json:"createAt"`
|
||||
UpdateAt float64 `json:"updateAt"`
|
||||
Type model.ChannelType `json:"type"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Name string `json:"name"`
|
||||
Header string `json:"header"`
|
||||
Purpose string `json:"purpose"`
|
||||
SchemeId string `json:"schemeId"`
|
||||
Cursor string `json:"cursor"`
|
||||
Team struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
} `json:"team"`
|
||||
} `json:"channels"`
|
||||
}
|
||||
|
||||
t.Run("all", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: `
|
||||
query channels {
|
||||
channels(userId: "me") {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
type
|
||||
displayName
|
||||
name
|
||||
header
|
||||
purpose
|
||||
schemeId
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 9)
|
||||
|
||||
numPrivate := 0
|
||||
numPublic := 0
|
||||
numOffTopic := 0
|
||||
numTownSquare := 0
|
||||
for _, ch := range q.Channels {
|
||||
assert.NotEmpty(t, ch.ID)
|
||||
assert.NotEmpty(t, ch.Name)
|
||||
assert.NotEmpty(t, ch.Cursor)
|
||||
assert.NotEmpty(t, ch.CreateAt)
|
||||
assert.NotEmpty(t, ch.UpdateAt)
|
||||
if ch.Type == model.ChannelTypeOpen {
|
||||
numPublic++
|
||||
} else if ch.Type == model.ChannelTypePrivate {
|
||||
numPrivate++
|
||||
}
|
||||
|
||||
if ch.DisplayName == "Off-Topic" {
|
||||
numOffTopic++
|
||||
} else if ch.DisplayName == "Town Square" {
|
||||
numTownSquare++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, numPrivate)
|
||||
assert.Equal(t, 7, numPublic)
|
||||
assert.Equal(t, 2, numOffTopic)
|
||||
assert.Equal(t, 2, numTownSquare)
|
||||
})
|
||||
|
||||
t.Run("user_perms", func(t *testing.T) {
|
||||
query := `query channels($userId: String = "") {
|
||||
channels(userId: $userId) {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
type
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`
|
||||
u1 := th.CreateUser()
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"userId": u1.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1)
|
||||
})
|
||||
|
||||
t.Run("pagination", func(t *testing.T) {
|
||||
query := `query channels($first: Int, $after: String = "") {
|
||||
channels(userId: "me", first: $first, after: $after) {
|
||||
id
|
||||
createAt
|
||||
updateAt
|
||||
type
|
||||
displayName
|
||||
name
|
||||
header
|
||||
purpose
|
||||
schemeId
|
||||
cursor
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 4)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"after": q.Channels[3].Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 4)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 4,
|
||||
"after": q.Channels[3].Cursor,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 1)
|
||||
})
|
||||
|
||||
t.Run("team_filter", func(t *testing.T) {
|
||||
query := `query channels($teamId: String, $first: Int) {
|
||||
channels(userId: "me", teamId: $teamId, first: $first) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 10,
|
||||
"teamId": myTeam.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 4)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 2,
|
||||
"teamId": myTeam.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 2)
|
||||
})
|
||||
|
||||
t.Run("team_data", func(t *testing.T) {
|
||||
query := `query channels($teamId: String, $first: Int) {
|
||||
channels(userId: "me", teamId: $teamId, first: $first) {
|
||||
id
|
||||
team {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"first": 1,
|
||||
"teamId": myTeam.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 1)
|
||||
|
||||
gotTeam := q.Channels[0].Team
|
||||
assert.Equal(t, myTeam.Id, gotTeam.ID)
|
||||
assert.Equal(t, myTeam.DisplayName, gotTeam.DisplayName)
|
||||
})
|
||||
|
||||
t.Run("Delete+Update", func(t *testing.T) {
|
||||
query := `query channels($lastDeleteAt: Float = 0,
|
||||
$lastUpdateAt: Float = 0,
|
||||
$first: Int = 60,
|
||||
$includeDeleted: Boolean) {
|
||||
channels(userId: "me", lastDeleteAt: $lastDeleteAt, lastUpdateAt: $lastUpdateAt, first: $first, includeDeleted: $includeDeleted) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
input := graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": false,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 9)
|
||||
|
||||
now := model.GetMillis()
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": true,
|
||||
"lastUpdateAt": float64(now),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1) // no channels found
|
||||
|
||||
th.BasicChannel.Purpose = "newpurpose"
|
||||
_, _, err = th.Client.UpdateChannel(th.BasicChannel)
|
||||
require.NoError(t, err)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": true,
|
||||
"lastUpdateAt": float64(now),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 1)
|
||||
|
||||
_, err = th.Client.DeleteChannel(ch1.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = th.Client.DeleteChannel(ch2.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": false,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 7)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": true,
|
||||
"lastDeleteAt": float64(model.GetMillis()),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 7)
|
||||
|
||||
input = graphQLInput{
|
||||
OperationName: "channels",
|
||||
Query: query,
|
||||
Variables: map[string]interface{}{
|
||||
"includeDeleted": true,
|
||||
"lastDeleteAt": float64(model.GetMillis()),
|
||||
"first": 5,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err = th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.Channels, 5)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPrettyDNForUsers(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
t.Run("nickname_full_name", func(t *testing.T) {
|
||||
users := []*model.User{
|
||||
{
|
||||
Nickname: "nick1",
|
||||
Username: "user1",
|
||||
FirstName: "first1",
|
||||
LastName: "last1",
|
||||
},
|
||||
{
|
||||
Nickname: "nick2",
|
||||
Username: "user2",
|
||||
FirstName: "first2",
|
||||
LastName: "last2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "nick1, nick2", getPrettyDNForUsers("nickname_full_name", users))
|
||||
|
||||
users = []*model.User{
|
||||
{
|
||||
Username: "user1",
|
||||
FirstName: "first1",
|
||||
LastName: "last1",
|
||||
},
|
||||
{
|
||||
Username: "user2",
|
||||
FirstName: "first2",
|
||||
LastName: "last2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "first1 last1, first2 last2", getPrettyDNForUsers("nickname_full_name", users))
|
||||
})
|
||||
|
||||
t.Run("full_name", func(t *testing.T) {
|
||||
users := []*model.User{
|
||||
{
|
||||
Nickname: "nick1",
|
||||
Username: "user1",
|
||||
FirstName: "first1",
|
||||
LastName: "last1",
|
||||
},
|
||||
{
|
||||
Nickname: "nick2",
|
||||
Username: "user2",
|
||||
FirstName: "first2",
|
||||
LastName: "last2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "first1 last1, first2 last2", getPrettyDNForUsers("full_name", users))
|
||||
|
||||
users = []*model.User{
|
||||
{
|
||||
Username: "user1",
|
||||
},
|
||||
{
|
||||
Username: "user2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "user1, user2", getPrettyDNForUsers("full_name", users))
|
||||
})
|
||||
|
||||
t.Run("username", func(t *testing.T) {
|
||||
users := []*model.User{
|
||||
{
|
||||
Nickname: "nick1",
|
||||
Username: "user1",
|
||||
FirstName: "first1",
|
||||
LastName: "last1",
|
||||
},
|
||||
{
|
||||
Nickname: "nick2",
|
||||
Username: "user2",
|
||||
FirstName: "first2",
|
||||
LastName: "last2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "user1, user2", getPrettyDNForUsers("username", users))
|
||||
})
|
||||
}
|
||||
|
||||
func TestChannelCursor(t *testing.T) {
|
||||
ch := channel{
|
||||
Channel: model.Channel{Id: "testid"},
|
||||
}
|
||||
cur := ch.Cursor()
|
||||
|
||||
id, ok := parseChannelCursor(*cur)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, ch.Id, id)
|
||||
}
|
||||
31
api4/resolver_team.go
Обычный файл
31
api4/resolver_team.go
Обычный файл
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func getGraphQLTeam(ctx context.Context, id string) (*model.Team, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
team, appErr := c.App.GetTeam(id)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if (!team.AllowOpenInvite || team.Type != model.TeamOpen) &&
|
||||
!c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), team.Id, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
team = c.App.SanitizeTeam(*c.AppContext.Session(), team)
|
||||
return team, nil
|
||||
}
|
||||
69
api4/resolver_team_member.go
Обычный файл
69
api4/resolver_team_member.go
Обычный файл
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
// teamMember is an internal graphQL wrapper struct to add resolver methods.
|
||||
type teamMember struct {
|
||||
model.TeamMember
|
||||
}
|
||||
|
||||
// match with api4.getTeam
|
||||
func (tm *teamMember) Team(ctx context.Context) (*model.Team, error) {
|
||||
return getGraphQLTeam(ctx, tm.TeamId)
|
||||
}
|
||||
|
||||
// match with api4.getUser
|
||||
func (tm *teamMember) User(ctx context.Context) (*user, error) {
|
||||
return getGraphQLUser(ctx, tm.UserId)
|
||||
}
|
||||
|
||||
// match with api4.getCategoriesForTeamForUser
|
||||
func (tm *teamMember) SidebarCategories(ctx context.Context) ([]*model.SidebarCategoryWithChannels, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), tm.UserId) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
categories, appErr := c.App.GetSidebarCategories(tm.UserId, tm.TeamId)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// TODO: look into optimizing this.
|
||||
// create map
|
||||
orderMap := make(map[string]*model.SidebarCategoryWithChannels, len(categories.Categories))
|
||||
for _, category := range categories.Categories {
|
||||
orderMap[category.Id] = category
|
||||
}
|
||||
|
||||
// create a new slice based on the order
|
||||
res := make([]*model.SidebarCategoryWithChannels, 0, len(categories.Categories))
|
||||
for _, categoryId := range categories.Order {
|
||||
res = append(res, orderMap[categoryId])
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// match with api4.getRolesByNames
|
||||
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, strings.Fields(tm.Roles))
|
||||
}
|
||||
252
api4/resolver_team_member_test.go
Обычный файл
252
api4/resolver_team_member_test.go
Обычный файл
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLTeamMembers(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
TeamMembers []struct {
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
NickName string `json:"nickname"`
|
||||
} `json:"user"`
|
||||
Team struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
} `json:"team"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
SchemeManaged bool `json:"schemeManaged"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
} `json:"roles"`
|
||||
DeleteAt float64 `json:"deleteAt"`
|
||||
SchemeGuest bool `json:"schemeGuest"`
|
||||
SchemeUser bool `json:"schemeUser"`
|
||||
SchemeAdmin bool `json:"schemeAdmin"`
|
||||
SidebarCategories []struct {
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Sorting model.SidebarCategorySorting `json:"sorting"`
|
||||
ChannelIDs []string `json:"channelIds"`
|
||||
} `json:"sidebarCategories"`
|
||||
} `json:"teamMembers"`
|
||||
}
|
||||
|
||||
t.Run("User", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "teamMembers",
|
||||
Query: `
|
||||
query teamMembers($userId: String = "", $teamId: String = "") {
|
||||
teamMembers(userId: $userId, teamId: $teamId) {
|
||||
team {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
user {
|
||||
id
|
||||
username
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
schemeGuest
|
||||
schemeUser
|
||||
schemeAdmin
|
||||
sidebarCategories {
|
||||
id
|
||||
displayName
|
||||
sorting
|
||||
channelIds
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"userId": "me",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.TeamMembers, 1)
|
||||
|
||||
tm := q.TeamMembers[0]
|
||||
assert.Equal(t, th.BasicTeam.Id, tm.Team.ID)
|
||||
assert.Equal(t, th.BasicTeam.DisplayName, tm.Team.DisplayName)
|
||||
|
||||
assert.Equal(t, th.BasicUser.Id, tm.User.ID)
|
||||
assert.Equal(t, th.BasicUser.Username, tm.User.Username)
|
||||
assert.Equal(t, th.BasicUser.Email, tm.User.Email)
|
||||
assert.Equal(t, th.BasicUser.FirstName, tm.User.FirstName)
|
||||
assert.Equal(t, th.BasicUser.LastName, tm.User.LastName)
|
||||
|
||||
require.Len(t, tm.Roles, 1)
|
||||
assert.NotEmpty(t, tm.Roles[0].ID)
|
||||
assert.Equal(t, "team_user", tm.Roles[0].Name)
|
||||
assert.False(t, tm.SchemeGuest)
|
||||
assert.True(t, tm.SchemeUser)
|
||||
assert.False(t, tm.SchemeAdmin)
|
||||
|
||||
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(th.BasicUser.Id, th.BasicTeam.Id, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(tm.SidebarCategories, func(i, j int) bool {
|
||||
return tm.SidebarCategories[i].ID < tm.SidebarCategories[j].ID
|
||||
})
|
||||
sort.Slice(categories.Categories, func(i, j int) bool {
|
||||
return categories.Categories[i].Id < categories.Categories[j].Id
|
||||
})
|
||||
|
||||
for i := range categories.Categories {
|
||||
assert.Equal(t, categories.Categories[i].Id, tm.SidebarCategories[i].ID)
|
||||
assert.Equal(t, categories.Categories[i].DisplayName, tm.SidebarCategories[i].DisplayName)
|
||||
assert.Equal(t, categories.Categories[i].Sorting, tm.SidebarCategories[i].Sorting)
|
||||
assert.Equal(t, categories.Categories[i].ChannelIds(), tm.SidebarCategories[i].ChannelIDs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User+Team", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "teamMembers",
|
||||
Query: `
|
||||
query teamMembers($userId: String = "", $teamId: String = "") {
|
||||
teamMembers(userId: $userId, teamId: $teamId) {
|
||||
team {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
user {
|
||||
id
|
||||
username
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"userId": "me",
|
||||
"teamId": th.BasicTeam.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.TeamMembers, 1)
|
||||
|
||||
tm := q.TeamMembers[0]
|
||||
assert.Equal(t, th.BasicTeam.Id, tm.Team.ID)
|
||||
assert.Equal(t, th.BasicTeam.DisplayName, tm.Team.DisplayName)
|
||||
|
||||
assert.Equal(t, th.BasicUser.Id, tm.User.ID)
|
||||
assert.Equal(t, th.BasicUser.Username, tm.User.Username)
|
||||
assert.Equal(t, th.BasicUser.Email, tm.User.Email)
|
||||
assert.Equal(t, th.BasicUser.FirstName, tm.User.FirstName)
|
||||
assert.Equal(t, th.BasicUser.LastName, tm.User.LastName)
|
||||
|
||||
require.Len(t, tm.Roles, 1)
|
||||
assert.NotEmpty(t, tm.Roles[0].ID)
|
||||
assert.Equal(t, "team_user", tm.Roles[0].Name)
|
||||
})
|
||||
|
||||
t.Run("NewTeam", func(t *testing.T) {
|
||||
// Adding another team with more channels (public and private)
|
||||
myTeam := th.CreateTeam()
|
||||
ch1 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypeOpen, myTeam.Id)
|
||||
ch2 := th.CreateChannelWithClientAndTeam(th.Client, model.ChannelTypePrivate, myTeam.Id)
|
||||
th.LinkUserToTeam(th.BasicUser, myTeam)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch1, false)
|
||||
th.App.AddUserToChannel(th.BasicUser, ch2, false)
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "teamMembers",
|
||||
Query: `
|
||||
query teamMembers($userId: String = "", $teamId: String = "") {
|
||||
teamMembers(userId: $userId, teamId: $teamId) {
|
||||
team {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"userId": "me",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.TeamMembers, 2)
|
||||
|
||||
sort.Slice(q.TeamMembers, func(i, j int) bool {
|
||||
return q.TeamMembers[i].Team.ID < q.TeamMembers[j].Team.ID
|
||||
})
|
||||
|
||||
expectedTeams := []*model.Team{th.BasicTeam, myTeam}
|
||||
sort.Slice(expectedTeams, func(i, j int) bool {
|
||||
return expectedTeams[i].Id < expectedTeams[j].Id
|
||||
})
|
||||
|
||||
for i := range q.TeamMembers {
|
||||
tm := q.TeamMembers[i]
|
||||
|
||||
if tm.Team.ID == myTeam.Id {
|
||||
require.Len(t, tm.Roles, 2)
|
||||
sort.Slice(tm.Roles, func(i, j int) bool {
|
||||
return tm.Roles[i].Name < tm.Roles[j].Name
|
||||
})
|
||||
assert.Equal(t, "team_admin", tm.Roles[0].Name)
|
||||
assert.Equal(t, "team_user", tm.Roles[1].Name)
|
||||
} else {
|
||||
require.Len(t, tm.Roles, 1)
|
||||
assert.NotEmpty(t, tm.Roles[0].ID)
|
||||
assert.Equal(t, "team_user", tm.Roles[0].Name)
|
||||
}
|
||||
|
||||
expectedTeams[i].Id = tm.Team.ID
|
||||
expectedTeams[i].DisplayName = tm.Team.DisplayName
|
||||
}
|
||||
})
|
||||
}
|
||||
145
api4/resolver_test.go
Обычный файл
145
api4/resolver_test.go
Обычный файл
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLConfig(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t)
|
||||
th.LoginBasicWithGraphQL()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
Config map[string]string `json:"config"`
|
||||
}
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "config",
|
||||
Query: `
|
||||
query config {
|
||||
config
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
cfg, _, err := th.Client.GetOldClientConfig("")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Equal(t, cfg, q.Config)
|
||||
}
|
||||
|
||||
func TestGraphQLLicense(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t)
|
||||
th.LoginBasicWithGraphQL()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
License map[string]string `json:"license"`
|
||||
}
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "license",
|
||||
Query: `
|
||||
query license {
|
||||
license
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
cfg, _, err := th.Client.GetOldClientLicense("")
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Equal(t, cfg, q.License)
|
||||
}
|
||||
|
||||
func TestGraphQLChannelsLeft(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
ChannelsLeft []string `json:"channelsLeft"`
|
||||
}
|
||||
|
||||
t.Run("NotLeft", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "channelsLeft",
|
||||
Query: `
|
||||
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
|
||||
channelsLeft(userId: $userId, since: $since)
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelsLeft, 0)
|
||||
})
|
||||
|
||||
t.Run("Left", func(t *testing.T) {
|
||||
_, err := th.Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "channelsLeft",
|
||||
Query: `
|
||||
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
|
||||
channelsLeft(userId: $userId, since: $since)
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelsLeft, 1)
|
||||
})
|
||||
|
||||
t.Run("LeftAfterTime", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "channelsLeft",
|
||||
Query: `
|
||||
query channelsLeft($userId: String = "me", $since: Float = 0.0) {
|
||||
channelsLeft(userId: $userId, since: $since)
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"since": model.GetMillis(),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Len(t, q.ChannelsLeft, 0)
|
||||
})
|
||||
}
|
||||
118
api4/resolver_user.go
Обычный файл
118
api4/resolver_user.go
Обычный файл
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/web"
|
||||
)
|
||||
|
||||
// user is an internal graphQL wrapper struct to add resolver methods.
|
||||
type user struct {
|
||||
model.User
|
||||
}
|
||||
|
||||
// match with api4.getUser
|
||||
func getGraphQLUser(ctx context.Context, id string) (*user, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id == model.Me {
|
||||
id = c.AppContext.Session().UserId
|
||||
}
|
||||
|
||||
if !model.IsValidId(id) {
|
||||
return nil, web.NewInvalidParamError("user_id")
|
||||
}
|
||||
|
||||
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext.Session().UserId, id)
|
||||
if appErr != nil || !canSee {
|
||||
c.SetPermissionError(model.PermissionViewMembers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
usr, appErr := c.App.GetUser(id)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if c.IsSystemAdmin() || c.AppContext.Session().UserId == usr.Id {
|
||||
userTermsOfService, appErr := c.App.GetUserTermsOfService(usr.Id)
|
||||
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if userTermsOfService != nil {
|
||||
usr.TermsOfServiceId = userTermsOfService.TermsOfServiceId
|
||||
usr.TermsOfServiceCreateAt = userTermsOfService.CreateAt
|
||||
}
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId == usr.Id {
|
||||
usr.Sanitize(map[string]bool{})
|
||||
} else {
|
||||
c.App.SanitizeProfile(usr, c.IsSystemAdmin())
|
||||
}
|
||||
c.App.UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
|
||||
|
||||
return &user{*usr}, nil
|
||||
}
|
||||
|
||||
// match with api4.getRolesByNames
|
||||
func (u *user) Roles(ctx context.Context) ([]*model.Role, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames := u.GetRoles()
|
||||
if len(roleNames) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return getGraphQLRoles(c, roleNames)
|
||||
}
|
||||
|
||||
// match with api4.getPreferences
|
||||
func (u *user) Preferences(ctx context.Context) ([]model.Preference, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), u.Id) {
|
||||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return nil, c.Err
|
||||
}
|
||||
|
||||
preferences, appErr := c.App.GetPreferencesForUser(u.Id)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
return preferences, nil
|
||||
}
|
||||
|
||||
// match with api4.getUserStatus
|
||||
func (u *user) Status(ctx context.Context) (*model.Status, error) {
|
||||
c, err := getCtx(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statuses, appErr := c.App.GetUserStatusesByIds([]string{u.Id})
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
if len(statuses) == 0 {
|
||||
return nil, model.NewAppError("UserStatus", "api.status.user_not_found.app_error", nil, "", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return statuses[0], nil
|
||||
}
|
||||
200
api4/resolver_user_test.go
Обычный файл
200
api4/resolver_user_test.go
Обычный файл
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGraphQLUser(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
var q struct {
|
||||
User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
NickName string `json:"nickname"`
|
||||
IsBot bool `json:"isBot"`
|
||||
IsSystemAdmin bool `json:"isSystemAdmin"`
|
||||
CreateAt float64 `json:"createAt"`
|
||||
CustomStatus struct {
|
||||
Emoji string `json:"emoji"`
|
||||
Text string `json:"text"`
|
||||
Duration string `json:"duration"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
} `json:"customStatus"`
|
||||
Timezone model.StringMap `json:"timezone"`
|
||||
Props model.StringMap `json:"props"`
|
||||
NotifyProps model.StringMap `json:"notifyProps"`
|
||||
Position string `json:"position"`
|
||||
Roles []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"Name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
SchemeManaged bool `json:"schemeManaged"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
} `json:"roles"`
|
||||
Preferences []struct {
|
||||
UserID string `json:"userId"`
|
||||
Category string `json:"category"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
} `json:"preferences"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
t.Run("Basic", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "user",
|
||||
Query: `
|
||||
query user($id: String = "me") {
|
||||
user(id: $id) {
|
||||
id
|
||||
username
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
isBot
|
||||
isGuest
|
||||
isSystemAdmin
|
||||
timezone
|
||||
props
|
||||
notifyProps
|
||||
roles {
|
||||
id
|
||||
name
|
||||
}
|
||||
preferences {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
assert.Equal(t, th.BasicUser.Id, q.User.ID)
|
||||
assert.Equal(t, th.BasicUser.Username, q.User.Username)
|
||||
assert.Equal(t, th.BasicUser.Email, q.User.Email)
|
||||
assert.Equal(t, th.BasicUser.FirstName, q.User.FirstName)
|
||||
assert.Equal(t, th.BasicUser.IsBot, q.User.IsBot)
|
||||
assert.Equal(t, th.BasicUser.IsSystemAdmin(), q.User.IsSystemAdmin)
|
||||
assert.Equal(t, th.BasicUser.Timezone, q.User.Timezone)
|
||||
assert.Equal(t, th.BasicUser.Props, q.User.Props)
|
||||
assert.Equal(t, th.BasicUser.NotifyProps, q.User.NotifyProps)
|
||||
|
||||
roles, _, err := th.Client.GetRolesByNames(th.BasicUser.GetRoles())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, q.User.Roles, 1)
|
||||
assert.Len(t, roles, 1)
|
||||
assert.Equal(t, roles[0].Id, q.User.Roles[0].ID)
|
||||
assert.Equal(t, roles[0].Name, q.User.Roles[0].Name)
|
||||
|
||||
prefs, _, err := th.Client.GetPreferences(th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(prefs, func(i, j int) bool {
|
||||
return prefs[i].Name < prefs[j].Name
|
||||
})
|
||||
|
||||
sort.Slice(q.User.Preferences, func(i, j int) bool {
|
||||
return q.User.Preferences[i].Name < q.User.Preferences[j].Name
|
||||
})
|
||||
|
||||
for i := range prefs {
|
||||
assert.Equal(t, q.User.Preferences[i].Name, prefs[i].Name)
|
||||
assert.Equal(t, q.User.Preferences[i].Value, prefs[i].Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
th.BasicUser.Props = map[string]string{"testpropkey": "testpropvalue"}
|
||||
th.App.UpdateUser(th.BasicUser, false)
|
||||
|
||||
input := graphQLInput{
|
||||
OperationName: "user",
|
||||
Query: `
|
||||
query user($id: String = "me") {
|
||||
user(id: $id) {
|
||||
id
|
||||
props
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
|
||||
assert.Equal(t, th.BasicUser.Props, q.User.Props)
|
||||
})
|
||||
|
||||
t.Run("DifferentUser", func(t *testing.T) {
|
||||
input := graphQLInput{
|
||||
OperationName: "user",
|
||||
Query: `
|
||||
query user($id: String = "me") {
|
||||
user(id: $id) {
|
||||
id
|
||||
props
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"id": th.BasicUser2.Id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 0)
|
||||
require.NoError(t, json.Unmarshal(resp.Data, &q))
|
||||
|
||||
assert.Equal(t, q.User.ID, th.BasicUser2.Id)
|
||||
})
|
||||
|
||||
t.Run("BadUser", func(t *testing.T) {
|
||||
id := model.NewId()
|
||||
input := graphQLInput{
|
||||
OperationName: "user",
|
||||
Query: `
|
||||
query user($id: String = "me") {
|
||||
user(id: $id) {
|
||||
id
|
||||
props
|
||||
}
|
||||
}
|
||||
`,
|
||||
Variables: map[string]interface{}{
|
||||
"id": id,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := th.MakeGraphQLRequest(&input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Errors, 1)
|
||||
})
|
||||
}
|
||||
157
api4/schema.graphqls
Обычный файл
157
api4/schema.graphqls
Обычный файл
@@ -0,0 +1,157 @@
|
||||
schema {
|
||||
query: Query
|
||||
}
|
||||
|
||||
type Query {
|
||||
user(id: String!): User
|
||||
config(): StringMap!
|
||||
license(): StringMap!
|
||||
teamMembers(userId: String!,
|
||||
teamId: String = ""): [TeamMember]!
|
||||
channels(userId: String!,
|
||||
teamId: String = "",
|
||||
includeDeleted: Boolean = false,
|
||||
lastDeleteAt: Float = 0,
|
||||
lastUpdateAt: Float = 0,
|
||||
first: Int = 60,
|
||||
after: String = ""): [Channel]!
|
||||
channelsLeft(userId: String!,
|
||||
since: Float!): [String!]!
|
||||
channelMembers(userId: String!,
|
||||
channelId: String = "",
|
||||
first: Int = 60,
|
||||
after: String = "",
|
||||
lastUpdateAt: Float = 0): [ChannelMember]!
|
||||
}
|
||||
|
||||
scalar ChannelType
|
||||
|
||||
scalar SidebarCategoryType
|
||||
|
||||
scalar SidebarCategorySorting
|
||||
|
||||
scalar StringMap
|
||||
|
||||
scalar Time
|
||||
|
||||
type Channel {
|
||||
id : String!
|
||||
createAt : Float!
|
||||
updateAt : Float!
|
||||
deleteAt : Float!
|
||||
type : ChannelType!
|
||||
displayName: String!
|
||||
prettyDisplayName: String!
|
||||
name: String!
|
||||
header: String!
|
||||
purpose: String!
|
||||
creatorId: String!
|
||||
schemeId: String
|
||||
team: Team
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type ChannelMember {
|
||||
channel : Channel
|
||||
user : User
|
||||
roles : [Role]!
|
||||
lastViewedAt : Float!
|
||||
msgCount : Float!
|
||||
mentionCount : Float!
|
||||
mentionCountRoot : Float!
|
||||
notifyProps : StringMap!
|
||||
lastUpdateAt : Float!
|
||||
schemeGuest : Boolean!
|
||||
schemeUser : Boolean!
|
||||
schemeAdmin : Boolean!
|
||||
explicitRoles : String!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String!
|
||||
username: String!
|
||||
email: String!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
nickname: String!
|
||||
isBot: Boolean!
|
||||
isGuest: Boolean!
|
||||
isSystemAdmin: Boolean!
|
||||
createAt: Float!
|
||||
deleteAt: Float!
|
||||
authService: String!
|
||||
customStatus: CustomStatus
|
||||
status: Status
|
||||
props: StringMap!
|
||||
notifyProps: StringMap!
|
||||
lastPictureUpdateAt: Float!
|
||||
locale: String!
|
||||
timezone: StringMap!
|
||||
position: String!
|
||||
roles: [Role]!
|
||||
preferences: [Preference!]!
|
||||
}
|
||||
|
||||
type CustomStatus {
|
||||
emoji: String!
|
||||
text: String!
|
||||
duration: String!
|
||||
expiresAt: Time!
|
||||
}
|
||||
|
||||
type Status {
|
||||
status: String!
|
||||
manual: Boolean!
|
||||
lastActivityAt: Float!
|
||||
activeChannel: String!
|
||||
dndEndTime: Float!
|
||||
}
|
||||
|
||||
type Role {
|
||||
id: String!
|
||||
name: String!
|
||||
permissions: [String!]!
|
||||
schemeManaged: Boolean!
|
||||
builtIn: Boolean!
|
||||
}
|
||||
|
||||
type Preference {
|
||||
userId: String!
|
||||
category: String!
|
||||
name: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type Team {
|
||||
id: String!
|
||||
displayName : String!
|
||||
name : String!
|
||||
description : String!
|
||||
email : String!
|
||||
type : String!
|
||||
companyName : String!
|
||||
allowedDomains : String!
|
||||
inviteId : String!
|
||||
}
|
||||
|
||||
type TeamMember {
|
||||
team: Team
|
||||
user: User
|
||||
roles: [Role]!
|
||||
deleteAt: Float!
|
||||
schemeGuest: Boolean!
|
||||
schemeUser: Boolean!
|
||||
schemeAdmin: Boolean!
|
||||
sidebarCategories: [SidebarCategory]!
|
||||
}
|
||||
|
||||
type SidebarCategory {
|
||||
id: String!
|
||||
sorting: SidebarCategorySorting!
|
||||
type: SidebarCategoryType!
|
||||
displayName: String!
|
||||
muted: Boolean!
|
||||
collapsed: Boolean!
|
||||
channelIds: [String!]!
|
||||
}
|
||||
@@ -752,7 +752,8 @@ func TestServerBusy503(t *testing.T) {
|
||||
|
||||
func TestPushNotificationAck(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
api := Init(th.Server)
|
||||
api, err := Init(th.Server)
|
||||
require.NoError(t, err)
|
||||
session, _ := th.App.GetSession(th.Client.AuthToken)
|
||||
defer th.TearDown()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user