diff --git a/changelog.d/3-bug-fixes/gundeck-arn-parsing b/changelog.d/3-bug-fixes/gundeck-arn-parsing new file mode 100644 index 00000000000..31a489112a0 --- /dev/null +++ b/changelog.d/3-bug-fixes/gundeck-arn-parsing @@ -0,0 +1 @@ +gundeck: Fix parsing errors for SNS ARN for VOIP Tokens diff --git a/deploy/dockerephemeral/init.sh b/deploy/dockerephemeral/init.sh index 7f11fc7ee0c..676e1b4a106 100755 --- a/deploy/dockerephemeral/init.sh +++ b/deploy/dockerephemeral/init.sh @@ -32,6 +32,7 @@ for suffix in "" "2" "3" "4" "5" "-federation-v0"; do # Create SNS resources for gundeck's notifications exec_until_ready "aws --endpoint-url=http://sns:4575 sns create-platform-application --name integration-test$suffix --platform GCM --attributes PlatformCredential=testkey" exec_until_ready "aws --endpoint-url=http://sns:4575 sns create-platform-application --name integration-test$suffix --platform APNS_SANDBOX --attributes PlatformCredential=testprivatekey" + exec_until_ready "aws --endpoint-url=http://sns:4575 sns create-platform-application --name integration-test$suffix --platform APNS_VOIP_SANDBOX --attributes PlatformCredential=testprivatekey" exec_until_ready "aws --endpoint-url=http://sns:4575 sns create-platform-application --name integration-com.wire.ent$suffix --platform APNS_SANDBOX --attributes PlatformCredential=testprivatekey" # Cargohold's bucket; creating a bucket is not idempotent so we just try once and wait until it is ready diff --git a/integration/default.nix b/integration/default.nix index a259708844e..6abbb50c753 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -10,6 +10,7 @@ , async , attoparsec , base +, base16-bytestring , base64-bytestring , bytestring , bytestring-conversion @@ -99,6 +100,7 @@ mkDerivation { async attoparsec base + base16-bytestring base64-bytestring bytestring bytestring-conversion diff --git a/integration/integration.cabal b/integration/integration.cabal index 56bcb614de8..e9563261c78 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -136,6 +136,7 @@ library Test.MLS.Unreachable Test.Notifications Test.Presence + Test.PushToken Test.Roles Test.Search Test.Services @@ -174,6 +175,7 @@ library , async , attoparsec , base + , base16-bytestring , base64-bytestring , bytestring , bytestring-conversion diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 066c360a422..cdc4b11c2d4 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -4,7 +4,8 @@ import Control.Monad import Control.Monad.IO.Class import Data.Array ((!)) import qualified Data.Array as Array -import System.Random (randomRIO) +import qualified Data.ByteString as BS +import System.Random (randomIO, randomRIO) import Testlib.Prelude teamRole :: String -> Int @@ -43,14 +44,24 @@ randomHandleWithRange min' max' = liftIO $ do chars = mkArray $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-." pick = (chars !) <$> randomRIO (Array.bounds chars) +randomBytes :: Int -> App ByteString +randomBytes n = liftIO $ BS.pack <$> replicateM n randomIO + randomHex :: Int -> App String randomHex n = liftIO $ replicateM n pick where chars = mkArray (['0' .. '9'] <> ['a' .. 'f']) pick = (chars !) <$> randomRIO (Array.bounds chars) +-- Should not have leading 0. randomClientId :: App String -randomClientId = randomHex 16 +randomClientId = do + second <- randomHex 15 + first <- pick + pure $ first : second + where + chars = mkArray (['1' .. '9'] <> ['a' .. 'f']) + pick = (chars !) <$> randomRIO (Array.bounds chars) mkArray :: [a] -> Array.Array Int a mkArray l = Array.listArray (0, length l - 1) l diff --git a/integration/test/API/Gundeck.hs b/integration/test/API/Gundeck.hs index 1f2c1a76429..d44603ca2aa 100644 --- a/integration/test/API/Gundeck.hs +++ b/integration/test/API/Gundeck.hs @@ -1,6 +1,9 @@ module API.Gundeck where import API.Common +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text import Testlib.Prelude data GetNotifications = GetNotifications @@ -58,37 +61,83 @@ getLastNotification user opts = do baseRequest user Gundeck Versioned "/notifications/last" submit "GET" $ req & addQueryParams [("client", c) | c <- toList opts.client] -data PostPushToken = PostPushToken +data GeneratePushToken = GeneratePushToken { transport :: String, app :: String, - token :: Maybe String, tokenSize :: Int } -instance Default PostPushToken where +instance Default GeneratePushToken where def = - PostPushToken + GeneratePushToken { transport = "GCM", app = "test", - token = Nothing, tokenSize = 16 } +generateAndPostPushToken :: + (HasCallStack, MakesValue user, MakesValue client) => + user -> + client -> + GeneratePushToken -> + App Response +generateAndPostPushToken user client args = do + token <- generateToken args.tokenSize + clientId <- make client & asString + postPushToken user $ PushToken args.transport args.app token clientId + +data PushToken = PushToken + { transport :: String, + app :: String, + token :: String, + client :: String + } + deriving (Show, Eq) + +instance ToJSON PushToken where + toJSON pt = + object + [ "transport" .= pt.transport, + "app" .= pt.app, + "token" .= pt.token, + "client" .= pt.client + ] + +instance MakesValue PushToken where + make = pure . toJSON + +generateToken :: Int -> App String +generateToken = + fmap (Text.unpack . Text.decodeUtf8 . Base16.encode) . randomBytes + postPushToken :: - (HasCallStack, MakesValue u, MakesValue c) => - u -> - c -> - PostPushToken -> + ( HasCallStack, + MakesValue token, + MakesValue user + ) => + user -> + token -> App Response -postPushToken u client args = do - req <- baseRequest u Gundeck Versioned "/push/tokens" - token <- maybe (randomHex (args.tokenSize * 2)) pure args.token - c <- make client - let t = - object - [ "transport" .= args.transport, - "app" .= args.app, - "token" .= token, - "client" .= c - ] - submit "POST" $ req & addJSON t +postPushToken user token = do + req <- baseRequest user Gundeck Versioned "/push/tokens" + tokenJson <- make token + submit "POST" $ req & addJSON tokenJson + +listPushTokens :: (MakesValue user) => user -> App Response +listPushTokens user = do + req <- + baseRequest user Gundeck Versioned $ + joinHttpPath ["/push/tokens"] + submit "GET" req + +unregisterClient :: + (MakesValue user, MakesValue client) => + user -> + client -> + App Response +unregisterClient user client = do + cid <- asString client + req <- + baseRequest user Gundeck Unversioned $ + joinHttpPath ["/i/clients", cid] + submit "DELETE" req diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 1d6803e0beb..341505a11de 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -13,12 +13,15 @@ import Control.Monad.Reader import Crypto.Random (getRandomBytes) import Data.Aeson hiding ((.=)) import qualified Data.Aeson.Types as Aeson +import qualified Data.ByteString.Base16 as Base16 import qualified Data.ByteString.Base64.URL as B64Url import Data.ByteString.Char8 (unpack) import qualified Data.CaseInsensitive as CI import Data.Default import Data.Function import Data.String.Conversions (cs) +import qualified Data.Text as Text +import Data.Text.Encoding (decodeUtf8) import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack @@ -183,6 +186,15 @@ createMLSOne2OnePartner domain other convDomain = loop randomToken :: HasCallStack => App String randomToken = unpack . B64Url.encode <$> liftIO (getRandomBytes 16) +data TokenLength = GCM | APNS + +randomSnsToken :: HasCallStack => TokenLength -> App String +randomSnsToken = \case + GCM -> mkTok 16 + APNS -> mkTok 32 + where + mkTok = fmap (Text.unpack . decodeUtf8 . Base16.encode) . randomBytes + randomId :: HasCallStack => App String randomId = liftIO (show <$> nextRandom) diff --git a/integration/test/Test/EJPD.hs b/integration/test/Test/EJPD.hs index b20e74a6634..36301a9cb96 100644 --- a/integration/test/Test/EJPD.hs +++ b/integration/test/Test/EJPD.hs @@ -44,23 +44,23 @@ setupEJPD = toks1 <- do cl11 <- objId $ addClient (usr1 %. "qualified_id") def >>= getJSON 201 - bindResponse (postPushToken usr1 cl11 def) $ \resp -> do + bindResponse (generateAndPostPushToken usr1 cl11 def) $ \resp -> do resp.status `shouldMatchInt` 201 tok <- resp.json %. "token" & asString pure [tok] toks2 <- do cl21 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 cl22 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 - t1 <- bindResponse (postPushToken usr2 cl21 def) $ \resp -> do + t1 <- bindResponse (generateAndPostPushToken usr2 cl21 def) $ \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "token" & asString - t2 <- bindResponse (postPushToken usr2 cl22 def) $ \resp -> do + t2 <- bindResponse (generateAndPostPushToken usr2 cl22 def) $ \resp -> do resp.status `shouldMatchInt` 201 resp.json %. "token" & asString pure [t1, t2] toks4 <- do cl41 <- objId $ addClient (usr4 %. "qualified_id") def >>= getJSON 201 - bindResponse (postPushToken usr4 cl41 def) $ \resp -> do + bindResponse (generateAndPostPushToken usr4 cl41 def) $ \resp -> do resp.status `shouldMatchInt` 201 tok <- resp.json %. "token" & asString pure [tok] diff --git a/integration/test/Test/Presence.hs b/integration/test/Test/Presence.hs index a733d2bb539..e6252ea7e2a 100644 --- a/integration/test/Test/Presence.hs +++ b/integration/test/Test/Presence.hs @@ -27,7 +27,7 @@ testRemoveUser :: HasCallStack => App () testRemoveUser = do -- register alice and add a push token (alice, c) <- registerUser - void $ postPushToken alice c def >>= getJSON 201 + void $ generateAndPostPushToken alice c def >>= getJSON 201 do t <- getPushTokens alice >>= getJSON 200 tokens <- t %. "tokens" & asList diff --git a/integration/test/Test/PushToken.hs b/integration/test/Test/PushToken.hs new file mode 100644 index 00000000000..24f30573b03 --- /dev/null +++ b/integration/test/Test/PushToken.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +module Test.PushToken where + +import API.Common +import API.Gundeck +import SetupHelpers +import Testlib.Prelude + +testRegisterPushToken :: App () +testRegisterPushToken = do + alice <- randomUser OwnDomain def + aliceC2 <- randomClientId + aliceC1 <- randomClientId + + -- Client 1 with 4 tokens + c1Apns1 <- randomSnsToken APNS + c1Apns1Overlap <- randomSnsToken APNS + c1Apns2 <- randomSnsToken APNS + c1Gcm1 <- randomSnsToken GCM + + -- Client 2 with 1 token + c2Apns1 <- randomSnsToken APNS + c2Gcm1 <- randomSnsToken GCM + c2Gcm1Overlap <- randomSnsToken GCM + + let apnsToken = PushToken "APNS_SANDBOX" "test" + let gcmToken = PushToken "GCM" "test" + + let c1Apns1Token = apnsToken c1Apns1 aliceC1 + let c1Apns1OverlapToken = apnsToken c1Apns1Overlap aliceC1 + let c1Apns2Token = (apnsToken c1Apns2 aliceC1 :: PushToken) {app = "com.wire.ent"} -- diff app prevents overlap + let c1Gcm1Token = gcmToken c1Gcm1 aliceC1 + + let c2Apns1Token = apnsToken c2Apns1 aliceC2 + let c2Gcm1Token = gcmToken c2Gcm1 aliceC2 + let c2Gcm1OverlapToken = gcmToken c2Gcm1Overlap aliceC2 + + -- Register non-overlapping tokens for client 1 + assertStatus 201 =<< (postPushToken alice c1Apns1Token) + assertStatus 201 =<< (postPushToken alice c1Apns2Token) + assertStatus 201 =<< (postPushToken alice c1Gcm1Token) + + -- register non-overlapping tokens for client 2 + assertStatus 201 =<< (postPushToken alice c2Apns1Token) + assertStatus 201 =<< (postPushToken alice c2Gcm1Token) + + bindResponse (listPushTokens alice) \resp -> do + resp.status `shouldMatchInt` 200 + allTokens <- resp.json %. "tokens" + allTokens + `shouldMatchSet` [ c1Apns1Token, + c1Apns2Token, + c1Gcm1Token, + c2Apns1Token, + c2Gcm1Token + ] + + -- Resistering an overlapping token overwrites it. + assertStatus 201 =<< postPushToken alice c1Apns1OverlapToken + assertStatus 201 =<< postPushToken alice c2Gcm1OverlapToken + + bindResponse (listPushTokens alice) \resp -> do + resp.status `shouldMatchInt` 200 + allTokens <- resp.json %. "tokens" + allTokens + `shouldMatchSet` [ c1Apns1OverlapToken, + c1Apns2Token, + c1Gcm1Token, + c2Apns1Token, + c2Gcm1OverlapToken + ] + + -- Push tokens are deleted alongside clients + assertStatus 200 =<< unregisterClient alice aliceC1 + assertStatus 200 =<< unregisterClient alice aliceC2 + + bindResponse (listPushTokens alice) \resp -> do + resp.status `shouldMatchInt` 200 + allTokens <- resp.json %. "tokens" + allTokens + `shouldMatchSet` ([] :: [PushToken]) + +testVoipTokenRegistrationFails :: App () +testVoipTokenRegistrationFails = do + alice <- randomUser OwnDomain def + aliceC2 <- randomClientId + + token <- randomSnsToken APNS + let apnsVoipToken = PushToken "APNS_VOIP_SANDBOX" "test" token aliceC2 + postPushToken alice apnsVoipToken `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "apns-voip-not-supported" diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index c087d911135..b8794553a45 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -45,14 +45,12 @@ module Gundeck.Types.Push.V2 recipientClients, Route (..), ApsData, - ApsPreference (..), ApsLocKey (..), ApsSound (..), apsData, apsLocKey, apsLocArgs, apsSound, - apsPreference, apsBadge, -- * Priority (re-export) @@ -176,24 +174,10 @@ newtype ApsSound = ApsSound {fromSound :: Text} newtype ApsLocKey = ApsLocKey {fromLocKey :: Text} deriving (Eq, Show, ToJSON, FromJSON, Arbitrary) -data ApsPreference - = ApsStdPreference - deriving (Eq, Show, Generic) - deriving (Arbitrary) via GenericUniform ApsPreference - -instance ToJSON ApsPreference where - toJSON ApsStdPreference = "std" - -instance FromJSON ApsPreference where - parseJSON = withText "ApsPreference" $ \case - "std" -> pure ApsStdPreference - x -> fail $ "Invalid preference: " ++ show x - data ApsData = ApsData { _apsLocKey :: !ApsLocKey, _apsLocArgs :: [Text], _apsSound :: !(Maybe ApsSound), - _apsPreference :: !(Maybe ApsPreference), _apsBadge :: !Bool } deriving (Eq, Show, Generic) @@ -202,15 +186,14 @@ data ApsData = ApsData makeLenses ''ApsData apsData :: ApsLocKey -> [Text] -> ApsData -apsData lk la = ApsData lk la Nothing Nothing True +apsData lk la = ApsData lk la Nothing True instance ToJSON ApsData where - toJSON (ApsData k a s p b) = + toJSON (ApsData k a s b) = object $ "loc_key" .= k # "loc_args" .= a # "sound" .= s - # "preference" .= p # "badge" .= b # [] @@ -220,7 +203,6 @@ instance FromJSON ApsData where <$> o .: "loc_key" <*> o .:? "loc_args" .!= [] <*> o .:? "sound" - <*> o .:? "preference" <*> o .:? "badge" .!= True ----------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/Error/Gundeck.hs b/libs/wire-api/src/Wire/API/Error/Gundeck.hs index ac9b6ce363f..2fa60fbad31 100644 --- a/libs/wire-api/src/Wire/API/Error/Gundeck.hs +++ b/libs/wire-api/src/Wire/API/Error/Gundeck.hs @@ -26,6 +26,7 @@ data GundeckError | AddTokenErrorInvalid | AddTokenErrorTooLong | AddTokenErrorMetadataTooLong + | AddTokenErrorApnsVoipNotSupported | TokenNotFound | NotificationNotFound @@ -42,6 +43,8 @@ type instance MapError 'AddTokenErrorTooLong = 'StaticError 413 "token-too-long" type instance MapError 'AddTokenErrorMetadataTooLong = 'StaticError 413 "metadata-too-long" "Tried to add token to endpoint resulting in metadata length > 2048" +type instance MapError 'AddTokenErrorApnsVoipNotSupported = 'StaticError 400 "apns-voip-not-supported" "Adding APNS_VOIP tokens is not supported" + 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/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index 79f282b4d0f..29560be5fe8 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -115,6 +115,8 @@ data Transport = GCM | APNS | APNSSandbox + | APNSVoIP + | APNSVoIPSandbox deriving stock (Eq, Ord, Show, Bounded, Enum, Generic) deriving (Arbitrary) via (GenericUniform Transport) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Transport) @@ -125,7 +127,9 @@ instance ToSchema Transport where mconcat [ element "GCM" GCM, element "APNS" APNS, - element "APNS_SANDBOX" APNSSandbox + element "APNS_SANDBOX" APNSSandbox, + element "APNS_VOIP" APNSVoIP, + element "APNS_VOIP_SANDBOX" APNSVoIPSandbox ] instance FromByteString Transport where @@ -134,6 +138,8 @@ instance FromByteString Transport where "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 newtype Token = Token @@ -169,7 +175,8 @@ type AddTokenErrorResponses = ErrorResponse 'E.AddTokenErrorNotFound, ErrorResponse 'E.AddTokenErrorInvalid, ErrorResponse 'E.AddTokenErrorTooLong, - ErrorResponse 'E.AddTokenErrorMetadataTooLong + ErrorResponse 'E.AddTokenErrorMetadataTooLong, + ErrorResponse 'E.AddTokenErrorApnsVoipNotSupported ] type AddTokenSuccessResponses = @@ -187,6 +194,7 @@ data AddTokenError | AddTokenErrorInvalid | AddTokenErrorTooLong | AddTokenErrorMetadataTooLong + | AddTokenErrorApnsVoipNotSupported deriving (Show, Generic) deriving (AsUnion AddTokenErrorResponses) via GenericAsUnion AddTokenErrorResponses AddTokenError diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 8940de2043c..9778eec5ec5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -806,6 +806,12 @@ tests = ), ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_3, "testObject_Push_2eToken_2eTransport_user_3.json" + ), + ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_4, + "testObject_Push_2eToken_2eTransport_user_4.json" + ), + ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_5, + "testObject_Push_2eToken_2eTransport_user_5.json" ) ], testGroup "Golden: Token_user" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs index fc7c1ed7f14..96739ba620c 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs @@ -17,7 +17,7 @@ module Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user where -import Wire.API.Push.Token (Transport (APNS, APNSSandbox, GCM)) +import Wire.API.Push.Token (Transport (APNS, APNSSandbox, APNSVoIP, APNSVoIPSandbox, GCM)) import Wire.API.Push.Token qualified as Push.Token (Transport) testObject_Push_2eToken_2eTransport_user_1 :: Push.Token.Transport @@ -28,3 +28,9 @@ testObject_Push_2eToken_2eTransport_user_2 = APNS testObject_Push_2eToken_2eTransport_user_3 :: Push.Token.Transport testObject_Push_2eToken_2eTransport_user_3 = APNSSandbox + +testObject_Push_2eToken_2eTransport_user_4 :: Push.Token.Transport +testObject_Push_2eToken_2eTransport_user_4 = APNSVoIP + +testObject_Push_2eToken_2eTransport_user_5 :: Push.Token.Transport +testObject_Push_2eToken_2eTransport_user_5 = APNSVoIPSandbox 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 ffb00e875b7..57daad1dd22 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 @@ -43,6 +43,7 @@ import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize +import Test.Wire.API.Golden.Manual.Token import Test.Wire.API.Golden.Manual.UserClientPrekeyMap import Test.Wire.API.Golden.Manual.UserEvent import Test.Wire.API.Golden.Manual.UserIdList @@ -147,6 +148,9 @@ tests = testGroup "GroupId" $ testObjects [(testObject_GroupId_1, "testObject_GroupId_1.json")], + testGroup "PushToken" $ + testObjects + [(testObject_Token_1, "testObject_Token_1.json")], testGroup "TeamSize" $ testObjects [ (testObject_TeamSize_1, "testObject_TeamSize_1.json"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs new file mode 100644 index 00000000000..2fa8207ddc9 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs @@ -0,0 +1,29 @@ +-- 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 Test.Wire.API.Golden.Manual.Token where + +import Data.Id +import Wire.API.Push.V2.Token + +testObject_Token_1 :: PushToken +testObject_Token_1 = + pushToken + APNSVoIPSandbox + (AppName {appNameText = "j{\110746\SOH_\1084873M"}) + (Token {tokenText = "K"}) + (ClientId {clientToWord64 = 6}) diff --git a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json new file mode 100644 index 00000000000..d177fe0e9d7 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json @@ -0,0 +1 @@ +"APNS_VOIP" diff --git a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json new file mode 100644 index 00000000000..fd689b4ac10 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json @@ -0,0 +1 @@ +"APNS_VOIP_SANDBOX" 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 00000000000..36f8ff69bd8 --- /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 3ea6d980ab9..9c9087dcf58 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -593,6 +593,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.SearchResultContact Test.Wire.API.Golden.Manual.SubConversation Test.Wire.API.Golden.Manual.TeamSize + Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap Test.Wire.API.Golden.Manual.UserEvent Test.Wire.API.Golden.Manual.UserIdList diff --git a/services/brig/docs/swagger-v3.json b/services/brig/docs/swagger-v3.json index b844341e756..e252a739717 100644 --- a/services/brig/docs/swagger-v3.json +++ b/services/brig/docs/swagger-v3.json @@ -14,7 +14,9 @@ "enum": [ "GCM", "APNS", - "APNS_SANDBOX" + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" ], "type": "string" }, diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json index 7ff1394f344..937aafdefc9 100644 --- a/services/brig/docs/swagger-v4.json +++ b/services/brig/docs/swagger-v4.json @@ -4828,7 +4828,9 @@ "enum": [ "GCM", "APNS", - "APNS_SANDBOX" + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" ], "type": "string" }, diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index b1636f33b7c..ea5fe968866 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -369,12 +369,17 @@ newtype Attributes = Attributes -- Note [VoIP TTLs] -- ~~~~~~~~~~~~~~~~ --- For GCM, APNS and APNS_SANDBOX, SNS treats the TTL "0" +-- The TTL message attributes for APNS_VOIP and APNS_VOIP_SANDBOX are not +-- documented but appear to work. The reason might be that TTLs were +-- introduced before support for VoIP notifications. There is a catch, +-- however. For GCM, APNS and APNS_SANDBOX, SNS treats the TTL "0" -- specially, i.e. it forwards it to the provider where it has a special --- meaning. Which means if the TTL is lower than the "dwell time" in SNS, --- the notification is never sent to the provider. So we must specify a --- reasonably large TTL for transient VoIP notifications, so that they are --- not discarded already by SNS. +-- meaning. That does not appear to be the case for APNS_VOIP and +-- APNS_VOIP_SANDBOX, for which the TTL is interpreted normally, which means +-- if the TTL is lower than the "dwell time" in SNS, the notification is +-- never sent to the provider. So we must specify a reasonably large TTL +-- for transient VoIP notifications, so that they are not discarded +-- already by SNS. -- -- cf. http://docs.aws.amazon.com/sns/latest/dg/sns-ttl.html @@ -390,9 +395,13 @@ timeToLive t s = Attributes (Endo (ttlAttr s)) ttlNow GCM = "0" ttlNow APNS = "0" ttlNow APNSSandbox = "0" + ttlNow APNSVoIP = "15" -- See note [VoIP TTLs] + ttlNow APNSVoIPSandbox = "15" -- See note [VoIP TTLs] ttlKey GCM = "AWS.SNS.MOBILE.GCM.TTL" ttlKey APNS = "AWS.SNS.MOBILE.APNS.TTL" ttlKey APNSSandbox = "AWS.SNS.MOBILE.APNS_SANDBOX.TTL" + ttlKey APNSVoIP = "AWS.SNS.MOBILE.APNS_VOIP.TTL" + ttlKey APNSVoIPSandbox = "AWS.SNS.MOBILE.APNS_VOIP_SANDBOX.TTL" publish :: EndpointArn -> LT.Text -> Attributes -> Amazon (Either PublishError ()) publish arn txt attrs = do diff --git a/services/gundeck/src/Gundeck/Aws/Arn.hs b/services/gundeck/src/Gundeck/Aws/Arn.hs index 6c09b4bf362..17588d08106 100644 --- a/services/gundeck/src/Gundeck/Aws/Arn.hs +++ b/services/gundeck/src/Gundeck/Aws/Arn.hs @@ -135,6 +135,8 @@ arnTransportText :: Transport -> Text arnTransportText GCM = "GCM" arnTransportText APNS = "APNS" arnTransportText APNSSandbox = "APNS_SANDBOX" +arnTransportText APNSVoIP = "APNS_VOIP" +arnTransportText APNSVoIPSandbox = "APNS_VOIP_SANDBOX" -- Parsers -------------------------------------------------------------------- @@ -163,5 +165,7 @@ endpointTopicParser = do transportParser :: Parser Transport transportParser = string "GCM" $> GCM + <|> string "APNS_VOIP_SANDBOX" $> APNSVoIPSandbox + <|> string "APNS_VOIP" $> APNSVoIP <|> string "APNS_SANDBOX" $> APNSSandbox <|> string "APNS" $> APNS diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index 8b5b334f15f..83ab2a692b4 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -34,22 +34,21 @@ import Gundeck.Aws.Arn (EndpointArn) import Gundeck.Types import Imports --- | We provide a instance for `Either Int Transport` so we can handle (ie., gracefully ignore --- rather than crash on) deprecated values in cassandra. See "Gundeck.Push.Data". -instance Cql (Either Int32 Transport) where +instance Cql Transport where ctype = Tagged IntColumn - toCql (Right GCM) = CqlInt 0 - toCql (Right APNS) = CqlInt 1 - toCql (Right APNSSandbox) = CqlInt 2 - toCql (Left i) = CqlInt i -- (this is weird, but it's helpful for cleaning up deprecated tokens.) + toCql GCM = CqlInt 0 + toCql APNS = CqlInt 1 + toCql APNSSandbox = CqlInt 2 + toCql APNSVoIP = CqlInt 3 + toCql APNSVoIPSandbox = CqlInt 4 fromCql (CqlInt i) = case i of - 0 -> pure $ Right GCM - 1 -> pure $ Right APNS - 2 -> pure $ Right APNSSandbox - 3 -> pure (Left 3) -- `APNSVoIPV1` tokens are deprecated and will be ignored - 4 -> pure (Left 4) -- `APNSVoIPSandboxV1` tokens are deprecated and will be ignored + 0 -> pure GCM + 1 -> pure APNS + 2 -> pure APNSSandbox + 3 -> pure APNSVoIP + 4 -> pure APNSVoIPSandbox n -> Left $ "unexpected transport: " ++ show n fromCql _ = Left "transport: int expected" diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 3e6fa5c05c6..b11785fa770 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -36,6 +36,7 @@ import Control.Error import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens (to, view, (.~), (^.)) import Control.Monad.Catch +import Control.Monad.Except (throwError) import Data.Aeson as Aeson (Object) import Data.Id import Data.List.Extra qualified as List @@ -350,10 +351,7 @@ nativeTargets psh rcps' alreadySent = addresses :: Recipient -> m [Address] addresses u = do addrs <- mntgtLookupAddresses (u ^. recipientId) - pure - $ preference - . filter (eligible u) - $ addrs + pure $ filter (eligible u) addrs eligible :: Recipient -> Address -> Bool eligible u a -- Never include the origin client. @@ -373,17 +371,7 @@ nativeTargets psh rcps' alreadySent = whitelistedOrNoWhitelist a = null (psh ^. pushConnections) || a ^. addrConn `elem` psh ^. pushConnections - -- Apply transport preference in case of alternative transports for the - -- same client. If no explicit preference is given, the default preference depends on the priority. - preference as = - let pref = psh ^. pushNativeAps >>= view apsPreference - in filter (pick (fromMaybe defPreference pref)) as - where - pick pr a = case a ^. addrTransport of - GCM -> True - APNS -> pr == ApsStdPreference - APNSSandbox -> pr == ApsStdPreference - defPreference = ApsStdPreference + check :: Either SomeException [a] -> m [a] check (Left e) = mntgtLogErr e >> pure [] check (Right r) = pure r @@ -391,21 +379,21 @@ nativeTargets psh rcps' alreadySent = type AddTokenResponse = Either Public.AddTokenError Public.AddTokenSuccess addToken :: UserId -> ConnId -> PushToken -> Gundeck AddTokenResponse -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)) - ~~ msg (val "Registering push token") - continue newtok cur - >>= either - pure - ( \a -> do - Native.deleteTokens old (Just a) - pure (Right $ Public.AddTokenSuccess newtok) - ) +addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) $ runExceptT $ do + when (newtok ^. tokenTransport `elem` [APNSVoIP, APNSVoIPSandbox]) $ + throwError Public.AddTokenErrorApnsVoipNotSupported + + (cur, old) <- lift $ foldl' (matching newtok) (Nothing, []) <$> Data.lookup uid Data.LocalQuorum + lift $ + Log.info $ + "user" + .= UUID.toASCIIBytes (toUUID uid) + ~~ "token" + .= Text.take 16 (tokenText (newtok ^. token)) + ~~ msg (val "Registering push token") + addr <- continue newtok cur + lift $ Native.deleteTokens old (Just addr) + pure $ Public.AddTokenSuccess newtok where matching :: PushToken -> @@ -424,14 +412,14 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) continue :: PushToken -> Maybe Address -> - Gundeck (Either AddTokenResponse Address) + ExceptT Public.AddTokenError Gundeck Address continue t Nothing = create (0 :: Int) t continue t (Just a) = update (0 :: Int) t (a ^. addrEndpoint) create :: Int -> PushToken -> - Gundeck (Either AddTokenResponse Address) + ExceptT Public.AddTokenError Gundeck Address create n t = do let trp = t ^. tokenTransport let app = t ^. tokenApp @@ -441,32 +429,33 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) ept <- Aws.execute aws' (Aws.createEndpoint uid trp env app tok) case ept of Left (Aws.EndpointInUse arn) -> do - Log.info $ "arn" .= toText arn ~~ msg (val "ARN in use") + lift $ Log.info $ "arn" .= toText arn ~~ msg (val "ARN in use") update (n + 1) t arn Left (Aws.AppNotFound app') -> do - Log.info $ msg ("Push token of unknown application: '" <> appNameText app' <> "'") - pure (Left (Left Public.AddTokenErrorNotFound)) + lift $ Log.info $ msg ("Push token of unknown application: '" <> appNameText app' <> "'") + throwError Public.AddTokenErrorNotFound Left (Aws.InvalidToken _) -> do - Log.info $ - "token" - .= tokenText tok - ~~ msg (val "Invalid push token.") - pure (Left (Left Public.AddTokenErrorInvalid)) + lift $ + Log.info $ + "token" + .= tokenText tok + ~~ msg (val "Invalid push token.") + throwError Public.AddTokenErrorInvalid Left (Aws.TokenTooLong l) -> do - Log.info $ msg ("Push token is too long: token length = " ++ show l) - pure (Left (Left Public.AddTokenErrorTooLong)) + lift $ Log.info $ msg ("Push token is too long: token length = " ++ show l) + throwError Public.AddTokenErrorTooLong Right arn -> do Data.insert uid trp app tok arn cid (t ^. tokenClient) - pure (Right (mkAddr t arn)) + pure $ mkAddr t arn update :: Int -> PushToken -> SnsArn EndpointTopic -> - Gundeck (Either AddTokenResponse Address) + ExceptT Public.AddTokenError Gundeck Address update n t arn = do when (n >= 3) $ do - Log.err $ msg (val "AWS SNS inconsistency w.r.t. " +++ toText arn) + lift $ Log.err $ msg (val "AWS SNS inconsistency w.r.t. " +++ toText arn) throwM (mkError status500 "server-error" "Server Error") aws' <- view awsEnv ept <- Aws.execute aws' (Aws.lookupEndpoint arn) @@ -474,7 +463,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) Nothing -> create (n + 1) t Just ep -> do - updateEndpoint uid t arn ep + lift $ updateEndpoint uid t arn ep Data.insert uid (t ^. tokenTransport) @@ -483,7 +472,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) arn cid (t ^. tokenClient) - pure (Right (mkAddr t arn)) + pure $ mkAddr t arn `catch` \case -- Note: If the endpoint was recently deleted (not necessarily -- concurrently), we may get an EndpointNotFound error despite @@ -492,7 +481,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) -- 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 (Left Public.AddTokenErrorMetadataTooLong)) + Aws.InvalidCustomData {} -> throwError Public.AddTokenErrorMetadataTooLong ex -> throwM ex mkAddr :: diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index fa495b0e1fe..c688f64f4db 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -38,29 +38,26 @@ import System.Logger.Class qualified as Log lookup :: (MonadClient m, MonadLogger m) => UserId -> Consistency -> m [Address] lookup u c = foldM mk [] =<< retry x1 (query q (params c (Identity u))) where - q :: PrepQuery R (Identity UserId) (UserId, Either Int32 Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) + q :: PrepQuery R (Identity UserId) (UserId, Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) q = "select usr, transport, app, ptoken, arn, connection, client from user_push where usr = ?" mk as r = maybe as (: as) <$> mkAddr r insert :: MonadClient m => UserId -> Transport -> AppName -> Token -> EndpointArn -> ConnId -> ClientId -> m () -insert u t a p e o c = retry x5 $ write q (params LocalQuorum (u, Right t, a, p, e, o, c)) +insert u t a p e o c = retry x5 $ write q (params LocalQuorum (u, t, a, p, e, o, c)) where - q :: PrepQuery W (UserId, Either Int32 Transport, AppName, Token, EndpointArn, ConnId, ClientId) () + q :: PrepQuery W (UserId, Transport, AppName, Token, EndpointArn, ConnId, ClientId) () q = "insert into user_push (usr, transport, app, ptoken, arn, connection, client) values (?, ?, ?, ?, ?, ?, ?)" updateArn :: MonadClient m => UserId -> Transport -> AppName -> Token -> EndpointArn -> m () -updateArn uid transport app token arn = retry x5 $ write q (params LocalQuorum (arn, uid, Right transport, app, token)) +updateArn uid transport app token arn = retry x5 $ write q (params LocalQuorum (arn, uid, transport, app, token)) where - q :: PrepQuery W (EndpointArn, UserId, Either Int32 Transport, AppName, Token) () + q :: PrepQuery W (EndpointArn, UserId, Transport, AppName, Token) () q = {- `IF EXISTS`, but that requires benchmarking -} "update user_push set arn = ? where usr = ? and transport = ? and app = ? and ptoken = ?" delete :: MonadClient m => UserId -> Transport -> AppName -> Token -> m () -delete u t = deleteAux u (Right t) - -deleteAux :: MonadClient m => UserId -> Either Int32 Transport -> AppName -> Token -> m () -deleteAux u t a p = retry x5 $ write q (params LocalQuorum (u, t, a, p)) +delete u t a p = retry x5 $ write q (params LocalQuorum (u, t, a, p)) where - q :: PrepQuery W (UserId, Either Int32 Transport, AppName, Token) () + q :: PrepQuery W (UserId, Transport, AppName, Token) () q = "delete from user_push where usr = ? and transport = ? and app = ? and ptoken = ?" erase :: MonadClient m => UserId -> m () @@ -71,20 +68,16 @@ erase u = retry x5 $ write q (params LocalQuorum (Identity u)) mkAddr :: (MonadClient m, MonadLogger m) => - (UserId, Either Int32 Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) -> + (UserId, Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) -> m (Maybe Address) -mkAddr (usr, trp, app, tok, arn, con, clt) = case (trp, clt, arn) of - (Right t, Just c, Just a) -> pure $! Just $! Address usr a con (pushToken t app tok c) +mkAddr (usr, trp, app, tok, arn, con, clt) = case (clt, arn) of + (Just c, Just a) -> pure $! Just $! Address usr a con (pushToken trp app tok c) _ -> do Log.info $ field "user" (toByteString usr) ~~ field "transport" (show trp) ~~ field "app" (appNameText app) ~~ field "token" (tokenText tok) - ~~ msg - ( val - "Deleting legacy push token without a client or ARN, or with deprecated \ - \APNSVoIP* transports (transport type not shown in this message)." - ) - deleteAux usr trp app tok + ~~ msg (val "Deleting legacy push token without a client or ARN.") + delete usr trp app tok pure Nothing diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index 07f783c36d9..bf9e0e491cc 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -54,6 +54,8 @@ renderText t prio x = case t of GCM -> trim "GCM" (jsonString gcmJson) APNS -> trim "APNS" (jsonString stdApnsJson) APNSSandbox -> trim "APNS_SANDBOX" (jsonString stdApnsJson) + APNSVoIP -> trim "APNS_VOIP" (jsonString voipApnsJson) + APNSVoIPSandbox -> trim "APNS_VOIP_SANDBOX" (jsonString voipApnsJson) where gcmJson = object @@ -65,6 +67,11 @@ renderText t prio x = case t of [ "aps" .= apsDict, "data" .= x ] + voipApnsJson = + object + [ "aps" .= object [], + "data" .= x + ] -- https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications -- Must contain `mutable-content: 1` and include an alert dictionary with title, subtitle, or body information. -- Since we have no useful data here, we send a default payload that gets overridden by the client @@ -87,6 +94,8 @@ maxPayloadSize :: Transport -> Int64 maxPayloadSize GCM = 4096 maxPayloadSize APNS = 4096 maxPayloadSize APNSSandbox = 4096 +maxPayloadSize APNSVoIP = 5120 +maxPayloadSize APNSVoIPSandbox = 5120 gcmPriority :: Priority -> Text gcmPriority LowPriority = "normal" diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 0d9f128b4ae..a91a075ac00 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -106,8 +106,7 @@ tests s = ], testGroup "Tokens" - [ test s "register a push token" testRegisterPushToken, - test s "unregister a push token" testUnregisterPushToken + [ test s "unregister a push token" testUnregisterPushToken ], testGroup "Websocket pingpong" @@ -724,49 +723,6 @@ testUnregisterClient = do -- ${env}-test (FCM), ${env}-test (APNS_SANDBOX), ${env}-com.wire.ent (APNS_SANDBOX), -- with ${env} normally being integration. -testRegisterPushToken :: TestM () -testRegisterPushToken = do - g <- view tsGundeck - uid <- randomUser - -- Client 1 with 4 distinct tokens - c1 <- randomClientId - t11 <- randomToken c1 apnsToken - t11' <- randomToken c1 apnsToken -- overlaps - t12 <- randomToken c1 apnsToken {tName = AppName "com.wire.ent"} -- different app - t13 <- randomToken c1 gcmToken -- different transport - - -- Client 2 with 1 token - c2 <- randomClientId - t21 <- randomToken c2 apnsToken - t22 <- randomToken c2 gcmToken -- different transport - t22' <- randomToken c2 gcmToken -- overlaps - - -- Register non-overlapping tokens - _ <- registerPushToken uid t11 - _ <- registerPushToken uid t12 - _ <- registerPushToken uid t13 - _ <- registerPushToken uid t21 - _ <- registerPushToken uid t22 - -- Check tokens - _tokens <- sortPushTokens <$> listPushTokens uid - let _expected = sortPushTokens [t11, t12, t13, t21, t22] - liftIO $ assertEqual "unexpected tokens" _expected _tokens - -- Register overlapping tokens. The previous overlapped - -- tokens should be removed, but none of the others. - _ <- registerPushToken uid t11' - _ <- registerPushToken uid t22' - -- Check tokens - _tokens <- sortPushTokens <$> listPushTokens uid - let _expected = sortPushTokens [t11', t12, t13, t21, t22'] - liftIO $ assertEqual "unexpected tokens" _expected _tokens - -- Native push tokens are deleted together with the client - unregisterClient g uid c1 !!! const 200 === statusCode - unregisterClient g uid c1 !!! const 200 === statusCode -- (deleting a non-existing token is ok.) - unregisterClient g uid c2 !!! const 200 === statusCode - unregisterClient g uid c2 !!! const 200 === statusCode -- (deleting a non-existing token is ok.) - _tokens <- listPushTokens uid - liftIO $ assertEqual "unexpected tokens" [] _tokens - -- TODO: Try to make this test more performant, this test takes too long right now testRegisterTooManyTokens :: TestM () testRegisterTooManyTokens = do @@ -838,8 +794,9 @@ testSharePushToken = do gcmTok <- Token . T.decodeUtf8 . toByteString' <$> randomId apsTok <- Token . T.decodeUtf8 . B16.encode <$> randomBytes 32 let tok1 = pushToken GCM "test" gcmTok - let tok2 = pushToken APNS "com.wire.int.ent" apsTok - forM_ [tok1, tok2] $ \tk -> do + let tok2 = pushToken APNSVoIP "com.wire.dev.ent" apsTok + let tok3 = pushToken APNS "com.wire.int.ent" apsTok + forM_ [tok1, tok2, tok3] $ \tk -> do u1 <- randomUser u2 <- randomUser c1 <- randomClientId @@ -995,10 +952,6 @@ connectUsersAndDevicesWithSendingClientsRaw ca uidsAndConnIds = do assertPresences :: (UserId, [ConnId]) -> TestM () assertPresences (uid, conns) = wsAssertPresences uid (length conns) --- | Sort 'PushToken's based on the actual 'token' values. -sortPushTokens :: [PushToken] -> [PushToken] -sortPushTokens = sortBy (compare `on` view token) - wsRun :: HasCallStack => CannonR -> UserId -> ConnId -> WS.ClientApp () -> TestM (Async ()) wsRun ca uid (ConnId con) app = do liftIO $ async $ WS.runClientWith caHost caPort caPath caOpts caHdrs app diff --git a/services/gundeck/test/unit/Native.hs b/services/gundeck/test/unit/Native.hs index 500ec668ff6..2e525f7cf1f 100644 --- a/services/gundeck/test/unit/Native.hs +++ b/services/gundeck/test/unit/Native.hs @@ -73,6 +73,8 @@ instance FromJSON SnsNotification where [("GCM", String n)] -> parseGcm n [("APNS", String n)] -> parseApns APNS n [("APNS_SANDBOX", String n)] -> parseApns APNSSandbox n + [("APNS_VOIP", String n)] -> parseApns APNSVoIP n + [("APNS_VOIP_SANDBOX", String n)] -> parseApns APNSVoIPSandbox n _ -> mempty where parseApns t n =