```release-note
NONE
```
Этот коммит содержится в:
Agniva De Sarker
2023-10-12 09:47:35 +05:30
коммит произвёл GitHub
родитель d172ff1881
Коммит 3dd9e3715c
49 изменённых файлов: 12 добавлений и 4727 удалений

Просмотреть файл

@@ -7,7 +7,6 @@ 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/public/model"
@@ -142,7 +141,6 @@ type Routes struct {
type API struct {
srv *app.Server
schema *graphql.Schema
BaseRoutes *Routes
}
@@ -306,9 +304,6 @@ func Init(srv *app.Server) (*API, error) {
api.InitUsage()
api.InitHostedCustomer()
api.InitDrafts()
if err := api.InitGraphQL(); err != nil {
return nil, err
}
srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))

Просмотреть файл

@@ -4,9 +4,7 @@
package api4
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -21,7 +19,6 @@ 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"
@@ -47,7 +44,6 @@ type TestHelper struct {
Context *request.Context
Client *model.Client4
GraphQLClient *graphQLClient
BasicUser *model.User
BasicUser2 *model.User
TeamAdminUser *model.User
@@ -196,7 +192,6 @@ 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()
@@ -811,16 +806,10 @@ 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() {
@@ -842,13 +831,6 @@ 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(context.Background(), th.BasicUser2.Email, th.BasicUser2.Password)
if err != nil {
@@ -1301,22 +1283,3 @@ 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
}

Просмотреть файл

@@ -1,193 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/graph-gophers/dataloader/v6"
graphql "github.com/graph-gophers/graphql-go"
gqlerrors "github.com/graph-gophers/graphql-go/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/web"
)
type graphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]any `json:"variables"`
}
// Unique type to hold our context.
type ctxKey int
const (
webCtx ctxKey = 0
rolesLoaderCtx ctxKey = 1
channelsLoaderCtx ctxKey = 2
teamsLoaderCtx ctxKey = 3
usersLoaderCtx ctxKey = 4
)
const loaderBatchCapacity = web.PerPageMaximum
//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(loaderBatchCapacity), // This is dangerous if the query
// uses any non-dataloader backed object. So we need to be a bit careful here.
}
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
}
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 {
c.Logger.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(&params); 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
}
c.GraphQLOperationName = params.OperationName
// Populate the context with required info.
reqCtx := r.Context()
reqCtx = context.WithValue(reqCtx, webCtx, c)
rolesLoader := dataloader.NewBatchedLoader(graphQLRolesLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, rolesLoaderCtx, rolesLoader)
channelsLoader := dataloader.NewBatchedLoader(graphQLChannelsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, channelsLoaderCtx, channelsLoader)
teamsLoader := dataloader.NewBatchedLoader(graphQLTeamsLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, teamsLoaderCtx, teamsLoader)
usersLoader := dataloader.NewBatchedLoader(graphQLUsersLoader, dataloader.WithBatchCapacity(loaderBatchCapacity))
reqCtx = context.WithValue(reqCtx, usersLoaderCtx, usersLoader)
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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" integrity="sha256-gSgd+on4bTXigueyd/NSRNAy4cBY42RAVNaXnQDjOW8=" crossorigin="anonymous"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha256-OI3N9zCKabDov2rZFzl8lJUXCcP7EmsGcGoP6DMXQCo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js" integrity="sha256-aB35laj7IZhLTx58xw/Gm1EKOoJJKZt6RY+bH1ReHxs=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js" integrity="sha256-wouRkivKKXA3y6AuyFwcDcF50alCNV8LbghfYCH6Z98=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js" integrity="sha256-9hrJxD4IQsWHdNpzLkJKYGiY/SEZFJJSUqyeZPNKd8g=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js" integrity="sha256-oeWyQyKKUurcnbFRsfeSgrdOpXXiRYopnPjTVZ+6UmI=" crossorigin="anonymous"></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"
}

Просмотреть файл

@@ -1,87 +0,0 @@
// 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/public/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, "", http.StatusInternalServerError).Wrap(jsonErr)
}
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
}

Просмотреть файл

@@ -1,34 +0,0 @@
// 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")
}

Просмотреть файл

