hscim-0.4.0: test/Test/Class/UserSpec.hs
{-# LANGUAGE QuasiQuotes #-}
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 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.Class.UserSpec
( spec,
)
where
import Data.ByteString.Lazy (ByteString)
import Network.Wai (Application)
import Servant (Proxy (Proxy))
import Servant.API.Generic
import Test.Hspec
import Test.Hspec.Wai hiding (patch, post, put, shouldRespondWith)
import Web.Scim.Server (UserAPI, mkapp, userServer)
import Web.Scim.Server.Mock
import Web.Scim.Test.Util
app :: IO Application
app = do
storage <- emptyTestStorage
let auth = Just "authorized"
pure $ mkapp @Mock (Proxy @(UserAPI Mock)) (toServant (userServer auth)) (nt storage)
spec :: Spec
spec = with app $ do
describe "GET & POST /Users" $ do
it "responds with [] in empty environment" $ do
get "/" `shouldRespondWith` emptyList
it "can insert then retrieve stored Users" $ do
post "/" newBarbara `shouldRespondWith` 201
post "/" newJim `shouldRespondWith` 201
get "/" `shouldRespondWith` allUsers
describe "filtering" $ do
it "can filter by username" $ do
post "/" newBarbara `shouldRespondWith` 201
get "/?filter=userName eq \"bjensen\"" `shouldRespondWith` onlyBarbara
it "is case-insensitive regarding syntax" $ do
post "/" newBarbara `shouldRespondWith` 201
get "/?filter=USERName EQ \"bjensen\"" `shouldRespondWith` onlyBarbara
it "is case-insensitive regarding usernames" $ do
post "/" newBarbara `shouldRespondWith` 201
get "/?filter=userName eq \"BJensen\"" `shouldRespondWith` onlyBarbara
it "handles malformed filter syntax" $ do
post "/" newBarbara `shouldRespondWith` 201
get "/?filter=userName eqq \"bjensen\"" `shouldRespondWith` 400
-- TODO: would be nice to check the error message as well
it "handles type errors in comparisons" $ do
post "/" newBarbara `shouldRespondWith` 201
get "/?filter=userName eq true" `shouldRespondWith` 400
describe "GET /Users/:id" $ do
it "responds with 404 for unknown user" $ do
get "/9999" `shouldRespondWith` 404
-- FUTUREWORK: currently it returns 404:
-- https://github.com/haskell-servant/servant/issues/1155
xit "responds with 401 for unparseable user ID" $ do
get "/unparseable" `shouldRespondWith` 401
it "retrieves stored user" $ do
post "/" newBarbara `shouldRespondWith` 201
-- the test implementation stores users with uid [0,1..n-1]
get "/0" `shouldRespondWith` barbara
describe "PUT /Users/:id" $ do
it "overwrites the user" $ do
post "/" newBarbara `shouldRespondWith` 201
put "/0" barbUpdate0 `shouldRespondWith` updatedBarb0
it "does not create new users" $ do
post "/" newBarbara `shouldRespondWith` 201
put "/9999" newBarbara `shouldRespondWith` 404
-- TODO(arianvp): Perhaps we want to make this an acceptance spec.
describe "PATCH /Users/:id" $ do
describe "Add" $ do
-- TODO(arianvp): Implement and test multi-value fields properly
-- TODO(arianvp): We need to merge multi-value fields, but not supported yet
-- TODO(arianvp): Add and Replace tests currently identical, because of lack of multi-value
it "adds all fields if no target" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Add",
"value": {
"userName": "arian",
"displayName": "arian"
}
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "arian",
"displayName": "arian",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
it "adds fields if they didn't exist yet" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Add",
"path": "displayName",
"value": "arian"
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "bjensen",
"displayName": "arian",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
it "replaces individual simple fields" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Add",
"path": "userName",
"value": "arian"
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "arian",
"displayName": "bjensen2",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
-- TODO(arianvp): I think this is better done with quickcheck test.
-- Generate some adds, replaces, removes and then an invalid one However,
-- for this we need to be able to generate valid patches but a patch does
-- not limit by type what fields it lenses in to. It is a very untyped
-- thingy currently.
it "PatchOp is atomic. Either fully applies or not at all" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Add",
"path": "userName",
"value": "arian"
},
{ "op": "Add",
"path": "displayName",
"value": 5
}]}|]
`shouldRespondWith` 400
get "/0" `shouldRespondWith` smallUserGet {matchStatus = 200}
describe "Replace" $ do
-- TODO(arianvp): Implement and test multi-value fields properly
it "adds all fields if no target" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Replace",
"value": {
"userName": "arian",
"displayName": "arian"
}
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "arian",
"displayName": "arian",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
it "adds fields if they didn't exist yet" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Replace",
"path": "displayName",
"value": "arian"
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "bjensen",
"displayName": "arian",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
it "replaces individual simple fields" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Replace",
"path": "userName",
"value": "arian"
}]
}|]
`shouldRespondWith` [scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "arian",
"displayName": "bjensen2",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
{ matchStatus = 200
}
it "PatchOp is atomic. Either fully applies or not at all" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{
"op": "Replace",
"path": "userName",
"value": "arian"
},
{ "op": "Replace",
"path": "displayName",
"value": 5
}]}|]
`shouldRespondWith` 400
get "/0" `shouldRespondWith` smallUserGet {matchStatus = 200}
describe "Remove" $ do
it "fails if no target" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" barbUpdate0 -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{ "op": "Remove" }]
}|]
`shouldRespondWith` [scim|{
"scimType":"noTarget","status":"400","schemas":["urn:ietf:params:scim:api:messages:2.0:Error"]
}|]
{ matchStatus = 400
}
it "fails if removing immutable" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" barbUpdate0 -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{ "op": "Remove", "path": "userName"}]
}|]
`shouldRespondWith` [scim|{
"scimType":"mutability","status":"400","schemas":["urn:ietf:params:scim:api:messages:2.0:Error"]
}|]
{ matchStatus = 400
}
it "deletes the specified attribute" $ do
post "/" newBarbara `shouldRespondWith` 201
_ <- put "/0" smallUser -- reset
patch
"/0"
[scim|{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [{ "op": "Remove", "path": "displayName"}]
}|]
`shouldRespondWith` [scim|{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "bjensen",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}|]
{ matchStatus = 200
}
describe "DELETE /Users/:id" $ do
it "responds with 404 for unknown user" $ do
delete "/9999" `shouldRespondWith` 404
it "deletes a stored user" $ do
post "/" newBarbara `shouldRespondWith` 201
delete "/0" `shouldRespondWith` 204
-- user should be gone
get "/0" `shouldRespondWith` 404
delete "/0" `shouldRespondWith` 404
smallUser :: ByteString
smallUser =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"bjensen",
"displayName": "bjensen2"
}|]
smallUserGet :: ResponseMatcher
smallUserGet =
[scim|
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
],
"userName": "bjensen",
"displayName": "bjensen2",
"id": "0",
"meta": {
"resourceType": "User",
"location": "https://example.com/Users/id",
"created": "2018-01-01T00:00:00Z",
"version": "W/\"testVersion\"",
"lastModified": "2018-01-01T00:00:00Z"
}
}
|]
newBarbara :: ByteString
newBarbara =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"bjensen",
"externalId":"bjensen",
"name":{
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen",
"givenName":"Barbara"
}
}|]
newJim :: ByteString
newJim =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"jim",
"externalId":"jim",
"name":{
"formatted":"Jim",
"familyName":"",
"givenName":"Jim"
}
}|]
barbara :: ResponseMatcher
barbara =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"bjensen",
"name": {
"givenName":"Barbara",
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen"
},
"id":"0",
"externalId":"bjensen",
"meta":{
"resourceType":"User",
"location":"https://example.com/Users/id",
"created":"2018-01-01T00:00:00Z",
"version":"W/\"testVersion\"",
"lastModified":"2018-01-01T00:00:00Z"
}
}|]
allUsers :: ResponseMatcher
allUsers =
[scim|
{ "schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 2,
"itemsPerPage": 2,
"startIndex": 1,
"Resources":
[{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"bjensen",
"externalId":"bjensen",
"id" : "0",
"name":{
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen",
"givenName":"Barbara"
},
"meta":{
"resourceType":"User",
"location":"https://example.com/Users/id",
"created":"2018-01-01T00:00:00Z",
"version":"W/\"testVersion\"",
"lastModified":"2018-01-01T00:00:00Z"
}
},
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"jim",
"externalId":"jim",
"id" : "1",
"name":{
"formatted":"Jim",
"familyName":"",
"givenName":"Jim"
},
"meta":{
"resourceType":"User",
"location":"https://example.com/Users/id",
"created":"2018-01-01T00:00:00Z",
"version":"W/\"testVersion\"",
"lastModified":"2018-01-01T00:00:00Z"
}
}]
}|]
onlyBarbara :: ResponseMatcher
onlyBarbara =
[scim|
{ "schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"itemsPerPage": 1,
"startIndex": 1,
"Resources":
[{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName":"bjensen",
"externalId":"bjensen",
"id" : "0",
"name":{
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen",
"givenName":"Barbara"
},
"meta":{
"resourceType":"User",
"location":"https://example.com/Users/id",
"created":"2018-01-01T00:00:00Z",
"version":"W/\"testVersion\"",
"lastModified":"2018-01-01T00:00:00Z"
}
}]
}|]
-- source: https://tools.ietf.org/html/rfc7644#section-3.5.1 (p. 30)
barbUpdate0 :: ByteString
barbUpdate0 =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"id":"0",
"userName":"bjensen",
"externalId":"bjensen",
"name":{
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen",
"givenName":"Barbara",
"middleName":"Jane"
},
"roles":[],
"emails":[
{
"value":"bjensen@example.com"
},
{
"value":"babs@jensen.org"
}
]
}|]
updatedBarb0 :: ResponseMatcher
updatedBarb0 =
[scim|
{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
"id":"0",
"userName":"bjensen",
"externalId":"bjensen",
"name":{
"formatted":"Ms. Barbara J Jensen III",
"familyName":"Jensen",
"givenName":"Barbara",
"middleName":"Jane"
},
"emails":[
{
"value":"bjensen@example.com"
},
{
"value":"babs@jensen.org"
}
],
"meta":{
"resourceType":"User",
"location":"https://example.com/Users/id",
"created":"2018-01-01T00:00:00Z",
"version":"W/\"testVersion\"",
"lastModified":"2018-01-01T00:00:00Z"
}
}|]
emptyList :: ResponseMatcher
emptyList =
[scim|
{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"Resources":[],
"totalResults":0,
"itemsPerPage":0,
"startIndex":1
}|]