{-# LANGUAGE OverloadedRecordDot #-}

module Hydra.ConfigSpec where

import Hydra.Prelude
import Test.Hydra.Prelude

import Hydra.Cardano.Api (NetworkId (..), NetworkMagic (..))
import Hydra.Config (isSelfAddress, loadConfig, resolvePaths)
import Hydra.Logging (Verbosity (..))
import Hydra.Network (Host (..))
import Hydra.Options (
  CardanoChainConfig (..),
  ChainBackendOptions (..),
  ChainConfig (..),
  Command (..),
  DirectOptions (..),
  LedgerConfig (..),
  OfflineChainConfig (..),
  RunOptions (..),
  defaultCardanoChainConfig,
  defaultRunOptions,
  parseHydraCommandFromArgsWith,
  validateRunOptions,
 )
import System.FilePath (takeDirectory, (</>))
import System.IO (hClose, hPutStr)
import System.IO.Temp (withSystemTempFile)

spec :: Spec
spec :: Spec
spec = do
  String -> Spec -> Spec
forall a. HasCallStack => String -> SpecWith a -> SpecWith a
describe String
"loadConfig" (Spec -> Spec) -> Spec -> Spec
forall a b. (a -> b) -> a -> b
$ do
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"loads a minimal YAML file and uses defaults" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"{}\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions
opts RunOptions -> RunOptions -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String -> RunOptions -> RunOptions
resolvePaths String
dir RunOptions
defaultRunOptions

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses node-id" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"node-id: my-node\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> NodeId
nodeId RunOptions
opts NodeId -> NodeId -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` NodeId
"my-node"

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses listen as HOST:PORT string" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"listen: \"127.0.0.1:9001\"\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> Host
listen RunOptions
opts Host -> Host -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Text -> PortNumber -> Host
Host Text
"127.0.0.1" PortNumber
9001

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses peers list (plain string format)" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"peers:\n  - \"peer1:5001\"\n  - \"peer2:5002\"\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer1" PortNumber
5001, Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses peers with hydra and cardano verification keys" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"peers:\n\
            \  - address: \"peer1:5001\"\n\
            \    hydra-verification-key: peer1.hydra.vk\n\
            \    cardano-verification-key: peer1.cardano.vk\n\
            \  - address: \"peer2:5002\"\n\
            \    hydra-verification-key: peer2.hydra.vk\n\
            \    cardano-verification-key: peer2.cardano.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer1" PortNumber
5001, Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]
        RunOptions -> [String]
hydraVerificationKeys RunOptions
opts [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer1.hydra.vk", String
dir String -> String -> String
</> String
"peer2.hydra.vk"]
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg
              [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer1.cardano.vk", String
dir String -> String -> String
</> String
"peer2.cardano.vk"]
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses mirror peers with null keys" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"peers:\n\
            \  - address: \"peer1:5001\"\n\
            \    hydra-verification-key: peer1.hydra.vk\n\
            \    cardano-verification-key: peer1.cardano.vk\n\
            \  - address: \"mirror:5002\"\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer1" PortNumber
5001, Text -> PortNumber -> Host
Host Text
"mirror" PortNumber
5002]
        RunOptions -> [String]
hydraVerificationKeys RunOptions
opts [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer1.hydra.vk"]
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer1.cardano.vk"]
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses peers with mixed formats" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"peers:\n\
            \  - \"peer1:5001\"\n\
            \  - address: \"peer2:5002\"\n\
            \    hydra-verification-key: peer2.hydra.vk\n\
            \    cardano-verification-key: peer2.cardano.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer1" PortNumber
5001, Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]
        RunOptions -> [String]
hydraVerificationKeys RunOptions
opts [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.hydra.vk"]
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.cardano.vk"]
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"filters self from peers when address matches advertise" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"listen: \"0.0.0.0:5001\"\n\
            \advertise: \"127.0.0.1:5001\"\n\
            \peers:\n\
            \  - address: \"127.0.0.1:5001\"\n\
            \    hydra-verification-key: self.hydra.vk\n\
            \    cardano-verification-key: self.cardano.vk\n\
            \  - address: \"peer2:5002\"\n\
            \    hydra-verification-key: peer2.hydra.vk\n\
            \    cardano-verification-key: peer2.cardano.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]
        RunOptions -> [String]
hydraVerificationKeys RunOptions
opts [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.hydra.vk"]
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.cardano.vk"]
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"filters self from peers when address matches listen (no advertise)" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"listen: \"0.0.0.0:5001\"\n\
            \peers:\n\
            \  - address: \"0.0.0.0:5001\"\n\
            \  - \"peer2:5002\"\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"filters self when listen is wildcard 0.0.0.0 and peer uses 127.0.0.1" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      -- Regression: string equality on "0.0.0.0" vs "127.0.0.1" would not
      -- match. The normalized self-filter treats wildcard listen addresses
      -- as matching any peer on the same port.
      let yaml :: String
yaml =
            String
"listen: \"0.0.0.0:5001\"\n\
            \peers:\n\
            \  - address: \"127.0.0.1:5001\"\n\
            \    hydra-verification-key: self.hydra.vk\n\
            \    cardano-verification-key: self.cardano.vk\n\
            \  - address: \"peer2:5002\"\n\
            \    hydra-verification-key: peer2.hydra.vk\n\
            \    cardano-verification-key: peer2.cardano.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]
        RunOptions -> [String]
hydraVerificationKeys RunOptions
opts [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.hydra.vk"]
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [String
dir String -> String -> String
</> String
"peer2.cardano.vk"]
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"filters self when listen uses 127.0.0.1 and peer uses localhost" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      -- Both sides are loopback; normalized filter treats them as equivalent.
      let yaml :: String
yaml =
            String
"listen: \"127.0.0.1:5001\"\n\
            \peers:\n\
            \  - address: \"localhost:5001\"\n\
            \  - \"peer2:5002\"\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> [Host]
peers RunOptions
opts [Host] -> [Host] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` [Text -> PortNumber -> Host
Host Text
"peer2" PortNumber
5002]

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"rejects peer entry with only hydra-verification-key" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"peers:\n\
            \  - address: \"peer1:5001\"\n\
            \    hydra-verification-key: peer1.hydra.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"rejects peer entry with only cardano-verification-key" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"peers:\n\
            \  - address: \"peer1:5001\"\n\
            \    cardano-verification-key: peer1.cardano.vk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"rejects chain config that sets both network and hydra-scripts-tx-id" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"chain:\n\
            \  mode: cardano\n\
            \  network: preview\n\
            \  hydra-scripts-tx-id:\n\
            \    - \"0000000000000000000000000000000000000000000000000000000000000000\"\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"reports a readable YAML decode error (wraps underlying parser output)" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      -- ':' without a key produces a YAML-level parse error; the custom
      -- wrapper should include the file path and the YAML library's
      -- pretty-printed message.
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
":broken\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses api-host and api-port" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"api-host: \"0.0.0.0\"\napi-port: 9000\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        (IP -> String
forall b a. (Show a, IsString b) => a -> b
show (RunOptions -> IP
apiHost RunOptions
opts) :: String) String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
"0.0.0.0"
        RunOptions -> PortNumber