@@ -1,427 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"errors"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/channels/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(c.AppContext, 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(c.AppContext, 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.Srv().Platform().LimitedClientConfigWithComputed(), nil
}
return c.App.Srv().Platform().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
ExcludeTeam bool
}) ([]*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, 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 != "" && !args.ExcludeTeam {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
tm, appErr2 := c.App.GetTeamMember(c.AppContext, args.TeamID, args.UserID)
if appErr2 != nil {
return nil, appErr2
}
return []*teamMember{{*tm}}, nil
}
excludeTeamID := ""
if args.TeamID != "" && args.ExcludeTeam {
excludeTeamID = args.TeamID
}
// Do not return archived team members
members, appErr := c.App.GetTeamMembersForUser(c.AppContext, args.UserID, excludeTeamID, false)
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
TeamID string
ChannelID string
ExcludeTeam bool
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, *c.AppContext.Session(), args.ChannelID, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
ctx := c.AppContext
ctx.SetContext(app.WithMaster(ctx.Context()))
member, appErr := c.App.GetChannelMember(ctx, 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)
}
}
if args.TeamID != "" {
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*channelMember{}, appErr
}
args.TeamID = team.Id
} else {
return []*channelMember{}, nil
}
}
}
opts := &store.ChannelMemberGraphQLSearchOpts{
AfterChannel: afterChannel,
AfterUser: afterUser,
Limit: limit,
LastUpdateAt: int(args.LastUpdateAt),
ExcludeTeam: args.ExcludeTeam,
}
members, err := c.App.Srv().Store().Channel().GetMembersForUserWithCursor(args.UserID, args.TeamID, opts)
if err != nil {
return nil, err
}
res := make([]*channelMember, 0, len(members))
for _, cm := range members {
res = append(res, &channelMember{cm})
}
return res, nil
}
// match with api4.getCategoriesForTeamForUser
func (*resolver) SidebarCategories(ctx context.Context, args struct {
UserID string
TeamID string
ExcludeTeam bool
}) ([]*model.SidebarCategoryWithChannels, error) {
c, err := getCtx(ctx)
if err != nil {
return nil, err
}
// Fallback to primary team logic
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), args.TeamID, model.PermissionViewTeam) {
primaryTeam := *c.App.Config().TeamSettings.ExperimentalPrimaryTeam
if primaryTeam != "" {
team, appErr := c.App.GetTeamByName(primaryTeam)
if appErr != nil {
return []*model.SidebarCategoryWithChannels{}, appErr
}
args.TeamID = team.Id
} else {
return []*model.SidebarCategoryWithChannels{}, nil
}
}
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 it's only for a single team.
var categories *model.OrderedSidebarCategories
var appErr *model.AppError
if !args.ExcludeTeam {
categories, appErr = c.App.GetSidebarCategoriesForTeamForUser(c.AppContext, args.UserID, args.TeamID)
if appErr != nil {
return nil, appErr
}
} else {
opts := &store.SidebarCategorySearchOpts{
TeamID: args.TeamID,
ExcludeTeam: args.ExcludeTeam,
}
categories, appErr = c.App.GetSidebarCategories(c.AppContext, args.UserID, opts)
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
}
// 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(webCtx).(*web.Context)
if !ok {
return nil, errors.New("no web.Context found in context")
}
return c, nil
}
// getRolesLoader returns the roles loader out of the context.
func getRolesLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(rolesLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getChannelsLoader returns the channels loader out of the context.
func getChannelsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(channelsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getTeamsLoader returns the teams loader out of the context.
func getTeamsLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(teamsLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}
// getUsersLoader returns the users loader out of the context.
func getUsersLoader(ctx context.Context) (*dataloader.Loader, error) {
l, ok := ctx.Value(usersLoaderCtx).(*dataloader.Loader)
if !ok {
return nil, errors.New("no dataloader.Loader found in context")
}
return l, nil
}

Просмотреть файл

@@ -1,157 +0,0 @@
// 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/public/model"
"github.com/mattermost/mattermost/server/v8/channels/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
}
prefix, id, found := strings.Cut(string(decoded), "-")
if !found {
return "", false
}
if cursorPrefix(prefix) != channelCursorPrefix {
return "", false
}
return id, 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 and set empty maps as well.
var channelIDs []string
for _, ch := range channels {
if ch.IsGroupOrDirect() {
channelIDs = append(channelIDs, ch.Id)
}
// This is needed to avoid sending null, which
// does not match with the schema since props is not nullable.
// And making it nullable would mean taking pointer of a map,
// which is not very idiomatic.
ch.MakeNonNil()
}
var nameFormat string
var userInfo map[string][]*model.User
var err error
// Avoiding unnecessary queries unless necessary.
if len(channelIDs) > 0 {
userInfo, err = c.App.Srv().Store().Channel().GetMembersInfoByChannelIds(channelIDs)
if err != nil {
return nil, err
}
user := &model.User{Id: c.AppContext.Session().UserId}
nameFormat = c.App.GetNotificationNameFormat(user)
}
// Convert to the wrapper format.
nameCache := make(map[string]string)
res := make([]*channel, len(channels))
for i, 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(nameFormat, users, c.AppContext.Session().UserId, nameCache)
}
res[i] = &channel{Channel: *ch, PrettyDisplayName: prettyName}
}
return res, nil
}
func getPrettyDNForUsers(displaySetting string, users []*model.User, omitUserId string, cache map[string]string) string {
displayNames := make([]string, 0, len(users))
for _, u := range users {
if u.Id == omitUserId {
continue
}
displayNames = append(displayNames, getPrettyDNForUser(displaySetting, u, cache))
}
sort.Strings(displayNames)
result := strings.Join(displayNames, ", ")
if result == "" {
// Self DM
result = getPrettyDNForUser(displaySetting, users[0], cache)
}
return result
}
func getPrettyDNForUser(displaySetting string, user *model.User, cache map[string]string) string {
// use the cache first
if name, ok := cache[user.Id]; ok {
return name
}
var displayName string
switch displaySetting {
case "nickname_full_name":
displayName = user.Nickname
if strings.TrimSpace(displayName) == "" {
displayName = user.GetFullName()
}
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
case "full_name":
displayName = user.GetFullName()
if strings.TrimSpace(displayName) == "" {
displayName = user.Username
}
default: // the "username" case also falls under this one.
displayName = user.Username
}
// update the cache
cache[user.Id] = displayName
return displayName
}

