diff --git a/changelog.d/5-internal/pr-2769 b/changelog.d/5-internal/pr-2769 new file mode 100644 index 0000000000..666ab11bce --- /dev/null +++ b/changelog.d/5-internal/pr-2769 @@ -0,0 +1 @@ +Gundeck push token API and notification API is migrated to Servant diff --git a/libs/wire-api/src/Wire/API/Error/Gundeck.hs b/libs/wire-api/src/Wire/API/Error/Gundeck.hs new file mode 100644 index 0000000000..f28432f45f --- /dev/null +++ b/libs/wire-api/src/Wire/API/Error/Gundeck.hs @@ -0,0 +1,46 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- 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 . + +module Wire.API.Error.Gundeck where + +import Wire.API.Error + +data GundeckError + = AddTokenErrorNoBudget + | AddTokenErrorNotFound + | AddTokenErrorInvalid + | AddTokenErrorTooLong + | AddTokenErrorMetadataTooLong + | TokenNotFound + | NotificationNotFound + +instance KnownError (MapError e) => IsSwaggerError (e :: GundeckError) where + addToSwagger = addStaticErrorToSwagger @(MapError e) + +type instance MapError 'AddTokenErrorNoBudget = 'StaticError 413 "sns-thread-budget-reached" "Too many concurrent calls to SNS; is SNS down?" + +type instance MapError 'AddTokenErrorNotFound = 'StaticError 404 "app-not-found" "App does not exist" + +type instance MapError 'AddTokenErrorInvalid = 'StaticError 404 "invalid-token" "Invalid push token" + +type instance MapError 'AddTokenErrorTooLong = 'StaticError 413 "token-too-long" "Push token length must be < 8192 for GCM or 400 for APNS" + +type instance MapError 'AddTokenErrorMetadataTooLong = 'StaticError 413 "metadata-too-long" "Tried to add token to endpoint resulting in metadata length > 2048" + +type instance MapError 'TokenNotFound = 'StaticError 404 "not-found" "Push token not found" + +type instance MapError 'NotificationNotFound = 'StaticError 404 "not-found" "Some notifications not found" diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index 04a2c8c55f..eef2c3f34c 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -20,6 +20,7 @@ module Wire.API.Notification ( NotificationId, + RawNotificationId (..), Event, -- * QueuedNotification @@ -32,6 +33,7 @@ module Wire.API.Notification queuedNotifications, queuedHasMore, queuedTime, + GetNotificationsResponse (..), -- * Swagger modelEvent, @@ -46,11 +48,16 @@ import qualified Data.Aeson.Types as Aeson import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty) +import Data.SOP import Data.Schema +import Data.String.Conversions (cs) +import Data.Swagger (ToParamSchema (..)) import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import Data.Time.Clock (UTCTime) import Imports +import Servant +import Wire.API.Routes.MultiVerb import Wire.Arbitrary (Arbitrary, GenericUniform (..)) type NotificationId = Id QueuedNotification @@ -84,8 +91,10 @@ instance ToSchema QueuedNotification where schema = object "QueuedNotification" $ QueuedNotification - <$> _queuedNotificationId .= field "id" schema - <*> _queuedNotificationPayload .= field "payload" (nonEmptyArray jsonObject) + <$> _queuedNotificationId + .= field "id" schema + <*> _queuedNotificationPayload + .= field "payload" (nonEmptyArray jsonObject) makeLenses ''QueuedNotification @@ -121,8 +130,31 @@ instance ToSchema QueuedNotificationList where schema = object "QueuedNotificationList" $ QueuedNotificationList - <$> _queuedNotifications .= field "notifications" (array schema) - <*> _queuedHasMore .= fmap (fromMaybe False) (optField "has_more" schema) - <*> _queuedTime .= maybe_ (optField "time" utcTimeSchema) + <$> _queuedNotifications + .= field "notifications" (array schema) + <*> _queuedHasMore + .= fmap (fromMaybe False) (optField "has_more" schema) + <*> _queuedTime + .= maybe_ (optField "time" utcTimeSchema) makeLenses ''QueuedNotificationList + +newtype RawNotificationId = RawNotificationId {unRawNotificationId :: ByteString} + deriving stock (Eq, Show, Generic) + +instance FromHttpApiData RawNotificationId where + parseUrlPiece = pure . RawNotificationId . cs + +instance ToParamSchema RawNotificationId where + toParamSchema _ = toParamSchema (Proxy @Text) + +data GetNotificationsResponse + = GetNotificationsWithStatusNotFound QueuedNotificationList + | GetNotificationsSuccess QueuedNotificationList + +instance AsUnion '[Respond 404 "Notification list" QueuedNotificationList, Respond 200 "Notification list" QueuedNotificationList] GetNotificationsResponse where + toUnion (GetNotificationsSuccess xs) = S (Z (I xs)) + toUnion (GetNotificationsWithStatusNotFound xs) = Z (I xs) + fromUnion (S (Z (I xs))) = GetNotificationsSuccess xs + fromUnion (Z (I xs)) = GetNotificationsWithStatusNotFound xs + fromUnion (S (S x)) = case x of {} diff --git a/libs/wire-api/src/Wire/API/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index ef3cc83754..d55d905552 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -34,21 +34,29 @@ module Wire.API.Push.V2.Token Token (..), AppName (..), - -- * Swagger - modelPushToken, - modelPushTokenList, - typeTransport, + -- * API types + AddTokenError (..), + AddTokenSuccess (..), + AddTokenResponses, + DeleteTokenResponses, ) where -import Control.Lens (makeLenses) -import Data.Aeson +import Control.Lens (makeLenses, (?~), (^.)) +import qualified Data.Aeson as A import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Conversion import Data.Id -import Data.Json.Util -import qualified Data.Swagger.Build.Api as Doc +import Data.SOP +import Data.Schema +import Data.Swagger (ToParamSchema) +import qualified Data.Swagger as S +import qualified Generics.SOP as GSOP import Imports +import Servant +import Wire.API.Error +import qualified Wire.API.Error.Gundeck as E +import Wire.API.Routes.MultiVerb import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- @@ -59,19 +67,14 @@ newtype PushTokenList = PushTokenList } deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PushTokenList) -modelPushTokenList :: Doc.Model -modelPushTokenList = Doc.defineModel "PushTokenList" $ do - Doc.description "List of Native Push Tokens" - Doc.property "tokens" (Doc.array (Doc.ref modelPushToken)) $ - Doc.description "Push tokens" - -instance ToJSON PushTokenList where - toJSON (PushTokenList t) = object ["tokens" .= t] - -instance FromJSON PushTokenList where - parseJSON = withObject "PushTokenList" $ \p -> - PushTokenList <$> p .: "tokens" +instance ToSchema PushTokenList where + schema = + objectWithDocModifier "PushTokenList" (description ?~ "List of Native Push Tokens") $ + PushTokenList + <$> pushTokens + .= fieldWithDocModifier "tokens" (description ?~ "Push tokens") (array schema) data PushToken = PushToken { _tokenTransport :: Transport, @@ -81,39 +84,29 @@ data PushToken = PushToken } deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform PushToken) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema PushToken) pushToken :: Transport -> AppName -> Token -> ClientId -> PushToken pushToken = PushToken -modelPushToken :: Doc.Model -modelPushToken = Doc.defineModel "PushToken" $ do - Doc.description "Native Push Token" - Doc.property "transport" typeTransport $ - Doc.description "Transport" - Doc.property "app" Doc.string' $ - Doc.description "Application" - Doc.property "token" Doc.bytes' $ - Doc.description "Access Token" - Doc.property "client" Doc.bytes' $ do - Doc.description "Client ID" - Doc.optional - -instance ToJSON PushToken where - toJSON p = - object $ - "transport" .= _tokenTransport p - # "app" .= _tokenApp p - # "token" .= _token p - # "client" .= _tokenClient p - # [] - -instance FromJSON PushToken where - parseJSON = withObject "PushToken" $ \p -> - PushToken - <$> p .: "transport" - <*> p .: "app" - <*> p .: "token" - <*> p .: "client" +instance ToSchema PushToken where + schema = + objectWithDocModifier "PushToken" desc $ + PushToken + <$> _tokenTransport + .= fieldWithDocModifier "transport" transDesc schema + <*> _tokenApp + .= fieldWithDocModifier "app" appDesc schema + <*> _token + .= fieldWithDocModifier "token" tokenDesc schema + <*> _tokenClient + .= fieldWithDocModifier "client" clientIdDesc schema + where + desc = description ?~ "Native Push Token" + transDesc = description ?~ "Transport" + appDesc = description ?~ "Application" + tokenDesc = description ?~ "Access Token" + clientIdDesc = description ?~ "Client ID" -------------------------------------------------------------------------------- -- Transport @@ -126,33 +119,18 @@ data Transport | APNSVoIPSandbox deriving stock (Eq, Ord, Show, Bounded, Enum, Generic) deriving (Arbitrary) via (GenericUniform Transport) - -typeTransport :: Doc.DataType -typeTransport = - Doc.string $ - Doc.enum - [ "GCM", - "APNS", - "APNS_SANDBOX", - "APNS_VOIP", - "APNS_VOIP_SANDBOX" - ] - -instance ToJSON Transport where - toJSON GCM = "GCM" - toJSON APNS = "APNS" - toJSON APNSSandbox = "APNS_SANDBOX" - toJSON APNSVoIP = "APNS_VOIP" - toJSON APNSVoIPSandbox = "APNS_VOIP_SANDBOX" - -instance FromJSON Transport where - parseJSON = withText "transport" $ \case - "GCM" -> pure GCM - "APNS" -> pure APNS - "APNS_SANDBOX" -> pure APNSSandbox - "APNS_VOIP" -> pure APNSVoIP - "APNS_VOIP_SANDBOX" -> pure APNSVoIPSandbox - x -> fail $ "Invalid push transport: " ++ show x + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Transport) + +instance ToSchema Transport where + schema = + enum @Text "Access" $ + mconcat + [ element "GCM" GCM, + element "APNS" APNS, + element "APNS_SANDBOX" APNSSandbox, + element "APNS_VOIP" APNSVoIP, + element "APNS_VOIP_SANDBOX" APNSVoIPSandbox + ] instance FromByteString Transport where parser = @@ -168,12 +146,72 @@ newtype Token = Token { tokenText :: Text } deriving stock (Eq, Ord, Show) - deriving newtype (FromJSON, ToJSON, FromByteString, ToByteString, Arbitrary) + deriving newtype (FromHttpApiData, ToHttpApiData, FromByteString, ToByteString, Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Token) + +instance ToParamSchema Token where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance ToSchema Token where + schema = Token <$> tokenText .= schema newtype AppName = AppName { appNameText :: Text } deriving stock (Eq, Ord, Show) - deriving newtype (FromJSON, ToJSON, IsString, Arbitrary) + deriving newtype (IsString, Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema AppName) + +instance ToSchema AppName where + schema = AppName <$> appNameText .= schema makeLenses ''PushToken + +-------------------------------------------------------------------------------- +-- Add token types + +type AddTokenErrorResponses = + '[ ErrorResponse 'E.AddTokenErrorNoBudget, + ErrorResponse 'E.AddTokenErrorNotFound, + ErrorResponse 'E.AddTokenErrorInvalid, + ErrorResponse 'E.AddTokenErrorTooLong, + ErrorResponse 'E.AddTokenErrorMetadataTooLong + ] + +type AddTokenSuccessResponses = + WithHeaders + '[ Header "Location" Token + ] + AddTokenSuccess + (Respond 201 "Push token registered" PushToken) + +type AddTokenResponses = AddTokenErrorResponses .++ '[AddTokenSuccessResponses] + +data AddTokenError + = AddTokenErrorNoBudget + | AddTokenErrorNotFound + | AddTokenErrorInvalid + | AddTokenErrorTooLong + | AddTokenErrorMetadataTooLong + deriving (Show, Generic) + deriving (AsUnion AddTokenErrorResponses) via GenericAsUnion AddTokenErrorResponses AddTokenError + +instance GSOP.Generic AddTokenError + +data AddTokenSuccess = AddTokenSuccess PushToken + +instance AsHeaders '[Token] PushToken AddTokenSuccess where + fromHeaders (I _ :* Nil, t) = AddTokenSuccess t + toHeaders (AddTokenSuccess t) = (I (t ^. token) :* Nil, t) + +instance (res ~ AddTokenResponses) => AsUnion res (Either AddTokenError AddTokenSuccess) where + toUnion = eitherToUnion (toUnion @AddTokenErrorResponses) (Z . I) + fromUnion = eitherFromUnion (fromUnion @AddTokenErrorResponses) (unI . unZ) + +-------------------------------------------------------------------------------- +-- Delete token types + +type DeleteTokenResponses = + '[ ErrorResponse 'E.TokenNotFound, + RespondEmpty 204 "Push token unregistered" + ] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs b/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs new file mode 100644 index 0000000000..85b11f0c66 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Gundeck.hs @@ -0,0 +1,136 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- 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 . + +module Wire.API.Routes.Public.Gundeck where + +import Data.Id (ClientId) +import Data.Range +import Data.SOP +import qualified Data.Swagger as Swagger +import Imports +import Servant +import Servant.Swagger +import Wire.API.Error +import Wire.API.Error.Gundeck as E +import Wire.API.Notification +import Wire.API.Push.V2.Token +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.Version + +type GundeckAPI = PushAPI :<|> NotificationAPI + +type PushAPI = + Named + "register-push-token" + ( Summary "Register a native push token" + :> ZUser + :> ZConn + :> "push" + :> "tokens" + :> ReqBody '[JSON] PushToken + :> MultiVerb 'POST '[JSON] AddTokenResponses (Either AddTokenError AddTokenSuccess) + ) + :<|> Named + "delete-push-token" + ( Summary "Unregister a native push token" + :> ZUser + :> "push" + :> "tokens" + :> Capture' '[Description "The push token to delete"] "pid" Token + :> MultiVerb 'DELETE '[JSON] DeleteTokenResponses (Maybe ()) + ) + :<|> Named + "get-push-tokens" + ( Summary "List the user's registered push tokens" + :> ZUser + :> "push" + :> "tokens" + :> Get + '[JSON] + PushTokenList + ) + +type NotificationAPI = + Named + "get-notification-by-id" + ( Summary "Fetch a notification by ID" + :> ZUser + :> "notifications" + :> Capture' '[Description "Notification ID"] "id" NotificationId + :> QueryParam' [Optional, Strict, Description "Only return notifications targeted at the given client"] "client" ClientId + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'E.NotificationNotFound, + Respond 200 "Notification found" QueuedNotification + ] + (Maybe QueuedNotification) + ) + :<|> Named + "get-last-notification" + ( Summary "Fetch the last notification" + :> ZUser + :> "notifications" + :> "last" + :> QueryParam' [Optional, Strict, Description "Only return notifications targeted at the given client"] "client" ClientId + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'E.NotificationNotFound, + Respond 200 "Notification found" QueuedNotification + ] + (Maybe QueuedNotification) + ) + :<|> Named + "get-notifications@v2" + ( Summary "Fetch notifications" + :> Until 'V3 + :> ZUser + :> "notifications" + :> QueryParam' [Optional, Strict, Description "Only return notifications more recent than this"] "since" RawNotificationId + :> QueryParam' [Optional, Strict, Description "Only return notifications targeted at the given client"] "client" ClientId + :> QueryParam' [Optional, Strict, Description "Maximum number of notifications to return"] "size" (Range 100 10000 Int32) + :> MultiVerb + 'GET + '[JSON] + '[ Respond 404 "Notification list" QueuedNotificationList, + Respond 200 "Notification list" QueuedNotificationList + ] + GetNotificationsResponse + ) + :<|> Named + "get-notifications" + ( Summary "Fetch notifications" + :> From 'V3 + :> ZUser + :> "notifications" + :> QueryParam' [Optional, Strict, Description "Only return notifications more recent than this"] "since" NotificationId + :> QueryParam' [Optional, Strict, Description "Only return notifications targeted at the given client"] "client" ClientId + :> QueryParam' [Optional, Strict, Description "Maximum number of notifications to return"] "size" (Range 100 10000 Int32) + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'E.NotificationNotFound, + Respond 200 "Notification list" QueuedNotificationList + ] + (Maybe QueuedNotificationList) + ) + +swaggerDoc :: Swagger.Swagger +swaggerDoc = toSwagger (Proxy @GundeckAPI) diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 20aaddca84..586d4814be 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -30,7 +30,6 @@ import qualified Wire.API.Message as Message import qualified Wire.API.Notification as Notification import qualified Wire.API.Properties as Properties import qualified Wire.API.Provider.Service as Provider.Service -import qualified Wire.API.Push.Token as Push.Token import qualified Wire.API.Team as Team import qualified Wire.API.Team.Conversation as Team.Conversation import qualified Wire.API.Team.Invitation as Team.Invitation @@ -85,8 +84,6 @@ models = Properties.modelPropertyValue, Properties.modelPropertyDictionary, Provider.Service.modelServiceRef, - Push.Token.modelPushToken, - Push.Token.modelPushTokenList, Team.modelTeam, Team.modelTeamList, Team.modelTeamDelete, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index 29a08634ca..e070ad4c3d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -33,6 +33,7 @@ import Test.Wire.API.Golden.Manual.GroupId import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact +import Test.Wire.API.Golden.Manual.Token import Test.Wire.API.Golden.Manual.UserClientPrekeyMap import Test.Wire.API.Golden.Manual.UserIdList import Test.Wire.API.Golden.Runner @@ -122,5 +123,8 @@ tests = [(testObject_SearchResultContact_1, "testObject_SearchResultContact_1.json")], testGroup "GroupId" $ testObjects - [(testObject_GroupId_1, "testObject_GroupId_1.json")] + [(testObject_GroupId_1, "testObject_GroupId_1.json")], + testGroup "PushToken" $ + testObjects + [(testObject_Token_1, "testObject_Token_1.json")] ] diff --git a/services/gundeck/src/Gundeck/API/Error.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs similarity index 63% rename from services/gundeck/src/Gundeck/API/Error.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs index f3d1631350..edf7a8edd7 100644 --- a/services/gundeck/src/Gundeck/API/Error.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs @@ -15,17 +15,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.API.Error where +module Test.Wire.API.Golden.Manual.Token where -import Data.Text.Lazy (Text) -import Network.HTTP.Types.Status -import Network.Wai.Utilities.Error (Error (..), mkError) +import Data.Id +import Wire.API.Push.V2.Token -notificationNotFound :: Error -notificationNotFound = mkError status404 "not-found" "Some notifications not found." - -clientError :: Text -> Error -clientError = mkError status400 "client-error" - -invalidNotificationId :: Error -invalidNotificationId = clientError "Notification ID must be a version 1 UUID" +testObject_Token_1 :: PushToken +testObject_Token_1 = + pushToken + APNSVoIPSandbox + (AppName {appNameText = "j{\110746\SOH_\1084873M"}) + (Token {tokenText = "K"}) + (ClientId {client = "6"}) diff --git a/libs/wire-api/test/golden/testObject_Token_1.json b/libs/wire-api/test/golden/testObject_Token_1.json new file mode 100644 index 0000000000..36f8ff69bd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Token_1.json @@ -0,0 +1,6 @@ +{ + "app": "j{𛂚\u0001_􈷉M", + "client": "6", + "token": "K", + "transport": "APNS_VOIP_SANDBOX" +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 126ac7d697..4b99afef88 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -32,6 +32,7 @@ library Wire.API.Error.Cargohold Wire.API.Error.Empty Wire.API.Error.Galley + Wire.API.Error.Gundeck Wire.API.Event.Conversation Wire.API.Event.FeatureConfig Wire.API.Event.Team @@ -87,6 +88,7 @@ library Wire.API.Routes.Public.Cannon Wire.API.Routes.Public.Cargohold Wire.API.Routes.Public.Galley + Wire.API.Routes.Public.Gundeck Wire.API.Routes.Public.Spar Wire.API.Routes.Public.Util Wire.API.Routes.QualifiedCapture @@ -508,6 +510,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact + Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap Test.Wire.API.Golden.Manual.UserIdList Test.Wire.API.Golden.Protobuf diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 029594b6e4..05fc876dc3 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -118,6 +118,7 @@ import Wire.API.Routes.Public.Brig import qualified Wire.API.Routes.Public.Cannon as CannonAPI import qualified Wire.API.Routes.Public.Cargohold as CargoholdAPI import qualified Wire.API.Routes.Public.Galley as GalleyAPI +import qualified Wire.API.Routes.Public.Gundeck as GundeckAPI import qualified Wire.API.Routes.Public.Spar as SparAPI import qualified Wire.API.Routes.Public.Util as Public import Wire.API.Routes.Version @@ -151,6 +152,7 @@ swaggerDocsAPI (Just V3) = <> SparAPI.swaggerDoc <> CargoholdAPI.swaggerDoc <> CannonAPI.swaggerDoc + <> GundeckAPI.swaggerDoc ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 1751d4ae14..6b191b5027 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -12,8 +12,9 @@ , metrics-wai, MonadRandom, mtl, multiset, network, network-uri , optparse-applicative, psqueues, QuickCheck, quickcheck-instances , quickcheck-state-machine, random, raw-strings-qq, resourcet -, retry, safe, safe-exceptions, scientific, streaming-commons -, string-conversions, swagger, tagged, tasty, tasty-hunit +, retry, safe, safe-exceptions, scientific, servant, servant-server +, servant-swagger, servant-swagger-ui, streaming-commons +, string-conversions, swagger, swagger2, tagged, tasty, tasty-hunit , tasty-quickcheck, text, time, tinylog, tls, tree-diff , types-common, types-common-aws, unix, unliftio , unordered-containers, uuid, wai, wai-extra, wai-middleware-gunzip @@ -33,10 +34,11 @@ mkDerivation { extra gundeck-types hedis HsOpenSSL http-client http-client-tls http-types imports lens lens-aeson metrics-core metrics-wai mtl network network-uri optparse-applicative psqueues resourcet retry - safe-exceptions swagger text time tinylog tls types-common - types-common-aws unliftio unordered-containers uuid wai wai-extra - wai-middleware-gunzip wai-predicates wai-routing wai-utilities - wire-api yaml + safe-exceptions servant servant-server servant-swagger + servant-swagger-ui swagger swagger2 text time tinylog tls + types-common types-common-aws unliftio unordered-containers uuid + wai wai-extra wai-middleware-gunzip wai-predicates wai-routing + wai-utilities wire-api yaml ]; executableHaskellDepends = [ aeson async base base16-bytestring bilge bytestring diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index ccb7d9d00e..62d8efbeef 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -18,7 +18,6 @@ flag static library exposed-modules: Gundeck.API - Gundeck.API.Error Gundeck.API.Internal Gundeck.API.Public Gundeck.Aws @@ -135,7 +134,12 @@ library , resourcet >=1.1 , retry >=0.5 , safe-exceptions - , swagger >=0.1 + , servant + , servant-server + , servant-swagger + , servant-swagger-ui + , swagger + , swagger2 , text >=1.1 , time >=1.4 , tinylog >=0.10 diff --git a/services/gundeck/src/Gundeck/API.hs b/services/gundeck/src/Gundeck/API.hs index 7db416c325..c831a15be5 100644 --- a/services/gundeck/src/Gundeck/API.hs +++ b/services/gundeck/src/Gundeck/API.hs @@ -22,12 +22,9 @@ where import qualified Data.Swagger.Build.Api as Doc import qualified Gundeck.API.Internal as Internal -import qualified Gundeck.API.Public as Public import Gundeck.Monad (Gundeck) import Network.Wai.Routing (Routes) sitemap :: Routes Doc.ApiBuilder Gundeck () sitemap = do - Public.sitemap - Public.apiDocs Internal.sitemap diff --git a/services/gundeck/src/Gundeck/API/Public.hs b/services/gundeck/src/Gundeck/API/Public.hs index a410c068a3..0ee9b83d7f 100644 --- a/services/gundeck/src/Gundeck/API/Public.hs +++ b/services/gundeck/src/Gundeck/API/Public.hs @@ -16,185 +16,40 @@ -- with this program. If not, see . module Gundeck.API.Public - ( sitemap, - apiDocs, + ( servantSitemap, ) where -import Control.Lens ((^.)) import Data.Id import Data.Range -import Data.Swagger.Build.Api hiding (Response, def, min) -import qualified Data.Swagger.Build.Api as Swagger -import Data.Text.Encoding (decodeLatin1) -import qualified Data.Text.Encoding as Text import Data.UUID as UUID import qualified Data.UUID.Util as UUID -import Gundeck.API.Error import Gundeck.Monad import qualified Gundeck.Notification as Notification +import qualified Gundeck.Notification.Data as Data import qualified Gundeck.Push as Push import Imports -import Network.HTTP.Types -import Network.Wai -import Network.Wai.Predicate hiding (setStatus) -import Network.Wai.Routing hiding (route) -import Network.Wai.Utilities -import Network.Wai.Utilities.Swagger -import Wire.API.Notification (NotificationId) +import Servant (HasServer (..), (:<|>) (..)) import qualified Wire.API.Notification as Public -import qualified Wire.API.Push.Token as Public -import qualified Wire.API.Swagger as Public.Swagger +import Wire.API.Routes.Named (Named (Named)) +import Wire.API.Routes.Public.Gundeck -sitemap :: Routes ApiBuilder Gundeck () -sitemap = do - -- Push API ----------------------------------------------------------- +------------------------------------------------------------------------------- +-- Servant API - post "/push/tokens" (continue addTokenH) $ - header "Z-User" - .&. header "Z-Connection" - .&. jsonRequest @Public.PushToken - .&. accept "application" "json" - document "POST" "registerPushToken" $ do - summary "Register a native push token" - body (ref Public.modelPushToken) $ - description "JSON body" - returns (ref Public.modelPushToken) - response 201 "Push token registered" end - response 404 "App does not exist" end - - delete "/push/tokens/:pid" (continue deleteTokenH) $ - header "Z-User" - .&. param "pid" - .&. accept "application" "json" - document "DELETE" "unregisterPushToken" $ do - summary "Unregister a native push token" - parameter Path "pid" bytes' $ - description "The push token to delete" - response 204 "Push token unregistered" end - response 404 "Push token does not exist" end - - get "/push/tokens" (continue listTokensH) $ - header "Z-User" - .&. accept "application" "json" - document "GET" "getPushTokens" $ do - summary "List the user's registered push tokens." - returns (ref Public.modelPushTokenList) - response 200 "Object containing list of push tokens" end - - -- Notification API -------------------------------------------------------- - - get "/notifications" (continue paginateH) $ - accept "application" "json" - .&. header "Z-User" - .&. opt (query "since") - .&. opt (query "client") - .&. def (unsafeRange 1000) (query "size") - document "GET" "fetchNotifications" $ do - summary "Fetch notifications" - parameter Query "since" bytes' $ do - optional - description "Only return notifications more recent than this." - parameter Query "client" bytes' $ do - optional - description "Only return notifications targeted at the given client." - parameter Query "size" (int32 (Swagger.def 1000)) $ do - optional - description "Maximum number of notifications to return." - returns (ref Public.modelNotificationList) - response 200 "Notification list" end - errorResponse' notificationNotFound Public.modelNotificationList - - get "/notifications/:id" (continue getByIdH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "id" - .&. opt (query "client") - document "GET" "getNotification" $ do - summary "Fetch a notification by ID." - parameter Query "id" bytes' $ - description "Notification ID" - parameter Query "client" bytes' $ do - optional - description "Only return notifications targeted at the given client." - returns (ref Public.modelNotification) - response 200 "Notification found" end - errorResponse notificationNotFound - - get "/notifications/last" (continue getLastH) $ - accept "application" "json" - .&. header "Z-User" - .&. opt (query "client") - document "GET" "getLastNotification" $ do - summary "Fetch the last notification." - parameter Query "client" bytes' $ do - optional - description "Only return the last notification targeted at the given client." - returns (ref Public.modelNotification) - response 200 "Notification found" end - errorResponse notificationNotFound - -apiDocs :: Routes ApiBuilder Gundeck () -apiDocs = do - get "/push/api-docs" (continue docsH) $ - query "base_url" .&. accept "application" "json" - -type JSON = Media "application" "json" - -docsH :: ByteString ::: JSON -> Gundeck Response -docsH (url ::: _) = - let doc = mkSwaggerApi (decodeLatin1 url) Public.Swagger.models sitemap - in pure $ json doc - -addTokenH :: UserId ::: ConnId ::: JsonRequest Public.PushToken ::: JSON -> Gundeck Response -addTokenH (uid ::: cid ::: req ::: _) = do - newtok <- fromJsonBody req - handleAddTokenResponse <$> Push.addToken uid cid newtok - -handleAddTokenResponse :: Push.AddTokenResponse -> Response -handleAddTokenResponse = \case - Push.AddTokenSuccess newtok -> success newtok - Push.AddTokenNoBudget -> snsThreadBudgetReached - Push.AddTokenNotFound -> notFound - Push.AddTokenInvalid -> invalidToken - Push.AddTokenTooLong -> tokenTooLong - Push.AddTokenMetadataTooLong -> metadataTooLong - -success :: Public.PushToken -> Response -success t = - let loc = Text.encodeUtf8 . Public.tokenText $ t ^. Public.token - in json t & setStatus status201 & addHeader hLocation loc - -invalidToken :: Response -invalidToken = - json (mkError status400 "invalid-token" "Invalid push token") - & setStatus status404 - -snsThreadBudgetReached :: Response -snsThreadBudgetReached = - json (mkError status400 "sns-thread-budget-reached" "Too many concurrent calls to SNS; is SNS down?") - & setStatus status413 - -tokenTooLong :: Response -tokenTooLong = - json (mkError status400 "token-too-long" "Push token length must be < 8192 for GCM or 400 for APNS") - & setStatus status413 - -metadataTooLong :: Response -metadataTooLong = - json (mkError status400 "metadata-too-long" "Tried to add token to endpoint resulting in metadata length > 2048") - & setStatus status413 - -notFound :: Response -notFound = empty & setStatus status404 - -deleteTokenH :: UserId ::: Public.Token ::: JSON -> Gundeck Response -deleteTokenH (uid ::: tok ::: _) = - setStatus status204 empty <$ Push.deleteToken uid tok +servantSitemap :: ServerT GundeckAPI Gundeck +servantSitemap = pushAPI :<|> notificationAPI + where + pushAPI = + Named @"register-push-token" Push.addToken + :<|> Named @"delete-push-token" Push.deleteToken + :<|> Named @"get-push-tokens" Push.listTokens -listTokensH :: UserId ::: JSON -> Gundeck Response -listTokensH (uid ::: _) = - setStatus status200 . json @Public.PushTokenList <$> Push.listTokens uid + notificationAPI = + Named @"get-notification-by-id" Data.fetchId + :<|> Named @"get-last-notification" Data.fetchLast + :<|> Named @"get-notifications@v2" paginateUntilV2 + :<|> Named @"get-notifications" paginate -- | Returns a list of notifications for given 'uid' -- @@ -228,28 +83,39 @@ listTokensH (uid ::: _) = -- -- (arianvp): I am not sure why it is convenient for clients to distinct -- between these two cases. -paginateH :: JSON ::: UserId ::: Maybe ByteString ::: Maybe ClientId ::: Range 100 10000 Int32 -> Gundeck Response -paginateH (_ ::: uid ::: sinceRaw ::: clt ::: size) = do - Notification.PaginateResult gap page <- Notification.paginate uid (join since) clt size - pure . updStatus gap . json $ (page :: Public.QueuedNotificationList) +paginateUntilV2 :: + UserId -> + Maybe Public.RawNotificationId -> + Maybe ClientId -> + Maybe (Range 100 10000 Int32) -> + Gundeck Public.GetNotificationsResponse +paginateUntilV2 uid mbSince mbClient mbSize = do + let size = fromMaybe (unsafeRange 1000) mbSize + Notification.PaginateResult gap page <- Notification.paginate uid (join since) mbClient size + pure $ + if gap + then Public.GetNotificationsWithStatusNotFound page + else case since of + Just (Just _) -> Public.GetNotificationsSuccess page + Nothing -> Public.GetNotificationsSuccess page + Just Nothing -> Public.GetNotificationsWithStatusNotFound page where - since :: Maybe (Maybe NotificationId) - since = parseUUID <$> sinceRaw - parseUUID :: ByteString -> Maybe NotificationId - parseUUID = UUID.fromASCIIBytes >=> isV1UUID >=> pure . Id + since :: Maybe (Maybe Public.NotificationId) + since = parseUUID <$> mbSince + + parseUUID :: Public.RawNotificationId -> Maybe Public.NotificationId + parseUUID = (UUID.fromASCIIBytes . Public.unRawNotificationId) >=> isV1UUID >=> pure . Id + isV1UUID :: UUID -> Maybe UUID isV1UUID u = if UUID.version u == 1 then Just u else Nothing - updStatus :: Bool -> Response -> Response - updStatus True = setStatus status404 - updStatus False = case since of - Just (Just _) -> id - Nothing -> id - Just Nothing -> setStatus status404 - -getByIdH :: JSON ::: UserId ::: NotificationId ::: Maybe ClientId -> Gundeck Response -getByIdH (_ ::: uid ::: nid ::: cid) = - json @Public.QueuedNotification <$> Notification.getById uid nid cid -getLastH :: JSON ::: UserId ::: Maybe ClientId -> Gundeck Response -getLastH (_ ::: uid ::: cid) = - json @Public.QueuedNotification <$> Notification.getLast uid cid +paginate :: + UserId -> + Maybe Public.NotificationId -> + Maybe ClientId -> + Maybe (Range 100 10000 Int32) -> + Gundeck (Maybe Public.QueuedNotificationList) +paginate uid mbSince mbClient mbSize = do + let size = fromMaybe (unsafeRange 1000) mbSize + Notification.PaginateResult gap page <- Notification.paginate uid mbSince mbClient size + pure $ if gap then Nothing else Just page diff --git a/services/gundeck/src/Gundeck/Notification.hs b/services/gundeck/src/Gundeck/Notification.hs index d7529611af..1269561535 100644 --- a/services/gundeck/src/Gundeck/Notification.hs +++ b/services/gundeck/src/Gundeck/Notification.hs @@ -18,17 +18,13 @@ module Gundeck.Notification ( paginate, PaginateResult (..), - getById, - getLast, ) where -import Control.Monad.Catch (throwM) import Data.Id import Data.Misc (Milliseconds (..)) import Data.Range import Data.Time.Clock.POSIX -import Gundeck.API.Error import Gundeck.Monad import qualified Gundeck.Notification.Data as Data import Imports hiding (getLast) @@ -51,13 +47,3 @@ paginate uid since clt size = do (Data.resultHasMore rs) (Just (millisToUTC time)) millisToUTC = posixSecondsToUTCTime . fromIntegral . (`div` 1000) . ms - -getById :: UserId -> NotificationId -> Maybe ClientId -> Gundeck QueuedNotification -getById uid nid clt = do - mn <- Data.fetchId uid nid clt - maybe (throwM notificationNotFound) pure mn - -getLast :: UserId -> Maybe ClientId -> Gundeck QueuedNotification -getLast uid clt = do - mn <- Data.fetchLast uid clt - maybe (throwM notificationNotFound) pure mn diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 486d9d5a27..c8a2d1e596 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -18,7 +18,6 @@ module Gundeck.Push ( push, - AddTokenResponse (..), addToken, listTokens, deleteToken, @@ -401,27 +400,23 @@ nativeTargets psh rcps' alreadySent = check (Left e) = mntgtLogErr e >> pure [] check (Right r) = pure r -data AddTokenResponse - = AddTokenSuccess Public.PushToken - | AddTokenNoBudget - | AddTokenNotFound - | AddTokenInvalid - | AddTokenTooLong - | AddTokenMetadataTooLong +type AddTokenResponse = Either Public.AddTokenError Public.AddTokenSuccess addToken :: UserId -> ConnId -> PushToken -> Gundeck AddTokenResponse -addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do +addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) $ do (cur, old) <- foldl' (matching newtok) (Nothing, []) <$> Data.lookup uid Data.LocalQuorum Log.info $ - "user" .= UUID.toASCIIBytes (toUUID uid) - ~~ "token" .= Text.take 16 (tokenText (newtok ^. token)) + "user" + .= UUID.toASCIIBytes (toUUID uid) + ~~ "token" + .= Text.take 16 (tokenText (newtok ^. token)) ~~ msg (val "Registering push token") continue newtok cur >>= either pure ( \a -> do Native.deleteTokens old (Just a) - pure (AddTokenSuccess newtok) + pure (Right $ Public.AddTokenSuccess newtok) ) where matching :: @@ -462,15 +457,16 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do update (n + 1) t arn Left (Aws.AppNotFound app') -> do Log.info $ msg ("Push token of unknown application: '" <> appNameText app' <> "'") - pure (Left AddTokenNotFound) + pure (Left (Left Public.AddTokenErrorNotFound)) Left (Aws.InvalidToken _) -> do Log.info $ - "token" .= tokenText tok + "token" + .= tokenText tok ~~ msg (val "Invalid push token.") - pure (Left AddTokenInvalid) + pure (Left (Left Public.AddTokenErrorInvalid)) Left (Aws.TokenTooLong l) -> do Log.info $ msg ("Push token is too long: token length = " ++ show l) - pure (Left AddTokenTooLong) + pure (Left (Left Public.AddTokenErrorTooLong)) Right arn -> do Data.insert uid trp app tok arn cid (t ^. tokenClient) pure (Right (mkAddr t arn)) @@ -508,7 +504,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do -- possibly updates in general). We make another attempt to (re-)create -- the endpoint in these cases instead of failing immediately. Aws.EndpointNotFound {} -> create (n + 1) t - Aws.InvalidCustomData {} -> pure (Left AddTokenMetadataTooLong) + Aws.InvalidCustomData {} -> pure (Left (Left Public.AddTokenErrorMetadataTooLong)) ex -> throwM ex mkAddr :: @@ -536,17 +532,22 @@ updateEndpoint uid t arn e = do equalTransport = t ^. tokenTransport == arn ^. snsTopic . endpointTransport equalApp = t ^. tokenApp == arn ^. snsTopic . endpointAppName logMessage a r tk m = - "user" .= UUID.toASCIIBytes (toUUID a) - ~~ "token" .= Text.take 16 (tokenText tk) - ~~ "arn" .= toText r + "user" + .= UUID.toASCIIBytes (toUUID a) + ~~ "token" + .= Text.take 16 (tokenText tk) + ~~ "arn" + .= toText r ~~ msg (val m) -deleteToken :: UserId -> Token -> Gundeck () +deleteToken :: UserId -> Token -> Gundeck (Maybe ()) deleteToken uid tok = do - as <- filter (\x -> x ^. addrToken == tok) <$> Data.lookup uid Data.LocalQuorum - when (null as) $ - throwM (mkError status404 "not-found" "Push token not found") - Native.deleteTokens as Nothing + Data.lookup uid Data.LocalQuorum + >>= ( \case + [] -> pure Nothing + xs -> Native.deleteTokens xs Nothing $> Just () + ) + . filter (\x -> x ^. addrToken == tok) listTokens :: UserId -> Gundeck PushTokenList listTokens uid = PushTokenList . map (^. addrPushToken) <$> Data.lookup uid Data.LocalQuorum diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 3a5819ba6b..012c7802d0 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -22,6 +22,7 @@ import AWS.Util (readAuthExpiration) import qualified Amazonka as AWS import Cassandra (runClient, shutdown) import Cassandra.Schema (versionCheck) +import Control.Error (ExceptT (ExceptT)) import Control.Exception (finally) import Control.Lens hiding (enum) import Control.Monad.Extra @@ -29,9 +30,11 @@ import Data.Metrics (Metrics) import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Middleware (metrics) import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) +import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) import qualified Database.Redis as Redis import Gundeck.API (sitemap) +import Gundeck.API.Public (servantSitemap) import qualified Gundeck.Aws as Aws import Gundeck.Env import qualified Gundeck.Env as Env @@ -45,9 +48,12 @@ import Network.Wai as Wai import qualified Network.Wai.Middleware.Gunzip as GZip import qualified Network.Wai.Middleware.Gzip as GZip import Network.Wai.Utilities.Server hiding (serverPort) +import Servant (Handler (Handler), (:<|>) (..)) +import qualified Servant import qualified System.Logger as Log import qualified UnliftIO.Async as Async import Util.Options +import Wire.API.Routes.Public.Gundeck (GundeckAPI) import Wire.API.Routes.Version.Wai run :: Opts -> IO () @@ -80,10 +86,22 @@ run o = do . GZip.gzip GZip.def . catchErrors (e ^. applog) [Right $ e ^. monitor] +type CombinedAPI = GundeckAPI :<|> Servant.Raw + mkApp :: Env -> Wai.Application -mkApp e r k = runGundeck e r (route routes r k) +mkApp env = + Servant.serve + (Proxy @CombinedAPI) + (servantSitemap' env :<|> Servant.Tagged (runGundeckWithRoutes env)) + where + runGundeckWithRoutes :: Env -> Wai.Application + runGundeckWithRoutes e r k = runGundeck e r (route (compile sitemap) r k) + +servantSitemap' :: Env -> Servant.Server GundeckAPI +servantSitemap' env = Servant.hoistServer (Proxy @GundeckAPI) toServantHandler servantSitemap where - routes = compile sitemap + toServantHandler :: Gundeck a -> Handler a + toServantHandler m = Handler . ExceptT $ Right <$> runDirect env m collectAuthMetrics :: MonadIO m => Metrics -> AWS.Env -> m () collectAuthMetrics m env = do diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 112bd708bc..c9d1c27cc1 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -93,10 +93,12 @@ tests s = test s "Fetch all notifications" testFetchAllNotifs, test s "Fetch new notifications" testFetchNewNotifs, test s "No new notifications" testNoNewNotifs, - test s "Missing notifications" testMissingNotifs, + test s "Missing notifications (until API Version 3)" testMissingNotifsV2, + test s "Missing notifications (from API Version 3)" testMissingNotifsV3, test s "Fetch last notification" testFetchLastNotif, test s "No last notification" testNoLastNotif, - test s "Bad 'since' parameter" testFetchNotifBadSince, + test s "Bad 'since' parameter (until API Version 3)" testFetchNotifBadSinceV2, + test s "Bad 'since' parameter (from API Version 3)" testFetchNotifBadSinceV3, test s "Fetch notification by ID" testFetchNotifById, test s "Filter notifications by client" testFilterNotifByClient, test s "Paging" testNotificationPaging @@ -181,7 +183,8 @@ replacePresence = do assertTrue "Cannon is not removed" $ elem localhost8080 . map resource . decodePresence setPresence gu pres2 - !!! const 201 === statusCode + !!! const 201 + === statusCode getPresence gu (showUser uid) !!! do const 2 === length . decodePresence assertTrue "New Cannon" $ @@ -481,7 +484,7 @@ testFetchNewNotifs = do get ( runGundeckR gu . zUser ally - . path "notifications" + . paths ["v3", "notifications"] . query [("since", Just (toByteString' (ns !! 1)))] ) !!! do @@ -497,31 +500,43 @@ testNoNewNotifs = do get ( runGundeckR gu . zUser ally - . path "notifications" + . paths ["v3", "notifications"] . query [("since", Just (toByteString' n))] ) !!! do const 200 === statusCode const (Just []) === parseNotificationIds -testMissingNotifs :: TestM () -testMissingNotifs = do - gu <- view tsGundeck - other <- randomId - sendPush (buildPush other [(other, RecipientClientsAll)] (textPayload "hello")) - (old : _) <- map (view queuedNotificationId) <$> listNotifications other Nothing - ally <- randomId - sendPush (buildPush ally [(ally, RecipientClientsAll)] (textPayload "hello")) - ns <- listNotifications ally Nothing - get - ( runGundeckR gu - . zUser ally - . path "notifications" - . query [("since", Just (toByteString' old))] - ) - !!! do +testMissingNotifsV2 :: TestM () +testMissingNotifsV2 = do + testMissingNotifs "v2" $ \ns -> do + const 404 === statusCode + const (Just ns) === parseNotifications + +testMissingNotifsV3 :: TestM () +testMissingNotifsV3 = + testMissingNotifs "v3" $ + const $ do const 404 === statusCode - const (Just ns) === parseNotifications + const Nothing === parseNotifications + +testMissingNotifs :: ByteString -> ([QueuedNotification] -> Assertions ()) -> TestM () +testMissingNotifs version checks = + do + gu <- view tsGundeck + other <- randomId + sendPush (buildPush other [(other, RecipientClientsAll)] (textPayload "hello")) + (old : _) <- map (view queuedNotificationId) <$> listNotifications other Nothing + ally <- randomId + sendPush (buildPush ally [(ally, RecipientClientsAll)] (textPayload "hello")) + ns <- listNotifications ally Nothing + get + ( runGundeckR gu + . zUser ally + . paths [version, "notifications"] + . query [("since", Just (toByteString' old))] + ) + !!! checks ns testFetchLastNotif :: TestM () testFetchLastNotif = do @@ -542,8 +557,22 @@ testNoLastNotif = do const 404 === statusCode const (Just "not-found") =~= responseBody -testFetchNotifBadSince :: TestM () -testFetchNotifBadSince = do +testFetchNotifBadSinceV3 :: TestM () +testFetchNotifBadSinceV3 = do + gu <- view tsGundeck + ally <- randomId + sendPush (buildPush ally [(ally, RecipientClientsAll)] (textPayload "first")) + get + ( runGundeckR gu + . zUser ally + . paths ["v3", "notifications"] + . query [("since", Just "jumberjack")] + ) + !!! do + const 400 === statusCode + +testFetchNotifBadSinceV2 :: TestM () +testFetchNotifBadSinceV2 = do gu <- view tsGundeck ally <- randomId sendPush (buildPush ally [(ally, RecipientClientsAll)] (textPayload "first")) @@ -711,7 +740,7 @@ testNotificationPaging = do maybe id (queryItem "client" . toByteString') c . maybe id (queryItem "since" . toByteString') start . queryItem "size" (toByteString' step) - r <- get (runGundeckR gu . path "/notifications" . zUser u . range) (getPresence g (toByteString' uid) Maybe ClientId -> TestM (Response (Maybe BL.ByteString)) @@ -1167,7 +1198,8 @@ randomUser = do "password" .= ("secret" :: Text) ] r <- post (runBrigR br . path "/i/users" . json p) - pure . readNote "unable to parse Location header" + pure + . readNote "unable to parse Location header" . C.unpack $ getHeader' "Location" r where