Skip to content

Commit 542abf5

Browse files
authored
[WPB-1753] list user groups (#4607)
1 parent e05de7f commit 542abf5

File tree

27 files changed

+1145
-136
lines changed

27 files changed

+1145
-136
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ingress-nginx-controller nginx-ingress-services reaper restund \
2121
k8ssandra-test-cluster ldap-scim-bridge wire-server-enterprise
2222
KIND_CLUSTER_NAME := wire-server
2323
HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests
24+
PSQL_DB ?= wire-server
2425

2526
package ?= all
2627
EXE_SCHEMA := ./dist/$(package)-schema
@@ -343,7 +344,8 @@ cqlsh:
343344
psql:
344345
@grep -q wire-server:wire-server ~/.pgpass || \
345346
echo "consider running 'echo localhost:5432:wire-server:wire-server:posty-the-gres > ~/.pgpass ; chmod 600 ~/.pgpass '"
346-
psql -h localhost -p 5432 -U wire-server -w
347+
psql -h localhost -p 5432 -d $(PSQL_DB) -U wire-server -w || \
348+
echo 'if the database is missing, consider running "make postgres-reset", or setting $$PSQL_DB to the correct table space.'
347349

348350
.PHONY: db-reset-package
349351
db-reset-package:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
New end-point `GET /user-groups?...` for filtering, sorting, and pagination.

deploy/dockerephemeral/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ services:
290290
POSTGRES_PASSWORD: "posty-the-gres"
291291
POSTGRES_USER: "wire-server"
292292
POSTGRES_DB: "backendA"
293+
command: postgres -c max_connections=50
293294

294295
cassandra:
295296
container_name: demo_wire_cassandra

integration/test/API/Brig.hs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,10 +1056,35 @@ getUserGroup user gid = do
10561056
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid]
10571057
submit "GET" req
10581058

1059-
getUserGroups :: (MakesValue user) => user -> Maybe Int -> Maybe String -> App Response
1060-
getUserGroups user mbLimit mbLastKey = do
1061-
req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups"]
1062-
submit "GET" $ req & addQueryParams (catMaybes [(("limit",) . show) <$> mbLimit, ("last_key",) <$> mbLastKey])
1059+
data GetUserGroupsArgs = GetUserGroupsArgs
1060+
{ q :: Maybe String,
1061+
sortByKeys :: Maybe String,
1062+
sortOrder :: Maybe String,
1063+
pSize :: Maybe Int,
1064+
lastName :: Maybe String,
1065+
lastCreatedAt :: Maybe String,
1066+
lastId :: Maybe String
1067+
}
1068+
1069+
instance Default GetUserGroupsArgs where
1070+
def = GetUserGroupsArgs Nothing Nothing Nothing Nothing Nothing Nothing Nothing
1071+
1072+
getUserGroups :: (MakesValue user) => user -> GetUserGroupsArgs -> App Response
1073+
getUserGroups user GetUserGroupsArgs {..} = do
1074+
req <- baseRequest user Brig Versioned "user-groups"
1075+
submit "GET" $
1076+
req
1077+
& addQueryParams
1078+
( catMaybes
1079+
[ ("q",) <$> q,
1080+
("sort_by",) <$> sortByKeys,
1081+
("sort_order",) <$> sortOrder,
1082+
(("page_size",) . show) <$> pSize,
1083+
("last_seen_name",) <$> lastName,
1084+
("last_seen_created_at",) <$> lastCreatedAt,
1085+
("last_seen_id",) <$> lastId
1086+
]
1087+
)
10631088

10641089
updateUserGroup :: (MakesValue user, MakesValue userGroupUpdate) => user -> String -> userGroupUpdate -> App Response
10651090
updateUserGroup user gid userGroupUpdate = do

integration/test/Test/UserGroup.hs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Test.UserGroup where
44