Просмотреть файл

@@ -1,226 +0,0 @@
// 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/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/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) {
loader, err := getChannelsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(cm.ChannelId))
result, err := thunk()
if err != nil {
return nil, err
}
channel := result.(*channel)
return channel, nil
}
func graphQLChannelsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
channels, err := getGraphQLChannels(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range channels {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLChannels(c *web.Context, channelIDs []string) ([]*channel, error) {
channels, appErr := c.App.GetChannels(c.AppContext, channelIDs)
if appErr != nil {
return nil, appErr
}
if len(channels) != len(channelIDs) {
return nil, fmt.Errorf("all channels were not found. Requested %d; Found %d", len(channelIDs), len(channels))
}
var openChannels, nonOpenChannels, teamsForOpenChannels []string
uniqueTeams := make(map[string]bool)
for _, ch := range channels {
if ch.Type == model.ChannelTypeOpen {
openChannels = append(openChannels, ch.Id)
uniqueTeams[ch.TeamId] = true
} else {
nonOpenChannels = append(nonOpenChannels, ch.Id)
}
}
for teamID := range uniqueTeams {
teamsForOpenChannels = append(teamsForOpenChannels, teamID)
}
if len(openChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), openChannels, model.PermissionReadChannel) &&
!c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsForOpenChannels, model.PermissionReadPublicChannel) {
c.SetPermissionError(model.PermissionReadPublicChannel)
return nil, c.Err
}
if len(nonOpenChannels) > 0 && !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), nonOpenChannels, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return nil, c.Err
}
appErr = c.App.FillInChannelsProps(c.AppContext, model.ChannelList(channels))
if appErr != nil {
return nil, appErr
}
res, err := postProcessChannels(c, channels)
if err != nil {
return nil, err
}
// The channels need to be in the exact same order as the input slice.
tmp := make(map[string]*channel)
for _, ch := range res {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the channels.
for i, id := range channelIDs {
res[i] = tmp[id]
}
return res, nil
}
func (cm *channelMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(cm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
func (cm *channelMember) Cursor() *string {
cursor := string(channelMemberCursorPrefix) + "-" + cm.ChannelId + "-" + cm.UserId
encoded := base64.StdEncoding.EncodeToString([]byte(cursor))
return model.NewString(encoded)
}
func graphQLRolesLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
roles, err := getGraphQLRoles(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, role := range roles {
result[i] = &dataloader.Result{Data: role}
}
return result
}
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
}
// The roles need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Role)
for _, r := range roles {
tmp[r.Name] = r
}
// We reuse the same slice and just rewrite the roles.
for i, roleName := range roleNames {
roles[i] = tmp[roleName]
}
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
}

Просмотреть файл

