store: read the names and chain settings

This commit is contained in:
shum committed 2026-10-21 22:08:32 +00:00
1 parent 4a8be1a7a7
commit bb08fcfc99
5 files changed
+351 -10

No files matched your search

@@ -12,6 +12,8 @@ module StoreService.Config
PollConfig (..),
BadgeIssuerKey (..),
GroupConfig (..),
NamesConfig (..),
ChainConfig (..),
ServiceConfig (..),
defaultExpiryMinutes,
defaultSessionMinutes,
@@ -22,17 +24,26 @@ where
import qualified Control.Exception as E
import StoreService.Log (logWarn)
import Control.Monad (mfilter)
import Control.Monad (mfilter, unless, when)
import Data.Attoparsec.Text (Parser, endOfInput, isEndOfLine, parseOnly, satisfy, skipMany, skipSpace, skipWhile)
import qualified Data.ByteArray as BA
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
import qualified Data.ByteString.Char8 as B
import Data.Ini (Ini, iniGlobals, iniParser, keys, lookupValue, sections)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.IO as TIO
import Data.Time.Clock (UTCTime)
import Data.Time.Format.ISO8601 (iso8601ParseM)
import Data.Word (Word64)
import Simplex.Chat.Library.Commands (mkValidName)
import Simplex.Messaging.Crypto.BBS (BBSSecretKey)
import Simplex.Messaging.Crypto.Secp256k1 (Secp256k1PrivateKey, mkPrivateKey)
import Simplex.Messaging.Encoding.String (strDecode)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.SimplexName (SimplexTLD (..))
import System.IO.Error (ioeGetErrorString)
import Text.Read (readMaybe)
@@ -42,7 +53,10 @@ data ListenerConfig = ListenerConfig
lStaticDir :: FilePath,
lServeWebapp :: Bool,
lWebappExportDir :: Maybe FilePath,
lTrustForwardedFor :: Bool
lTrustForwardedFor :: Bool,
lNamesStaticDir :: Maybe FilePath,
lNamesExportDir :: Maybe FilePath,
lNamesHosts :: [Text]
}
deriving (Eq, Show)
@@ -107,13 +121,42 @@ data GroupConfig = GroupConfig
}
deriving (Eq, Show)
data NamesConfig = NamesConfig
{ nTLD :: SimplexTLD,
nYears :: Int,
nTermStart :: UTCTime,
nTermSlackSeconds :: Int,
nRevealMarginSeconds :: Int,
nMaxOrderMoves :: Int,
nChain :: ChainConfig
}
deriving (Eq, Show)
data ChainConfig = ChainConfig
{ cRpcUrl :: String,
cChainId :: Word64,
cController :: Address,
cSponsor :: Address,
cBaseRegistrar :: Address,
cResolver :: Address,
cServiceKey :: Secp256k1PrivateKey,
cConfirmations :: Int,
cGasLimitCap :: Word64
}
deriving (Eq)
-- The RPC URL can carry a provider's API key, so neither it nor the service key is shown.
instance Show ChainConfig where
show ChainConfig {cChainId} = "chain " <> show cChainId
data ServiceConfig = ServiceConfig
{ listener :: ListenerConfig,
btcpay :: Maybe BTCPayConfig,
stripe :: Maybe StripeConfig,
poll :: PollConfig,
issuer :: Maybe BadgeIssuerKey,
group :: Maybe GroupConfig
group :: Maybe GroupConfig,
names :: Maybe NamesConfig
}
deriving (Eq, Show)
@@ -137,6 +180,16 @@ requireHttps u
| "https://" `T.isPrefixOf` u = Right u
| otherwise = Left "btcpay.host must be an absolute https URL"
-- | The controller refuses a registration under 730 days.
minNameYears :: Int
minNameYears = 2
defaultTermStart :: String
defaultTermStart = "2027-01-01T00:00:00Z"
serviceKeyBytes :: Int
serviceKeyBytes = 32
-- | Well below 100, where BTCPay settles an invoice for one satoshi.
maxTolerance :: Double
maxTolerance = 10
@@ -160,12 +213,14 @@ trailingNoise = skipSpace *> skipMany (comment *> skipSpace)
knownSettings :: [(Text, [Text])]
knownSettings =
[ ("listener", ["host", "port", "static_dir", "serve_webapp", "webapp_export_dir", "trust_forwarded_for"]),
[ ("listener", ["host", "port", "static_dir", "serve_webapp", "webapp_export_dir", "trust_forwarded_for", "names_static_dir", "names_export_dir", "names_hosts"]),
("btcpay", ["host", "api_key", "store_id", "webhook_secret", "expiry_minutes", "speed_policy", "payment_tolerance"]),
("stripe", ["secret_key", "publishable_key", "webhook_secret", "session_minutes"]),
("poll", ["waiting_seconds", "idle_seconds"]),
("group", ["display_name", "description"]),
("issuer", ["index", "private_key"])
("issuer", ["index", "private_key"]),
("names", ["tld", "years", "term_start", "term_slack_seconds", "reveal_margin_seconds", "max_order_moves"]),
("chain", ["rpc_url", "chain_id", "controller", "sponsor", "base_registrar", "resolver", "service_key", "confirmations", "gas_limit_cap"])
]
unknownKeys :: Ini -> [Text]
@@ -190,20 +245,25 @@ parseConfig ini = do
lServeWebapp <- bool "listener" "serve_webapp" True
let lWebappExportDir = T.unpack <$> present "listener" "webapp_export_dir"
lTrustForwardedFor <- bool "listener" "trust_forwarded_for" False
let lNamesStaticDir = T.unpack <$> present "listener" "names_static_dir"
lNamesExportDir = T.unpack <$> present "listener" "names_export_dir"
lNamesHosts = maybe [] (filter (not . T.null) . map (T.toLower . T.strip) . T.splitOn ",") (present "listener" "names_hosts")
btc <- btcpaySection
str <- stripeSection
iss <- issuerSection
grp <- groupSection
nms <- namesSection
pWaitingSeconds <- cadence "waiting_seconds" 3
pIdleSeconds <- cadence "idle_seconds" 60
pure
ServiceConfig
{ listener = ListenerConfig {lHost, lPort, lStaticDir, lServeWebapp, lWebappExportDir, lTrustForwardedFor},
{ listener = ListenerConfig {lHost, lPort, lStaticDir, lServeWebapp, lWebappExportDir, lTrustForwardedFor, lNamesStaticDir, lNamesExportDir, lNamesHosts},
btcpay = btc,
stripe = str,
poll = PollConfig {pWaitingSeconds, pIdleSeconds},
issuer = iss,
group = grp
group = grp,
names = nms
}
where
hasSection s = s `elem` sections ini
@@ -295,6 +355,61 @@ parseConfig ini = do
sSessionMinutes <- sessionMinutes
let sHost = defaultStripeHost
pure (Just StripeConfig {sSecretKey, sPublishableKey, sWebhookSecret, sSessionMinutes, sHost})
namesSection
| not (hasSection "names") = if hasSection "chain" then Left "[chain] is set but [names] is absent, so nothing would use it" else Right Nothing
| not (hasSection "chain") = Left "[names] needs a [chain] section"
| otherwise = do
nTLD <- tldSetting
nYears <- num "names" "years" minNameYears
when (nYears < minNameYears) $ Left ("names.years must be at least " <> show minNameYears)
nTermStart <- termStart
nTermSlackSeconds <- atLeast 0 "names" "term_slack_seconds" 3600
nRevealMarginSeconds <- atLeast 0 "names" "reveal_margin_seconds" 900
nMaxOrderMoves <- atLeast 0 "names" "max_order_moves" 3
nChain <- chainSection
pure (Just NamesConfig {nTLD, nYears, nTermStart, nTermSlackSeconds, nRevealMarginSeconds, nMaxOrderMoves, nChain})
tldSetting =
required "names" "tld" >>= \case
"simplex" -> Right TLDSimplex
"testing" -> Right TLDTesting
other -> Left ("names.tld must be simplex or testing, not " <> T.unpack other)
termStart =
maybe (Left ("names.term_start must be an ISO 8601 UTC time such as " <> defaultTermStart)) Right $
iso8601ParseM (maybe defaultTermStart T.unpack (present "names" "term_start"))
atLeast lowest s k d = do
v <- num s k d
if v >= lowest then Right v else Left (T.unpack s <> "." <> T.unpack k <> " must be at least " <> show lowest)
-- No error below quotes a value, since the URL and the key are secrets.
chainSection = do
cRpcUrl <- rpcUrl
cChainId <- positiveWord "chain_id" Nothing
cController <- address "controller"
cSponsor <- address "sponsor"
cBaseRegistrar <- address "base_registrar"
cResolver <- address "resolver"
cServiceKey <- serviceKey
cConfirmations <- atLeast 0 "chain" "confirmations" 3
cGasLimitCap <- positiveWord "gas_limit_cap" (Just 8000000)
pure ChainConfig {cRpcUrl, cChainId, cController, cSponsor, cBaseRegistrar, cResolver, cServiceKey, cConfirmations, cGasLimitCap}
rpcUrl = do
v <- required "chain" "rpc_url"
unless (any (`T.isPrefixOf` v) ["http://", "https://"]) $ Left "chain.rpc_url must be an http or https URL"
Right (T.unpack v)
positiveWord k d = case (present "chain" k, d) of
(Nothing, Just v) -> Right v
(Nothing, Nothing) -> Left ("chain." <> T.unpack k <> " is required")
(Just v, _) -> case readMaybe (T.unpack v) :: Maybe Integer of
Just n | n >= 1, n <= toInteger (maxBound :: Word64) -> Right (fromInteger n)
_ -> Left ("chain." <> T.unpack k <> " must be a positive whole number")
address k = do
v <- required "chain" k
either (const (Left ("chain." <> T.unpack k <> " is not an Ethereum address"))) Right (strDecode (encodeUtf8 v) :: Either String Address)
serviceKey = do
v <- required "chain" "service_key"
let bad = Left "chain.service_key must be 0x and 64 hex digits of a valid secp256k1 key"
case T.stripPrefix "0x" v of
Just h | T.length h == 2 * serviceKeyBytes, Right bytes <- convertFromBase Base16 (encodeUtf8 h) -> either (const bad) Right (mkPrivateKey (BA.convert (bytes :: B.ByteString)))
_ -> bad
sessionMinutes = do
v <- num "stripe" "session_minutes" defaultSessionMinutes
if v >= minSessionMinutes && v <= maxSessionMinutes
@@ -15,6 +15,11 @@ serve_webapp = on
; proxy's own address, so one bucket is shared by everyone. Set it off only where this
; listener is reached directly, since a client can write the header itself.
trust_forwarded_for = on
; the names site (simplex.domains): its build directory, the directory it is exported to for a
; reverse proxy, and the comma-separated hosts it is served on in the all-in-one deployment
; names_static_dir = /srv/names
; names_export_dir = /srv/names-export
; names_hosts = simplex.domains
; omit this whole section to disable Bitcoin and Monero (the browser shows the provider-unavailable screen)
[btcpay]
@@ -60,3 +65,34 @@ idle_seconds = 60
;[group]
;display_name = SimpleX Badges
;description = Welcome to the badges group
;
; the names store: omit [names] and [chain] together to turn it off.
;[names]
; testing until the .simplex TLD is deployed
;tld = testing
; the paid years of a name; at least 2, the controller's minimum
;years = 2
; a name's term starts here; the days until then are free
;term_start = 2027-01-01T00:00:00Z
; how far an order's duration may be from the one the service computes
;term_slack_seconds = 3600
; a commitment with less than this left before its reveal deadline is not reused
;reveal_margin_seconds = 900
; how many times one order may move to another name
;max_order_moves = 3
;
; the chain the names are registered on. rpc_url can carry a provider's API key and service_key
; pays for every commit and register: keep both secret. Neither is ever logged.
;[chain]
;rpc_url = http://127.0.0.1:8545
;chain_id = 31337
;controller = 0x...
;sponsor = 0x...
;base_registrar = 0x...
;resolver = 0x...
; 0x and 64 hex digits: the registrar key the service sends transactions with
;service_key = 0x...
; blocks after a register's receipt before a name counts as registered
;confirmations = 3
; the most gas any one transaction may use
;gas_limit_cap = 8000000
+188
View File
@@ -4,12 +4,18 @@
module Bots.StoreService.ConfigTests where
import StoreService.Config
import qualified Data.ByteArray as BA
import qualified Data.ByteString.Char8 as B
import Data.Either (isLeft)
import Data.Ini (readIniFile)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..))
import Simplex.Messaging.Crypto.Secp256k1 (mkPrivateKey)
import Simplex.Messaging.Encoding.String (strDecode)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.SimplexName (SimplexTLD (..))
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>))
import Test.Hspec
@@ -408,3 +414,185 @@ testGroupAbsent :: IO ()
testGroupAbsent = do
r <- parseIni listenerIni
fmap group r `shouldBe` Right Nothing
namesConfigTests :: Spec
namesConfigTests = describe "StoreService names: config" $ do
it "reads [names] and [chain], applying every documented default" testNamesDefaults
it "reads every [names] and [chain] setting written out" testNamesWrittenOut
it "reads the names keys of [listener]" testNamesListenerKeys
it "leaves the names side off when [names] is absent" testNamesAbsent
it "refuses [names] without [chain], and [chain] without [names]" testNamesNeedsChain
it "refuses years below 2" testNamesYearsBelowTwo
it "refuses a term_start that is not an ISO 8601 UTC time" testNamesBadTermStart
it "refuses a TLD other than simplex or testing" testNamesBadTLD
it "refuses a bad or wrongly checksummed contract address, naming the key" testNamesBadAddress
it "refuses a bad service key without quoting it" testNamesBadServiceKey
it "keeps the RPC URL and the service key out of errors and the shown config" testNamesSecretsHidden
it "names a [names] or [chain] setting nothing reads, and still boots" testNamesUnknownKeys
rpcUrl :: T.Text
rpcUrl = "https://eth.example.org/v2/rpc-token-value"
-- anvil's first dev account, a real secp256k1 key
serviceKeyHex :: T.Text
serviceKeyHex = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
controllerAddr, sponsorAddr, registrarAddr, resolverAddr :: T.Text
controllerAddr = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
sponsorAddr = "0x0B306BF915C4d645ff596e518fAf3F9669b97016"
registrarAddr = "0x014f9c09CcEeDEF1B713110d04D3a07d5B5D9b73"
resolverAddr = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"
namesSection :: [T.Text]
namesSection = ["[names]", "tld = testing"]
chainSection :: [T.Text]
chainSection =
[ "[chain]",
"rpc_url = " <> rpcUrl,
"chain_id = 31337",
"controller = " <> controllerAddr,
"sponsor = " <> sponsorAddr,
"base_registrar = " <> registrarAddr,
"resolver = " <> resolverAddr,
"service_key = " <> serviceKeyHex
]
namesIni :: [T.Text] -> [T.Text] -> T.Text
namesIni ns cs = fullIni <> T.unlines (ns <> cs)
namesRefusal :: [T.Text] -> [T.Text] -> IO String
namesRefusal ns cs =
parseIni (namesIni ns cs) >>= \r -> case r of
Left e -> pure e
Right cfg -> expectationFailure ("must not boot, it read " <> show (names cfg)) >> pure ""
parsedNames :: [T.Text] -> [T.Text] -> IO NamesConfig
parsedNames ns cs =
parseIni (namesIni ns cs) >>= \r -> case r of
Right cfg -> maybe (fail "[names] was present but not read") pure (names cfg)
Left e -> fail ("must boot, but: " <> e)
address :: HasCallStack => T.Text -> Address
address t = either error id (strDecode (B.pack (T.unpack t)))
testNamesDefaults :: IO ()
testNamesDefaults = do
NamesConfig {nTLD, nYears, nTermStart, nTermSlackSeconds, nRevealMarginSeconds, nMaxOrderMoves, nChain} <- parsedNames namesSection chainSection
nTLD `shouldBe` TLDTesting
nYears `shouldBe` 2
nTermStart `shouldBe` UTCTime (fromGregorian 2027 1 1) 0
nTermSlackSeconds `shouldBe` 3600
nRevealMarginSeconds `shouldBe` 900
nMaxOrderMoves `shouldBe` 3
let ChainConfig {cRpcUrl, cChainId, cController, cSponsor, cBaseRegistrar, cResolver, cServiceKey, cConfirmations, cGasLimitCap} = nChain
cRpcUrl `shouldBe` T.unpack rpcUrl
cChainId `shouldBe` 31337
(cController, cSponsor, cBaseRegistrar, cResolver) `shouldBe` (address controllerAddr, address sponsorAddr, address registrarAddr, address resolverAddr)
Right expectedKey <- pure (mkPrivateKey (BA.pack [0xac, 0x09, 0x74, 0xbe, 0xc3, 0x9a, 0x17, 0xe3, 0x6b, 0xa4, 0xa6, 0xb4, 0xd2, 0x38, 0xff, 0x94, 0x4b, 0xac, 0xb4, 0x78, 0xcb, 0xed, 0x5e, 0xfc, 0xae, 0x78, 0x4d, 0x7b, 0xf4, 0xf2, 0xff, 0x80]))
(cServiceKey == expectedKey) `shouldBe` True
cConfirmations `shouldBe` 3
cGasLimitCap `shouldBe` 8000000
testNamesWrittenOut :: IO ()
testNamesWrittenOut = do
let ns = ["[names]", "tld = simplex", "years = 5", "term_start = 2027-03-01T12:00:00Z", "term_slack_seconds = 60", "reveal_margin_seconds = 0", "max_order_moves = 0"]
cs = chainSection <> ["confirmations = 0", "gas_limit_cap = 500000"]
NamesConfig {nTLD, nYears, nTermStart, nTermSlackSeconds, nRevealMarginSeconds, nMaxOrderMoves, nChain = ChainConfig {cConfirmations, cGasLimitCap}} <- parsedNames ns cs
nTLD `shouldBe` TLDSimplex
nYears `shouldBe` 5
nTermStart `shouldBe` UTCTime (fromGregorian 2027 3 1) (12 * 3600)
(nTermSlackSeconds, nRevealMarginSeconds, nMaxOrderMoves) `shouldBe` (60, 0, 0)
(cConfirmations, cGasLimitCap) `shouldBe` (0, 500000)
namesRefusal ns (chainSection <> ["confirmations = -1"]) `shouldReturn` "chain.confirmations must be at least 0"
namesRefusal ns (chainSection <> ["gas_limit_cap = 0"]) `shouldReturn` "chain.gas_limit_cap must be a positive whole number"
namesRefusal ns (filter (not . ("chain_id" `T.isPrefixOf`)) chainSection) `shouldReturn` "chain.chain_id is required"
namesRefusal (namesSection <> ["max_order_moves = -1"]) chainSection `shouldReturn` "names.max_order_moves must be at least 0"
testNamesListenerKeys :: IO ()
testNamesListenerKeys = do
let withKeys = T.replace "static_dir = /srv/badges\n" "static_dir = /srv/badges\nnames_static_dir = /srv/names\nnames_export_dir = /srv/names-out\nnames_hosts = simplex.domains, WWW.simplex.domains ,\n" fullIni
Right cfg <- parseIni withKeys
let ListenerConfig {lNamesStaticDir, lNamesExportDir, lNamesHosts} = listener cfg
lNamesStaticDir `shouldBe` Just "/srv/names"
lNamesExportDir `shouldBe` Just "/srv/names-out"
lNamesHosts `shouldBe` ["simplex.domains", "www.simplex.domains"]
Right plain <- parseIni fullIni
let ListenerConfig {lNamesStaticDir = noDir, lNamesExportDir = noExport, lNamesHosts = noHosts} = listener plain
(noDir, noExport, noHosts) `shouldBe` (Nothing, Nothing, [])
testNamesAbsent :: IO ()
testNamesAbsent = do
Right cfg <- parseIni fullIni
names cfg `shouldBe` Nothing
testNamesNeedsChain :: IO ()
testNamesNeedsChain = do
namesRefusal namesSection [] `shouldReturn` "[names] needs a [chain] section"
namesRefusal [] chainSection `shouldReturn` "[chain] is set but [names] is absent, so nothing would use it"
testNamesYearsBelowTwo :: IO ()
testNamesYearsBelowTwo = do
mapM_ (\v -> namesRefusal (namesSection <> ["years = " <> v]) chainSection `shouldReturn` "names.years must be at least 2") ["1", "0", "-2"]
NamesConfig {nYears} <- parsedNames (namesSection <> ["years = 2"]) chainSection
nYears `shouldBe` 2
testNamesBadTermStart :: IO ()
testNamesBadTermStart =
mapM_
(\v -> namesRefusal (namesSection <> ["term_start = " <> v]) chainSection >>= (`shouldContain` "names.term_start must be an ISO 8601 UTC time"))
["2027-01-01", "2027-01-01T00:00:00", "2027-13-01T00:00:00Z", "1 January 2027"]
testNamesBadTLD :: IO ()
testNamesBadTLD = do
namesRefusal ["[names]", "tld = simplex.chat"] chainSection `shouldReturn` "names.tld must be simplex or testing, not simplex.chat"
namesRefusal ["[names]", "years = 2"] chainSection `shouldReturn` "names.tld is required"
testNamesBadAddress :: IO ()
testNamesBadAddress = do
let withSponsor v = map (\l -> if "sponsor =" `T.isPrefixOf` l then "sponsor = " <> v else l) chainSection
refused v = namesRefusal namesSection (withSponsor v) `shouldReturn` "chain.sponsor is not an Ethereum address"
-- the mixed-case form of sponsorAddr with one letter's case flipped fails its EIP-55 checksum
refused "0x0b306BF915C4d645ff596e518fAf3F9669b97016"
refused "0x0B306BF915C4d645ff596e518fAf3F9669b970"
refused "0x0B306BF915C4d645ff596e518fAf3F9669b97016ff"
refused "not-an-address"
NamesConfig {nChain = ChainConfig {cSponsor}} <- parsedNames namesSection (withSponsor (T.toLower sponsorAddr))
cSponsor `shouldBe` address sponsorAddr
testNamesBadServiceKey :: IO ()
testNamesBadServiceKey = do
let withKey v = map (\l -> if "service_key =" `T.isPrefixOf` l then "service_key = " <> v else l) chainSection
refused v = do
e <- namesRefusal namesSection (withKey v)
e `shouldBe` "chain.service_key must be 0x and 64 hex digits of a valid secp256k1 key"
refused (T.drop 2 serviceKeyHex)
refused (T.dropEnd 2 serviceKeyHex)
refused ("0x" <> T.replicate 64 "0")
-- the curve order n is not a valid key
refused "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"
refused "0xzz0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
testNamesSecretsHidden :: IO ()
testNamesSecretsHidden = do
let secretUrl = "ftp://eth.example.org/v2/rpc-token-value"
withUrl v = map (\l -> if "rpc_url =" `T.isPrefixOf` l then "rpc_url = " <> v else l) chainSection
e <- namesRefusal namesSection (withUrl secretUrl)
e `shouldBe` "chain.rpc_url must be an http or https URL"
e `shouldNotContain` "rpc-token-value"
other <- namesRefusal namesSection (chainSection <> ["confirmations = many"])
other `shouldNotContain` "rpc-token-value"
other `shouldNotContain` T.unpack (T.drop 2 serviceKeyHex)
Right cfg <- parseIni (namesIni namesSection chainSection)
show cfg `shouldNotContain` "rpc-token-value"
show cfg `shouldNotContain` T.unpack (T.drop 2 serviceKeyHex)
show cfg `shouldContain` "chain 31337"
testNamesUnknownKeys :: IO ()
testNamesUnknownKeys = do
let t = namesIni (namesSection <> ["yeers = 3"]) (chainSection <> ["rpc = x"])
withIni t $ \p -> do
Right ini <- readIniFile p
unknownKeys ini `shouldMatchList` ["names.yeers", "chain.rpc"]
NamesConfig {nYears} <- parsedNames (namesSection <> ["yeers = 3"]) (chainSection <> ["rpc = x"])
nYears `shouldBe` 2
+4 -3
View File
@@ -974,12 +974,13 @@ prepareStaticDir root = do
testServiceConfig :: FilePath -> Bool -> ServiceConfig
testServiceConfig staticDir trustForwarded =
ServiceConfig
{ listener = ListenerConfig {lHost = "127.0.0.1", lPort = 0, lStaticDir = staticDir, lServeWebapp = True, lWebappExportDir = Nothing, lTrustForwardedFor = trustForwarded},
{ listener = ListenerConfig {lHost = "127.0.0.1", lPort = 0, lStaticDir = staticDir, lServeWebapp = True, lWebappExportDir = Nothing, lTrustForwardedFor = trustForwarded, lNamesStaticDir = Nothing, lNamesExportDir = Nothing, lNamesHosts = []},
btcpay = Nothing,
stripe = Nothing,
poll = PollConfig {pWaitingSeconds = 3, pIdleSeconds = 60},
issuer = Nothing,
group = Nothing
group = Nothing,
names = Nothing
}
testServeWebappOff :: IO ()
@@ -1003,7 +1004,7 @@ testExportWebapp = do
withTempDirectory "tests/tmp" "badge-export" $ \root -> do
staticDir <- prepareStaticDir root
let outDir = root </> "out"
lc = ListenerConfig {lHost = "127.0.0.1", lPort = 0, lStaticDir = staticDir, lServeWebapp = False, lWebappExportDir = Just outDir, lTrustForwardedFor = False}
lc = ListenerConfig {lHost = "127.0.0.1", lPort = 0, lStaticDir = staticDir, lServeWebapp = False, lWebappExportDir = Just outDir, lTrustForwardedFor = False, lNamesStaticDir = Nothing, lNamesExportDir = Nothing, lNamesHosts = []}
stripeCfg = Just StripeConfig {sSecretKey = "rk_test_x", sPublishableKey = "pk_test_injected", sWebhookSecret = "whsec_x", sSessionMinutes = 60, sHost = "https://api.stripe.com"}
exportWebapp lc stripeCfg
idx <- LB.readFile (outDir </> "index.html")
+1
View File
@@ -86,6 +86,7 @@ main = do
badgeBTCPayTests
badgeStripeTests
badgeNameRegistrationTests
namesConfigTests
describe "SimpleX chat markdown" markdownTests
describe "JSON Tests" jsonTests
describe "Member relations" memberRelationsTests