55
import API.Brig
66
import API.Galley
7+
import Control.Error (lastMay)
78
import Notifications (isUserGroupCreatedNotif)
89
import SetupHelpers
910
import Testlib.Prelude
@@ -60,6 +61,10 @@ testUserGroupSmoke = do
6061
resp.json %. "name" `shouldMatch` "also good"
6162
resp.json %. "members" `shouldMatch` [mem2id, mem3id]
6263

64+
bindResponse (getUserGroups owner def) $ \resp -> do
65+
resp.status `shouldMatchInt` 200
66+
resp.json %. "page.0.name" `shouldMatch` "also good"
67+
6368
bindResponse (deleteUserGroup owner badGid) $ \resp -> do
6469
resp.status `shouldMatchInt` 404
6570

@@ -74,3 +79,181 @@ testUserGroupSmoke = do
7479

7580
bindResponse (removeUserFromGroup owner gid mem1id) $ \resp -> do
7681
resp.status `shouldMatchInt` 404
82+
83+
testUserGroupGetGroups :: (HasCallStack) => App ()
84+
testUserGroupGetGroups = do
85+
(owner, _team, []) <- createTeam OwnDomain 1
86+
87+
let groupNames = ["First group", "CC", "CCC"] <> ((: []) <$> ['A' .. 'G'])
88+
forM_ groupNames $ \gname -> do
89+
let newGroup = object ["name" .= gname, "members" .= ([] :: [()])]
90+
bindResponse (createUserGroup owner newGroup) $ \resp -> do
91+
resp.status `shouldMatchInt` 200
92+
resp.json %. "name" `shouldMatch` gname
93+
resp.json %. "members" `shouldMatch` ([] :: [()])
94+
95+
-- Default sort by is createdAt, and sortOrder is DESC
96+
_ <- runSearch owner def {q = Just "C"} ["C", "CCC", "CC"]
97+
98+
-- Default sortOrder is DESC, regardless of sortBy
99+
_ <- runSearch owner def {q = Just "CC", sortByKeys = Just "name"} ["CCC", "CC"]
100+
101+
-- Test combinations of sortBy and sortOrder:
102+
_ <-
103+
runSearch
104+
owner
105+
def {sortByKeys = Just "name", sortOrder = Just "asc"}
106+
[ "A",
107+
"B",
108+
"C",
109+
"CC",
110+
"CCC",
111+
"D",
112+
"E",
113+
"F",
114+
"First group",
115+
"G"
116+
]
117+
_ <-
118+
runSearch
119+
owner
120+
def {sortByKeys = Just "name", sortOrder = Just "desc"}
121+
( reverse
122+
[ "A",
123+
"B",
124+
"C",
125+
"CC",
126+
"CCC",
127+
"D",
128+
"E",
129+
"F",
130+
"First group",
131+
"G"
132+
]
133+
)
134+
_ <-
135+
runSearch
136+
owner
137+
def {sortByKeys = Just "created_at", sortOrder = Just "asc"}
138+
[ "First group",
139+
"CC",
140+
"CCC",
141+
"A",
142+
"B",
143+
"C",
144+
"D",
145+
"E",
146+
"F",
147+
"G"
148+
]
149+
_ <-
150+
runSearch
151+
owner
152+
def {sortByKeys = Just "created_at", sortOrder = Just "desc"}
153+
( reverse
154+
[ "First group",
155+
"CC",
156+
"CCC",
157+
"A",
158+
"B",
159+
"C",
160+
"D",
161+
"E",
162+
"F",
163+
"G"
164+
]
165+
)
166+
167+
-- Test sorting and filtering works across pages
168+
let firstPageParams = def {sortByKeys = Just "name", sortOrder = Just "desc", pSize = Just 3}
169+
Just (name1, createdAt1, id1) <-
170+
runSearch
171+
owner
172+
firstPageParams
173+
[ "G",
174+
"First group",
175+
"F"
176+
]
177+
Just (name2, createdAt2, id2) <-
178+
runSearch
179+
owner
180+
firstPageParams {lastName = Just name1, lastCreatedAt = Just createdAt1, lastId = Just id1}
181+
[ "E",
182+
"D",
183+
"CCC"
184+
]
185+
Just (name3, createdAt3, id3) <-
186+
runSearch
187+
owner
188+
firstPageParams {lastName = Just name2, lastCreatedAt = Just createdAt2, lastId = Just id2}
189+
[ "CC",
190+
"C",
191+
"B"
192+
]
193+
194+
void
195+
$ runSearch
196+
owner
197+
firstPageParams {lastName = Just name3, lastCreatedAt = Just createdAt3, lastId = Just id3}
198+
["A"]
199+
200+
runSearch :: (HasCallStack, MakesValue owner) => owner -> GetUserGroupsArgs -> [String] -> App (Maybe (String, String, String))
201+
runSearch owner args expected =
202+
bindResponse (getUserGroups owner args) $ \resp -> do
203+
resp.status `shouldMatchInt` 200
204+
found <- ((%. "name") `mapM`) =<< asList =<< resp.json %. "page"
205+
found `shouldMatch` expected
206+
results <- asList $ resp.json %. "page"
207+
for (lastMay results) $ \lastGroup ->
208+
(,,)
209+
<$> asString (lastGroup %. "name")
210+
<*> asString (lastGroup %. "createdAt")
211+
<*> asString (lastGroup %. "id")
212+
213+
testUserGroupGetGroupsAllInputs :: (HasCallStack) => App ()
214+
testUserGroupGetGroupsAllInputs = do
215+
(owner, _team, []) <- createTeam OwnDomain 1
216+
for_ ((: []) <$> ['A' .. 'Z']) $ \gname -> do
217+
let newGroup = object ["name" .= gname, "members" .= ([] :: [()])]
218+
createUserGroup owner newGroup >>= assertSuccess
219+
220+
Just (ln, ltz, lid) <- runSearch owner def {pSize = Just 3} ["Z", "Y", "X"]
221+
let getUserGroupArgs = getUserGroupArgsCombinations ln ltz lid
222+
for_ getUserGroupArgs $ \args -> do
223+
bindResponse (getUserGroups owner args) $ \resp -> do
224+
-- most important check is that all combinations return 200
225+
resp.status `shouldMatchInt` 200
226+
-- additionally we can check a few invariants
227+
groups <- resp.json %. "page" >>= asList
228+
case (args.q, args.lastName, args.lastCreatedAt, args.lastId) of
229+
(Nothing, Nothing, Nothing, Nothing) -> length groups `shouldMatchInt` (fromMaybe 15 args.pSize)
230+
(Just _, Nothing, Nothing, Nothing) -> length groups `shouldMatchInt` 1
231+
_ -> pure ()
232+
where
233+
getUserGroupArgsCombinations :: String -> String -> String -> [GetUserGroupsArgs]
234+
getUserGroupArgsCombinations ln ltz lid =
235+
[ GetUserGroupsArgs
236+
{ q = q',
237+
sortByKeys = sortBy',
238+
sortOrder = sortOrder',
239+
pSize = pSize',
240+
lastName = lastName',
241+
lastCreatedAt = lastCreatedAt',
242+
lastId = lastId'
243+
}
244+
| q' <- qs,
245+
sortBy' <- sortByKeysList,
246+
sortOrder' <- sortOrders,
247+
pSize' <- pSizes,
248+
lastName' <- lastNames,
249+
lastCreatedAt' <- lastCreatedAts,
250+
lastId' <- lastIds
251+
]
252+
where
253+
qs = [Nothing, Just "A"]
254+
sortByKeysList = [Nothing, Just "name", Just "created_at"]
255+
sortOrders = [Nothing, Just "asc", Just "desc"]
256+
pSizes = [Nothing, Just 3]
257+
lastNames = [Nothing, Just ln]
258+
lastCreatedAts = [Nothing, Just ltz]
259+
lastIds = [Nothing, Just lid]

