From 9441f0863e30efeae12d9aefdd55fbc6ae35c63b Mon Sep 17 00:00:00 2001 From: Anton Stuetz Date: Thu, 17 Oct 2019 21:09:29 +0200 Subject: [PATCH] Implement user directory for local users #649 Signed-off-by: Anton Stuetz --- .../auth/storage/accounts/profile_table.go | 52 +++- clientapi/auth/storage/accounts/storage.go | 69 ++++++ clientapi/routing/register.go | 1 - cmd/dendrite-monolith-server/main.go | 3 + cmd/dendrite-userdirectory-api-server/main.go | 34 +++ common/config/config.go | 4 +- common/config/config_test.go | 1 + dendrite-config.yaml | 1 + docker/dendrite-docker.yml | 1 + docker/docker-compose.yml | 12 + docker/services/userdirectory-api.sh | 5 + userdirectoryapi/README.md | 5 + userdirectoryapi/routing/routing.go | 50 ++++ userdirectoryapi/search/search.go | 80 +++++++ userdirectoryapi/search/search_test.go | 224 ++++++++++++++++++ userdirectoryapi/types/searchtypes.go | 21 ++ userdirectoryapi/userdirectoryapi.go | 28 +++ 17 files changed, 585 insertions(+), 6 deletions(-) create mode 100644 cmd/dendrite-userdirectory-api-server/main.go create mode 100644 docker/services/userdirectory-api.sh create mode 100644 userdirectoryapi/README.md create mode 100644 userdirectoryapi/routing/routing.go create mode 100644 userdirectoryapi/search/search.go create mode 100644 userdirectoryapi/search/search_test.go create mode 100644 userdirectoryapi/types/searchtypes.go create mode 100644 userdirectoryapi/userdirectoryapi.go diff --git a/clientapi/auth/storage/accounts/profile_table.go b/clientapi/auth/storage/accounts/profile_table.go index 157bb99b0..1ae2d6c88 100644 --- a/clientapi/auth/storage/accounts/profile_table.go +++ b/clientapi/auth/storage/accounts/profile_table.go @@ -18,6 +18,12 @@ import ( "context" "database/sql" + "github.com/matrix-org/dendrite/clientapi/userutil" + + "github.com/matrix-org/gomatrixserverlib" + + searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" ) @@ -39,6 +45,9 @@ const insertProfileSQL = "" + const selectProfileByLocalpartSQL = "" + "SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart = $1" +const selectSearchTermLikeLocalpartOrDisplayName = "" + + "SELECT localpart, display_name, avatar_url FROM account_profiles WHERE localpart LIKE $1 OR display_name LIKE $1 LIMIT $2" + const setAvatarURLSQL = "" + "UPDATE account_profiles SET avatar_url = $1 WHERE localpart = $2" @@ -46,10 +55,11 @@ const setDisplayNameSQL = "" + "UPDATE account_profiles SET display_name = $1 WHERE localpart = $2" type profilesStatements struct { - insertProfileStmt *sql.Stmt - selectProfileByLocalpartStmt *sql.Stmt - setAvatarURLStmt *sql.Stmt - setDisplayNameStmt *sql.Stmt + insertProfileStmt *sql.Stmt + selectProfileByLocalpartStmt *sql.Stmt + selectSearchTermLikeLocalpartOrDisplayNameStmt *sql.Stmt + setAvatarURLStmt *sql.Stmt + setDisplayNameStmt *sql.Stmt } func (s *profilesStatements) prepare(db *sql.DB) (err error) { @@ -69,6 +79,9 @@ func (s *profilesStatements) prepare(db *sql.DB) (err error) { if s.setDisplayNameStmt, err = db.Prepare(setDisplayNameSQL); err != nil { return } + if s.selectSearchTermLikeLocalpartOrDisplayNameStmt, err = db.Prepare(selectSearchTermLikeLocalpartOrDisplayName); err != nil { + return + } return } @@ -92,6 +105,37 @@ func (s *profilesStatements) selectProfileByLocalpart( return &profile, nil } +func (s *profilesStatements) selectSearchTermLikeLocalpartOrDisplayName( + ctx context.Context, searchTerm string, limit int8, serverName gomatrixserverlib.ServerName, +) (*[]searchtypes.SearchResult, bool, error) { + //increase limit by one to find out if the query was limited + rows, err := s.selectSearchTermLikeLocalpartOrDisplayNameStmt.QueryContext(ctx, searchTerm, limit+1) + if err != nil { + return nil, false, err + } + searchResults := []searchtypes.SearchResult{} + counter := int8(1) + limited := false + for rows.Next() { + if counter > limit { + limited = true + break + } + var r searchtypes.SearchResult + err = rows.Scan( + &r.UserId, &r.DisplayName, &r.AvatarUrl, + ) + r.UserId = userutil.MakeUserID(r.UserId, serverName) + if err != nil { + return &searchResults, false, err + } + searchResults = append(searchResults, r) + counter++ + } + + return &searchResults, limited, nil +} + func (s *profilesStatements) setAvatarURL( ctx context.Context, localpart string, avatarURL string, ) (err error) { diff --git a/clientapi/auth/storage/accounts/storage.go b/clientapi/auth/storage/accounts/storage.go index 020a38376..d9d204be5 100644 --- a/clientapi/auth/storage/accounts/storage.go +++ b/clientapi/auth/storage/accounts/storage.go @@ -19,6 +19,8 @@ import ( "database/sql" "errors" + searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/gomatrixserverlib" @@ -28,6 +30,69 @@ import ( _ "github.com/lib/pq" ) +type AccountDatabase interface { + GetAccountByPassword( + ctx context.Context, localpart, plaintextPassword string, + ) (*authtypes.Account, error) + GetProfileByLocalpart( + ctx context.Context, localpart string, + ) (*authtypes.Profile, error) + SetAvatarURL( + ctx context.Context, localpart string, avatarURL string, + ) error + SetDisplayName( + ctx context.Context, localpart string, displayName string, + ) error + CreateAccount( + ctx context.Context, localpart, plaintextPassword, appserviceID string, + ) (*authtypes.Account, error) + UpdateMemberships( + ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string, + ) error + GetMembershipInRoomByLocalpart( + ctx context.Context, localpart, roomID string, + ) (authtypes.Membership, error) + GetMembershipsByLocalpart( + ctx context.Context, localpart string, + ) (memberships []authtypes.Membership, err error) + SaveAccountData( + ctx context.Context, localpart, roomID, dataType, content string, + ) error + GetAccountData(ctx context.Context, localpart string) ( + global []gomatrixserverlib.ClientEvent, + rooms map[string][]gomatrixserverlib.ClientEvent, + err error, + ) + GetAccountDataByType( + ctx context.Context, localpart, roomID, dataType string, + ) (data *gomatrixserverlib.ClientEvent, err error) + GetNewNumericLocalpart( + ctx context.Context, + ) (int64, error) + SaveThreePIDAssociation( + ctx context.Context, threepid, localpart, medium string, + ) (err error) + RemoveThreePIDAssociation( + ctx context.Context, threepid string, medium string, + ) (err error) + GetLocalpartForThreePID( + ctx context.Context, threepid string, medium string, + ) (localpart string, err error) + GetThreePIDsForLocalpart( + ctx context.Context, localpart string, + ) (threepids []authtypes.ThreePID, err error) + GetFilter( + ctx context.Context, localpart string, filterID string, + ) (*gomatrixserverlib.Filter, error) + PutFilter( + ctx context.Context, localpart string, filter *gomatrixserverlib.Filter, + ) (string, error) + CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) + GetAccountByLocalpart(ctx context.Context, localpart string, + ) (*authtypes.Account, error) + SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error) +} + // Database represents an account database type Database struct { db *sql.DB @@ -379,3 +444,7 @@ func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string, ) (*authtypes.Account, error) { return d.accounts.selectAccountByLocalpart(ctx, localpart) } + +func (d *Database) SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error) { + return d.profiles.selectSearchTermLikeLocalpartOrDisplayName(ctx, searchTerm, limit, d.serverName) +} diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index d0f36a6fd..42b006f6b 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -517,7 +517,6 @@ func handleRegistrationFlow( deviceDB *devices.Database, ) util.JSONResponse { // TODO: Shared secret registration (create new user scripts) - // TODO: Enable registration config flag // TODO: Guest account upgrading // TODO: Handle loading of previous session parameters from database. diff --git a/cmd/dendrite-monolith-server/main.go b/cmd/dendrite-monolith-server/main.go index 0a320616e..8381f576c 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -18,6 +18,8 @@ import ( "flag" "net/http" + "github.com/matrix-org/dendrite/userdirectoryapi" + "github.com/matrix-org/dendrite/appservice" "github.com/matrix-org/dendrite/clientapi" "github.com/matrix-org/dendrite/common" @@ -71,6 +73,7 @@ func main() { mediaapi.SetupMediaAPIComponent(base, deviceDB) publicroomsapi.SetupPublicRoomsAPIComponent(base, deviceDB) syncapi.SetupSyncAPIComponent(base, deviceDB, accountDB, query) + userdirectoryapi.SetupUserDirectoryApi(base, accountDB, deviceDB) httpHandler := common.WrapHandlerInCORS(base.APIMux) diff --git a/cmd/dendrite-userdirectory-api-server/main.go b/cmd/dendrite-userdirectory-api-server/main.go new file mode 100644 index 000000000..ead3eb80c --- /dev/null +++ b/cmd/dendrite-userdirectory-api-server/main.go @@ -0,0 +1,34 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/matrix-org/dendrite/common/basecomponent" + "github.com/matrix-org/dendrite/userdirectoryapi" +) + +func main() { + cfg := basecomponent.ParseFlags() + base := basecomponent.NewBaseDendrite(cfg, "PublicRoomsAPI") + defer base.Close() // nolint: errcheck + + deviceDB := base.CreateDeviceDB() + accountDB := base.CreateAccountsDB() + + userdirectoryapi.SetupUserDirectoryApi(base, accountDB, deviceDB) + + base.SetupAndServeHTTP(string(base.Cfg.Bind.UserDirectoryAPI), string(base.Cfg.Listen.UserDirectoryAPI)) + +} diff --git a/common/config/config.go b/common/config/config.go index 0332d0358..ed732eb0f 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -30,7 +30,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/sirupsen/logrus" "golang.org/x/crypto/ed25519" - yaml "gopkg.in/yaml.v2" + "gopkg.in/yaml.v2" jaegerconfig "github.com/uber/jaeger-client-go/config" jaegermetrics "github.com/uber/jaeger-lib/metrics" @@ -206,6 +206,7 @@ type Dendrite struct { RoomServer Address `yaml:"room_server"` FederationSender Address `yaml:"federation_sender"` PublicRoomsAPI Address `yaml:"public_rooms_api"` + UserDirectoryAPI Address `yaml:"user_directory_api"` TypingServer Address `yaml:"typing_server"` } `yaml:"bind"` @@ -219,6 +220,7 @@ type Dendrite struct { RoomServer Address `yaml:"room_server"` FederationSender Address `yaml:"federation_sender"` PublicRoomsAPI Address `yaml:"public_rooms_api"` + UserDirectoryAPI Address `yaml:"user_directory_api"` TypingServer Address `yaml:"typing_server"` } `yaml:"listen"` diff --git a/common/config/config_test.go b/common/config/config_test.go index 110c8b84c..6a6b4cdb5 100644 --- a/common/config/config_test.go +++ b/common/config/config_test.go @@ -63,6 +63,7 @@ listen: media_api: "localhost:7774" appservice_api: "localhost:7777" typing_server: "localhost:7778" + user_directory_api: "localhost:7779" logging: - type: "file" level: "info" diff --git a/dendrite-config.yaml b/dendrite-config.yaml index a8d39aa1e..fb9854485 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -115,6 +115,7 @@ listen: federation_sender: "localhost:7776" appservice_api: "localhost:7777" typing_server: "localhost:7778" + user_directory_api: "localhost:7779" # The configuration for tracing the dendrite components. tracing: diff --git a/docker/dendrite-docker.yml b/docker/dendrite-docker.yml index abb8c3307..fdd813ff4 100644 --- a/docker/dendrite-docker.yml +++ b/docker/dendrite-docker.yml @@ -115,6 +115,7 @@ listen: public_rooms_api: "public_rooms_api:7775" federation_sender: "federation_sender:7776" typing_server: "typing_server:7777" + user_directory_api: "localhost:7779" # The configuration for tracing the dendrite components. tracing: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9cf67457c..f291ab775 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -134,6 +134,18 @@ services: networks: - internal + userdirectory_api: + container_name: dendrite_userdirectory_api + hostname: userdirectory_api + entrypoint: ["bash", "./docker/services/userdirectory-api.sh"] + build: ./ + volumes: + - ..:/build + depends_on: + - postgres + networks: + - internal + federation_sender: container_name: dendrite_federation_sender hostname: federation_sender diff --git a/docker/services/userdirectory-api.sh b/docker/services/userdirectory-api.sh new file mode 100644 index 000000000..0daf30443 --- /dev/null +++ b/docker/services/userdirectory-api.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +bash ./docker/build.sh + +./bin/dendrite-userdirectory-api-server --config dendrite.yaml diff --git a/userdirectoryapi/README.md b/userdirectoryapi/README.md new file mode 100644 index 000000000..88dd38ab5 --- /dev/null +++ b/userdirectoryapi/README.md @@ -0,0 +1,5 @@ +# User Directory API + +This server is responsible for serving requests hitting `/user_directory` as per: + +https://matrix.org/docs/spec/client_server/r0.5.0#post-matrix-client-r0-user-directory-search diff --git a/userdirectoryapi/routing/routing.go b/userdirectoryapi/routing/routing.go new file mode 100644 index 000000000..55e840e9e --- /dev/null +++ b/userdirectoryapi/routing/routing.go @@ -0,0 +1,50 @@ +// Copyright 2019 Anton Stuetz +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routing + +import ( + "net/http" + + "github.com/matrix-org/dendrite/userdirectoryapi/search" + + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" + + "github.com/matrix-org/dendrite/clientapi/auth" + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/common" + "github.com/matrix-org/util" + + "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" +) + +const pathPrefixR0 = "/_matrix/client/r0" + +func Setup(apiMux *mux.Router, accountDB *accounts.Database, deviceDB *devices.Database) { + + r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() + + authData := auth.Data{ + AccountDB: nil, + DeviceDB: deviceDB, + AppServices: nil, + } + + r0mux.Handle("/user_directory/search", + common.MakeAuthAPI("user_directory_search", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse { + return search.Search(req, accountDB) + })).Methods(http.MethodPost) + +} diff --git a/userdirectoryapi/search/search.go b/userdirectoryapi/search/search.go new file mode 100644 index 000000000..5573e97f5 --- /dev/null +++ b/userdirectoryapi/search/search.go @@ -0,0 +1,80 @@ +// Copyright 2019 Anton Stuetz +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package search + +import ( + "errors" + "net/http" + + "github.com/matrix-org/dendrite/clientapi/httputil" + + "github.com/matrix-org/dendrite/clientapi/jsonerror" + searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types" + "github.com/matrix-org/util" + + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" +) + +const ( + DEFAULT_LIMIT = 10 +) + +type userSearchRequest struct { + SearchTerm string `json:"search_term"` + Limit int8 `json:"limit"` +} +type userSearchResponse struct { + Results *[]searchtypes.SearchResult `json:"results"` + Limited bool `json:"limited"` +} + +func Search(req *http.Request, accountDB accounts.AccountDatabase) util.JSONResponse { + var r userSearchRequest + resErr := httputil.UnmarshalJSONRequest(req, &r) + if resErr != nil { + return *resErr + } + + err := validateSearchTerm(r.SearchTerm) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.InvalidArgumentValue(err.Error()), + } + } + if r.Limit == 0 { + r.Limit = DEFAULT_LIMIT + } + results, limited, err := accountDB.SearchUserIdAndDisplayName(req.Context(), r.SearchTerm+"%", r.Limit) + if err != nil { + return util.ErrorResponse(err) + } + return mapProfilesToResponse(results, limited) +} + +func validateSearchTerm(search_term string) error { + if len(search_term) == 0 { + return errors.New("Search term is empty") + } + return nil +} + +func mapProfilesToResponse(results *[]searchtypes.SearchResult, limited bool) util.JSONResponse { + return util.JSONResponse{ + Code: http.StatusOK, + JSON: userSearchResponse{results, limited}, + } + +} diff --git a/userdirectoryapi/search/search_test.go b/userdirectoryapi/search/search_test.go new file mode 100644 index 000000000..ebce3a1ec --- /dev/null +++ b/userdirectoryapi/search/search_test.go @@ -0,0 +1,224 @@ +package search + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + searchtypes "github.com/matrix-org/dendrite/userdirectoryapi/types" + "github.com/matrix-org/gomatrixserverlib" +) + +type MockAccountDatabase struct { + limited bool + wantError bool +} + +func (d *MockAccountDatabase) GetAccountByPassword( + ctx context.Context, localpart, plaintextPassword string, +) (*authtypes.Account, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetProfileByLocalpart( + ctx context.Context, localpart string, +) (*authtypes.Profile, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) SetAvatarURL( + ctx context.Context, localpart string, avatarURL string, +) error { + panic("implement me") +} + +func (d *MockAccountDatabase) SetDisplayName( + ctx context.Context, localpart string, displayName string, +) error { + panic("implement me") +} + +func (d *MockAccountDatabase) CreateAccount( + ctx context.Context, localpart, plaintextPassword, appserviceID string, +) (*authtypes.Account, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) UpdateMemberships( + ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string, +) error { + panic("implement me") +} + +func (d *MockAccountDatabase) GetMembershipInRoomByLocalpart( + ctx context.Context, localpart, roomID string, +) (authtypes.Membership, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetMembershipsByLocalpart( + ctx context.Context, localpart string, +) (memberships []authtypes.Membership, err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) SaveAccountData( + ctx context.Context, localpart, roomID, dataType, content string, +) error { + panic("implement me") +} + +func (d *MockAccountDatabase) GetAccountData(ctx context.Context, localpart string) ( + global []gomatrixserverlib.ClientEvent, + rooms map[string][]gomatrixserverlib.ClientEvent, + err error, +) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetAccountDataByType( + ctx context.Context, localpart, roomID, dataType string, +) (data *gomatrixserverlib.ClientEvent, err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetNewNumericLocalpart( + ctx context.Context, +) (int64, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) SaveThreePIDAssociation( + ctx context.Context, threepid, localpart, medium string, +) (err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) RemoveThreePIDAssociation( + ctx context.Context, threepid string, medium string, +) (err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetLocalpartForThreePID( + ctx context.Context, threepid string, medium string, +) (localpart string, err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetThreePIDsForLocalpart( + ctx context.Context, localpart string, +) (threepids []authtypes.ThreePID, err error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetFilter( + ctx context.Context, localpart string, filterID string, +) (*gomatrixserverlib.Filter, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) PutFilter( + ctx context.Context, localpart string, filter *gomatrixserverlib.Filter, +) (string, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) GetAccountByLocalpart(ctx context.Context, localpart string, +) (*authtypes.Account, error) { + panic("implement me") +} + +func (d *MockAccountDatabase) SearchUserIdAndDisplayName(ctx context.Context, searchTerm string, limit int8) (*[]searchtypes.SearchResult, bool, error) { + var searchResults []searchtypes.SearchResult + searchResults = append(searchResults, searchtypes.SearchResult{UserId: "alice:localhost", AvatarUrl: "", DisplayName: ""}) + if d.wantError { + return nil, false, fmt.Errorf("") + } else { + return &searchResults, d.limited, nil + } +} + +func TestSearch(t *testing.T) { + type testCase struct { + name string + wantError bool + errorCode int + results []string + limited bool + requestBody string + } + tt := []testCase{ + { + "Find user do not hit limit", + false, + 0, + []string{"alice:localhost"}, + false, + "{ \"search_term\": \"alice\" }", + }, + { + "Find user do hit limit", + false, + 0, + []string{"alice:localhost"}, + true, + "{ \"search_term\": \"alice\" }", + }, + { + "Find user and fail", + true, + 500, + []string{"alice:localhost"}, + false, + "{ \"search_term\": \"alice\" }", + }, + { + "Find user and fail to parse request", + true, + 400, + []string{"alice:localhost"}, + false, + "{ \"search_term\": \"alicINVALID }", + }, + } + + setupAccountDb := func(limited bool, wantError bool) accounts.AccountDatabase { + return &MockAccountDatabase{limited, wantError} + } + + for _, tc := range tt { + fmt.Printf("Executing %s", tc.name) + requestBody := ioutil.NopCloser(strings.NewReader(tc.requestBody)) + req := &http.Request{Body: requestBody} + searchResults := Search(req, setupAccountDb(tc.limited, tc.wantError)) + if tc.wantError { + assert.EqualValues(t, searchResults.Code, tc.errorCode) + } else { + response := searchResults.JSON.(userSearchResponse) + for _, result := range tc.results { + exists := false + for _, item := range *response.Results { + if item.UserId == result { + exists = true + break + } + } + assert.EqualValues(t, true, exists, "Expected value not found in response") + } + } + } + +} diff --git a/userdirectoryapi/types/searchtypes.go b/userdirectoryapi/types/searchtypes.go new file mode 100644 index 000000000..1c8dd5be3 --- /dev/null +++ b/userdirectoryapi/types/searchtypes.go @@ -0,0 +1,21 @@ +// Copyright 2019 Anton Stuetz +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package searchtypes + +type SearchResult struct { + UserId string `json:"user_id"` + DisplayName string `json:"display_name,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` +} diff --git a/userdirectoryapi/userdirectoryapi.go b/userdirectoryapi/userdirectoryapi.go new file mode 100644 index 000000000..c098585cb --- /dev/null +++ b/userdirectoryapi/userdirectoryapi.go @@ -0,0 +1,28 @@ +// Copyright 2019 Anton Stuetz +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package userdirectoryapi + +import ( + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/auth/storage/devices" + "github.com/matrix-org/dendrite/common/basecomponent" + "github.com/matrix-org/dendrite/userdirectoryapi/routing" +) + +func SetupUserDirectoryApi(base *basecomponent.BaseDendrite, accountDB *accounts.Database, deviceDB *devices.Database) { + + routing.Setup(base.APIMux, accountDB, deviceDB) + +}