apiPort RunOptions
opts PortNumber -> PortNumber -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` PortNumber
9000

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses quiet flag" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"quiet: true\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> Verbosity
verbosity RunOptions
opts Verbosity -> Verbosity -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Verbosity
Quiet

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses hydra-signing-key" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"hydra-signing-key: my.sk\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> String
hydraSigningKey RunOptions
opts String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
dir String -> String -> String
</> String
"my.sk"

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses persistence-dir (absolute path unchanged)" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"persistence-dir: /some/path\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> String
persistenceDir RunOptions
opts String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
"/some/path"

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses ledger-protocol-parameters" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"ledger-protocol-parameters: my-params.json\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        RunOptions -> LedgerConfig
ledgerConfig RunOptions
opts LedgerConfig -> LedgerConfig -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String -> LedgerConfig
CardanoLedgerConfig (String
dir String -> String -> String
</> String
"my-params.json")

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses chain with direct backend options" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"chain:\n\
            \  mode: cardano\n\
            \  cardano-signing-key: cardano.sk\n\
            \  backend:\n\
            \    mode: direct\n\
            \    testnet-magic: 2\n\
            \    node-socket: node.socket\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg -> do
            CardanoChainConfig -> String
cardanoSigningKey CardanoChainConfig
cfg String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
dir String -> String -> String
</> String
"cardano.sk"
            case CardanoChainConfig -> ChainBackendOptions
chainBackendOptions CardanoChainConfig
cfg of
              Direct DirectOptions{NetworkId
networkId :: NetworkId
$sel:networkId:DirectOptions :: DirectOptions -> NetworkId
networkId, SocketPath
nodeSocket :: SocketPath
$sel:nodeSocket:DirectOptions :: DirectOptions -> SocketPath
nodeSocket} -> do
                NetworkId
networkId NetworkId -> NetworkId -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` NetworkMagic -> NetworkId
Testnet (Word32 -> NetworkMagic
NetworkMagic Word32
2)
                SocketPath