@@ -1,400 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
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.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, 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"`
UrgentMentionCount float64 `json:"urgentMentionCount"`
MsgCountRoot float64 `json:"msgCountRoot"`
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
urgentMentionCount
msgCountRoot
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
urgentMentionCount
}
}
`,
Variables: map[string]any{
"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]any{
"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]any{
"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]any{
"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]any{
"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]any{
"channelId": model.NewId(),
"first": 3,
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
})
t.Run("team_filter", func(t *testing.T) {
query := `query channelMembers($teamId: String, $excludeTeam: Boolean = false) {
channelMembers(userId: "me", teamId: $teamId, excludeTeam: $excludeTeam) {
channel {
id
}
}
}
`
input := graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"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.ChannelMembers, 5)
input = graphQLInput{
OperationName: "channelMembers",
Query: query,
Variables: map[string]any{
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
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)
})
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]any{
"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]any{
"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)
}

Просмотреть файл

@@ -1,513 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
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.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
th.CreateDmChannel(th.BasicUser2)
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"`
PrettyDisplayName string `json:"prettyDisplayName"`
Name string `json:"name"`
Header string `json:"header"`
Purpose string `json:"purpose"`
SchemeId string `json:"schemeId"`
TotalMsgCountRoot float64 `json:"totalMsgCountRoot"`
LastRootPostAt float64 `json:"lastRootPostAt"`
Cursor string `json:"cursor"`
Props map[string]any `json:"props"`
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
prettyDisplayName
name
header
purpose
schemeId
totalMsgCountRoot
lastRootPostAt
cursor
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.Len(t, q.Channels, 10)
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.PrettyDisplayName)
assert.NotEmpty(t, ch.CreateAt)
assert.NotEmpty(t, ch.UpdateAt)
assert.NotNil(t, ch.Props)
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]any{
"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]any{
"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]any{
"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]any{
"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, 2)
})
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]any{
"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, 5)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"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]any{
"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)
// Iterating because one of them can be a DM channel.
for _, ch := range q.Channels {
if ch.Team.ID != "" {
assert.Equal(t, myTeam.Id, ch.Team.ID)
assert.Equal(t, myTeam.DisplayName, ch.Team.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]any{
"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, 10)
now := model.GetMillis()
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"includeDeleted": true,
"lastUpdateAt": float64(now),
},
}
resp, err = th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 0) // no errors for no channels found
th.BasicChannel.Purpose = "newpurpose"
_, _, err = th.Client.UpdateChannel(context.Background(), th.BasicChannel)
require.NoError(t, err)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"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(context.Background(), ch1.Id)
require.NoError(t, err)
_, err = th.Client.DeleteChannel(context.Background(), ch2.Id)
require.NoError(t, err)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"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, 8)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"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, 8)
input = graphQLInput{
OperationName: "channels",
Query: query,
Variables: map[string]any{
"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{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "nick2", getPrettyDNForUsers("nickname_full_name", users, "user1", map[string]string{}))
users = []*model.User{
{
Id: "user1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("nickname_full_name", users, "user1", map[string]string{}))
})
t.Run("full_name", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("full_name", users, "user1", map[string]string{}))
users = []*model.User{
{
Id: "user1",
Username: "user1",
},
{
Id: "user2",
Username: "user2",
},
}
assert.Equal(t, "user2", getPrettyDNForUsers("full_name", users, "user1", map[string]string{}))
})
t.Run("username", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
assert.Equal(t, "user2", getPrettyDNForUsers("username", users, "user1", map[string]string{}))
})
t.Run("cache", func(t *testing.T) {
users := []*model.User{
{
Id: "user1",
Nickname: "nick1",
Username: "user1",
FirstName: "first1",
LastName: "last1",
},
{
Id: "user2",
Nickname: "nick2",
Username: "user2",
FirstName: "first2",
LastName: "last2",
},
}
cache := map[string]string{}
assert.Equal(t, "first2 last2", getPrettyDNForUsers("full_name", users, "user1", cache))
cache["user2"] = "teststring!!"
assert.Equal(t, "teststring!!", getPrettyDNForUsers("full_name", users, "user1", cache))
})
}
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)
}

Просмотреть файл

