bloodhound-1.0.0.0: tests/Test/SecurityUsersSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.SecurityUsersSpec (spec) where
import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as L
import TestsUtils.Import
import Prelude
-- | A single-user GET response entry — ES emits @null@ for unset
-- full_name / email.
sampleUserBytes :: L.ByteString
sampleUserBytes =
"{\
\ \"username\": \"alice\",\
\ \"roles\": [\"admin\", \"reader\"],\
\ \"full_name\": null,\
\ \"email\": \"alice@example.com\",\
\ \"enabled\": true,\
\ \"metadata\": {\"_reserved\": true}\
\}"
-- | @POST /_security/user/{username}/_has_privileges@ response.
sampleHasPrivilegesResponseBytes :: L.ByteString
sampleHasPrivilegesResponseBytes =
"{\
\ \"username\": \"alice\",\
\ \"has_all_requested\": false,\
\ \"cluster\": {\"monitor\": true, \"manage\": false},\
\ \"index\": {\
\ \"logs-*\": {\"read\": true, \"write\": false}\
\ },\
\ \"application\": {\
\ \"myapp\": {\"resource:1\": {\"read\": true}}\
\ }\
\}"
-- | @GET /_security/user/_privileges@ response.
sampleUserPrivilegesBytes :: L.ByteString
sampleUserPrivilegesBytes =
"{\
\ \"username\": \"alice\",\
\ \"cluster\": [\"monitor\", \"read_ilm\"],\
\ \"index\": [\
\ {\"names\": [\"logs-*\"], \"privileges\": [\"read\"]}\
\ ],\
\ \"application\": [\
\ {\"application\": \"myapp\", \"privileges\": [\"read\"], \"resources\": [\"*\"]}\
\ ],\
\ \"run_as\": [\"other\"]\
\}"
spec :: Spec
spec = do
describe "User" $ do
it "decodes with null full_name and keeps extras" $ do
let Just u = decode sampleUserBytes :: Maybe User
uUsername u `shouldBe` "alice"
uRoles u `shouldBe` [RoleName "admin", RoleName "reader"]
uFullName u `shouldBe` Nothing
uEmail u `shouldBe` Just "alice@example.com"
uEnabled u `shouldBe` True
it "defaults enabled to true when absent" $ do
let Just u = decode "{\"username\":\"x\"}" :: Maybe User
uEnabled u `shouldBe` True
describe "UserCreateBody" $ do
it "omits Nothing fields on encode (omitNulls drops the empty roles array)" $ do
let body = defaultUserCreateBody
encode body `shouldBe` "{\"metadata\":{}}"
it "round-trips a body with password and roles" $ do
let body =
(defaultUserCreateBody)
{ ucbPassword = Just "secret",
ucbRoles = [RoleName "admin"]
}
let Just rt = decode (encode body) :: Maybe UserCreateBody
ucbPassword rt `shouldBe` Just "secret"
ucbRoles rt `shouldBe` [RoleName "admin"]
describe "UserCreatedResponse" $ do
it "decodes {created:bool}" $ do
let Just uc = decode "{\"created\":true}" :: Maybe UserCreatedResponse
ucCreated uc `shouldBe` True
describe "UserDeletedResponse" $ do
it "decodes found + acknowledged" $ do
let Just ud = decode "{\"found\":true,\"acknowledged\":true}" :: Maybe UserDeletedResponse
udFound ud `shouldBe` Just True
describe "UsersListResponse" $ do
it "decodes an object keyed by username" $ do
let bytes =
"{\
\ \"alice\": {\"username\":\"alice\",\"roles\":[\"admin\"]},\
\ \"bob\": {\"username\":\"bob\",\"roles\":[]}\
\}"
let Just resp = decode bytes :: Maybe UsersListResponse
length (usersListEntries resp) `shouldBe` 2
describe "HasPrivilegesRequest" $ do
it "encodes the nested index/application structure and round-trips" $ do
let req =
(defaultHasPrivilegesRequest)
{ hprCluster = ["monitor"],
hprIndex = [(IndexPattern "logs-*", ["read", "write"])],
hprApplication =
[ ( "myapp",
[("resource:1", ["read"])]
)
]
}
let bytes = encode req
-- Round-trip recovers the request, proving the nested
-- index/application structure survived encode→decode intact.
let Just rt = decode bytes :: Maybe HasPrivilegesRequest
hprCluster rt `shouldBe` ["monitor"]
hprIndex rt `shouldBe` [(IndexPattern "logs-*", ["read", "write"])]
hprApplication rt `shouldBe` [("myapp", [("resource:1", ["read"])])]
describe "HasPrivilegesResponse" $ do
it "decodes the nested boolean tree" $ do
let Just resp = decode sampleHasPrivilegesResponseBytes :: Maybe HasPrivilegesResponse
hpUsername resp `shouldBe` "alice"
hpHasAllRequested resp `shouldBe` False
-- Cluster section: monitor=true, manage=false.
lookup "monitor" (hpCluster resp) `shouldBe` Just True
lookup "manage" (hpCluster resp) `shouldBe` Just False
-- Index section: one pattern, read=true.
case hpIndex resp of
[(_, ixResults)] -> lookup "read" ixResults `shouldBe` Just True
other -> expectationFailure ("expected one index entry, got " <> show other)
-- Application section nested two levels deep.
case hpApplication resp of
[("myapp", resResults)] ->
case lookup "resource:1" resResults of
Just appPrivs -> lookup "read" appPrivs `shouldBe` Just True
Nothing -> expectationFailure "missing resource:1"
other -> expectationFailure ("expected myapp, got " <> show other)
describe "UserPrivileges" $ do
it "decodes cluster/index/application/run_as" $ do
let Just up = decode sampleUserPrivilegesBytes :: Maybe UserPrivileges
upCluster up `shouldBe` ["monitor", "read_ilm"]
length (upIndex up) `shouldBe` 1
upRunAs up `shouldBe` ["other"]
describe "ChangePasswordBody" $ do
it "encodes {password}" $ do
encode (ChangePasswordBody "pw") `shouldBe` "{\"password\":\"pw\"}"