nodeSocket SocketPath -> SocketPath -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String -> SocketPath
forall a. IsString a => String -> a
fromString (String
dir String -> String -> String
</> String
"node.socket")
              ChainBackendOptions
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Direct, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainBackendOptions -> String
forall b a. (Show a, IsString b) => a -> b
show ChainBackendOptions
other
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"parses offline chain config" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let seed :: String
seed = Int -> Char -> String
forall a. Int -> a -> [a]
replicate Int
64 Char
'0'
          yaml :: String
yaml = String
"chain:\n  mode: offline\n  offline-head-seed: \"" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
seed String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\"\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Offline OfflineChainConfig{String
initialUTxOFile :: String
$sel:initialUTxOFile:OfflineChainConfig :: OfflineChainConfig -> String
initialUTxOFile, Maybe String
ledgerGenesisFile :: Maybe String
$sel:ledgerGenesisFile:OfflineChainConfig :: OfflineChainConfig -> Maybe String
ledgerGenesisFile} -> do
            String
initialUTxOFile String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
dir String -> String -> String
</> String
"utxo.json"
            Maybe String
ledgerGenesisFile Maybe String -> Maybe String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Maybe String
forall a. Maybe a
Nothing
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Offline chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"fails with helpful error on invalid chain mode" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"chain:\n  mode: unknown\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"fails with helpful error on invalid HOST:PORT format" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"listen: \"not-a-host\"\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"fails with helpful error on unknown top-level key" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"api-prot: 4001\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"fails with helpful error on unknown chain key" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"chain:\n  mode: cardano\n  unknown-key: foo\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        String -> IO RunOptions
loadConfig String
path IO RunOptions -> Selector SomeException -> IO ()
forall e a.
(HasCallStack, Exception e) =>
IO a -> Selector e -> IO ()
`shouldThrow` Selector SomeException
anyException

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"defaults to cardano chain config when chain section is absent" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"{}\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
opts <- String -> IO RunOptions
loadConfig String
path
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg -> do
            -- Non-path fields match defaults
            CardanoChainConfig
cfg.hydraScriptsTxId [TxId] -> [TxId] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` CardanoChainConfig
defaultCardanoChainConfig.hydraScriptsTxId
            CardanoChainConfig
cfg.cardanoVerificationKeys [String] -> [String] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` []
            CardanoChainConfig
cfg.startChainFrom Maybe ChainPoint -> Maybe ChainPoint -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Maybe ChainPoint
forall a. Maybe a
Nothing
            CardanoChainConfig
cfg.contestationPeriod ContestationPeriod -> ContestationPeriod -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` CardanoChainConfig
defaultCardanoChainConfig.contestationPeriod
            CardanoChainConfig
cfg.depositPeriod DepositPeriod -> DepositPeriod -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` CardanoChainConfig
defaultCardanoChainConfig.depositPeriod
            CardanoChainConfig