@@ -1,142 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"os"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
func TestGraphQLSidebarCategories(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 {
SidebarCategories []struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
Sorting model.SidebarCategorySorting `json:"sorting"`
ChannelIDs []string `json:"channelIds"`
TeamID string `json:"teamId"`
SortOrder int64 `json:"sortOrder"`
} `json:"sidebarCategories"`
}
input := graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
sortOrder
}
}
`,
Variables: map[string]any{
"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.SidebarCategories, 3)
categories, _, err := th.Client.GetSidebarCategoriesForTeamForUser(context.Background(), th.BasicUser.Id, th.BasicTeam.Id, "")
require.NoError(t, err)
sort.Slice(q.SidebarCategories, func(i, j int) bool {
return q.SidebarCategories[i].ID < q.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, q.SidebarCategories[i].ID)
assert.Equal(t, categories.Categories[i].DisplayName, q.SidebarCategories[i].DisplayName)
assert.Equal(t, categories.Categories[i].Sorting, q.SidebarCategories[i].Sorting)
assert.Equal(t, categories.Categories[i].ChannelIds(), q.SidebarCategories[i].ChannelIDs)
assert.Equal(t, categories.Categories[i].SortOrder, q.SidebarCategories[i].SortOrder)
}
input = graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
sortOrder
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
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.SidebarCategories, 0)
// Adding a new team
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.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, th.BasicUser, ch2, false)
input = graphQLInput{
OperationName: "sidebarCategories",
Query: `
query sidebarCategories($userId: String = "", $teamId: String = "", $excludeTeam: Boolean = false) {
sidebarCategories(userId: $userId, teamId: $teamId, excludeTeam: $excludeTeam) {
id
displayName
sorting
channelIds
teamId
sortOrder
}
}
`,
Variables: map[string]any{
"userId": "me",
"teamId": th.BasicTeam.Id,
"excludeTeam": true,
},
}
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.SidebarCategories, 3)
for _, cat := range q.SidebarCategories {
assert.Equal(t, myTeam.Id, cat.TeamID)
}
}

Просмотреть файл

@@ -1,95 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"fmt"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/web"
)
func getGraphQLTeam(ctx context.Context, id string) (*model.Team, error) {
loader, err := getTeamsLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
team := result.(*model.Team)
return team, nil
}
func graphQLTeamsLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
teams, err := getGraphQLTeams(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, ch := range teams {
result[i] = &dataloader.Result{Data: ch}
}
return result
}
func getGraphQLTeams(c *web.Context, teamIDs []string) ([]*model.Team, error) {
teams, appErr := c.App.GetTeams(teamIDs)
if appErr != nil {
return nil, appErr
}
if len(teams) != len(teamIDs) {
return nil, fmt.Errorf("all teams were not found. Requested %d; Found %d", len(teamIDs), len(teams))
}
var teamsToCheck []string
for _, team := range teams {
if !team.AllowOpenInvite || team.Type != model.TeamOpen {
teamsToCheck = append(teamsToCheck, team.Id)
}
}
if !c.App.SessionHasPermissionToTeams(c.AppContext, *c.AppContext.Session(), teamsToCheck, model.PermissionViewTeam) {
c.SetPermissionError(model.PermissionViewTeam)
return nil, c.Err
}
for i, team := range teams {
teams[i] = c.App.SanitizeTeam(*c.AppContext.Session(), team)
}
// The teams need to be in the exact same order as the input slice.
tmp := make(map[string]*model.Team, len(teams))
for _, ch := range teams {
tmp[ch.Id] = ch
}
// We reuse the same slice and just rewrite the teams.
for i, id := range teamIDs {
teams[i] = tmp[id]
}
return teams, nil
}

Просмотреть файл

@@ -1,50 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"strings"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost/server/public/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.getRolesByNames
func (tm *teamMember) Roles_(ctx context.Context) ([]*model.Role, error) {
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(strings.Fields(tm.Roles)))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}

Просмотреть файл

@@ -1,413 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"os"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
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"`
Name string `json:"name"`
CreateAt float64 `json:"createAt"`
DeleteAt float64 `json:"deleteAt"`
SchemeId *string `json:"schemeId"`
PolicyId *string `json:"policyId"`
CloudLimitsArchived bool `json:"cloudLimitsArchived"`
} `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"`
} `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
}
}
`,
Variables: map[string]any{
"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)
})
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
name
createAt
deleteAt
schemeId
policyId
cloudLimitsArchived
}
user {
id
username
email
firstName
lastName
}
roles {
id
name
}
}
}
`,
Variables: map[string]any{
"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.BasicTeam.Name, tm.Team.Name)
assert.Equal(t, th.BasicTeam.CreateAt_(), tm.Team.CreateAt)
assert.Equal(t, th.BasicTeam.DeleteAt_(), tm.Team.DeleteAt)
assert.Equal(t, th.BasicTeam.SchemeId, tm.Team.SchemeId)
assert.Equal(t, th.BasicTeam.PolicyID, tm.Team.PolicyId)
assert.Equal(t, th.BasicTeam.CloudLimitsArchived, tm.Team.CloudLimitsArchived)
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.Context, th.BasicUser, ch1, false)
th.App.AddUserToChannel(th.Context, 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]any{
"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
}
// Negate team
input = graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId, excludeTeam: true) {
team {
id
displayName
}
}
}
`,
Variables: map[string]any{
"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)
input = graphQLInput{
OperationName: "teamMembers",
Query: `
query teamMembers($userId: String = "", $teamId: String = "") {
teamMembers(userId: $userId, teamId: $teamId) {
team {
id
displayName
}
}
}
`,
Variables: map[string]any{
"userId": "me",
},
}
// Removing from a team and ensuring we get the right response.
th.UnlinkUserFromTeam(th.BasicUser, myTeam)
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)
})
}
func TestGraphQLTeamMembersAsGuest(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_GRAPHQL", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_GRAPHQL")
th := Setup(t)
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: GenerateTestTeamName(),
Email: th.GenerateTestEmail(),
Type: model.TeamOpen,
AllowOpenInvite: true,
}
var err error
team, _, err = th.Client.CreateTeam(context.Background(), team)
require.NoError(t, err)
th.BasicTeam = team
th.BasicChannel = th.CreatePublicChannel()
th.LinkUserToTeam(th.BasicUser, th.BasicTeam)
th.App.AddUserToChannel(th.Context, th.BasicUser, th.BasicChannel, false)
th.LoginBasic()
defer th.TearDown()
require.Nil(t, th.App.DemoteUserToGuest(th.Context, th.BasicUser))
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"`
Name string `json:"name"`
CreateAt float64 `json:"createAt"`
DeleteAt float64 `json:"deleteAt"`
SchemeId *string `json:"schemeId"`
PolicyId *string `json:"policyId"`
CloudLimitsArchived bool `json:"cloudLimitsArchived"`
} `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"`
} `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
}
}
`,
Variables: map[string]any{
"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_guest", tm.Roles[0].Name)
assert.True(t, tm.SchemeGuest)
assert.False(t, tm.SchemeUser)
assert.False(t, tm.SchemeAdmin)
})
}

