packages feed

git-remote-ipfs-0.1.0.0: test/e2e/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators     #-}
{-# LANGUAGE ViewPatterns      #-}

{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

module Main where

import           Control.Applicative (liftA2)
import           Control.Exception.Safe (throwM, throwString)
import           Control.Lens (firstOf)
import           Control.Monad (void)
import           Data.Aeson.Lens (key, _String)
import           Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as Bytes
import           Data.ByteString.Lazy (fromStrict, toStrict)
import qualified Data.ByteString.Lazy.Char8 as Lazy
import           Data.Foldable (foldlM)
import qualified Data.Git.Monad as Git
import           Data.Git.Ref (SHA1)
import qualified Data.Git.Storage as Git (initRepo)
import           Data.Git.Types (GitTime(..))
import           Data.Maybe (fromMaybe)
import           Data.Proxy (Proxy(..))
import           Data.String (fromString)
import           Data.Text (Text)
import qualified Data.Text as Text
import           Data.Text.Encoding (decodeUtf8)
import           GHC.Stack (HasCallStack)
import qualified Network.HTTP.Client as Http
import           Network.IPFS.API (ApiV0KeyGen, ApiV0NamePublish)
import           Servant.API
import qualified Servant.Client as Servant
import           System.Entropy (getEntropy)
import           System.Environment (lookupEnv)
import           System.Exit (ExitCode(..))
import           System.FilePath (takeBaseName, takeDirectory, (</>))
import           System.Hourglass (timeCurrent, timezoneCurrent)
import           System.IO.Temp (withSystemTempDirectory)
import           System.Process.Typed

import           Test.Tasty
import           Test.Tasty.HUnit

type Step = String -> IO ()

main :: IO ()
main =
    withSystemTempDirectory "git-remote-ipfs-e2e" $ \tmp -> defaultMain $
        testGroup "E2E Tests"
            [ testCaseSteps "Push, clone: IPFS"     $ testPushCloneSimple tmp
            , testCaseSteps "Push, clone: IPNS"     $ testPushCloneIPNS   tmp
            , testCaseSteps "Push, clone: LOB IPFS" $ testLargeObjects    tmp
            ]

testPushCloneSimple :: FilePath -> Step -> IO ()
testPushCloneSimple root step = runPushCloneTest PushCloneOpts
    { pcoRepo      = root </> "simple" </> ".git"
    , pcoClone     = root </> "simple-clone" </> ".git"
    , pcoRemoteUrl = "ipfs://"
    , pcoHistory   = simpleHistory
    , pcoLog       = step
    }

testPushCloneIPNS :: FilePath -> Step -> IO ()
testPushCloneIPNS root step = do
    let keyName = Text.pack (takeBaseName root) -- piggypack on randomness of tmp
    step "Creating IPNS name"
    ipnsName <- Text.unpack <$> ipfs (createIpnsName keyName)
    runPushCloneTest PushCloneOpts
        { pcoRepo      = root </> ipnsName </> ".git"
        , pcoClone     = root </> ipnsName ++ "-clone" </> ".git"
        , pcoRemoteUrl = "ipfs://ipns/" <> ipnsName
        , pcoHistory   = simpleHistory
        , pcoLog       = step
        }
  where
    ipfs m = servantEnv >>= Servant.runClientM m >>= either throwM pure

testLargeObjects :: FilePath -> Step -> IO ()
testLargeObjects root step = runPushCloneTest PushCloneOpts
    { pcoRepo      = root </> "lobs" </> ".git"
    , pcoClone     = root </> "lobs-clone" </> ".git"
    , pcoRemoteUrl = "ipfs://"
    , pcoHistory   = lobHistory
    , pcoLog       = step
    }

data PushCloneOpts = PushCloneOpts
    { pcoRepo      :: FilePath
    , pcoClone     :: FilePath
    , pcoRemoteUrl :: String
    , pcoHistory   :: FilePath -> IO ()
    , pcoLog       :: Step
    }

runPushCloneTest :: PushCloneOpts -> IO ()
runPushCloneTest PushCloneOpts{..} = do
    initRepo pcoLog pcoRepo pcoHistory

    url <- pushIpfs pcoLog pcoRepo pcoRemoteUrl
    cloneIpfs pcoLog (takeDirectory pcoClone) url

    assertSameRepos pcoLog pcoRepo pcoClone

initRepo :: HasCallStack => Step -> FilePath -> (FilePath -> IO ()) -> IO ()
initRepo step repo history = do
    step $ "Initializing repo at " <> repo
    let repo' = fromString repo
    Git.initRepo repo'
    (>>= either throwString pure) . Git.withRepo repo' $
        Git.headSet $ pure "master"
    history repo

pushIpfs :: Step -> FilePath -> String -> IO String
pushIpfs step repo url = do
    step $ "Pushing master to " <> url
    git_ repo ["remote", "add", "origin", url]
    git_ repo ["push", "--quiet", "origin", "master"]
    Text.unpack . Text.strip . decodeUtf8
        <$> git repo ["remote", "get-url", "origin"]

cloneIpfs :: Step -> FilePath -> String -> IO ()
cloneIpfs step repo url = do
    step $ "Cloning " <> url <> " to " <> repo
    git_ repo ["clone", "--quiet", url, repo]

assertSameRepos :: Step -> FilePath -> FilePath -> IO ()
assertSameRepos step src clone = do
    step $ "Fetching " <> src <> " to " <> clone
    git_ clone ["remote", "add", "src", src]
    git_ clone ["fetch", "--quiet", "src"]
    step "Comparing origin/master src/master"
    void $ gitAssert "Source and cloned repository differ"
        clone ["diff", "--quiet", "origin/master", "src/master"]

simpleHistory :: FilePath -> IO ()
simpleHistory repo =
    (>>= either throwString (const $ pure ())) . Git.withRepo (fromString repo) $ do
        initial <- mkCommit Nothing Nothing 0
        foldlM (\(bs, rev) i -> mkCommit bs rev i) initial [1..42]
  where
    mkCommit
        :: Maybe Lazy.ByteString
        -> Maybe String
        -> Int
        -> Git.GitM (Maybe Lazy.ByteString, Maybe String)
    mkCommit readme branch (show -> i) = do
        person  <- mkPerson <$> currentTimeGit
        (r, ()) <-
            Git.withNewCommit person branch $ do
                Git.setMessage $ "Commit " <> Bytes.pack i
                Git.setFile ["README"] $
                    fromMaybe mempty readme <> Lazy.pack i <> "\n"
        Git.branchWrite "master" r
        (,Just "master") <$>
            Git.withCommit ("master" :: String) (Git.getFile ["README"])

lobHistory :: FilePath -> IO ()
lobHistory repo = do
    blob <- getEntropy 4096000
    (>>= either throwString pure) . Git.withRepo (fromString repo) $ do
        person  <- mkPerson <$> currentTimeGit
        (r, ()) <-
            Git.withNewCommit person (Nothing :: Maybe (Git.Ref SHA1)) $ do
                Git.setMessage "Huuuuge"
                Git.setFile ["huge.file"] $ fromStrict blob
        Git.branchWrite "master" r

mkPerson :: GitTime -> Git.Person
mkPerson = Git.Person "LeBoeuf" "le@boe.uf"

currentTimeGit :: Git.GitM GitTime
currentTimeGit = Git.liftGit $ liftA2 GitTime timeCurrent timezoneCurrent

git :: FilePath -> [String] -> IO ByteString
git = gitAssert mempty

git_ :: FilePath -> [String] -> IO ()
git_ repo args = void $ git repo args

gitAssert :: String -> FilePath -> [String] -> IO ByteString
gitAssert msg repo args = do
    let cfg = proc "git" $ "--git-dir" : repo : args
    readProcess cfg >>= \case
        (ExitSuccess, out, _err) -> pure $ toStrict out
        (failure    , out, err ) -> assertFailure $ unlines
            [ msg
            , show failure
            , show cfg
            , show out
            , show err
            ]

type IPFS = ApiV0KeyGen :<|> ApiV0NamePublish

ipfsKeyGen :<|> ipfsNamePublish = Servant.client (Proxy @IPFS)

servantEnv :: IO Servant.ClientEnv
servantEnv = liftA2 Servant.mkClientEnv mgr base
  where
    mgr = Http.newManager Http.defaultManagerSettings
            { Http.managerResponseTimeout = Http.responseTimeoutNone }

    base =
        Servant.parseBaseUrl
            =<< fromMaybe "http://localhost:5001" <$> lookupEnv "IPFS_API_URL"

createIpnsName :: HasCallStack => Text -> Servant.ClientM Text
createIpnsName keyName = do
    keyId <-
        maybe (throwString "Missing key Id") pure
            . firstOf (key "Id" . _String)
            =<< ipfsKeyGen keyName (Just "ed25519") Nothing
    void $
        ipfsNamePublish ("/ipfs/" <> emptyRepo)
                        (Just True)  -- resolve
                        (Just "5m")  -- lifetime
                        Nothing      -- caching
                        (Just keyId) -- key
    pure keyId
  where
    emptyRepo = "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"