packages feed

consul-haskell-0.5.0: tests/Main.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

import Control.Concurrent
import Control.Monad (when)
import Control.Monad.IO.Class
import Control.Retry
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import Data.Maybe
#if MIN_VERSION_base(4,11,0)
-- (<>) is part of Prelude
#else
import Data.Monoid ((<>))
#endif
import Data.Text (Text)
import Data.UUID
import Network.Consul (createSession, deleteKey, destroySession,getKey, getSequencerForLock,getSessionInfo,initializeConsulClient, isValidSequencer,putKey,putKeyAcquireLock,withSession,ConsulClient(..),runService,getServiceHealth)
import Network.Consul.Types
import qualified Network.Consul.Internal as I
import Network.HTTP.Client
import Network.Socket (PortNumber(..))
import System.IO (hFlush)
import System.Process.Typed (proc, stopProcess)
import qualified System.Process.Typed as PT
import System.Random
import Test.Tasty
import Test.Tasty.HUnit
import UnliftIO.Temporary (withSystemTempFile)

import SocketUtils (isPortOpen, simpleSockAddr)

consulPort :: PortNumber
consulPort = 18500

newClient :: IO ConsulClient
newClient = initializeConsulClient "localhost" consulPort Nothing

{- Internal Tests -}
internalKVTests :: TestTree
internalKVTests = testGroup "Internal Key Value" [testGetInvalidKey, testPutKey, testPutKeyAcquireLock,testPutKeyReleaseLock,
  testGetKey,testGetKeys,testListKeys,testDeleteKey,testGetNullValueKey,testDeleteRecursive]

testGetInvalidKey :: TestTree
testGetInvalidKey = testCase "testGetInvalidKey" $ do
  client@ConsulClient{..} <- newClient
  x <- I.getKey ccManager (I.hostWithScheme client) ccPort "nokey" Nothing Nothing Nothing
  assertEqual "testGetInvalidKey: Found a key that doesn't exist" x Nothing

testPutKey :: TestTree
testPutKey = testCase "testPutKey" $ do
  client@ConsulClient{..} <- newClient
  let put = KeyValuePut "/testPutKey" "Test" Nothing Nothing
  x <- I.putKey ccManager (I.hostWithScheme client) ccPort put Nothing
  assertEqual "testPutKey: Write failed" True x

testPutKeyAcquireLock :: TestTree
testPutKeyAcquireLock = testCase "testPutKeyAcquireLock" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testPutKeyAcquireLock") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Nothing -> assertFailure "testPutKeyAcquireLock: No session was created"
    Just session -> do
      let put = KeyValuePut "/testPutKeyAcquireLock" "Test" Nothing Nothing
      x <- I.putKeyAcquireLock ccManager (I.hostWithScheme client) ccPort put session Nothing
      assertEqual "testPutKeyAcquireLock: Write failed" True x
      Just kv <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testPutKeyAcquireLock" Nothing Nothing Nothing
      let Just returnedSession = kvSession kv
      assertEqual "testPutKeyAcquireLock: Session was not found on key" returnedSession (sId session)

testPutKeyReleaseLock :: TestTree
testPutKeyReleaseLock = testCase "testPutKeyReleaseLock" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testPutKeyReleaseLock") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Nothing -> assertFailure "testPutKeyReleaseLock: No session was created"
    Just session -> do
      let put = KeyValuePut "/testPutKeyReleaseLock" "Test" Nothing Nothing
      x <- I.putKeyAcquireLock ccManager (I.hostWithScheme client) ccPort put session Nothing
      assertEqual "testPutKeyReleaseLock: Write failed" True x
      Just kv <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testPutKeyReleaseLock" Nothing Nothing Nothing
      let Just returnedSession = kvSession kv
      assertEqual "testPutKeyReleaseLock: Session was not found on key" returnedSession (sId session)
      let put2 = KeyValuePut "/testPutKeyReleaseLock" "Test" Nothing Nothing
      x2 <- I.putKeyReleaseLock ccManager (I.hostWithScheme client) ccPort put2 session Nothing
      assertEqual "testPutKeyReleaseLock: Release failed" True x2
      Just kv2 <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testPutKeyReleaseLock" Nothing Nothing Nothing
      assertEqual "testPutKeyAcquireLock: Session still held" Nothing (kvSession kv2)



