Commit b6d34e83 authored by Alfredo Di Napoli's avatar Alfredo Di Napoli

Setup tests for UpdateList via the JSON API

parent 113fffba
...@@ -28,6 +28,7 @@ data-files: ...@@ -28,6 +28,7 @@ data-files:
ekg-assets/chart_line_add.png ekg-assets/chart_line_add.png
ekg-assets/cross.png ekg-assets/cross.png
test-data/ngrams/GarganText_NgramsTerms-QuantumComputing.json test-data/ngrams/GarganText_NgramsTerms-QuantumComputing.json
test-data/ngrams/simple.json
test-data/phylo/bpa_phylo_test.json test-data/phylo/bpa_phylo_test.json
test-data/phylo/open_science.json test-data/phylo/open_science.json
test-data/test_config.ini test-data/test_config.ini
...@@ -58,6 +59,7 @@ library ...@@ -58,6 +59,7 @@ library
Gargantext.API.Errors.Types.Backend Gargantext.API.Errors.Types.Backend
Gargantext.API.HashedResponse Gargantext.API.HashedResponse
Gargantext.API.Ngrams Gargantext.API.Ngrams
Gargantext.API.Ngrams.List.Types
Gargantext.API.Ngrams.Prelude Gargantext.API.Ngrams.Prelude
Gargantext.API.Ngrams.Tools Gargantext.API.Ngrams.Tools
Gargantext.API.Ngrams.Types Gargantext.API.Ngrams.Types
...@@ -186,7 +188,6 @@ library ...@@ -186,7 +188,6 @@ library
Gargantext.API.Members Gargantext.API.Members
Gargantext.API.Metrics Gargantext.API.Metrics
Gargantext.API.Ngrams.List Gargantext.API.Ngrams.List
Gargantext.API.Ngrams.List.Types
Gargantext.API.Ngrams.NgramsTree Gargantext.API.Ngrams.NgramsTree
Gargantext.API.Node.Contact Gargantext.API.Node.Contact
Gargantext.API.Node.Corpus.Annuaire Gargantext.API.Node.Corpus.Annuaire
...@@ -939,10 +940,12 @@ test-suite garg-test-tasty ...@@ -939,10 +940,12 @@ test-suite garg-test-tasty
build-depends: build-depends:
QuickCheck ^>= 2.14.2 QuickCheck ^>= 2.14.2
, aeson ^>= 1.5.6.0 , aeson ^>= 1.5.6.0
, aeson-qq
, async ^>= 2.2.4 , async ^>= 2.2.4
, base ^>= 4.14.3.0 , base ^>= 4.14.3.0
, boolexpr ^>= 0.2 , boolexpr ^>= 0.2
, bytestring ^>= 0.10.12.0 , bytestring ^>= 0.10.12.0
, case-insensitive
, conduit ^>= 1.3.4.2 , conduit ^>= 1.3.4.2
, containers ^>= 0.6.5.1 , containers ^>= 0.6.5.1
, crawlerArxiv , crawlerArxiv
...@@ -957,6 +960,7 @@ test-suite garg-test-tasty ...@@ -957,6 +960,7 @@ test-suite garg-test-tasty
, hspec-expectations >= 0.8 && < 0.9 , hspec-expectations >= 0.8 && < 0.9
, hspec-wai , hspec-wai
, hspec-wai-json , hspec-wai-json
, http-api-data
, http-client ^>= 0.6.4.1 , http-client ^>= 0.6.4.1
, http-client-tls ^>= 0.3.5.3 , http-client-tls ^>= 0.3.5.3
, http-types , http-types
...@@ -975,6 +979,7 @@ test-suite garg-test-tasty ...@@ -975,6 +979,7 @@ test-suite garg-test-tasty
, recover-rtti >= 0.4 && < 0.5 , recover-rtti >= 0.4 && < 0.5
, resource-pool >= 0.2.3.2 && < 0.2.4 , resource-pool >= 0.2.3.2 && < 0.2.4
, servant-auth , servant-auth
, servant-auth
, servant-auth-client , servant-auth-client
, servant-client , servant-client
, servant-job , servant-job
...@@ -1007,6 +1012,7 @@ test-suite garg-test-hspec ...@@ -1007,6 +1012,7 @@ test-suite garg-test-hspec
Test.API.GraphQL Test.API.GraphQL
Test.API.Private Test.API.Private
Test.API.Setup Test.API.Setup
Test.API.UpdateList
Test.Database.Operations Test.Database.Operations
Test.Database.Operations.DocumentSearch Test.Database.Operations.DocumentSearch
Test.Database.Operations.NodeStory Test.Database.Operations.NodeStory
...@@ -1045,6 +1051,7 @@ test-suite garg-test-hspec ...@@ -1045,6 +1051,7 @@ test-suite garg-test-hspec
build-depends: build-depends:
QuickCheck ^>= 2.14.2 QuickCheck ^>= 2.14.2
, aeson ^>= 1.5.6.0 , aeson ^>= 1.5.6.0
, aeson-qq
, async ^>= 2.2.4 , async ^>= 2.2.4
, base ^>= 4.14.3.0 , base ^>= 4.14.3.0
, boolexpr ^>= 0.2 , boolexpr ^>= 0.2
...@@ -1064,6 +1071,7 @@ test-suite garg-test-hspec ...@@ -1064,6 +1071,7 @@ test-suite garg-test-hspec
, hspec-expectations >= 0.8 && < 0.9 , hspec-expectations >= 0.8 && < 0.9
, hspec-wai , hspec-wai
, hspec-wai-json , hspec-wai-json
, http-api-data
, http-types , http-types
, http-client ^>= 0.6.4.1 , http-client ^>= 0.6.4.1
, http-client-tls ^>= 0.3.5.3 , http-client-tls ^>= 0.3.5.3
......
...@@ -161,7 +161,7 @@ postAsync' l (WithJsonFile m _) jobHandle = do ...@@ -161,7 +161,7 @@ postAsync' l (WithJsonFile m _) jobHandle = do
markProgress 1 jobHandle markProgress 1 jobHandle
corpus_node <- getNode l -- (Proxy :: Proxy HyperdataList) corpus_node <- getNode l -- (Proxy :: Proxy HyperdataList)
let corpus_id = fromMaybe (panic "") (_node_parent_id corpus_node) let corpus_id = fromMaybe (panic "no parent_id") (_node_parent_id corpus_node)
_ <- reIndexWith corpus_id l NgramsTerms (Set.fromList [MapTerm, CandidateTerm]) _ <- reIndexWith corpus_id l NgramsTerms (Set.fromList [MapTerm, CandidateTerm])
markComplete jobHandle markComplete jobHandle
......
{ "Authors": { "version":30 ,"data":{ "Alain Connes":{"size":2,"list":"MapTerm","children":[]} } }
, "Institutes": { "version":30 ,"data":{ "College de France":{"size":3,"list":"MapTerm","children":[]} } }
, "Sources": { "version":30 ,"data":{ "Annales Henri Poincare 3 (2002) 411-433":{"size":6,"list":"MapTerm","children":[]} } }
, "NgramsTerms":{ "version":30 ,"data":{ "abelian group":{"size":2,"list":"MapTerm","children":[]} } }
}
...@@ -4,9 +4,10 @@ module Test.API where ...@@ -4,9 +4,10 @@ module Test.API where
import Prelude import Prelude
import Test.Hspec import Test.Hspec
import qualified Test.API.Authentication as Auth import qualified Test.API.Authentication as Auth
import qualified Test.API.Private as Private
import qualified Test.API.GraphQL as GraphQL
import qualified Test.API.Errors as Errors import qualified Test.API.Errors as Errors
import qualified Test.API.GraphQL as GraphQL
import qualified Test.API.Private as Private
import qualified Test.API.UpdateList as UpdateList
tests :: Spec tests :: Spec
tests = describe "API" $ do tests = describe "API" $ do
...@@ -14,3 +15,4 @@ tests = describe "API" $ do ...@@ -14,3 +15,4 @@ tests = describe "API" $ do
Private.tests Private.tests
GraphQL.tests GraphQL.tests
Errors.tests Errors.tests
UpdateList.tests
...@@ -10,10 +10,13 @@ module Test.API.Private ( ...@@ -10,10 +10,13 @@ module Test.API.Private (
, withValidLogin , withValidLogin
, getJSON , getJSON
, protected , protected
, protectedWith , protectedJSON
, postJSONUrlEncoded
, protectedNewError , protectedNewError
, protectedWith
) where ) where
import Data.Aeson qualified as JSON
import Data.ByteString.Lazy qualified as L import Data.ByteString.Lazy qualified as L
import Data.CaseInsensitive qualified as CI import Data.CaseInsensitive qualified as CI
import Data.Text.Encoding qualified as TE import Data.Text.Encoding qualified as TE
...@@ -24,7 +27,7 @@ import Gargantext.Prelude hiding (get) ...@@ -24,7 +27,7 @@ import Gargantext.Prelude hiding (get)
import Network.HTTP.Client hiding (Proxy) import Network.HTTP.Client hiding (Proxy)
import Network.HTTP.Types import Network.HTTP.Types
import Network.Wai.Handler.Warp qualified as Wai import Network.Wai.Handler.Warp qualified as Wai
import Network.Wai.Test (SResponse) import Network.Wai.Test (SResponse (..))
import Prelude qualified import Prelude qualified
import Servant import Servant
import Servant.Auth.Client () import Servant.Auth.Client ()
...@@ -36,19 +39,46 @@ import Test.Hspec ...@@ -36,19 +39,46 @@ import Test.Hspec
import Test.Hspec.Wai hiding (pendingWith) import Test.Hspec.Wai hiding (pendingWith)
import Test.Hspec.Wai.Internal (withApplication) import Test.Hspec.Wai.Internal (withApplication)
import Test.Utils (jsonFragment, shouldRespondWith') import Test.Utils (jsonFragment, shouldRespondWith')
import qualified Data.Map.Strict as Map
import qualified Data.ByteString.Lazy.Char8 as C8L
-- | Issue a request with a valid 'Authorization: Bearer' inside. -- | Issue a request with a valid 'Authorization: Bearer' inside.
protected :: Token -> Method -> ByteString -> L.ByteString -> WaiSession () SResponse protected :: Token -> Method -> ByteString -> L.ByteString -> WaiSession () SResponse
protected tkn mth url = protectedWith mempty tkn mth url protected tkn mth url = protectedWith mempty tkn mth url
protectedJSON :: forall a. (JSON.FromJSON a, Typeable a)
=> Token
-> Method
-> ByteString
-> JSON.Value
-> WaiSession () a
protectedJSON tkn mth url = protectedJSONWith mempty tkn mth url
protectedJSONWith :: forall a. (JSON.FromJSON a, Typeable a)
=> [Network.HTTP.Types.Header]
-> Token
-> Method
-> ByteString
-> JSON.Value
-> WaiSession () a
protectedJSONWith hdrs tkn mth url jsonV = do
SResponse{..} <- protectedWith hdrs tkn mth url (JSON.encode jsonV)
case JSON.eitherDecode simpleBody of
Left err -> Prelude.fail $ "protectedJSON failed when parsing " <> show (typeRep $ Proxy @a) <> ": " <> err
Right x -> pure x
protectedWith :: [Network.HTTP.Types.Header] protectedWith :: [Network.HTTP.Types.Header]
-> Token -> Token
-> Method -> ByteString -> L.ByteString -> WaiSession () SResponse -> Method -> ByteString -> L.ByteString -> WaiSession () SResponse
protectedWith extraHeaders tkn mth url payload = protectedWith extraHeaders tkn mth url payload =
request mth url ([ (hAccept, "application/json;charset=utf-8") -- Using a map means that if any of the extra headers contains a clashing header name,
, (hContentType, "application/json") -- the extra headers will take precedence.
, (hAuthorization, "Bearer " <> TE.encodeUtf8 tkn) let defaultHeaders = [ (hAccept, "application/json;charset=utf-8")
] <> extraHeaders) payload , (hContentType, "application/json")
, (hAuthorization, "Bearer " <> TE.encodeUtf8 tkn)
]
hdrs = Map.toList $ Map.fromList $ defaultHeaders <> extraHeaders
in request mth url hdrs payload
protectedNewError :: Token -> Method -> ByteString -> L.ByteString -> WaiSession () SResponse protectedNewError :: Token -> Method -> ByteString -> L.ByteString -> WaiSession () SResponse
protectedNewError tkn mth url = protectedWith newErrorFormat tkn mth url protectedNewError tkn mth url = protectedWith newErrorFormat tkn mth url
...@@ -59,6 +89,17 @@ getJSON :: ByteString -> WaiSession () SResponse ...@@ -59,6 +89,17 @@ getJSON :: ByteString -> WaiSession () SResponse
getJSON url = getJSON url =
request "GET" url [(hContentType, "application/json")] "" request "GET" url [(hContentType, "application/json")] ""
postJSONUrlEncoded :: forall a. (JSON.FromJSON a, Typeable a)
=> Token
-> ByteString
-> L.ByteString
-> WaiSession () a
postJSONUrlEncoded tkn url queryPaths = do
SResponse{..} <- protectedWith [(hContentType, "application/x-www-form-urlencoded")] tkn "POST" url queryPaths
case JSON.eitherDecode simpleBody of
Left err -> Prelude.fail $ "postJSONUrlEncoded failed when parsing " <> show (typeRep $ Proxy @a) <> ": " <> err <> "\nPayload was: " <> (C8L.unpack simpleBody)
Right x -> pure x
withValidLogin :: (MonadFail m, MonadIO m) => Wai.Port -> Username -> GargPassword -> (Token -> m a) -> m a withValidLogin :: (MonadFail m, MonadIO m) => Wai.Port -> Username -> GargPassword -> (Token -> m a) -> m a
withValidLogin port ur pwd act = do withValidLogin port ur pwd act = do
baseUrl <- liftIO $ parseBaseUrl "http://localhost" baseUrl <- liftIO $ parseBaseUrl "http://localhost"
......
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NumericUnderscores #-}
module Test.API.UpdateList (
tests
) where
import Data.Aeson qualified as JSON
import Data.Aeson.QQ
import Data.ByteString.Lazy qualified as BL
import Data.String (fromString)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.IO qualified as TIO
import Fmt
import Gargantext.API.Admin.Auth.Types (Token)
import Gargantext.API.Admin.Orchestrator.Types
import Gargantext.Core.Types.Individu
import Gargantext.Database.Action.User
import Gargantext.Database.Admin.Types.Hyperdata.Corpus
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Query.Table.Node
import Gargantext.Database.Query.Tree.Root
import Gargantext.Prelude hiding (get)
import Network.Wai.Handler.Warp qualified as Wai
import Paths_gargantext (getDataFileName)
import Prelude (error)
import Test.API.Private (withValidLogin, protectedJSON, postJSONUrlEncoded)
import Test.API.Setup (withTestDBAndPort, setupEnvironment, mkUrl, createAliceAndBob)
import Test.Database.Types
import Test.Hspec
import Test.Hspec.Wai.Internal (withApplication, WaiSession)
import Web.FormUrlEncoded
data JobPollHandle = JobPollHandle {
_jph_id :: !Text
, _jph_log :: [JobLog]
, _jph_status :: !Text
, _jph_error :: !(Maybe Text)
}
instance JSON.FromJSON JobPollHandle where
parseJSON = JSON.withObject "JobPollHandle" $ \o -> do
_jph_id <- o JSON..: "id"
_jph_log <- o JSON..: "log"
_jph_status <- o JSON..: "status"
_jph_error <- o JSON..:? "error"
pure JobPollHandle{..}
instance JSON.ToJSON JobPollHandle where
toJSON JobPollHandle{..} = JSON.object [
"id" JSON..= JSON.toJSON _jph_id
, "log" JSON..= JSON.toJSON _jph_log
, "status" JSON..= JSON.toJSON _jph_status
, "error" JSON..= JSON.toJSON _jph_error
]
newCorpusForUser :: TestEnv -> T.Text -> IO NodeId
newCorpusForUser env uname = flip runReaderT env $ runTestMonad $ do
uid <- getUserId (UserName uname)
parentId <- getRootId (UserName uname)
let corpusName = "Test_Corpus"
(corpusId:_) <- mk (Just corpusName) (Nothing :: Maybe HyperdataCorpus) parentId uid
pure corpusId
-- | Poll the given URL every second until it finishes.
-- Retries up to 60 times (i.e. for 1 minute, before giving up)
pollUntilFinished :: Token -> Wai.Port -> (JobPollHandle -> Builder) -> JobPollHandle -> WaiSession () JobPollHandle
pollUntilFinished tkn port mkUrlPiece = go 60
where
go :: Int -> JobPollHandle -> WaiSession () JobPollHandle
go 0 h = error $ T.unpack $ "pollUntilFinished exhausted attempts. Last found JobPollHandle: " <> T.decodeUtf8 (BL.toStrict $ JSON.encode h)
go n h = case _jph_status h == "IsPending" || _jph_status h == "IsRunning" of
True -> do
liftIO $ threadDelay 1_000_000
h' <- protectedJSON tkn "GET" (mkUrl port $ mkUrlPiece h) ""
go (n-1) h'
False -> pure h
tests :: Spec
tests = sequential $ aroundAll withTestDBAndPort $ do
describe "UpdateList API" $ do
it "setup DB triggers and users" $ \((testEnv, _), _) -> do
setupEnvironment testEnv
createAliceAndBob testEnv
describe "POST /api/v1.0/lists/:id/add/form/async (JSON)" $ do
it "allows uploading a JSON ngrams file" $ \((testEnv, port), app) -> do
cId <- newCorpusForUser testEnv "alice"
withApplication app $ do
withValidLogin port "alice" (GargPassword "alice") $ \token -> do
([listId] :: [NodeId]) <- protectedJSON token "POST" (mkUrl port ("/node/" <> (fromString $ show $ _NodeId cId))) [aesonQQ|{"pn_typename":"NodeList","pn_name":"Testing"}|]
-- Upload the JSON doc
simpleNgrams <- liftIO (TIO.readFile =<< getDataFileName "test-data/ngrams/simple.json")
let jsonFileFormData = [ (T.pack "_wjf_data", simpleNgrams)
, ("_wjf_filetype", "JSON")
, ("_wjf_name", "simple_ngrams.json")
]
let url = "/lists/" <> (fromString $ show $ _NodeId listId) <> "/add/form/async"
let mkPollUrl j = "/corpus/" <> (fromString $ show $ _NodeId listId) <> "/add/form/async/" +|_jph_id j|+ "/poll?limit=1"
(j :: JobPollHandle) <- postJSONUrlEncoded token (mkUrl port url) (urlEncodeFormStable $ toForm jsonFileFormData)
j' <- pollUntilFinished token port mkPollUrl j
liftIO (_jph_status j' `shouldBe` "IsFinished")
...@@ -10,6 +10,7 @@ import Data.Aeson ...@@ -10,6 +10,7 @@ import Data.Aeson
import Data.Aeson.QQ.Simple (aesonQQ) import Data.Aeson.QQ.Simple (aesonQQ)
import Data.Char (isSpace) import Data.Char (isSpace)
import Language.Haskell.TH.Quote import Language.Haskell.TH.Quote
import Network.HTTP.Types
import Network.Wai.Test import Network.Wai.Test
import Prelude import Prelude
import Test.Hspec.Expectations import Test.Hspec.Expectations
...@@ -17,6 +18,7 @@ import Test.Hspec.Wai ...@@ -17,6 +18,7 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Test.Hspec.Wai.Matcher import Test.Hspec.Wai.Matcher
import Test.Tasty.HUnit import Test.Tasty.HUnit
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
...@@ -65,6 +67,14 @@ instance FromValue JsonFragmentResponseMatcher where ...@@ -65,6 +67,14 @@ instance FromValue JsonFragmentResponseMatcher where
breakAt c = fmap (B.drop 1) . B.break (== c) breakAt c = fmap (B.drop 1) . B.break (== c)
strip = B.reverse . B.dropWhile isSpace . B.reverse . B.dropWhile isSpace strip = B.reverse . B.dropWhile isSpace . B.reverse . B.dropWhile isSpace
shouldRespondWithJSON :: (FromJSON a, ToJSON a, HasCallStack)
=> WaiSession st a
-> JsonFragmentResponseMatcher
-> WaiExpectation st
shouldRespondWithJSON action matcher = do
r <- action
forM_ (match (SResponse status200 mempty (JSON.encode r)) (getJsonMatcher matcher)) (liftIO . expectationFailure)
containsJSON :: Value -> MatchBody containsJSON :: Value -> MatchBody
containsJSON expected = MatchBody matcher containsJSON expected = MatchBody matcher
where where
......
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