Просмотреть файл

@@ -1,229 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
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(context.Background(), "")
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(context.Background(), "")
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(context.Background(), 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]any{
"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)
})
}
func TestGraphQLRolesLoader(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"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
} `json:"user"`
ChannelMembers []struct {
MsgCount float64 `json:"msgCount"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
} `json:"channelMembers"`
TeamMembers []struct {
SchemeUser bool `json:"schemeUser"`
Roles []struct {
ID string `json:"id"`
Name string `json:"Name"`
} `json:"roles"`
}
}
input := graphQLInput{
OperationName: "channelMembers",
Query: `
query channelMembers {
user(id: "me") {
id
username
roles {
id
name
}
}
channelMembers(userId: "me") {
msgCount
roles {
id
name
}
}
teamMembers(userId: "me") {
schemeUser
roles {
id
name
}
}
}
`,
}
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.User.Roles, 1)
assert.Equal(t, "system_user", q.User.Roles[0].Name)
require.Len(t, q.ChannelMembers, 5)
for _, cm := range q.ChannelMembers {
require.Len(t, cm.Roles, 1)
assert.Equal(t, "channel_user", cm.Roles[0].Name)
}
require.Len(t, q.TeamMembers, 1)
for _, tm := range q.TeamMembers {
require.Len(t, tm.Roles, 1)
assert.Equal(t, "team_user", tm.Roles[0].Name)
}
}

Просмотреть файл

@@ -1,222 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"net/http"
"github.com/graph-gophers/dataloader/v6"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/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")
}
loader, err := getUsersLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.Load(ctx, dataloader.StringKey(id))
result, err := thunk()
if err != nil {
return nil, err
}
usr := result.(*model.User)
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
}
}
c.App.Srv().Platform().UpdateLastActivityAtIfNeeded(*c.AppContext.Session())
return &user{*usr}, nil
}
// match with api4.getRolesByNames
func (u *user) Roles(ctx context.Context) ([]*model.Role, error) {
roleNames := u.GetRoles()
if len(roleNames) == 0 {
return nil, nil
}
loader, err := getRolesLoader(ctx)
if err != nil {
return nil, err
}
thunk := loader.LoadMany(ctx, dataloader.NewKeysFromStrings(roleNames))
results, errs := thunk()
// All errors are the same. We just return the first one.
if len(errs) > 0 && errs[0] != nil {
return nil, err
}
roles := make([]*model.Role, len(results))
for i, res := range results {
roles[i] = res.(*model.Role)
}
return roles, nil
}
// 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
}
// match with api4.getSessions
func (u *user) Sessions(ctx context.Context) ([]*model.Session, 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
}
sessions, appErr := c.App.GetSessions(c.AppContext, u.Id)
if appErr != nil {
return nil, appErr
}
for _, session := range sessions {
session.Sanitize()
}
return sessions, nil
}
func graphQLUsersLoader(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
stringKeys := keys.Keys()
result := make([]*dataloader.Result, len(stringKeys))
c, err := getCtx(ctx)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
users, err := getGraphQLUsers(c, stringKeys)
if err != nil {
for i := range result {
result[i] = &dataloader.Result{Error: err}
}
return result
}
for i, user := range users {
result[i] = &dataloader.Result{Data: user}
}
return result
}
func getGraphQLUsers(c *web.Context, userIDs []string) ([]*model.User, error) {
// Usually this will be called only for one user
// and cached for the rest of the query. So it's not an issue
// to run this in a loop.
for _, id := range userIDs {
canSee, appErr := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, id)
if appErr != nil || !canSee {
c.SetPermissionError(model.PermissionViewMembers)
return nil, c.Err
}
}
users, appErr := c.App.GetUsers(userIDs)
if appErr != nil {
return nil, appErr
}
// Same as earlier, we want to pre-compute this only once
// because otherwise the resolvers run in multiple goroutines
// and *User.Sanitize causes a race, and we want to avoid
// deep-copying every user in all goroutines.
for _, user := range users {
if c.AppContext.Session().UserId == user.Id {
user.Sanitize(map[string]bool{})
} else {
c.App.SanitizeProfile(user, c.IsSystemAdmin())
}
}
// The users need to be in the exact same order as the input slice.
tmp := make(map[string]*model.User)
for _, u := range users {
tmp[u.Id] = u
}
// We reuse the same slice and just rewrite the roles.
for i, uID := range userIDs {
users[i] = tmp[uID]
}
return users, nil
}

Просмотреть файл

@@ -1,244 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"context"
"encoding/json"
"os"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
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"`
DeleteAt float64 `json:"deleteAt"`
UpdateAt float64 `json:"updateAt"`
AuthData *string `json:"authData"`
EmailVerified bool `json:"emailVerified"`
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"`
CreateAt float64 `json:"createAt"`
DeleteAt float64 `json:"deleteAt"`
UpdateAt float64 `json:"updateAt"`
} `json:"roles"`
Preferences []struct {
UserID string `json:"userId"`
Category string `json:"category"`
Name string `json:"name"`
Value string `json:"value"`
} `json:"preferences"`
Sessions []struct {
ID string `json:"id"`
CreateAt float64 `json:"createAt"`
LastActivityAt float64 `json:"lastActivityAt"`
DeviceId string `json:"deviceId"`
Roles string `json:"roles"`
} `json:"sessions"`
} `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
createAt
updateAt
deleteAt
firstName
lastName
emailVerified
isBot
isGuest
isSystemAdmin
timezone
props
notifyProps
roles {
id
name
createAt
updateAt
deleteAt
}
preferences {
name
value
}
sessions {
id
createAt
lastActivityAt
roles
}
}
}
`,
}
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, float64(th.BasicUser.CreateAt), q.User.CreateAt)
assert.Equal(t, float64(th.BasicUser.DeleteAt), q.User.DeleteAt)
assert.NotZero(t, q.User.UpdateAt)
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(context.Background(), 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)
assert.Equal(t, float64(roles[0].CreateAt), q.User.Roles[0].CreateAt)
assert.Equal(t, float64(roles[0].UpdateAt), q.User.Roles[0].UpdateAt)
assert.Equal(t, float64(roles[0].DeleteAt), q.User.Roles[0].DeleteAt)
prefs, _, err := th.Client.GetPreferences(context.Background(), 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)
}
assert.Len(t, q.User.Sessions, 2)
now := float64(model.GetMillis())
for _, session := range q.User.Sessions {
assert.NotEmpty(t, session.ID)
assert.Less(t, session.CreateAt, now)
assert.Less(t, session.LastActivityAt, now)
assert.Equal(t, model.SystemUserRoleId, session.Roles)
}
})
t.Run("Update", func(t *testing.T) {
th.BasicUser.Props = map[string]string{"testpropkey": "testpropvalue"}
th.App.UpdateUser(th.Context, 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]any{
"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]any{
"id": id,
},
}
resp, err := th.MakeGraphQLRequest(&input)
require.NoError(t, err)
require.Len(t, resp.Errors, 1)
})
}