testGetKey :: TestTree
testGetKey = testCase "testGetKey" $ do
  client@ConsulClient{..} <- newClient
  let put = KeyValuePut "/testGetKey" "Test" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put Nothing
  assertEqual "testGetKey: Write failed" True x1
  x2 <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testGetKey" Nothing Nothing Nothing
  case x2 of
    Just x -> assertEqual "testGetKey: Incorrect Value" (kvValue x) (Just "Test")
    Nothing -> assertFailure "testGetKey: No value returned"

testGetNullValueKey :: TestTree
testGetNullValueKey = testCase "testGetNullValueKey" $ do
  client@ConsulClient{..} <- newClient
  let put = KeyValuePut "/testGetNullValueKey" "" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put Nothing
  assertEqual "testGetNullValueKey: Write failed" True x1
  liftIO $ threadDelay (500 * 1000)
  x2 <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testGetNullValueKey" Nothing Nothing Nothing
  case x2 of
    Just x -> assertEqual "testGetNullValueKey: Incorrect Value" (kvValue x) Nothing
    Nothing -> assertFailure "testGetNullValueKey: No value returned"

testGetKeys :: TestTree
testGetKeys = testCase "testGetKeys" $ do
  client@ConsulClient{..} <- newClient
  let put1 = KeyValuePut "/testGetKeys/key1" "Test" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put1 Nothing
  assertEqual "testGetKeys: Write failed" True x1
  let put2 = KeyValuePut "/testGetKeys/key2" "Test" Nothing Nothing
  x2 <- I.putKey ccManager (I.hostWithScheme client) ccPort put2 Nothing
  assertEqual "testGetKeys: Write failed" True x2
  x3 <- I.getKeys ccManager (I.hostWithScheme client) ccPort "/testGetKeys" Nothing Nothing Nothing
  assertEqual "testGetKeys: Incorrect number of results" 2 (length x3)

testListKeys :: TestTree
testListKeys = testCase "testListKeys" $ do
  client@ConsulClient{..} <- newClient
  let put1 = KeyValuePut "/testListKeys/key1" "Test" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put1 Nothing
  assertEqual "testListKeys: Write failed" True x1
  let put2 = KeyValuePut "/testListKeys/key2" "Test" Nothing Nothing
  x2 <- I.putKey ccManager (I.hostWithScheme client) ccPort put2 Nothing
  assertEqual "testListKeys: Write failed" True x2
  x3 <- I.listKeys ccManager (I.hostWithScheme client) ccPort "/testListKeys/" Nothing Nothing Nothing
  assertEqual "testListKeys: Incorrect number of results" 2 (length x3)

testDeleteKey :: TestTree
testDeleteKey = testCase "testDeleteKey" $ do
  client@ConsulClient{..} <- newClient
  let put1 = KeyValuePut "/testDeleteKey" "Test" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put1 Nothing
  assertEqual "testDeleteKey: Write failed" True x1
  x2 <- I.deleteKey ccManager (I.hostWithScheme client) ccPort "/testDeleteKey" False Nothing
  assertEqual "testDeleteKey: Delete Failed" True x2
  x3 <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testDeleteKey" Nothing Nothing Nothing
  assertEqual "testDeleteKey: Key was not deleted" Nothing x3