libs/types-common/src/Data/Json/Util.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ newtype UTCTimeMillis = UTCTimeMillis {fromUTCTimeMillis :: UTCTime}
101101
deriving (Eq, Ord, Generic)
102102
deriving (FromJSON, ToJSON, S.ToSchema) via Schema UTCTimeMillis
103103

104+
instance S.ToParamSchema UTCTimeMillis
105+
104106
instance ToSchema UTCTimeMillis where
105107
schema =
106108
UTCTimeMillis
@@ -144,6 +146,16 @@ instance Show UTCTimeMillis where
144146
instance BS.ToByteString UTCTimeMillis where
145147
builder = BB.byteString . UTF8.fromString . show
146148

149+
instance ToHttpApiData UTCTimeMillis where
150+
toUrlPiece = showUTCTimeMillis
151+
152+
instance FromHttpApiData UTCTimeMillis where
153+
parseUrlPiece raw =
154+
maybe (Left $ "Could not parse UTCTimeMillis: " <> raw) Right
155+
. readUTCTimeMillis
156+
. Text.unpack
157+
$ raw
158+
147159
instance BS.FromByteString UTCTimeMillis where
148160
parser = maybe (fail "UTCTimeMillis") pure . readUTCTimeMillis =<< BS.parser
149161

libs/wire-api/src/Wire/API/Routes/Public/Brig.hs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Data.CommaSeparatedList (CommaSeparatedList)
2727
import Data.Domain
2828
import Data.Handle
2929
import Data.Id as Id
30+
import Data.Json.Util
3031
import Data.Misc
3132
import Data.Nonce (Nonce)
3233
import Data.OpenApi hiding (Contact, Header, Schema, ToSchema)
@@ -82,6 +83,7 @@ import Wire.API.User.Password (CompletePasswordReset, NewPasswordReset, Password
8283
import Wire.API.User.RichInfo (RichInfoAssocList)
8384
import Wire.API.User.Search (Contact, PagingState, RoleFilter, SearchResult, TeamContact, TeamUserSearchSortBy, TeamUserSearchSortOrder)
8485
import Wire.API.UserGroup
86+
import Wire.API.UserGroup.Pagination
8587
import Wire.API.UserMap
8688

8789
type BrigAPI =
@@ -290,6 +292,12 @@ type UserAPI =
290292
(Respond 200 "Protocols supported by the user" (Set BaseProtocolTag))
291293
)
292294

