packages feed

powerdns-0.2.1: spec/Spec.hs

{-# LANGUAGE OverloadedStrings #-}
import Control.Monad (unless)
import Data.Either (isRight)

import GHC.Stack (HasCallStack)

import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
import Servant.Client (ClientEnv, ClientM, mkClientEnv, runClientM)
import Servant.Client.Core (BaseUrl, ClientError(..), parseBaseUrl,
                            responseStatusCode)
import System.IO.Unsafe (unsafePerformIO)
import Test.Tasty
import Test.Tasty.HUnit

import PowerDNS.Client

{-# NOINLINE mgr #-}
mgr :: Manager
mgr = unsafePerformIO (newManager defaultManagerSettings)

{-# NOINLINE baseUrl #-}
baseUrl :: BaseUrl
baseUrl = unsafePerformIO (parseBaseUrl "pdns:8081")

envNoAuth :: ClientEnv
envNoAuth = mkClientEnv mgr baseUrl

envAuth :: ClientEnv
envAuth = applyXApiKey "secret" envNoAuth

main :: IO ()
main = defaultMain tree

tree :: TestTree
tree = testGroup "All specs" [ versionSpec
                             , authSpecs
                             , zoneSpecs
                             ]

run :: HasCallStack => ClientM a -> IO (Either ClientError a)
run = flip runClientM envAuth

runOk :: HasCallStack => ClientM a -> IO a
runOk c = do
  r <- run c
  assertRight "ClientM result" r

versionSpec :: TestTree
versionSpec = testCase "Verifies API version" $ do
    ver <- run listVersions
    assertEqual "API versions" ver (Right [Version 1 "/api/v1"])

zoneSpecs :: TestTree
zoneSpecs = testCaseSteps "Verifies zones can be created, displayed and deleted" $ \step -> do
    step "Ensure no zone is currently found"
    r1 <- run (listZones "localhost" Nothing Nothing)
    assertEqual "list of zones" (Right []) r1

    step "Create a new zone"
    r2 <- runOk (createZone "localhost" (Just True) new)
    zoneId <- assertJust "zone id" (zone_id r2)

    step "Check if zone is fetchable"
    runOk (getZone "localhost" zoneId (Just False))

    step "Add record to zone"
    runOk (updateZone "localhost" zoneId patch)

    step "Delete zone"
    runOk (deleteZone "localhost" zoneId)

    step "Ensure no zone is left over"
    r <- run (listZones "localhost" Nothing Nothing)
    assertEqual "list of zones" (Right []) r

    step "Done"

  where
    new = empty { zone_name   = Just "test.space."
                , zone_kind   = Just Native
                , zone_type   = Just "zone"
                , zone_rrsets = Just init }

    init = [ RRSet { rrset_name = "magic.test.space."
                   , rrset_type = A
                   , rrset_ttl = 86003
                   , rrset_changetype = Nothing
                   , rrset_records = Just [Record "127.0.0.1" False]
                   , rrset_comments = Nothing
                   }
           ]

    patch = empty { zone_type = Just "zone"
                  , zone_rrsets = Just added
                  }
    added = [ RRSet { rrset_name = "foo.test.space."
                    , rrset_type = AAAA
                    , rrset_ttl = 1234
                    , rrset_changetype = Nothing
                    , rrset_records = Just [Record "::1" False]
                    , rrset_comments = Nothing
                    }
            ]

-- | Erases all server generated data from a zone. Used to test for equality.
cleanse :: Zone -> Zone
cleanse z = z { zone_id = Nothing
              , zone_url = Nothing
              , zone_serial = Nothing
              , zone_soa_edit = Nothing
              , zone_soa_edit_api = Nothing
              , zone_notified_serial = Nothing
              , zone_edited_serial = Nothing
              , zone_rrsets = Nothing
              , zone_master_tsig_key_ids = Nothing
              , zone_slave_tsig_key_ids = Nothing
              }

authSpecs :: TestTree
authSpecs = testGroup "Authentication specs"
  [ testCase "X-API-Key header is correctly set" $
      assertIsSuccess =<< runClientM listServers envAuth

  , testCase "Access without X-API-Key is rejected" $
      assertIs401 =<< runClientM listServers envNoAuth
  ]

assertPredicate :: Show s => String -> (s -> Bool) -> s -> Assertion
assertPredicate name p v =
  unless (p v) (assertFailure msg)
 where msg = "failed to satisfy predicate: " ++ name ++ "\n" ++
             "with value: " ++ show v

assertIs401 :: Show a => Either ClientError a -> Assertion
assertIs401 = assertPredicate "has HTTP status 401" (hasStatus 401)

hasStatus :: Int -> Either ClientError a -> Bool
hasStatus i (Left (FailureResponse _req resp)) |
   responseStatusCode resp == toEnum i = True
hasStatus _i _        = False

assertIsSuccess :: Show a => Either ClientError a -> Assertion
assertIsSuccess = assertPredicate "successful HTTP response" isRight

assertJust :: HasCallStack => String -> Maybe a -> IO a
assertJust preface = maybe (assertFailure msg) pure
 where msg = (if null preface then "" else preface ++ "\n") ++
             "Unexpected Nothing"

assertRight :: (Show a, HasCallStack) => String -> Either a b -> IO b
assertRight preface e = case e of
  Left e  -> assertFailure (msg e)
  Right r -> pure r
 where msg v = (if null preface then "" else preface ++ "\n") ++
               "Unexpected Left: " ++ show v