testDeleteRecursive :: TestTree
testDeleteRecursive = testCase "testDeleteRecursive" $ do
  client@ConsulClient{..} <- newClient
  let put1 = KeyValuePut "/testDeleteRecursive/1" "Test" Nothing Nothing
      put2 = KeyValuePut "/testDeleteRecursive/2" "Test" Nothing Nothing
  x1 <- I.putKey ccManager (I.hostWithScheme client) ccPort put1 Nothing
  assertEqual "testDeleteKey: Write failed" True x1
  x2 <- I.putKey ccManager (I.hostWithScheme client) ccPort put2 Nothing
  assertEqual "testDeleteKey: Write failed" True x2
  I.deleteKey ccManager (I.hostWithScheme client) ccPort "/testDeleteRecursive/" True Nothing
  x3 <- I.getKey ccManager (I.hostWithScheme client) ccPort "/testDeleteRecursive/1" Nothing Nothing Nothing
  assertEqual "testDeleteKey: Key was not deleted" Nothing x3

{- Client KV -}
clientKVTests :: TestTree
clientKVTests = testGroup "Client KV Tests" [testDeleteRecursiveClient]

testDeleteRecursiveClient :: TestTree
testDeleteRecursiveClient = testCase "testDeleteRecursiveClient" $ do
  client <- newClient
  let put1 = KeyValuePut "/testDeleteRecursive/1" "Test" Nothing Nothing
      put2 = KeyValuePut "/testDeleteRecursive/2" "Test" Nothing Nothing
  x1 <- putKey client put1 Nothing
  assertEqual "testDeleteKey: Write failed" True x1
  x2 <- putKey client put2 Nothing
  assertEqual "testDeleteKey: Write failed" True x2
  deleteKey client "/testDeleteRecursive/" True Nothing
  x3 <- getKey client "/testDeleteRecursive/1" Nothing Nothing Nothing
  assertEqual "testDeleteKey: Key was not deleted" Nothing x3

{- Agent -}
testRegisterService :: TestTree
testRegisterService = testCase "testRegisterService" $ do
  client@ConsulClient{..} <- newClient
  let req = RegisterService Nothing "testService" ["test"] Nothing (Just $ Ttl "10s")
  val <- I.registerService ccManager (I.hostWithScheme client) ccPort req Nothing
  assertEqual "testRegisterService: Service was not created" val True
  mService <- I.getService ccManager (I.hostWithScheme client) ccPort "testService" Nothing Nothing
  case mService of
    Just _ -> return ()
    Nothing -> assertFailure "testRegisterService: Service was not found"

testGetSelf :: TestTree
testGetSelf = testCase "testGetSelf" $ do
  client@ConsulClient{..} <- newClient
  x <- I.getSelf ccManager (I.hostWithScheme client) ccPort
  assertEqual "testGetSelf: Self not returned" True (isJust x)

{-
testRegisterHealthCheck :: TestTree
testRegisterHealthCheck = testCase "testRegisterHealthCheck" $ do
  client@ConsulClient{..} <- newClient
  let check = RegisterHealthCheck "testHealthCheck" "testHealthCheck" "" Nothing Nothing (Just "15s")
  x1 <- I.registerHealthCheck ccManager (I.hostWithScheme client) ccPort check
  undefined -}

{- Health Checks -}
testGetServiceHealth :: TestTree
testGetServiceHealth = testCase "testGetServiceHealth" $ do
  client@ConsulClient{..} <- newClient
  let req = RegisterService (Just "testGetServiceHealth") "testGetServiceHealth" [] Nothing Nothing
  r1 <- I.registerService ccManager (I.hostWithScheme client) ccPort req Nothing
  case r1 of
    True -> do
      liftIO $ threadDelay 1000000
      r2 <- I.getServiceHealth ccManager (I.hostWithScheme client) ccPort "testGetServiceHealth"
      case r2 of
        Just [x] -> return ()
        Just [] -> assertFailure "testGetServiceHealth: No Services Returned"
        Nothing -> assertFailure "testGetServiceHealth: Failed to parse result"
    False -> assertFailure "testGetServiceHealth: Service was not created"

testHealth :: TestTree
testHealth = testGroup "Health Check Tests" [testGetServiceHealth]

{- Session -}
testCreateSession :: TestTree
testCreateSession = testCase "testCreateSession" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testCreateSession") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just _ -> return ()
    Nothing -> assertFailure "testCreateSession: No session was created"