295+
type LastSeenNameDesc = Description "`name` of the last seen user group, used to get the next page when sorting by name."
296+
297+
type LastSeenCreatedAtDesc = Description "`created_at` field of the last seen user group, used to get the next page when sorting by created_at."
298+
299+
type LastSeenIdDesc = Description "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page."
300+
293301
type UserGroupAPI =
294302
Named
295303
"create-user-group"
@@ -316,6 +324,20 @@ type UserGroupAPI =
316324
]
317325
(Maybe UserGroup)
318326
)
327+
:<|> Named
328+
"get-user-groups"
329+
( From 'V10
330+
:> ZLocalUser
331+
:> "user-groups"
332+
:> QueryParam' '[Optional, Strict, Description "Search string"] "q" Text
333+
:> QueryParam' '[Optional, Strict] "sort_by" SortBy
334+
:> QueryParam' '[Optional, Strict] "sort_order" SortOrder
335+
:> QueryParam' '[Optional, Strict] "page_size" PageSize
336+
:> QueryParam' '[Optional, Strict, LastSeenNameDesc] "last_seen_name" UserGroupName
337+
:> QueryParam' '[Optional, Strict, LastSeenCreatedAtDesc] "last_seen_created_at" UTCTimeMillis
338+
:> QueryParam' '[Optional, Strict, LastSeenIdDesc] "last_seen_id" UserGroupId
339+
:> Get '[JSON] UserGroupPage
340+
)
319341
:<|> Named
320342
"update-user-group"
321343
( From 'V10

0 commit comments

Comments
 (0)