From bb08fcfc99115fd883763963394623b58a7b54fe Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 21 Oct 2026 22:08:32 +0000 Subject: [PATCH] store: read the names and chain settings --- .../src/StoreService/Config.hs | 129 +++++++++++- .../store_service.ini.example | 36 ++++ tests/Bots/StoreService/ConfigTests.hs | 188 ++++++++++++++++++ tests/Bots/StoreService/WebTests.hs | 7 +- tests/Test.hs | 1 + 5 files changed, 351 insertions(+), 10 deletions(-) diff --git a/apps/simplex-store-service/src/StoreService/Config.hs b/apps/simplex-store-service/src/StoreService/Config.hs index a8c2545571..2ec4919175 100644 --- a/apps/simplex-store-service/src/StoreService/Config.hs +++ b/apps/simplex-store-service/src/StoreService/Config.hs @@ -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 diff --git a/apps/simplex-store-service/store_service.ini.example b/apps/simplex-store-service/store_service.ini.example index 47bcc39fa4..2c050adfa8 100644 --- a/apps/simplex-store-service/store_service.ini.example +++ b/apps/simplex-store-service/store_service.ini.example @@ -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 diff --git a/tests/Bots/StoreService/ConfigTests.hs b/tests/Bots/StoreService/ConfigTests.hs index 06fbb7cffa..16ecc3c581 100644 --- a/tests/Bots/StoreService/ConfigTests.hs +++ b/tests/Bots/StoreService/ConfigTests.hs @@ -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 diff --git a/tests/Bots/StoreService/WebTests.hs b/tests/Bots/StoreService/WebTests.hs index 6391ad5cac..cfad4943eb 100644 --- a/tests/Bots/StoreService/WebTests.hs +++ b/tests/Bots/StoreService/WebTests.hs @@ -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") diff --git a/tests/Test.hs b/tests/Test.hs index 1a1fcab74a..ab4934c616 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -86,6 +86,7 @@ main = do badgeBTCPayTests badgeStripeTests badgeNameRegistrationTests + namesConfigTests describe "SimpleX chat markdown" markdownTests describe "JSON Tests" jsonTests describe "Member relations" memberRelationsTests