module Testlib.RunServices (main) where

import Control.Concurrent
import Control.Monad.Codensity
import Options.Applicative
import System.Directory
import System.Exit
import System.FilePath
import System.Posix (getWorkingDirectory)
import System.Process
import Testlib.Prelude
import Testlib.ResourcePool

parentDir :: FilePath -> Maybe FilePath
parentDir :: String -> Maybe String
parentDir String
path =
  let dirs :: [String]
dirs = String -> [String]
splitPath String
path
   in if [String] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [String]
dirs
        then Maybe String
forall a. Maybe a
Nothing
        else String -> Maybe String
forall a. a -> Maybe a
Just (String -> Maybe String) -> String -> Maybe String
forall a b. (a -> b) -> a -> b
$ [String] -> String
joinPath ([String] -> [String]
forall a. HasCallStack => [a] -> [a]
init [String]
dirs)

containsGit :: FilePath -> IO Bool
containsGit :: String -> IO Bool
containsGit String
path =
  String -> IO Bool
doesPathExist (String -> IO Bool) -> String -> IO Bool
forall a b. (a -> b) -> a -> b
$ [String] -> String
joinPath [String
path, String
".git"]

findProjectRoot :: FilePath -> IO (Maybe FilePath)
findProjectRoot :: String -> IO (Maybe String)
findProjectRoot String
path = do
  Bool
c <- String -> IO Bool
containsGit String
path
  if Bool
c
    then Maybe String -> IO (Maybe String)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (String -> Maybe String
forall a. a -> Maybe a
Just String
path)
    else case String -> Maybe String
parentDir String
path of
      Maybe String
Nothing -> Maybe String -> IO (Maybe String)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe String
forall a. Maybe a
Nothing
      Just String
p -> String -> IO (Maybe String)
findProjectRoot String
p

data Opts = Opts
  { Opts -> Bool
withManualTestingOverrides :: Bool,
    Opts -> [String]
runSubprocess :: [String]
  }
  deriving (Int -> Opts -> ShowS
[Opts] -> ShowS
Opts -> String
(Int -> Opts -> ShowS)
-> (Opts -> String) -> ([Opts] -> ShowS) -> Show Opts
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Opts -> ShowS
showsPrec :: Int -> Opts -> ShowS
$cshow :: Opts -> String
show :: Opts -> String
$cshowList :: [Opts] -> ShowS
showList :: [Opts] -> ShowS
Show)

optsParser :: Parser Opts
optsParser :: Parser Opts
optsParser =
  Bool -> [String] -> Opts
Opts
    (Bool -> [String] -> Opts)
-> Parser Bool -> Parser ([String] -> Opts)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Mod FlagFields Bool -> Parser Bool
switch
      ( String -> Mod FlagFields Bool
forall (f :: * -> *) a. HasName f => String -> Mod f a
long String
"with-manual-testing-overrides"
          Mod FlagFields Bool -> Mod FlagFields Bool -> Mod FlagFields Bool
forall a. Semigroup a => a -> a -> a
<> Char -> Mod FlagFields Bool
forall (f :: * -> *) a. HasName f => Char -> Mod f a
short Char
'm'
          Mod FlagFields Bool -> Mod FlagFields Bool -> Mod FlagFields Bool
forall a. Semigroup a => a -> a -> a
<> String -> Mod FlagFields Bool
forall (f :: * -> *) a. String -> Mod f a
help String
"Run services with settings tuned for manual app usage (not recommended for running integration tests)"
      )
    Parser ([String] -> Opts) -> Parser [String] -> Parser Opts
forall a b. Parser (a -> b) -> Parser a -> Parser b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Parser String -> Parser [String]
forall a. Parser a -> Parser [a]
forall (f :: * -> *) a. Alternative f => f a -> f [a]
many
      ( Mod ArgumentFields String -> Parser String
forall s. IsString s => Mod ArgumentFields s -> Parser s
strArgument
          ( String -> Mod ArgumentFields String
forall (f :: * -> *) a. HasMetavar f => String -> Mod f a
metavar String
"COMMAND_WITH_ARGS"
              Mod ArgumentFields String
-> Mod ArgumentFields String -> Mod ArgumentFields String
forall a. Semigroup a => a -> a -> a
<> String -> Mod ArgumentFields String
forall (f :: * -> *) a. String -> Mod f a
help String
"When specified, the command will be run after services have started and service will be killed after the command exits"
          )
      )

main :: IO ()
main :: IO ()
main = do
  String
cwd <- IO String
getWorkingDirectory
  Maybe String
mbProjectRoot <- String -> IO (Maybe String)
findProjectRoot String
cwd
  Opts
opts <- ParserInfo Opts -> IO Opts
forall a. ParserInfo a -> IO a
execParser (Parser Opts -> InfoMod Opts -> ParserInfo Opts
forall a. Parser a -> InfoMod a -> ParserInfo a
info (Parser Opts
optsParser Parser Opts -> Parser (Opts -> Opts) -> Parser Opts
forall (f :: * -> *) a b. Applicative f => f a -> f (a -> b) -> f b
<**> Parser (Opts -> Opts)
forall a. Parser (a -> a)
helper) InfoMod Opts
forall a. InfoMod a
fullDesc)
  String
cfg <- case Maybe String
mbProjectRoot of
    Maybe String
Nothing -> String -> IO String
forall a. HasCallStack => String -> a
error String
"Could not find project root. Please make sure you call run-services from somewhere in wire-server."
    Just String