Просмотреть файл

@@ -1,222 +0,0 @@
schema {
query: Query
}
type Query {
user(id: String!): User
config(): StringMap!
license(): StringMap!
teamMembers(userId: String!,
teamId: String = "",
excludeTeam: Boolean = false): [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 = "",
teamId: String = "",
excludeTeam: Boolean = false,
first: Int = 60,
after: String = "",
lastUpdateAt: Float = 0): [ChannelMember]!
sidebarCategories(userId: String!,
teamId: String!,
excludeTeam: Boolean = false): [SidebarCategory]!
}
scalar ChannelType
scalar SidebarCategoryType
scalar SidebarCategorySorting
scalar StringMap
scalar StringInterface
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
groupConstrained: Boolean
shared: Boolean
lastPostAt: Float!
totalMsgCount: Float!
totalMsgCountRoot: Float!
lastRootPostAt: Float!
extraUpdateAt: Float!
props: StringInterface!
policyId: String
cursor: String
}
type ChannelMember {
channel : Channel
user : User
roles : [Role]!
lastViewedAt : Float!
msgCount : Float!
mentionCount : Float!
urgentMentionCount: Float!
mentionCountRoot : Float!
msgCountRoot : Float!
notifyProps : StringMap!
lastUpdateAt : Float!
schemeGuest : Boolean!
schemeUser : Boolean!
schemeAdmin : Boolean!
explicitRoles : String!
cursor : String
}
# Deliberately omitting password, authData, mfaSecret.
type User {
id: String!
username: String!
email: String!
firstName: String!
lastName: String!
nickname: String!
emailVerified: Boolean!
isBot: Boolean!
isGuest: Boolean!
isSystemAdmin: Boolean!
createAt: Float!
updateAt: Float!
deleteAt: Float!
authService: String!
customStatus: CustomStatus
status: Status
props: StringMap!
notifyProps: StringMap!
lastPictureUpdate: Float!
lastPasswordUpdate: Float!
failedAttempts: Float!
locale: String!
timezone: StringMap!
position: String!
mfaActive: Boolean!
allowMarketing: Boolean!
remoteId: String
lastActivityAt: Float!
botDescription: String!
botLastIconUpdate: Float!
termsOfServiceId: String!
termsOfServiceCreateAt: Float!
disableWelcomeEmail: Boolean!
roles: [Role]!
preferences: [Preference!]!
sessions: [Session]!
}
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!
displayName: String!
description: String!
createAt: Float!
updateAt: Float!
deleteAt: Float!
permissions: [String!]!
schemeManaged: Boolean!
builtIn: Boolean!
}
type Preference {
userId: String!
category: String!
name: String!
value: String!
}
type Team {
id: String!
displayName : String!
name : String!
createAt : Float!
updateAt : Float!
deleteAt : Float!
description : String!
email : String!
type : String!
companyName : String!
allowedDomains : String!
inviteId : String!
lastTeamIconUpdate: Float!
groupConstrained: Boolean
allowOpenInvite: Boolean!
schemeId : String
policyId : String
cloudLimitsArchived: Boolean!
}
type TeamMember {
team: Team
user: User
roles: [Role]!
deleteAt: Float!
schemeGuest: Boolean!
schemeUser: Boolean!
schemeAdmin: Boolean!
explicitRoles: String!
}
type SidebarCategory {
id: String!
sorting: SidebarCategorySorting!
type: SidebarCategoryType!
displayName: String!
muted: Boolean!
collapsed: Boolean!
teamId: String!
channelIds: [String!]!
sortOrder: Float!
}
# Deliberately leaving out teamMembers.
type Session {
id: String!
token: String!
createAt: Float!
expiresAt: Float!
lastActivityAt: Float!
deviceId: String!
roles: String!
isOAuth: Boolean!
expiredNotify: Boolean!
props: StringMap!
local: Boolean!
}