testGetSessionInfo :: TestTree
testGetSessionInfo = testCase "testGetSessionInfo" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testGetSessionInfo") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just x -> do
      x1 <- I.getSessionInfo ccManager (I.hostWithScheme client) ccPort x Nothing
      case x1 of
        Just _ -> return ()
        Nothing -> assertFailure "testGetSessionInfo: Session Info was not returned"
    Nothing -> assertFailure "testGetSessionInfo: No session was created"

testRenewSession :: TestTree
testRenewSession = testCase "testRenewSession" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testRenewSession") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just x -> do
      x1 <- I.renewSession ccManager (I.hostWithScheme client) ccPort x Nothing
      case x1 of
        True -> return ()
        False -> assertFailure "testRenewSession: Session was not renewed"
    Nothing -> assertFailure "testRenewSession: No session was created"

testRenewNonexistentSession :: TestTree
testRenewNonexistentSession = testCase "testRenewNonexistentSession" $ do
  client@ConsulClient{..} <- newClient
  sessId :: UUID <- randomIO
  let session = Session (toText sessId) Nothing
  x <- I.renewSession ccManager (I.hostWithScheme client) ccPort session Nothing
  case x of
    True -> assertFailure "testRenewNonexistentSession: Non-existent session was renewed"
    False -> return ()

testDestroySession :: TestTree
testDestroySession = testCase "testDestroySession" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testDestroySession") Nothing ["serfHealth"] (Just Release) (Just "30s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just x -> do
      _ <- I.destroySession ccManager (I.hostWithScheme client) ccPort x Nothing
      x1 <- I.getSessionInfo ccManager (I.hostWithScheme client) ccPort x Nothing
      assertBool "testDestroySession: Session info was returned after destruction" $ (x1 == Nothing) || (x1 == Just [])
    Nothing -> assertFailure "testDestroySession: No session was created"

testInternalSession :: TestTree
testInternalSession = testGroup "Internal Session Tests" [testCreateSession, testGetSessionInfo, testRenewSession, testRenewNonexistentSession, testDestroySession]


testSessionMaintained :: TestTree
testSessionMaintained = testCase "testSessionMaintained" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testSessionMaintained") Nothing ["serfHealth"] (Just Release) (Just "10s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just session -> do
      threadDelay (12 * 1000000)
      y <- getSessionInfo client session Nothing
      assertEqual "testSessionMaintained: Session not found" True (isJust y)
    Nothing -> assertFailure "testSessionMaintained: No Session was created"


testWithSessionCancel :: TestTree
testWithSessionCancel = testCase "testWithSessionCancel" $ do
  client@ConsulClient{..} <- newClient
  let req = SessionRequest Nothing (Just "testWithSessionCancel") Nothing ["serfHealth"] (Just Release) (Just "10s")
  result <- I.createSession ccManager (I.hostWithScheme client) ccPort req Nothing
  case result of
    Just session -> do
      x1 <- withSession client Nothing 5 session (\ y -> action y client ) cancelAction
      assertEqual "testWithSessionCancel: Incorrect value" "Canceled" x1
      z <- getSessionInfo client session Nothing
      assertBool "testWithSessionCancel: Session was found" $ (z == Nothing) || (z == Just [])
    Nothing -> assertFailure "testWithSessionCancel: No session was created"
  where
    action :: MonadIO m => Session -> ConsulClient -> m Text
    action x client@ConsulClient{..} = do
      I.destroySession ccManager (I.hostWithScheme client) ccPort x Nothing
      liftIO $ threadDelay (30 * 1000000)
      return ("NotCanceled" :: Text)

    cancelAction :: MonadIO m => m Text
    cancelAction = return ("Canceled" :: Text)