projectRoot ->
      String -> IO String
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (String -> IO String) -> String -> IO String
forall a b. (a -> b) -> a -> b
$ [String] -> String
joinPath [String
projectRoot, String
"services/integration.yaml"]

  let run :: IO ()
run = case Opts
opts.runSubprocess of
        [] -> do
          String -> IO ()
forall (m :: * -> *). MonadIO m => String -> m ()
putStrLn String
"services started"
          IO () -> IO ()
forall (f :: * -> *) a b. Applicative f => f a -> f b
forever (Int -> IO ()
threadDelay Int
forall a. Bounded a => a
maxBound)
        [String]
_ -> do
          let cp :: CreateProcess
cp = String -> [String] -> CreateProcess
proc String
"sh" ([String
"-c", String
"exec \"$@\"", String
"--"] [String] -> [String] -> [String]
forall a. Semigroup a => a -> a -> a
<> Opts
opts.runSubprocess)
          (Maybe Handle
_, Maybe Handle
_, Maybe Handle
_, ProcessHandle
ph) <- CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess CreateProcess
cp
          ExitCode -> IO ()
forall a. ExitCode -> IO a
exitWith (ExitCode -> IO ()) -> IO ExitCode -> IO ()
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< ProcessHandle -> IO ExitCode
waitForProcess ProcessHandle
ph

  Codensity IO Env -> forall b. (Env -> IO b) -> IO b
forall k (m :: k -> *) a.
Codensity m a -> forall (b :: k). (a -> m b) -> m b
runCodensity (String -> Codensity IO GlobalEnv
mkGlobalEnv String
cfg Codensity IO GlobalEnv
-> (GlobalEnv -> Codensity IO Env) -> Codensity IO Env
forall a b.
Codensity IO a -> (a -> Codensity IO b) -> Codensity IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Maybe String -> GlobalEnv -> Codensity IO Env
mkEnv Maybe String
forall a. Maybe a
Nothing) ((Env -> IO ()) -> IO ()) -> (Env -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Env
env ->
    Env -> App () -> IO ()
forall a. Env -> App a -> IO a
runAppWithEnv Env
env
      (App () -> IO ()) -> App () -> IO ()
forall a b. (a -> b) -> a -> b
$ Codensity App () -> App ()
forall (f :: * -> *) a. Applicative f => Codensity f a -> f a
lowerCodensity
      (Codensity App () -> App ()) -> Codensity App () -> App ()
forall a b. (a -> b) -> a -> b
$ do
        ()
_modifyEnv <-
          (HasCallStack => BackendResource -> Codensity App ())
-> HasCallStack => [BackendResource] -> Codensity App ()
forall a.
(HasCallStack => a -> Codensity App ())
-> HasCallStack => [a] -> Codensity App ()
traverseConcurrentlyCodensity
            ( \BackendResource
r ->
                Codensity App String -> Codensity App ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void
                  (Codensity App String -> Codensity App ())
-> Codensity App String -> Codensity App ()
forall a b. (a -> b) -> a -> b
$ if Opts
opts.withManualTestingOverrides
                    then HasCallStack =>
BackendResource -> ServiceOverrides -> Codensity App String
BackendResource -> ServiceOverrides -> Codensity App String
startDynamicBackend BackendResource
r ServiceOverrides
manualTestingOverrides
                    else HasCallStack =>
BackendResource -> ServiceOverrides -> Codensity App String
BackendResource -> ServiceOverrides -> Codensity App String
startDynamicBackend BackendResource
r ServiceOverrides
forall a. Monoid a => a
mempty
            )
            [BackendResource
backendA, BackendResource
backendB]
        IO () -> Codensity App ()
forall a. IO a -> Codensity App a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO IO ()
run

manualTestingOverrides :: ServiceOverrides
manualTestingOverrides :: ServiceOverrides
manualTestingOverrides =
  let smtpEndpoint :: Value
smtpEndpoint = [Pair] -> Value
object [String
"host" String -> String -> Pair
forall a. ToJSON a => String -> a -> Pair
.= String
"localhost", String
"port" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
2500 :: Int)]
      authSettings :: Value
authSettings =
        [Pair] -> Value
object
          [ String
"userTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
4838400 :: Int),
            String
"sessionTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
86400 :: Int),
            String
"accessTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
900 :: Int),
            String
"providerTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
900 :: Int),
            String
"legalHoldUserTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
4838400 :: Int),
            String
"legalHoldAccessTokenTimeout" String -> Int -> Pair
forall a. ToJSON a => String -> a -> Pair
.= (Int
900 :: Int)
          ]
   in ServiceOverrides
forall a. Default a => a
def
        { brigCfg =
            mergeField "emailSMS.email.smtpEndpoint" smtpEndpoint
              >=> setField "emailSMS.email.smtpConnType" "plain"
              >=> removeField "emailSMS.email.sesQueue"
              >=> removeField "emailSMS.email.sesEndpoint"
              >=> mergeField "zauth.authSettings" authSettings
              >=> setField @_ @Int "optSettings.setActivationTimeout" 3600
              >=> setField @_ @Int "optSettings.setVerificationTimeout" 3600
              >=> setField @_ @Int "optSettings.setTeamInvitationTimeout" 3600
              >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600
              >=> removeField "optSettings.setSuspendInactiveUsers"
        }