cfg.depositActivation DepositPeriod -> DepositPeriod -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` CardanoChainConfig
defaultCardanoChainConfig.depositActivation
            -- Path fields are resolved relative to the config directory
            CardanoChainConfig
cfg.cardanoSigningKey String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
dir String -> String -> String
</> CardanoChainConfig
defaultCardanoChainConfig.cardanoSigningKey
            case CardanoChainConfig
cfg.chainBackendOptions of
              Direct DirectOptions
d -> DirectOptions
d.nodeSocket SocketPath -> SocketPath -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String -> SocketPath
forall a. IsString a => String -> a
fromString (String
dir String -> String -> String
</> String
"node.socket")
              ChainBackendOptions
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Direct backend, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainBackendOptions -> String
forall b a. (Show a, IsString b) => a -> b
show ChainBackendOptions
other
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

  String -> Spec -> Spec
forall a. HasCallStack => String -> SpecWith a -> SpecWith a
describe String
"isSelfAddress" (Spec -> Spec) -> Spec -> Spec
forall a b. (a -> b) -> a -> b
$ do
    let h :: Text -> PortNumber -> Host
h = Text -> PortNumber -> Host
Host
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"matches exact host:port when no advertise is set" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"192.168.1.10" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"192.168.1.10" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"does not match different ports" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"192.168.1.10" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"192.168.1.10" PortNumber
5002) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
False
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"treats wildcard 0.0.0.0 as matching loopback peers on the same port" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"does NOT treat wildcard listen as matching an arbitrary remote host on the same port" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      -- A peer at 172.16.0.5:5001 is a legitimate other-host participant, not self.
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"172.16.0.5" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
False
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"treats wildcard listen with identical wildcard peer as self (self-entry convention)" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"treats loopback forms as equivalent" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"localhost" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"localhost" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"::1" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"does not match a non-loopback host when listen is loopback" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001) Maybe Host
forall a. Maybe a
Nothing (Text -> PortNumber -> Host
h Text
"example.com" PortNumber
5001) Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
False
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"requires exact match against advertise when set" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) (Host -> Maybe Host
forall a. a -> Maybe a
Just (Text -> PortNumber -> Host
h Text
"node.example.com" PortNumber
5001)) (Text -> PortNumber -> Host
h Text
"node.example.com" PortNumber
5001)
        Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
True
      -- A peer on a loopback address is NOT self when advertise is explicit.
      Host -> Maybe Host -> Host -> Bool
isSelfAddress (Text -> PortNumber -> Host
h Text
"0.0.0.0" PortNumber
5001) (Host -> Maybe Host
forall a. a -> Maybe a
Just (Text -> PortNumber -> Host
h Text
"node.example.com" PortNumber
5001)) (Text -> PortNumber -> Host
h Text
"127.0.0.1" PortNumber
5001)
        Bool -> Bool -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Bool
False

  String -> Spec -> Spec
forall a. HasCallStack => String -> SpecWith a -> SpecWith a
describe String
"RunOptions Semigroup (config file <> CLI overrides)" (Spec -> Spec) -> Spec -> Spec
forall a b. (a -> b) -> a -> b
$ do
    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"CLI flag overrides YAML value" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"node-id: from-yaml\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
base <- String -> IO RunOptions
loadConfig String
path
        RunOptions
cli <- [String] -> IO RunOptions
parseRunOptions [String
"--node-id", String
"from-cli"]
        RunOptions -> NodeId
nodeId (RunOptions
base RunOptions -> RunOptions -> RunOptions
forall a. Semigroup a => a -> a -> a
<> RunOptions
cli) NodeId -> NodeId -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` NodeId
"from-cli"

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"YAML value wins when CLI leaves field at default" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
"node-id: from-yaml\n" ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
_dir -> do
        RunOptions
base <- String -> IO RunOptions
loadConfig String
path
        RunOptions
cli <- [String] -> IO RunOptions
parseRunOptions []
        RunOptions -> NodeId
nodeId (RunOptions
base RunOptions -> RunOptions -> RunOptions
forall a. Semigroup a => a -> a -> a
<> RunOptions
cli) NodeId -> NodeId -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` NodeId
"from-yaml"

    String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it String
"CLI hydra-scripts-tx-id merges into YAML chain config" (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
      let yaml :: String
yaml =
            String
"chain:\n\
            \  mode: cardano\n\
            \  cardano-signing-key: yaml.sk\n"
      String -> (String -> String -> IO ()) -> IO ()
forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
yaml ((String -> String -> IO ()) -> IO ())
-> (String -> String -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \String
path String
dir -> do
        RunOptions
base <- String -> IO RunOptions
loadConfig String
path
        let txId :: String
txId = Int -> Char -> String
forall a. Int -> a -> [a]
replicate Int
64 Char
'0'
        RunOptions
cli <- [String] -> IO RunOptions
parseRunOptions [String
"--hydra-scripts-tx-id", String
txId]
        let merged :: RunOptions
merged = RunOptions
base RunOptions -> RunOptions -> RunOptions
forall a. Semigroup a => a -> a -> a
<> RunOptions
cli
        case RunOptions -> ChainConfig
chainConfig RunOptions
merged of
          Cardano CardanoChainConfig
cfg -> do
            CardanoChainConfig -> String
cardanoSigningKey CardanoChainConfig
cfg String -> String -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` String
dir String -> String -> String
</> String
"yaml.sk"
            CardanoChainConfig -> [TxId]
