Commit 8a2cabfd authored by Alfredo Di Napoli's avatar Alfredo Di Napoli

Add test for duplicate user creation

parent 35cb5887
...@@ -18,6 +18,7 @@ module Main where ...@@ -18,6 +18,7 @@ module Main where
import Gargantext.API.Dev (withDevEnv, runCmdDev) import Gargantext.API.Dev (withDevEnv, runCmdDev)
import Gargantext.API.Prelude (GargError) import Gargantext.API.Prelude (GargError)
import Gargantext.Database.Action.User.New (newUsers) import Gargantext.Database.Action.User.New (newUsers)
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude (Cmd'') import Gargantext.Database.Prelude (Cmd'')
import Gargantext.Prelude import Gargantext.Prelude
import System.Environment (getArgs) import System.Environment (getArgs)
...@@ -28,6 +29,6 @@ main = do ...@@ -28,6 +29,6 @@ main = do
(iniPath:mails) <- getArgs (iniPath:mails) <- getArgs
withDevEnv iniPath $ \env -> do withDevEnv iniPath $ \env -> do
x <- runCmdDev env ((newUsers $ map cs mails) :: Cmd'' DevEnv GargError Int64) x <- runCmdDev env ((newUsers $ map cs mails) :: Cmd'' DevEnv GargError [UserId])
putStrLn $ show x putStrLn $ show x
pure () pure ()
...@@ -123,6 +123,7 @@ library ...@@ -123,6 +123,7 @@ library
Gargantext.Database.Query.Table.Node Gargantext.Database.Query.Table.Node
Gargantext.Database.Query.Table.Node.Error Gargantext.Database.Query.Table.Node.Error
Gargantext.Database.Query.Table.Node.UpdateOpaleye Gargantext.Database.Query.Table.Node.UpdateOpaleye
Gargantext.Database.Query.Tree.Root
Gargantext.Database.Query.Table.User Gargantext.Database.Query.Table.User
Gargantext.Database.Schema.Ngrams Gargantext.Database.Schema.Ngrams
Gargantext.Database.Schema.User Gargantext.Database.Schema.User
...@@ -339,7 +340,6 @@ library ...@@ -339,7 +340,6 @@ library
Gargantext.Database.Query.Table.NodesNgramsRepo Gargantext.Database.Query.Table.NodesNgramsRepo
Gargantext.Database.Query.Tree Gargantext.Database.Query.Tree
Gargantext.Database.Query.Tree.Error Gargantext.Database.Query.Tree.Error
Gargantext.Database.Query.Tree.Root
Gargantext.Database.Schema.Context Gargantext.Database.Schema.Context
Gargantext.Database.Schema.ContextNodeNgrams Gargantext.Database.Schema.ContextNodeNgrams
Gargantext.Database.Schema.ContextNodeNgrams2 Gargantext.Database.Schema.ContextNodeNgrams2
...@@ -929,7 +929,7 @@ test-suite garg-test ...@@ -929,7 +929,7 @@ test-suite garg-test
NoImplicitPrelude NoImplicitPrelude
OverloadedStrings OverloadedStrings
RankNTypes RankNTypes
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N1
build-depends: build-depends:
QuickCheck ^>= 2.14.2 QuickCheck ^>= 2.14.2
, aeson ^>= 1.5.6.0 , aeson ^>= 1.5.6.0
......
...@@ -28,6 +28,7 @@ import Gargantext.Core.Mail ...@@ -28,6 +28,7 @@ import Gargantext.Core.Mail
import Gargantext.Core.Mail.Types (HasMail, mailSettings) import Gargantext.Core.Mail.Types (HasMail, mailSettings)
import Gargantext.Core.Types.Individu import Gargantext.Core.Types.Individu
import Gargantext.Database.Action.Flow (getOrMkRoot) import Gargantext.Database.Action.Flow (getOrMkRoot)
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude import Gargantext.Database.Prelude
import Gargantext.Database.Query.Table.Node.Error (HasNodeError(..), nodeError, NodeError(..)) import Gargantext.Database.Query.Table.Node.Error (HasNodeError(..), nodeError, NodeError(..))
import Gargantext.Database.Query.Table.User import Gargantext.Database.Query.Table.User
...@@ -41,13 +42,13 @@ import qualified Data.Text as Text ...@@ -41,13 +42,13 @@ import qualified Data.Text as Text
-- be valid (i.e. a valid username needs to be inferred via 'guessUsername'). -- be valid (i.e. a valid username needs to be inferred via 'guessUsername').
newUser :: (CmdM env err m, MonadRandom m, HasNodeError err, HasMail env) newUser :: (CmdM env err m, MonadRandom m, HasNodeError err, HasMail env)
=> EmailAddress => EmailAddress
-> m Int64 -> m UserId
newUser emailAddress = do newUser emailAddress = do
cfg <- view mailSettings cfg <- view mailSettings
pwd <- gargPass pwd <- gargPass
let nur = mkNewUser emailAddress (GargPassword pwd) let nur = mkNewUser emailAddress (GargPassword pwd)
affectedRows <- new_user nur new_user_id <- new_user nur
withNotification (SendEmail True) cfg Invitation $ pure (affectedRows, nur) withNotification (SendEmail True) cfg Invitation $ pure (new_user_id, nur)
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- | A DB-specific action to create a single user. -- | A DB-specific action to create a single user.
...@@ -56,8 +57,12 @@ newUser emailAddress = do ...@@ -56,8 +57,12 @@ newUser emailAddress = do
-- use 'newUser' instead for standard Gargantext code. -- use 'newUser' instead for standard Gargantext code.
new_user :: HasNodeError err new_user :: HasNodeError err
=> NewUser GargPassword => NewUser GargPassword
-> DBCmd err Int64 -> DBCmd err UserId
new_user = new_users . (:[]) new_user rq = do
ur <- new_users [rq]
case head ur of
Nothing -> nodeError MkNode
Just uid -> pure uid
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- | A DB-specific action to bulk-create users. -- | A DB-specific action to bulk-create users.
...@@ -67,17 +72,16 @@ new_user = new_users . (:[]) ...@@ -67,17 +72,16 @@ new_user = new_users . (:[])
new_users :: HasNodeError err new_users :: HasNodeError err
=> [NewUser GargPassword] => [NewUser GargPassword]
-- ^ A list of users to create. -- ^ A list of users to create.
-> DBCmd err Int64 -> DBCmd err [UserId]
new_users us = do new_users us = do
us' <- liftBase $ mapM toUserHash us us' <- liftBase $ mapM toUserHash us
r <- insertUsers $ map toUserWrite us' void $ insertUsers $ map toUserWrite us'
_ <- mapM getOrMkRoot $ map (\u -> UserName (_nu_username u)) us mapM (fmap fst . getOrMkRoot) $ map (\u -> UserName (_nu_username u)) us
pure r
------------------------------------------------------------------------ ------------------------------------------------------------------------
newUsers :: (CmdM env err m, MonadRandom m, HasNodeError err, HasMail env) newUsers :: (CmdM env err m, MonadRandom m, HasNodeError err, HasMail env)
=> [EmailAddress] => [EmailAddress]
-> m Int64 -> m [UserId]
newUsers us = do newUsers us = do
config <- view $ mailSettings config <- view $ mailSettings
us' <- mapM (\ea -> mkNewUser ea . GargPassword <$> gargPass) us us' <- mapM (\ea -> mkNewUser ea . GargPassword <$> gargPass) us
...@@ -102,14 +106,14 @@ guessUserName n = case splitOn "@" n of ...@@ -102,14 +106,14 @@ guessUserName n = case splitOn "@" n of
------------------------------------------------------------------------ ------------------------------------------------------------------------
newUsers' :: HasNodeError err newUsers' :: HasNodeError err
=> MailConfig -> [NewUser GargPassword] -> Cmd err Int64 => MailConfig -> [NewUser GargPassword] -> Cmd err [UserId]
newUsers' cfg us = do newUsers' cfg us = do
us' <- liftBase $ mapM toUserHash us us' <- liftBase $ mapM toUserHash us
r <- insertUsers $ map toUserWrite us' void $ insertUsers $ map toUserWrite us'
_ <- mapM getOrMkRoot $ map (\u -> UserName (_nu_username u)) us urs <- mapM (fmap fst . getOrMkRoot) $ map (\u -> UserName (_nu_username u)) us
_ <- mapM (\u -> mail cfg (Invitation u)) us _ <- mapM (\u -> mail cfg (Invitation u)) us
-- printDebug "newUsers'" us -- printDebug "newUsers'" us
pure r pure urs
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- | Updates a user's password, notifying the user via email, if necessary. -- | Updates a user's password, notifying the user via email, if necessary.
......
...@@ -195,7 +195,7 @@ getDocumentsWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeDocument ...@@ -195,7 +195,7 @@ getDocumentsWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeDocument
getListsModelWithParentId :: HasDBid NodeType => NodeId -> Cmd err [Node HyperdataModel] getListsModelWithParentId :: HasDBid NodeType => NodeId -> Cmd err [Node HyperdataModel]
getListsModelWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeModel) getListsModelWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeModel)
getCorporaWithParentId :: HasDBid NodeType => NodeId -> Cmd err [Node HyperdataCorpus] getCorporaWithParentId :: HasDBid NodeType => NodeId -> DBCmd err [Node HyperdataCorpus]
getCorporaWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeCorpus) getCorporaWithParentId n = runOpaQuery $ selectNodesWith' n (Just NodeCorpus)
------------------------------------------------------------------------ ------------------------------------------------------------------------
...@@ -271,7 +271,7 @@ getNodeWith nId _ = do ...@@ -271,7 +271,7 @@ getNodeWith nId _ = do
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- | Sugar to insert Node with NodeType in Database -- | Sugar to insert Node with NodeType in Database
insertDefaultNode :: HasDBid NodeType insertDefaultNode :: HasDBid NodeType
=> NodeType -> ParentId -> UserId -> Cmd err [NodeId] => NodeType -> ParentId -> UserId -> DBCmd err [NodeId]
insertDefaultNode nt p u = insertNode nt Nothing Nothing p u insertDefaultNode nt p u = insertNode nt Nothing Nothing p u
insertDefaultNodeIfNotExists :: HasDBid NodeType insertDefaultNodeIfNotExists :: HasDBid NodeType
...@@ -382,7 +382,7 @@ data CorpusType = CorpusDocument | CorpusContact ...@@ -382,7 +382,7 @@ data CorpusType = CorpusDocument | CorpusContact
class MkCorpus a class MkCorpus a
where where
mk :: HasDBid NodeType => Maybe Name -> Maybe a -> ParentId -> UserId -> Cmd err [NodeId] mk :: HasDBid NodeType => Maybe Name -> Maybe a -> ParentId -> UserId -> DBCmd err [NodeId]
instance MkCorpus HyperdataCorpus instance MkCorpus HyperdataCorpus
where where
......
...@@ -72,7 +72,7 @@ import Gargantext.Database.Admin.Config (nodeTypeId) ...@@ -72,7 +72,7 @@ import Gargantext.Database.Admin.Config (nodeTypeId)
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- TODO: on conflict, nice message -- TODO: on conflict, nice message
insertUsers :: [UserWrite] -> DBCmd err Int64 insertUsers :: [UserWrite] -> DBCmd err Int64
insertUsers us = mkCmd $ \c -> runInsert_ c insert insertUsers us = mkCmd $ \c -> runInsert c insert
where where
insert = Insert userTable us rCount Nothing insert = Insert userTable us rCount Nothing
......
...@@ -23,7 +23,7 @@ import Gargantext.Database.Action.User (getUserId, getUsername) ...@@ -23,7 +23,7 @@ import Gargantext.Database.Action.User (getUserId, getUsername)
import Gargantext.Database.Admin.Config import Gargantext.Database.Admin.Config
import Gargantext.Database.Admin.Types.Hyperdata (HyperdataUser) import Gargantext.Database.Admin.Types.Hyperdata (HyperdataUser)
import Gargantext.Database.Admin.Types.Node import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude (Cmd, runOpaQuery, DBCmd) import Gargantext.Database.Prelude (runOpaQuery, DBCmd)
import Gargantext.Database.Query.Table.Node import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Table.Node.Error import Gargantext.Database.Query.Table.Node.Error
import Gargantext.Database.Query.Table.User (queryUserTable, UserPoly(..)) import Gargantext.Database.Query.Table.User (queryUserTable, UserPoly(..))
...@@ -34,7 +34,7 @@ import Opaleye (restrict, (.==), Select) ...@@ -34,7 +34,7 @@ import Opaleye (restrict, (.==), Select)
import Opaleye.SqlTypes (sqlStrictText, sqlInt4) import Opaleye.SqlTypes (sqlStrictText, sqlInt4)
getRootId :: (HasNodeError err) => User -> Cmd err NodeId getRootId :: (HasNodeError err) => User -> DBCmd err NodeId
getRootId u = do getRootId u = do
maybeRoot <- head <$> getRoot u maybeRoot <- head <$> getRoot u
case maybeRoot of case maybeRoot of
...@@ -66,7 +66,7 @@ getOrMk_RootWithCorpus :: (HasNodeError err, MkCorpus a) ...@@ -66,7 +66,7 @@ getOrMk_RootWithCorpus :: (HasNodeError err, MkCorpus a)
=> User => User
-> Either CorpusName [CorpusId] -> Either CorpusName [CorpusId]
-> Maybe a -> Maybe a
-> Cmd err (UserId, RootId, CorpusId) -> DBCmd err (UserId, RootId, CorpusId)
getOrMk_RootWithCorpus user cName c = do getOrMk_RootWithCorpus user cName c = do
(userId, rootId) <- getOrMkRoot user (userId, rootId) <- getOrMkRoot user
corpusId'' <- if user == UserName userMaster corpusId'' <- if user == UserName userMaster
......
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-orphans #-}
...@@ -14,11 +15,9 @@ import Data.IORef ...@@ -14,11 +15,9 @@ import Data.IORef
import Data.Pool hiding (withResource) import Data.Pool hiding (withResource)
import Data.String import Data.String
import Gargantext.Core.Types.Individu import Gargantext.Core.Types.Individu
import Gargantext.Database.Action.User hiding (Username)
import Gargantext.Database.Action.User.New import Gargantext.Database.Action.User.New
import Gargantext.Database.Prelude import Gargantext.Database.Prelude
import Gargantext.Database.Query.Table.Node.Error import Gargantext.Database.Query.Table.Node.Error
import Gargantext.Database.Schema.User
import Gargantext.Prelude import Gargantext.Prelude
import Gargantext.Prelude.Config import Gargantext.Prelude.Config
import Prelude import Prelude
...@@ -29,7 +28,6 @@ import Test.Tasty.HUnit hiding (assert) ...@@ -29,7 +28,6 @@ import Test.Tasty.HUnit hiding (assert)
import Test.Tasty.Hspec import Test.Tasty.Hspec
import Test.Tasty.QuickCheck import Test.Tasty.QuickCheck
import qualified Data.Pool as Pool import qualified Data.Pool as Pool
import qualified Data.Set as S
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding as TE
import qualified Database.PostgreSQL.Simple as PG import qualified Database.PostgreSQL.Simple as PG
...@@ -38,13 +36,16 @@ import qualified Database.Postgres.Temp as Tmp ...@@ -38,13 +36,16 @@ import qualified Database.Postgres.Temp as Tmp
import qualified Shelly as SH import qualified Shelly as SH
import Paths_gargantext import Paths_gargantext
import Database.PostgreSQL.Simple
import Gargantext.Database.Action.User
-- | Keeps a log of usernames we have already generated, so that our -- | Keeps a log of usernames we have already generated, so that our
-- roundtrip tests won't fail. -- roundtrip tests won't fail.
uniqueArbitraryNewUser :: S.Set Username -> Gen (NewUser GargPassword) uniqueArbitraryNewUser :: Int -> Gen (NewUser GargPassword)
uniqueArbitraryNewUser alreadyTakenNames = do uniqueArbitraryNewUser currentIx = do
ur <- ascii_txt `suchThat` (not . flip S.member alreadyTakenNames) ur <- (`mappend` (T.pack (show currentIx) <> "-")) <$> ascii_txt
NewUser <$> pure ur <*> ascii_txt <*> elements arbitraryPassword let email = ur <> "@foo.com"
NewUser <$> pure ur <*> pure email <*> elements arbitraryPassword
where where
ascii_txt :: Gen T.Text ascii_txt :: Gen T.Text
ascii_txt = fmap (T.pack . getPrintableString) arbitrary ascii_txt = fmap (T.pack . getPrintableString) arbitrary
...@@ -53,13 +54,24 @@ uniqueArbitraryNewUser alreadyTakenNames = do ...@@ -53,13 +54,24 @@ uniqueArbitraryNewUser alreadyTakenNames = do
dbUser, dbPassword, dbName :: String dbUser, dbPassword, dbName :: String
dbUser = "gargantua" dbUser = "gargantua"
dbPassword = "gargantua_test" dbPassword = "gargantua_test"
dbName = "gargandbV5" dbName = "gargandb_test"
newtype Counter = Counter { _Counter :: IORef Int }
deriving Eq
instance Show Counter where
show (Counter _) = "Counter"
emptyCounter :: IO Counter
emptyCounter = Counter <$> newIORef 0
nextCounter :: Counter -> IO Int
nextCounter (Counter ref) = atomicModifyIORef' ref (\old -> (succ old, old))
data TestEnv = TestEnv { data TestEnv = TestEnv {
test_db :: !DBHandle test_db :: !DBHandle
, test_config :: !GargConfig , test_config :: !GargConfig
, test_usernameGen :: !(IORef (S.Set Username)) , test_usernameGen :: !Counter
} }
newtype TestMonad a = TestMonad { runTestMonad :: ReaderT TestEnv IO a } newtype TestMonad a = TestMonad { runTestMonad :: ReaderT TestEnv IO a }
...@@ -126,7 +138,7 @@ setup = do ...@@ -126,7 +138,7 @@ setup = do
pool <- createPool (PG.connectPostgreSQL (Tmp.toConnectionString db)) pool <- createPool (PG.connectPostgreSQL (Tmp.toConnectionString db))
(PG.close) 2 60 2 (PG.close) 2 60 2
bootstrapDB db pool gargConfig bootstrapDB db pool gargConfig
ugen <- newIORef mempty ugen <- emptyCounter
pure $ TestEnv (DBHandle pool db) gargConfig ugen pure $ TestEnv (DBHandle pool db) gargConfig ugen
tests :: TestTree tests :: TestTree
...@@ -136,18 +148,59 @@ tests = withResource setup teardown $ ...@@ -136,18 +148,59 @@ tests = withResource setup teardown $
unitTests :: IO TestEnv -> TestTree unitTests :: IO TestEnv -> TestTree
unitTests getEnv = testGroup "Read/Writes" unitTests getEnv = testGroup "Read/Writes"
[ testGroup "User creation" [ [ testGroup "User creation" [
testCase "Simple write" (write01 getEnv) testCase "Simple write/read" (writeRead01 getEnv)
, testCase "Simple duplicate" (mkUserDup getEnv)
, testProperty "Read/Write roundtrip" $ prop_userCreationRoundtrip getEnv , testProperty "Read/Write roundtrip" $ prop_userCreationRoundtrip getEnv
] ]
] ]
write01 :: IO TestEnv -> Assertion data ExpectedActual a =
write01 getEnv = do Expected a
| Actual a
deriving Show
instance Eq a => Eq (ExpectedActual a) where
(Expected a) == (Actual b) = a == b
(Actual a) == (Expected b) = a == b
_ == _ = False
writeRead01 :: IO TestEnv -> Assertion
writeRead01 getEnv = do
env <- getEnv env <- getEnv
flip runReaderT env $ runTestMonad $ do flip runReaderT env $ runTestMonad $ do
let nur = mkNewUser "alfredo@well-typed.com" (GargPassword "my_secret") let nur1 = mkNewUser "alfredo@well-typed.com" (GargPassword "my_secret")
x <- new_user nur let nur2 = mkNewUser "paul@acme.com" (GargPassword "my_secret")
liftBase $ x `shouldBe` 1
uid1 <- new_user nur1
uid2 <- new_user nur2
liftBase $ uid1 `shouldBe` 1
liftBase $ uid2 `shouldBe` 2
-- Getting the users by username returns the expected IDs
uid1' <- getUserId (UserName "alfredo")
uid2' <- getUserId (UserName "paul")
liftBase $ uid1' `shouldBe` 1
liftBase $ uid2' `shouldBe` 2
mkUserDup :: IO TestEnv -> Assertion
mkUserDup getEnv = do
env <- getEnv
let x = flip runReaderT env $ runTestMonad $ do
-- This should fail, because user 'alfredo' exists already.
let nur = mkNewUser "alfredo@well-typed.com" (GargPassword "my_secret")
new_user nur
--
-- SqlError {sqlState = "23505", sqlExecStatus = FatalError
-- , sqlErrorMsg = "duplicate key value violates unique constraint \"auth_user_username_idx1\""
-- , sqlErrorDetail = "Key (username)=(alfredo) already exists.", sqlErrorHint = ""
-- }
--
-- Postgres increments the underlying SERIAL for the user even if the request fails, see
-- https://stackoverflow.com/questions/37204749/serial-in-postgres-is-being-increased-even-though-i-added-on-conflict-do-nothing
-- This means that the next available ID is '3'.
x `shouldThrow` (\SqlError{..} -> sqlErrorDetail == "Key (username)=(alfredo) already exists.")
runEnv :: TestEnv -> TestMonad a -> PropertyM IO a runEnv :: TestEnv -> TestMonad a -> PropertyM IO a
runEnv env act = run (flip runReaderT env $ runTestMonad act) runEnv env act = run (flip runReaderT env $ runTestMonad act)
...@@ -155,10 +208,8 @@ runEnv env act = run (flip runReaderT env $ runTestMonad act) ...@@ -155,10 +208,8 @@ runEnv env act = run (flip runReaderT env $ runTestMonad act)
prop_userCreationRoundtrip :: IO TestEnv -> Property prop_userCreationRoundtrip :: IO TestEnv -> Property
prop_userCreationRoundtrip getEnv = monadicIO $ do prop_userCreationRoundtrip getEnv = monadicIO $ do
env <- run getEnv env <- run getEnv
alreadyTakenUsernames <- run (readIORef $ test_usernameGen env) nextAvailableCounter <- run (nextCounter $ test_usernameGen env)
nur <- pick (uniqueArbitraryNewUser alreadyTakenUsernames) nur <- pick (uniqueArbitraryNewUser nextAvailableCounter)
void $ runEnv env (new_user nur) uid <- runEnv env (new_user nur)
ur' <- runEnv env (getUserLightDB (UserName $ _nu_username nur)) ur' <- runEnv env (getUserId (UserName $ _nu_username nur))
assert (userLight_username ur' == _nu_username nur) run (Expected uid `shouldBe` Actual ur')
assert (userLight_email ur' == _nu_email nur)
run (writeIORef (test_usernameGen env) $ S.insert (_nu_username nur) alreadyTakenUsernames)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment