hscim-0.3.6: test/Test/Schema/PatchOpSpec.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE QuasiQuotes #-}
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2020 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.Schema.PatchOpSpec where
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Result (Error, Success), Value (String), fromJSON, toJSON)
import qualified Data.Aeson.Types as Aeson
import Data.Attoparsec.ByteString (parseOnly)
import Data.Either (isLeft)
import Data.Foldable (for_)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import HaskellWorks.Hspec.Hedgehog (require)
import Hedgehog (Gen, Property, forAll, property, tripping)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Test.FilterSpec (genAttrPath, genSubAttr, genValuePath)
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy, xit)
import Test.Schema.Util (mk_prop_caseInsensitive)
import Web.Scim.AttrName (AttrName (..))
import Web.Scim.Filter (AttrPath (..), CompValue (ValNull), CompareOp (OpEq), Filter (..), ValuePath (..))
import Web.Scim.Schema.PatchOp
import Web.Scim.Schema.Schema (Schema (User20))
import Web.Scim.Schema.User (UserTypes)
import Web.Scim.Schema.UserTypes (supportedSchemas)
import Web.Scim.Test.Util (TestTag, scim)
isSuccess :: Result a -> Bool
isSuccess (Success _) = True
isSuccess (Error _) = False
genPatchOp :: forall tag. UserTypes tag => Gen Value -> Gen (PatchOp tag)
genPatchOp genValue = PatchOp <$> Gen.list (Range.constant 0 20) ((genOperation @tag) genValue)
genSimplePatchOp :: forall tag. UserTypes tag => Gen (PatchOp tag)
genSimplePatchOp = genPatchOp @tag (String <$> Gen.text (Range.constant 0 20) Gen.unicode)
genOperation :: forall tag. UserTypes tag => Gen Value -> Gen Operation
genOperation genValue = Operation <$> Gen.enumBounded <*> Gen.maybe (genPath @tag) <*> Gen.maybe genValue
genPath :: forall tag. UserTypes tag => Gen Path
genPath =
Gen.choice
[ IntoValuePath <$> (genValuePath @tag) <*> Gen.maybe genSubAttr,
NormalPath <$> (genAttrPath @tag)
]
prop_roundtrip :: forall tag. UserTypes tag => Property
prop_roundtrip = property $ do
x <- forAll $ genPath @tag
tripping x (encodeUtf8 . rPath) (parseOnly $ pPath (supportedSchemas @tag))
prop_roundtrip_PatchOp :: forall tag. UserTypes tag => Property
prop_roundtrip_PatchOp = property $ do
-- Just some strings for now. However, should be constrained to what the
-- PatchOp is operating on in the future... We need better typed PatchOp for
-- this. TODO(arianvp)
x <- forAll (genSimplePatchOp @tag)
tripping x toJSON fromJSON
type PatchTestTag = TestTag () () () ()
spec :: Spec
spec = do
describe "Patchable" $ do
describe "HashMap Text Text" $ do
it "supports `Add` operation" $ do
let theMap :: HashMap Text Text = HM.empty
operation = Operation Add (Just $ NormalPath (AttrPath Nothing (AttrName "key") Nothing)) $ Just "value"
applyOperation theMap operation `shouldBe` (Right $ HM.singleton "key" "value")
it "supports `Replace` operation" $ do
let theMap :: HashMap Text Text = HM.singleton "key" "value1"
operation = Operation Replace (Just $ NormalPath (AttrPath Nothing (AttrName "key") Nothing)) $ Just "value2"
applyOperation theMap operation `shouldBe` (Right $ HM.singleton "key" "value2")
it "supports `Delete` operation" $ do
let theMap :: HashMap Text Text = HM.fromList [("key1", "value1"), ("key2", "value2")]
operation = Operation Remove (Just $ NormalPath (AttrPath Nothing (AttrName "key1") Nothing)) Nothing
applyOperation theMap operation `shouldBe` (Right $ HM.singleton "key2" "value2")
it "gracefully rejects invalid/unsupported operations" $ do
let theMap :: HashMap Text Text = HM.fromList [("key1", "value1"), ("key2", "value2")]
key1Path = (AttrPath Nothing (AttrName "key1") Nothing)
key2Path = (AttrPath Nothing (AttrName "key2") Nothing)
invalidOperations =
[ Operation Add (Just $ NormalPath key1Path) Nothing, -- Nothing to add
Operation Replace (Just $ NormalPath key1Path) Nothing, -- Nothing to replace
Operation Add (Just $ IntoValuePath (ValuePath key1Path (FilterAttrCompare key2Path OpEq ValNull)) Nothing) Nothing
-- IntoValuePaths don't make sense for HashMap Text Text
]
mapM_ (\o -> applyOperation theMap o `shouldSatisfy` isLeft) invalidOperations
describe "urn:ietf:params:scim:api:messages:2.0:PatchOp" $ do
describe "The body of each request MUST contain the \"schemas\" attribute with the URI value of \"urn:ietf:params:scim:api:messages:2.0:PatchOp\"." $ do
it "rejects an empty schemas list" $ do
fromJSON @(PatchOp PatchTestTag)
[scim| {
"schemas": [],
"operations": []
}|]
`shouldSatisfy` (not . isSuccess)
--TODO(arianvp): We don't support arbitrary path names (yet)
it "roundtrips Path" $ require $ prop_roundtrip @PatchTestTag
it "roundtrips PatchOp" $ require $ prop_roundtrip_PatchOp @PatchTestTag
it "case-insensitive" $ require $ mk_prop_caseInsensitive (genSimplePatchOp @PatchTestTag)
it "rejects invalid operations" $ do
fromJSON @(PatchOp PatchTestTag)
[scim| {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"operations": [{"op":"unknown"}]
}|]
`shouldSatisfy` (not . isSuccess)
-- TODO(arianvp/akshay): Implement if required
xit "rejects unknown paths" $ do
Aeson.parse (pathFromJSON [User20]) (Aeson.String "unknown.field") `shouldSatisfy` (not . isSuccess)
it "rejects invalid paths" $ do
Aeson.parse (pathFromJSON [User20]) "unknown]field" `shouldSatisfy` (not . isSuccess)
describe "Examples from https://tools.ietf.org/html/rfc7644#section-3.5.2 Figure 8" $ do
let examples =
[ "members",
"name.familyname",
"addresses[type eq \"work\"]",
"members[value eq \"2819c223-7f76-453a-919d-413861904646\"]",
"members[value eq \"2819c223-7f76-453a-919d-413861904646\"].displayname"
]
for_ examples $ \p -> it ("parses " ++ show p) $ (rPath <$> parseOnly (pPath (supportedSchemas @PatchTestTag)) p) `shouldBe` Right (decodeUtf8 p)