hydraScriptsTxId CardanoChainConfig
cfg [TxId] -> [TxId] -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldNotBe` []
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

  String -> Spec -> Spec
forall a. HasCallStack => String -> SpecWith a -> SpecWith a
describe String
"demo configs (demo/configs/*.yaml)" (Spec -> Spec) -> Spec -> Spec
forall a b. (a -> b) -> a -> b
$ do
    -- Guard against the committed demo configs drifting away from the
    -- validation rules enforced by loadConfig + validateRunOptions.
    [String] -> (String -> Spec) -> Spec
forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
t a -> (a -> m b) -> m ()
forM_ [String
"alice.yaml", String
"bob.yaml", String
"carol.yaml", String
"alice-mirror.yaml"] ((String -> Spec) -> Spec) -> (String -> Spec) -> Spec
forall a b. (a -> b) -> a -> b
$ \String
name ->
      String -> IO () -> SpecWith (Arg (IO ()))
forall a.
(HasCallStack, Example a) =>
String -> a -> SpecWith (Arg a)
it (String
"loads and validates " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
name) (IO () -> SpecWith (Arg (IO ())))
-> IO () -> SpecWith (Arg (IO ()))
forall a b. (a -> b) -> a -> b
$ do
        RunOptions
opts <- String -> IO RunOptions
loadConfig (String
"../demo/configs/" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
name)
        RunOptions -> Either InvalidOptions ()
validateRunOptions RunOptions
opts Either InvalidOptions () -> Either InvalidOptions () -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` () -> Either InvalidOptions ()
forall a b. b -> Either a b
Right ()
        -- Self entry (if any) is filtered out, leaving exactly 3 peers:
        -- the two other signers plus (depending on which config) either
        -- the mirror or the primary alice.
        [Host] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (RunOptions -> [Host]
peers RunOptions
opts) Int -> Int -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Int
3
        -- 2 other signing peers → 2 hydra-verification-keys.
        [String] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (RunOptions -> [String]
hydraVerificationKeys RunOptions
opts) Int -> Int -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Int
2
        case RunOptions -> ChainConfig
chainConfig RunOptions
opts of
          Cardano CardanoChainConfig
cfg ->
            [String] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (CardanoChainConfig -> [String]
cardanoVerificationKeys CardanoChainConfig
cfg) Int -> Int -> IO ()
forall a. (HasCallStack, Show a, Eq a) => a -> a -> IO ()
`shouldBe` Int
2
          ChainConfig
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String -> IO ()) -> String -> IO ()
forall a b. (a -> b) -> a -> b
$ String
"Expected Cardano chain, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ChainConfig -> String
forall b a. (Show a, IsString b) => a -> b
show ChainConfig
other

-- | Parse args as 'RunOptions', or fail the test.
parseRunOptions :: [String] -> IO RunOptions
parseRunOptions :: [String] -> IO RunOptions
parseRunOptions [String]
args = do
  Command
cmd <- [String] -> IO Command
parseHydraCommandFromArgsWith [String]
args
  case Command
cmd of
    Run RunOptions
opts -> RunOptions -> IO RunOptions
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure RunOptions
opts
    Command
other -> HasCallStack => String -> IO ()
String -> IO ()
expectationFailure (String
"Expected Run, got: " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> Command -> String
forall b a. (Show a, IsString b) => a -> b
show Command
other) IO () -> IO RunOptions -> IO RunOptions
forall a b. IO a -> IO b -> IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Text -> IO RunOptions
forall a t. (HasCallStack, IsText t) => t -> a
error Text
"Fail test"

-- | Write YAML content to a temporary file and run the action with its path
-- and the directory containing it (for constructing expected resolved paths).
withYaml :: String -> (FilePath -> FilePath -> IO a) -> IO a
withYaml :: forall a. String -> (String -> String -> IO a) -> IO a
withYaml String
content String -> String -> IO a
action =
  String -> (String -> Handle -> IO a) -> IO a
forall (m :: * -> *) a.
(MonadIO m, MonadMask m) =>
String -> (String -> Handle -> m a) -> m a
withSystemTempFile String
"hydra-config-.yaml" ((String -> Handle -> IO a) -> IO a)
-> (String -> Handle -> IO a) -> IO a
forall a b. (a -> b) -> a -> b
$ \String
path Handle
h -> do
    Handle -> String -> IO ()
hPutStr Handle
h String
content
    Handle -> IO ()
hClose Handle
h
    String -> String -> IO a
action String
path (String -> String
takeDirectory String
path)