packages feed

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\"}"