{-# OPTIONS -Wno-incomplete-patterns -Wno-ambiguous-fields #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2025 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Test.Apps where

import API.Brig
import qualified API.BrigInternal as BrigI
import API.Galley
import Data.Aeson.QQ.Simple
import SetupHelpers
import Testlib.Prelude

testCreateApp :: (HasCallStack) => App ()
testCreateApp :: HasCallStack => App ()
testCreateApp = do
  -- FUTUREWORK: what about federation?
  domain <- Domain -> App Value
forall a. (MakesValue a, HasCallStack) => a -> App Value
make Domain
OwnDomain
  (owner, tid, [regularMember]) <- createTeam domain 2
  let new =
        NewApp
forall a. Default a => a
def
          { name = "chappie",
            description = "some description of this app",
            category = "ai"
          } ::
          NewApp

  -- Regular team member can't create apps
  bindResponse (createApp regularMember tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"label" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"app-no-permission"

  -- Owner can create an app
  (appId, cookie) <- bindResponse (createApp owner tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    appId <- Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user.id" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString
    cookie <- resp.json %. "cookie" & asString
    pure (appId, cookie)

  -- App user should have type "app"
  let appIdObject = [Pair] -> Value
object [String
"domain" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Value
domain, String
"id" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
appId]
  bindResponse (getUser owner appIdObject) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"type" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"app"

  -- getApp, getApps
  bindResponse (getApp owner tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
  bindResponse (getApps owner tid) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    App Value -> App ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (App Value -> App ()) -> App Value -> App ()
forall a b. (a -> b) -> a -> b
$ Response
resp.json Maybe Value -> (Maybe Value -> App [Value]) -> App [Value]
forall a b. a -> (a -> b) -> b
& Maybe Value -> App [Value]
forall a. (HasCallStack, MakesValue a) => a -> App [Value]
asList App [Value] -> ([Value] -> App Value) -> App Value
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= [Value] -> App Value
forall (t :: * -> *) a. (HasCallStack, Foldable t) => t a -> App a
assertOne
  bindResponse (createApp owner tid (new {name = "fmappie"})) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
  bindResponse (getApps owner tid) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    apps <- Response
resp.json Maybe Value -> (Maybe Value -> App [Value]) -> App [Value]
forall a b. a -> (a -> b) -> b
& Maybe Value -> App [Value]
forall a. (HasCallStack, MakesValue a) => a -> App [Value]
asList
    name1 <- apps %. "0.name"
    name2 <- apps %. "1.name"
    [name1, name2] `shouldMatchSet` ["chappie", "fmappie"]

  -- Creator should have type "regular"
  bindResponse (getUser owner owner) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"type" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"regular"

  void $ bindResponse (renewToken domain cookie) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
appId
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"token_type" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"Bearer"
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"access_token" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString

  -- Get app for the app created above succeeds
  void $ getApp regularMember tid appId `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    (Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"name") App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"chappie"
    (Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"description") App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"some description of this app"
    (Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"category") App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"ai"

  -- A teamless user can't get the app
  outsideUser <- randomUser domain def
  bindResponse (getApp outsideUser tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"label" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"app-no-permission"

  -- Another team's owner nor member can't get the app
  (owner2, tid2, [regularMember2]) <- createTeam domain 2
  bindResponse (getApp owner2 tid appId) $ \Response
resp -> Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403
  bindResponse (getApp owner2 tid2 appId) $ \Response
resp -> Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
404
  bindResponse (getApp regularMember2 tid appId) $ \Response
resp -> Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403

  -- Category must be any of the values for the Category enum
  void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
400

  let foundUserType :: (HasCallStack) => Value -> String -> [String] -> App ()
      foundUserType Value
searcher String
exactMatchTerm [String]
aTypes =
        Value -> String -> Domain -> App Response
forall user searchTerm domain.
(MakesValue user, MakesValue searchTerm, MakesValue domain) =>
user -> searchTerm -> domain -> App Response
searchContacts Value
searcher String
exactMatchTerm Domain
OwnDomain App Response -> (Response -> App ()) -> App ()
forall a.
HasCallStack =>
App Response -> (Response -> App a) -> App a
`bindResponse` \Response
resp -> do
          Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
          foundDocs :: [Value] <- Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"documents" App Value -> (Value -> App [Value]) -> App [Value]
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Value -> App [Value]
forall a. (HasCallStack, MakesValue a) => a -> App [Value]
asList
          docsInTeam :: [Value] <- do
            -- make sure that matches from previous test runs don't get in the way.
            catMaybes
              <$> forM
                foundDocs
                ( \Value
doc -> do
                    tidActual <- Value
doc Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"team" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString
                    pure $ if tidActual == tid then Just doc else Nothing
                )

          (%. "type") `mapM` docsInTeam `shouldMatch` aTypes

  -- App's user is findable from /search/contacts
  BrigI.refreshIndex domain
  foundUserType owner new.name ["app"]
  foundUserType regularMember new.name ["app"]

  -- App's user is *not* findable from other team.
  BrigI.refreshIndex domain
  foundUserType owner2 new.name []

  -- Regular members still have the type "regular"
  memberName <- regularMember %. "name" & asString
  foundUserType owner memberName ["regular"]

testRefreshAppCookie :: (HasCallStack) => App ()
testRefreshAppCookie :: HasCallStack => App ()
testRefreshAppCookie = do
  (alice, tid, [bob]) <- Domain -> Int -> App (Value, String, [Value])
forall domain.
(HasCallStack, MakesValue domain) =>
domain -> Int -> App (Value, String, [Value])
createTeam Domain
OwnDomain Int
2
  charlie <- randomUser OwnDomain def

  let new = NewApp
forall a. Default a => a
def {name = "flexo"} :: NewApp

  (appId, cookie) <- bindResponse (createApp alice tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    appId <- Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user.id" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString
    cookie <- resp.json %. "cookie" & asString
    pure (appId, cookie)

  bindResponse (refreshAppCookie bob tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"label" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"app-no-permission"

  bindResponse (refreshAppCookie charlie tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
403
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"label" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"app-no-permission"

  cookie' <- bindResponse (refreshAppCookie alice tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"cookie" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString

  for_ [cookie, cookie'] $ \String
c ->
    App String -> App ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void (App String -> App ()) -> App String -> App ()
forall a b. (a -> b) -> a -> b
$ App Response -> (Response -> App String) -> App String
forall a.
HasCallStack =>
App Response -> (Response -> App a) -> App a
bindResponse (Domain -> String -> App Response
forall uid.
(HasCallStack, MakesValue uid) =>
uid -> String -> App Response
renewToken Domain
OwnDomain String
c) ((Response -> App String) -> App String)
-> (Response -> App String) -> App String
forall a b. (a -> b) -> a -> b
$ \Response
resp -> do
      Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
      Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
appId
      Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"token_type" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
"Bearer"
      Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"access_token" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString

testDeleteAppFromTeam :: (HasCallStack) => App ()
testDeleteAppFromTeam :: HasCallStack => App ()
testDeleteAppFromTeam = do
  domain <- Domain -> App Value
forall a. (MakesValue a, HasCallStack) => a -> App Value
make Domain
OwnDomain
  (owner, tid, _) <- createTeam domain 1
  let new = NewApp
forall a. Default a => a
def {name = "chappie"} :: NewApp
  appId <- bindResponse (createApp owner tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user.id" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString

  let appIdObject = [Pair] -> Value
object [String
"domain" String -> Value -> Pair
forall a. ToJSON a => String -> a -> Pair
.= Value
domain, String
"id" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
appId]

  bindResponse (deleteTeamMember tid owner appIdObject) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
202

  eventually $ do
    -- Check StoredApp is gone
    bindResponse (getApp owner tid appId) $ \Response
resp -> do
      Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
404

    -- Check StoredUser is deleted (via public API)
    bindResponse (getUser owner appIdObject) $ \Response
resp -> do
      Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
      Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"deleted" App Value -> Bool -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` Bool
True

    -- Check StoredUser is gone (via internal API)
    bindResponse (BrigI.getUsersId domain [appId]) $ \Response
resp -> do
      Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
      Response
resp.json Maybe Value -> [Value] -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` ([] :: [Value])

testPutApp :: (HasCallStack) => App ()
testPutApp :: HasCallStack => App ()
testPutApp = do
  domain <- Domain -> App Value
forall a. (MakesValue a, HasCallStack) => a -> App Value
make Domain
OwnDomain
  (owner, tid, _) <- createTeam domain 1
  let new = NewApp
forall a. Default a => a
def {name = "choppie"} :: NewApp
  appId <- bindResponse (createApp owner tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user.id" App Value -> (App Value -> App String) -> App String
forall a b. a -> (a -> b) -> b
& App Value -> App String
forall a. (HasCallStack, MakesValue a) => a -> App String
asString

  let Object appMetadata =
        [aesonQQ|
        {
          "accent_id": 2147483647,
          "assets": [
            {
              "key": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac",
              "size": "preview",
              "type": "image"
            }
          ],
          "name": "Appy McApp",
          "category": "security",
          "description": "This is the best app ever."
        }|]

  bindResponse (putAppMetadata tid owner appId (Object appMetadata)) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200

  bindResponse (getApp owner tid appId) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json
      Maybe Value -> Shape -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Shape -> App ()
`shouldMatchShape` [(String, Shape)] -> Shape
SObject
        [ (String
"accent_id", Shape
SNumber),
          (String
"assets", Shape -> Shape
SArray ([(String, Shape)] -> Shape
SObject [(String
"key", Shape
SString), (String
"size", Shape
SString), (String
"type", Shape
SString)])),
          (String
"name", Shape
SString),
          (String
"category", Shape
SString),
          (String
"description", Shape
SString),
          (String
"metadata", [(String, Shape)] -> Shape
SObject []),
          (String
"picture", Shape -> Shape
SArray Shape
SAny)
        ]

  let badAppId = String
"5e002eca-114f-11f1-b5a3-7306b8837f91"
  bindResponse (putAppMetadata tid owner badAppId (Object appMetadata)) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
404

testRetrieveUsersIncludingApps :: (HasCallStack) => App ()
testRetrieveUsersIncludingApps :: HasCallStack => App ()
testRetrieveUsersIncludingApps = do
  let userShape :: Shape
userShape =
        [(String, Shape)] -> Shape
SObject
          [ (String
"accent_id", Shape
SNumber),
            (String
"assets", Shape -> Shape
SArray Shape
SAny),
            (String
"id", Shape
SString),
            (String
"locale", Shape
SString),
            (String
"managed_by", Shape
SString),
            (String
"name", Shape
SString),
            (String
"picture", Shape -> Shape
SArray Shape
SAny),
            (String
"qualified_id", [(String, Shape)] -> Shape
SObject [(String
"domain", Shape
SString), (String
"id", Shape
SString)]),
            (String
"searchable", Shape
SBool),
            (String
"status", Shape
SString),
            (String
"supported_protocols", Shape -> Shape
SArray Shape
SString),
            (String
"team", Shape
SString),
            (String
"type", Shape
SString)
          ]
      memberShape :: Shape
memberShape =
        [(String, Shape)] -> Shape
SObject
          [ (String
"created_at", Shape
SString),
            (String
"created_by", Shape
SString),
            (String
"legalhold_status", Shape
SString),
            (String
"permissions", [(String, Shape)] -> Shape
SObject [(String
"copy", Shape
SNumber), (String
"self", Shape
SNumber)]),
            (String
"user", Shape
SString)
          ]
      appShape :: Shape
appShape =
        [(String, Shape)] -> Shape
SObject
          [ (String
"accent_id", Shape
SNumber),
            (String
"assets", Shape -> Shape
SArray Shape
SAny),
            (String
"category", Shape
SString),
            (String
"description", Shape
SString),
            (String
"metadata", [(String, Shape)] -> Shape
SObject []),
            (String
"name", Shape
SString),
            (String
"picture", Shape -> Shape
SArray Shape
SAny)
          ]
      appWithIdShape :: Shape
appWithIdShape = case Shape
appShape of
        SObject [(String, Shape)]
attrs ->
          [(String, Shape)] -> Shape
SObject ((String
"id", Shape
SString) (String, Shape) -> [(String, Shape)] -> [(String, Shape)]
forall a. a -> [a] -> [a]
: [(String, Shape)]
attrs)
      searchResultShape :: Shape
searchResultShape =
        [(String, Shape)] -> Shape
SObject
          [ (String
"accent_id", Shape
SNumber),
            (String
"handle", Shape
SAny), -- sometimes "string", but always "null" for apps
            (String
"id", Shape
SString),
            (String
"name", Shape
SString),
            (String
"qualified_id", [(String, Shape)] -> Shape
SObject [(String
"domain", Shape
SString), (String
"id", Shape
SString)]),
            (String
"team", Shape
SString),
            (String
"type", Shape
SString)
          ]

  domain <- Domain -> App Value
forall a. (MakesValue a, HasCallStack) => a -> App Value
make Domain
OwnDomain
  (owner, tid, [regular]) <- createTeam domain 2

  -- [`POST /teams/:tid/apps`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/create-app) (route id: "create-app")
  let new = NewApp
forall a. Default a => a
def {name = "chippie"} :: NewApp
  appCreated <- bindResponse (createApp owner tid new) $ \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Maybe Value -> App (Maybe Value)
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Response
resp.json
  appCreated
    `shouldMatchShape` SObject
      [ ("cookie", SString),
        ("user", userShape)
      ]
  appId <- appCreated %. "user.id" & asString

  -- [`GET /teams/:tid/members`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-team-members) (route id: "get-team-members")
  getTeamMembers owner tid `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"hasMore" App Value -> Bool -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` Bool
False
    mems <- Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"members" App Value -> (Value -> App [Value]) -> App [Value]
forall a b. App a -> (a -> App b) -> App b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Value -> App [Value]
forall a. (HasCallStack, MakesValue a) => a -> App [Value]
asList
    memIds <- (asString . (%. "user")) `mapM` mems
    memIds
      `shouldMatchSet` sequence
        [ pure appId,
          asString $ regular %. "qualified_id.id",
          asString $ owner %. "qualified_id.id"
        ]

  -- [`GET /teams/:tid/members/:uid`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-team-member) (route id: "get-team-member")
  getTeamMember owner tid appId `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"user" App Value -> String -> App ()
forall a b.
(MakesValue a, MakesValue b, HasCallStack) =>
a -> b -> App ()
`shouldMatch` String
appId
    Response
resp.json Maybe Value -> Shape -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Shape -> App ()
`shouldMatchShape` Shape
memberShape

  -- [`GET /teams/:tid/apps`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-apps) (route id: "get-apps")
  getApps owner tid `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    apps <- Response
resp.json Maybe Value -> (Maybe Value -> App Value) -> App Value
forall a b. a -> (a -> b) -> b
& App Value -> (Value -> App Value) -> Maybe Value -> App Value
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (String -> App Value
forall a. HasCallStack => String -> a
error String
"this shouldn't happen") Value -> App Value
forall a. a -> App a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
    apps `shouldMatchShape` SArray appWithIdShape

  -- [`GET /teams/:tid/apps/:uid`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/get-app) (route id: "get-app")
  getApp owner tid appId `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json Maybe Value -> Shape -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Shape -> App ()
`shouldMatchShape` Shape
appShape

  -- [`POST /list-users`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/list-users-by-ids-or-handles) (route id: "list-users-by-ids-or-handles")
  listUsers owner [appCreated %. "user"] `bindResponse` \Response
resp -> do
    Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
    Response
resp.json
      Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"found.0"
      App Value -> Shape -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Shape -> App ()
`shouldMatchShape` [(String, Shape)] -> Shape
SObject
        [ (String
"accent_id", Shape
SNumber),
          (String
"assets", Shape -> Shape
SArray Shape
SAny),
          (String
"id", Shape
SString),
          (String
"legalhold_status", Shape
SString),
          (String
"name", Shape
SString),
          (String
"picture", Shape -> Shape
SArray Shape
SAny),
          (String
"qualified_id", [(String, Shape)] -> Shape
SObject [(String
"domain", Shape
SString), (String
"id", Shape
SString)]),
          (String
"searchable", Shape
SBool),
          (String
"supported_protocols", Shape -> Shape
SArray Shape
SString),
          (String
"team", Shape
SString),
          (String
"type", Shape
SString)
          -- TODO: [("user", ...), ("app", ...)] ?
        ]

  -- [`GET /search/contacts`](https://staging-nginz-https.zinfra.io/v15/api/swagger-ui/#/default/search-contacts) (route id: "search-contacts")
  putSelf owner (def {name = Just "name-A1"}) >>= assertSuccess
  putSelf regular (def {name = Just "name-A2"}) >>= assertSuccess
  putSelf (appCreated %. "user") (def {name = Just "name-A3"}) >>= assertSuccess
  eventually
    $ searchContacts owner "name" domain
    `bindResponse` \Response
resp -> do
      Response
resp.status Int -> Int -> App ()
forall a. (MakesValue a, HasCallStack) => a -> Int -> App ()
`shouldMatchInt` Int
200
      hits :: [Value] <- Response
resp.json Maybe Value -> String -> App Value
forall a. (HasCallStack, MakesValue a) => a -> String -> App Value
%. String
"documents" App Value -> (App Value -> App [Value]) -> App [Value]
forall a b. a -> (a -> b) -> b
& App Value -> App [Value]
forall a. (HasCallStack, MakesValue a) => a -> App [Value]
asList
      length hits `shouldMatchInt` 2 -- owner doesn't find itself
      (`shouldMatchShape` searchResultShape) `mapM_` hits