testRunServiceTtl :: TestTree
testRunServiceTtl = testCase "testRunServiceTtl" $ do
  client@ConsulClient{..} <- newClient
  let register = RegisterService Nothing "testRunServiceTtl" [] (Just 8000) $ Just $ Ttl "10s"
  runService client register (action client) Nothing
  where
    action client = do
      threadDelay 15000000
      mHealth <- getServiceHealth client "testRunServiceTtl"
      case mHealth of
        Nothing -> assertFailure "testRunServiceTtl: No healthcheck was found"
        Just [x] -> do
          let checks = hChecks x
          mapM_ (testCheck) checks
    testCheck check = do
      assertBool "testRunServiceTtl: Check not passing" $ cStatus check == Passing


{-testSequencerLostSession :: TestTree
testSequencerLostSession = testCase "testSequencerLostSession" $ do
  client@ConsulClient{..} <- initializeConsulClient "localhost" consulPort Nothing
-}

testIsValidSequencer :: TestTree
testIsValidSequencer = testCase "testIsValidSequencer" $ do
  client@ConsulClient{..} <- initializeConsulClient "localhost" consulPort Nothing
  let req = SessionRequest Nothing (Just "testIsValidSequencer") Nothing ["serfHealth"] (Just Release) (Just "10s")
  result <- createSession client req Nothing
  case result of
    Nothing -> assertFailure "testIsValidSequencer: No session was created"
    Just session -> do
      let put = KeyValuePut "/testIsValidSequencer" "Test" Nothing Nothing
      x <- putKeyAcquireLock client put session Nothing
      assertEqual "testIsValidSequencer: Write failed" True x
      Just sequencer <- getSequencerForLock client "/testIsValidSequencer" session Nothing
      result1 <- isValidSequencer client sequencer Nothing
      assertEqual "testIsValidSequencer: Valid sequencer was invalid" True result1
      _ <- destroySession client session Nothing
      result2 <- isValidSequencer client sequencer Nothing
      assertEqual "testIsValidSequencer: Invalid session was valid" False result2


sessionWorkflowTests :: TestTree
sessionWorkflowTests = testGroup "Session Workflow Tests" [testWithSessionCancel,testSessionMaintained]

runServiceTests :: TestTree
runServiceTests = testGroup "Run Service Tests" [testRunServiceTtl]

agentTests :: TestTree
agentTests = testGroup "Agent Tests" [testGetSelf,testRegisterService]

sequencerTests :: TestTree
sequencerTests = testGroup "Sequencer Tests" [testIsValidSequencer]

allTests :: TestTree
allTests = testGroup "All Tests" [testInternalSession, internalKVTests, sessionWorkflowTests, agentTests,testHealth, clientKVTests, runServiceTests, sequencerTests]

-- Backwards compatible `withProcessTerm`.
withProcessTerm :: PT.ProcessConfig stdin stdout stderr -> (PT.Process stdin stdout stderr -> IO a) -> IO a
#if MIN_VERSION_typed_process(0,2,5)
withProcessTerm = PT.withProcessTerm
#else
withProcessTerm = PT.withProcess
#endif

waitForConsulOrFail :: IO ()
waitForConsulOrFail = do
  success <-
    retrying
      (constantDelay 50000 <> limitRetries 100) -- 100 times, 50 ms each
      (\_status isOpen -> return (not isOpen)) -- when to retry
      $ \_status -> do
        isPortOpen $ (simpleSockAddr (127,0,0,1) consulPort)
  when (not success) $ do
    error $ "Could not connect to Consul within reasonable time"

main :: IO ()
main = do
  -- We use a non-standard port in the test suite and spawn consul there,
  -- to ensure that the test suite doesn't mess with real consul deployments.
  withSystemTempFile "haskell-consul-test-config.json" $ \configFilePath h -> do
    BS8.hPutStrLn h "{ \"disable_update_check\": true }" >> hFlush h
    let consulProc =
          proc
            "consul"
            [ "agent", "-dev"
            , "-log-level", "err"
            , "-http-port", show (fromIntegral consulPort :: Int)
            , "-config-file", configFilePath
            ]
    withProcessTerm consulProc $ \_p -> do
      waitForConsulOrFail
      defaultMain allTests