mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-23 10:51:25 +00:00
* core, ui: plan per-server roles for self-hosted servers * core: add per-server roles field to UserServer * core: add nullable role columns to protocol_servers * core: persist per-server roles * core: validate server coverage using per-server roles * test: cover per-server roles resolution and coverage * multiplatform: per-server role toggles for self-hosted servers * ios: per-server role toggles for self-hosted servers * core: per-server role overrides with per-role defaults * test: cover three-state per-server role resolution * fix: derive Eq for ServerRolesOverride * multiplatform: three-state role dropdowns on saved servers * ios: three-state role pickers on saved servers * test: per-server roles independent across two servers * test: enable names role in name-resolution tests * style: trim comments in per-server roles code * chore: rename server_roles migration to 20260716 (last) * core: per-server roles override operator roles, inherit when unset * multiplatform: per-server role default inherits from operator * ios: per-server role default inherits from operator * refactor(servers): tidy per-server roles per review - dedup no-operator default into ServerRoles.noOperatorDefault (Kotlin/Swift) - iOS: move roles-section control flow to the call site via a named gate, and parse the server address once instead of up to three times - Kotlin: collapse redundant derivedState; revert defaultOn->default rename for cross-platform parity - drop unused Hashable conformance on Swift ServerRoles - add agentServerCfgs test for names inheritance from an operator - remove no-op enableNamesRole calls from dormant DirectoryTests - fix ChatClient import ordering * chore(migration): date server_roles migration 20260720 * only show roles when server is enabled, move section above QR code --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
263 lines
13 KiB
Haskell
263 lines
13 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedLists #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE StandaloneDeriving #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# OPTIONS_GHC -Wno-orphans #-}
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
module OperatorTests (operatorTests) where
|
|
|
|
import Data.Bifunctor (second)
|
|
import qualified Data.List.NonEmpty as L
|
|
import Simplex.Chat
|
|
import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..))
|
|
import Simplex.Chat.Operators
|
|
import Simplex.Chat.Operators.Presets
|
|
import Simplex.Chat.Protocol (RelayProfile (..), mkRelayProfile)
|
|
import Simplex.Chat.Types
|
|
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
|
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
|
|
import Simplex.Messaging.Agent.Store.Entity
|
|
import Simplex.Messaging.Encoding.String
|
|
import Simplex.Messaging.Protocol
|
|
import Test.Hspec
|
|
|
|
operatorTests :: Spec
|
|
operatorTests = describe "managing server operators" $ do
|
|
validateServersTest
|
|
updatedServersTest
|
|
perServerRolesTest
|
|
|
|
validateServersTest :: Spec
|
|
validateServersTest = describe "validate user servers" $ do
|
|
it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` ([], [])
|
|
it "should fail without servers" $ do
|
|
validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing])
|
|
validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing])
|
|
validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing, USWNoNamesServers Nothing])
|
|
it "should fail without servers with storage role" $ do
|
|
validateUserServers [invalidNoStorage] [] `shouldBe` ([USEStorageMissing aSMP Nothing], [])
|
|
it "should fail with duplicate host" $ do
|
|
validateUserServers [invalidDuplicateSrv] []
|
|
`shouldBe` ( [ USEDuplicateServer aSMP "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion" "smp8.simplex.im",
|
|
USEDuplicateServer aSMP "smp://abcd@smp8.simplex.im" "smp8.simplex.im"
|
|
],
|
|
[]
|
|
)
|
|
it "should warn without chat relays" $
|
|
validateUserServers [invalidNoChatRelays] [] `shouldBe` ([], [USWNoChatRelays Nothing])
|
|
it "should allow duplicate chat relay name" $
|
|
validateUserServers [duplicateChatRelayName] [] `shouldBe` ([], [])
|
|
it "should fail with duplicate chat relay address" $ do
|
|
validateUserServers [invalidDuplicateChatRelayAddress] []
|
|
`shouldBe` ( [ USEDuplicateChatRelayAddress "SimpleX Chat Relay 2" duplicateAddr,
|
|
USEDuplicateChatRelayAddress "chat_relay_4" duplicateAddr
|
|
],
|
|
[]
|
|
)
|
|
where
|
|
aSMP = AProtocolType SPSMP
|
|
aXFTP = AProtocolType SPXFTP
|
|
|
|
updatedServersTest :: Spec
|
|
updatedServersTest = describe "validate user servers" $ do
|
|
it "adding preset operators on first start" $ do
|
|
let ops' :: [(Maybe PresetOperator, Maybe AServerOperator)] =
|
|
updatedServerOperators operators []
|
|
length ops' `shouldBe` 2
|
|
all addedPreset ops' `shouldBe` True
|
|
let ops'' :: [(Maybe PresetOperator, Maybe ServerOperator)] =
|
|
saveOps ops' -- mock getUpdateServerOperators
|
|
uss <- groupByOperator' (ops'', [], [], []) -- no stored servers or relays
|
|
length uss `shouldBe` 3
|
|
[op1, op2, op3] <- pure $ map updatedUserServers uss
|
|
[p1, p2] <- pure operators -- presets
|
|
sameServers p1 op1
|
|
sameRelays p1 op1
|
|
sameServers p2 op2
|
|
sameRelays p2 op2
|
|
null (servers' SPSMP op3) `shouldBe` True
|
|
null (servers' SPXFTP op3) `shouldBe` True
|
|
null (chatRelays' op3) `shouldBe` True
|
|
it "adding preset operators and assigning servers to operator for existing users" $ do
|
|
let ops' = updatedServerOperators operators []
|
|
ops'' = saveOps ops'
|
|
uss <-
|
|
groupByOperator'
|
|
( ops'',
|
|
saveSrvs $ take 3 simplexChatSMPServers <> [newUserServer "smp://abcd@smp.example.im"],
|
|
saveSrvs $ map (presetServer True) $ L.take 3 defaultXFTPServers,
|
|
saveRelays $ take 2 simplexChatRelays <> [newChatRelay (mkRelayProfile "custom_relay" Nothing) ["example.im"] customRelayAddr]
|
|
)
|
|
[op1, op2, op3] <- pure $ map updatedUserServers uss
|
|
[p1, p2] <- pure operators -- presets
|
|
sameServers p1 op1
|
|
sameRelays p1 op1
|
|
sameServers p2 op2
|
|
sameRelays p2 op2
|
|
map srvHost' (servers' SPSMP op3) `shouldBe` [["smp.example.im"]]
|
|
null (servers' SPXFTP op3) `shouldBe` True
|
|
map relayName' (chatRelays' op3) `shouldBe` ["custom_relay"]
|
|
where
|
|
addedPreset = \case
|
|
(Just PresetOperator {operator = Just op}, Just (ASO SDBNew op')) -> operatorTag op == operatorTag op'
|
|
_ -> False
|
|
saveOps = zipWith (\i -> second ((\(ASO _ op) -> op {operatorId = DBEntityId i}) <$>)) [1 ..]
|
|
saveSrvs = zipWith (\i srv -> srv {serverId = DBEntityId i}) [1 ..]
|
|
saveRelays = zipWith (\i relay -> relay {chatRelayId = DBEntityId i}) [1 ..]
|
|
sameServers preset op = do
|
|
map srvHost (pServers SPSMP preset) `shouldBe` map srvHost' (servers' SPSMP op)
|
|
map srvHost (pServers SPXFTP preset) `shouldBe` map srvHost' (servers' SPXFTP op)
|
|
sameRelays PresetOperator {chatRelays = presetRelays} op =
|
|
map chatRelayAddress presetRelays `shouldBe` map relayAddr' (chatRelays' op)
|
|
srvHost' (AUS _ s) = srvHost s
|
|
relayAddr' (AUCR _ r) = chatRelayAddress r
|
|
relayName' (AUCR _ UserChatRelay {relayProfile = RelayProfile {displayName}}) = displayName
|
|
PresetServers {operators} = presetServers defaultChatConfig
|
|
customRelayAddr = either error id $ strDecode "https://relay.example.im/r#Pz9qz7ZVljMofoRxiDDpL_w2DZSazK8IgafxqnWKv6Y"
|
|
|
|
perServerRolesTest :: Spec
|
|
perServerRolesTest = describe "per-server roles" $ do
|
|
describe "agentServerCfgs resolution" $ do
|
|
it "self-hosted server keeps its per-server roles" $
|
|
case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride (Just True) (Just False) (Just True))] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Nothing
|
|
rolesTuple roles `shouldBe` (True, False, True)
|
|
cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs
|
|
it "self-hosted server without roles falls back to default roles" $
|
|
case agentServerCfgs SPSMP opDomains [selfHostedSMP emptyServerRolesOverride] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Nothing
|
|
rolesTuple roles `shouldBe` (True, True, False)
|
|
cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs
|
|
it "self-hosted partial override keeps defaults for unset roles" $
|
|
case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride Nothing Nothing (Just True))] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Nothing
|
|
rolesTuple roles `shouldBe` (True, True, True)
|
|
cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs
|
|
it "self-hosted explicit No overrides a default-yes role" $
|
|
case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride (Just False) Nothing Nothing)] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Nothing
|
|
rolesTuple roles `shouldBe` (False, True, False)
|
|
cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs
|
|
it "operator-matched server without override inherits operator roles" $
|
|
-- operator roles are all on; no override -> all inherited, including names
|
|
case agentServerCfgs SPSMP opDomains [opMatchedSMP emptyServerRolesOverride] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Just 1
|
|
rolesTuple roles `shouldBe` (True, True, True)
|
|
cfgs -> expectationFailure $ "expected one operator-matched ServerCfg, got: " <> show cfgs
|
|
it "operator-matched server applies its override over operator roles" $
|
|
-- operator roles are all on; override turns storage/names off, proxy inherits (on)
|
|
case agentServerCfgs SPSMP opDomains [opMatchedSMP (ServerRolesOverride (Just False) Nothing (Just False))] of
|
|
[ServerCfg {operator, roles}] -> do
|
|
operator `shouldBe` Just 1
|
|
rolesTuple roles `shouldBe` (False, True, False)
|
|
cfgs -> expectationFailure $ "expected one operator-matched ServerCfg, got: " <> show cfgs
|
|
it "two self-hosted servers resolve their three roles independently" $
|
|
-- agentServerCfgs preserves input order
|
|
let a = (newUserServer "smp://abcd@self.example.com" :: NewUserServer 'PSMP) {roles = ServerRolesOverride (Just False) (Just True) (Just True)}
|
|
b = (newUserServer "smp://abcd@self2.example.com" :: NewUserServer 'PSMP) {roles = ServerRolesOverride (Just True) (Just False) Nothing}
|
|
in case agentServerCfgs SPSMP opDomains [a, b] of
|
|
[ServerCfg {operator = opA, roles = rolesA}, ServerCfg {operator = opB, roles = rolesB}] -> do
|
|
(opA, opB) `shouldBe` (Nothing, Nothing)
|
|
rolesTuple rolesA `shouldBe` (False, True, True)
|
|
rolesTuple rolesB `shouldBe` (True, False, False)
|
|
cfgs -> expectationFailure $ "expected two self-hosted ServerCfgs, got: " <> show cfgs
|
|
describe "validateUserServers per-server names coverage" $ do
|
|
it "self-hosted-only user without names servers warns USWNoNamesServers" $ do
|
|
let (_errs, warns) = validateUserServers [selfHostedUser emptyServerRolesOverride] []
|
|
warns `shouldSatisfy` elem (USWNoNamesServers Nothing)
|
|
it "self-hosted user with a names server does not warn USWNoNamesServers" $ do
|
|
let (_errs, warns) = validateUserServers [selfHostedUser (ServerRolesOverride Nothing Nothing (Just True))] []
|
|
warns `shouldSatisfy` notElem (USWNoNamesServers Nothing)
|
|
where
|
|
testOp = operatorSimpleXChat {operatorId = DBEntityId 1}
|
|
opDomains = operatorDomains [testOp]
|
|
-- host matches no operator domain -> self-hosted
|
|
selfHostedSMP :: ServerRolesOverride -> NewUserServer 'PSMP
|
|
selfHostedSMP r = (newUserServer "smp://abcd@self.example.com" :: NewUserServer 'PSMP) {roles = r}
|
|
-- host matches operator domain simplex.im
|
|
opMatchedSMP :: ServerRolesOverride -> NewUserServer 'PSMP
|
|
opMatchedSMP r = (newUserServer "smp://abcd@smp8.simplex.im" :: NewUserServer 'PSMP) {roles = r}
|
|
selfHostedUser :: ServerRolesOverride -> UpdatedUserOperatorServers
|
|
selfHostedUser r =
|
|
UpdatedUserOperatorServers
|
|
{ operator = Nothing,
|
|
smpServers = [AUS SDBNew $ selfHostedSMP r],
|
|
xftpServers = [],
|
|
chatRelays = []
|
|
}
|
|
rolesTuple :: ServerRoles -> (Bool, Bool, Bool)
|
|
rolesTuple ServerRoles {storage, proxy, names} = (storage, proxy, names)
|
|
|
|
deriving instance Eq User
|
|
|
|
deriving instance Eq UserServersError
|
|
|
|
deriving instance Eq UserServersWarning
|
|
|
|
valid :: UpdatedUserOperatorServers
|
|
valid =
|
|
UpdatedUserOperatorServers
|
|
{ operator = Just operatorSimpleXChat {operatorId = DBEntityId 1},
|
|
smpServers = map (AUS SDBNew) simplexChatSMPServers,
|
|
xftpServers = map (AUS SDBNew . presetServer True) $ L.toList defaultXFTPServers,
|
|
chatRelays = map (AUCR SDBNew) simplexChatRelays
|
|
}
|
|
|
|
invalidNoServers :: UpdatedUserOperatorServers
|
|
invalidNoServers = (valid :: UpdatedUserOperatorServers) {smpServers = []}
|
|
|
|
invalidDisabled :: UpdatedUserOperatorServers
|
|
invalidDisabled =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ smpServers = map (AUS SDBNew . (\srv -> (srv :: NewUserServer 'PSMP) {enabled = False})) simplexChatSMPServers
|
|
}
|
|
|
|
invalidDisabledOp :: UpdatedUserOperatorServers
|
|
invalidDisabledOp =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, enabled = False}
|
|
}
|
|
|
|
invalidNoStorage :: UpdatedUserOperatorServers
|
|
invalidNoStorage =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, smpRoles = allRoles {storage = False}}
|
|
}
|
|
|
|
invalidDuplicateSrv :: UpdatedUserOperatorServers
|
|
invalidDuplicateSrv =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ smpServers = map (AUS SDBNew) $ simplexChatSMPServers <> [presetServer True "smp://abcd@smp8.simplex.im"]
|
|
}
|
|
|
|
invalidNoChatRelays :: UpdatedUserOperatorServers
|
|
invalidNoChatRelays = (valid :: UpdatedUserOperatorServers) {chatRelays = []}
|
|
|
|
duplicateChatRelayName :: UpdatedUserOperatorServers
|
|
duplicateChatRelayName =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ chatRelays = map (AUCR SDBNew) $ simplexChatRelays <> [presetChatRelay True (mkRelayProfile "chat_relay_1" Nothing) ["simplex.im"] (either error id $ strDecode "https://smp444.simplex.im/r#Pz9qz7ZVljMofoRxiDDpL_w2DZSazK8IgafxqnWKv6Y")]
|
|
}
|
|
|
|
invalidDuplicateChatRelayAddress :: UpdatedUserOperatorServers
|
|
invalidDuplicateChatRelayAddress =
|
|
(valid :: UpdatedUserOperatorServers)
|
|
{ chatRelays = map (AUCR SDBNew) $ simplexChatRelays <> [presetChatRelay True (mkRelayProfile "chat_relay_4" Nothing) ["simplex.im"] duplicateAddr]
|
|
}
|
|
|
|
duplicateAddr :: ShortLinkContact
|
|
duplicateAddr = either error id $ strDecode "https://smp6.simplex.im/r#_qlQfogHGDJ8MAF2wKmkglRBM-xHR142gDJstKiGRQQ"
|