diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 085d3e72c362f..968091e4555be 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -250,5 +250,7 @@ test-suite graphql-engine-test , yaml , http-client , http-client-tls + , unordered-containers >= 0.2 + , case-insensitive other-modules: Spec \ No newline at end of file diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index 365331075f2d0..0640257979338 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -224,7 +224,7 @@ dmlTxErrorHandler :: Q.PGTxErr -> QErr dmlTxErrorHandler p2Res = case err of Nothing -> defaultTxErrorHandler p2Res - Just msg -> err400 PostgresError msg + Just (code, msg) -> err400 code msg where err = simplifyError p2Res -- | col_name as col_name @@ -236,7 +236,7 @@ mkColExtrAl :: (IsIden a) => Maybe a -> (PGCol, PGColType) -> S.Extractor mkColExtrAl alM (c, pct) = if pct == PGGeometry || pct == PGGeography then S.mkAliasedExtrFromExp - ((S.SEFnApp "ST_AsGeoJSON" [S.mkSIdenExp c] Nothing) `S.SETyAnn` S.jsonType) alM + (S.SEFnApp "ST_AsGeoJSON" [S.mkSIdenExp c] Nothing `S.SETyAnn` S.jsonType) alM else S.mkAliasedExtr c alM -- validate headers @@ -247,7 +247,7 @@ validateHeaders depHeaders = do unless (hdr `elem` map T.toLower headers) $ throw400 NotFound $ hdr <<> " header is expected but not found" -simplifyError :: Q.PGTxErr -> Maybe T.Text +simplifyError :: Q.PGTxErr -> Maybe (Code, T.Text) simplifyError txErr = do stmtErr <- Q.getPGStmtErr txErr codeMsg <- getPGCodeMsg stmtErr @@ -257,32 +257,31 @@ simplifyError txErr = do (,) <$> Q.edStatusCode pged <*> Q.edMessage pged extractError = \case -- restrict violation - ("23501", msg) -> - return $ "Can not delete or update due to data being referred. " <> msg + ("23001", msg) -> + return (ConstraintViolation, "Can not delete or update due to data being referred. " <> msg) -- not null violation ("23502", msg) -> - return $ "Not-NULL violation. " <> msg + return (ConstraintViolation, "Not-NULL violation. " <> msg) -- foreign key violation ("23503", msg) -> - return $ "Foreign key violation. " <> msg + return (ConstraintViolation, "Foreign key violation. " <> msg) -- unique violation ("23505", msg) -> - return $ "Uniqueness violation. " <> msg + return (ConstraintViolation, "Uniqueness violation. " <> msg) -- check violation ("23514", msg) -> - return $ "Check constraint violation. " <> msg + return (PermissionError, "Check constraint violation. " <> msg) -- invalid text representation - ("22P02", msg) -> return msg + ("22P02", msg) -> return (DataException, msg) + -- invalid parameter value + ("22023", msg) -> return (DataException, msg) -- no unique constraint on the columns ("42P10", _) -> - return "there is no unique or exclusion constraint on target column(s)" + return (ConstraintError, "there is no unique or exclusion constraint on target column(s)") -- no constraint - ("42704", msg) -> return msg - -- invalid parameter value - ("22023", msg) -> return msg + ("42704", msg) -> return (ConstraintError, msg) _ -> Nothing - -- validate limit and offset int values onlyPositiveInt :: MonadError QErr m => Int -> m () onlyPositiveInt i = when (i < 0) $ throw400 NotSupported diff --git a/server/src-lib/Hasura/RQL/Types/Error.hs b/server/src-lib/Hasura/RQL/Types/Error.hs index 33db692041d04..5b1375c21e528 100644 --- a/server/src-lib/Hasura/RQL/Types/Error.hs +++ b/server/src-lib/Hasura/RQL/Types/Error.hs @@ -67,35 +67,39 @@ data Code | AlreadyUntracked | InvalidParams | AlreadyInit + | ConstraintViolation + | DataException -- Graphql error | NoTables | ValidationFailed deriving (Eq) instance Show Code where - show NotNullViolation = "not-null-violation" - show PermissionDenied = "permission-denied" - show NotExists = "not-exists" - show AlreadyExists = "already-exists" - show AlreadyTracked = "already-tracked" - show AlreadyUntracked = "already-untracked" - show PostgresError = "postgres-error" - show NotSupported = "not-supported" - show DependencyError = "dependency-error" - show InvalidHeaders = "invalid-headers" - show InvalidJSON = "invalid-json" - show AccessDenied = "access-denied" - show ParseFailed = "parse-failed" - show ConstraintError = "constraint-error" - show PermissionError = "permission-error" - show NotFound = "not-found" - show Unexpected = "unexpected" - show UnexpectedPayload = "unexpected-payload" - show NoUpdate = "no-update" - show InvalidParams = "invalid-params" - show AlreadyInit = "already-initialised" - show NoTables = "no-tables" - show ValidationFailed = "validation-failed" + show NotNullViolation = "not-null-violation" + show DataException = "data-exception" + show ConstraintViolation = "constraint-violation" + show PermissionDenied = "permission-denied" + show NotExists = "not-exists" + show AlreadyExists = "already-exists" + show AlreadyTracked = "already-tracked" + show AlreadyUntracked = "already-untracked" + show PostgresError = "postgres-error" + show NotSupported = "not-supported" + show DependencyError = "dependency-error" + show InvalidHeaders = "invalid-headers" + show InvalidJSON = "invalid-json" + show AccessDenied = "access-denied" + show ParseFailed = "parse-failed" + show ConstraintError = "constraint-error" + show PermissionError = "permission-error" + show NotFound = "not-found" + show Unexpected = "unexpected" + show UnexpectedPayload = "unexpected-payload" + show NoUpdate = "no-update" + show InvalidParams = "invalid-params" + show AlreadyInit = "already-initialised" + show NoTables = "no-tables" + show ValidationFailed = "validation-failed" data QErr = QErr diff --git a/server/test/Spec.hs b/server/test/Spec.hs index f06c20aba2d18..c7b630f3f04f2 100644 --- a/server/test/Spec.hs +++ b/server/test/Spec.hs @@ -3,25 +3,31 @@ module Spec (mkSpecs) where -import Hasura.Prelude hiding (get) -import Network.Wai (Application) +import Hasura.Prelude hiding (get) +import Network.Wai (Application) import Test.Hspec import Test.Hspec.Wai +import Test.Hspec.Wai.Matcher -import qualified Data.Aeson as J -import qualified Data.Aeson.Casing as J -import qualified Data.Aeson.TH as J -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import qualified Data.Yaml as Y +import qualified Data.Aeson as J +import qualified Data.Aeson.Casing as J +import qualified Data.Aeson.TH as J +import qualified Data.CaseInsensitive as CI +import qualified Data.HashMap.Strict as HM +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Yaml as Y +type Headers = HM.HashMap T.Text T.Text data TestCase = TestCase { tcDescription :: !T.Text , tcQuery :: !J.Value , tcUrl :: !T.Text + , tcHeaders :: !(Maybe Headers) , tcStatus :: !Int + , tcResponse :: !(Maybe J.Value) -- , tcDependsOn :: !(Maybe TestCase) } deriving (Show) @@ -35,6 +41,7 @@ querySpecFiles = , "create_author_article_relationship.yaml" , "create_author_article_permissions.yaml" , "create_address_resident_relationship_error.yaml" + , "create_user_permission_address.yaml" ] gqlSpecFiles :: [FilePath] @@ -52,6 +59,9 @@ gqlSpecFiles = , "insert_mutation/person.yaml" , "insert_mutation/person_array.yaml" , "insert_mutation/order.yaml" + , "insert_mutation/address_check_constraint_error.yaml" + , "insert_mutation/address_not_null_constraint_error.yaml" + , "insert_mutation/author_unique_constraint_error.yaml" , "nested_select_query_article.yaml" , "select_query_article_limit_offset.yaml" , "select_query_article_limit_offset_error_01.yaml" @@ -67,6 +77,7 @@ gqlSpecFiles = , "update_mutation/person_error_01.yaml" , "delete_mutation/article.yaml" , "delete_mutation/article_returning.yaml" + , "delete_mutation/author_foreign_key_violation.yaml" ] readTestCase :: FilePath -> IO TestCase @@ -83,9 +94,17 @@ mkSpec tc = do let desc = tcDescription tc url = tcUrl tc q = tcQuery tc - respStatus = (fromIntegral $ tcStatus tc) :: ResponseMatcher + mHeaders = tcHeaders tc + statusCode = tcStatus tc + mRespBody = tcResponse tc + headers = maybe [] (map toHeader . HM.toList) mHeaders + body = maybe matchAny bodyEquals $ fmap J.encode mRespBody + resp = ResponseMatcher statusCode [] body it (T.unpack desc) $ - post (T.encodeUtf8 url) (J.encode q) `shouldRespondWith` respStatus + request "POST" (T.encodeUtf8 url) headers (J.encode q) `shouldRespondWith` resp + where + matchAny = MatchBody (\_ _ -> Nothing) + toHeader (k, v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v) mkSpecs :: IO (SpecWith Application) diff --git a/server/test/testcases/create_tables.yaml b/server/test/testcases/create_tables.yaml index e27168c85016d..a4a8d42c74371 100644 --- a/server/test/testcases/create_tables.yaml +++ b/server/test/testcases/create_tables.yaml @@ -6,7 +6,7 @@ query: args: - type: run_sql args: - sql: "CREATE TABLE author (id SERIAL PRIMARY KEY, name TEXT)" + sql: "CREATE TABLE author (id SERIAL PRIMARY KEY, name TEXT UNIQUE)" - type: run_sql args: sql: | diff --git a/server/test/testcases/create_user_permission_address.yaml b/server/test/testcases/create_user_permission_address.yaml new file mode 100644 index 0000000000000..c1990c3ac12ac --- /dev/null +++ b/server/test/testcases/create_user_permission_address.yaml @@ -0,0 +1,11 @@ +description: Create a insert permission on address table for user role +url: /v1/query +status: 200 +query: + type: create_insert_permission + args: + table: address + role: merchant + permission: + check: + city: bengaluru diff --git a/server/test/testcases/delete_mutation/author_foreign_key_violation.yaml b/server/test/testcases/delete_mutation/author_foreign_key_violation.yaml new file mode 100644 index 0000000000000..5dc046bf6dd52 --- /dev/null +++ b/server/test/testcases/delete_mutation/author_foreign_key_violation.yaml @@ -0,0 +1,15 @@ +description: delete from author table (Foreign Key Violation Error) +url: /v1alpha1/graphql +status: 400 +query: + query: | + mutation { + delete_author(where: {id: {_eq: 2}}){ + affected_rows + } + } +response: + errors: + - path: $ + error: "Foreign key violation. update or delete on table \"author\" violates foreign key constraint \"article_author_id_fkey\" on table \"article\"" + code: constraint-violation diff --git a/server/test/testcases/insert_mutation/address_check_constraint_error.yaml b/server/test/testcases/insert_mutation/address_check_constraint_error.yaml new file mode 100644 index 0000000000000..0c22915e3a5c2 --- /dev/null +++ b/server/test/testcases/insert_mutation/address_check_constraint_error.yaml @@ -0,0 +1,17 @@ +description: Insert into order table as user role (Check Constraint Error) +url: /v1alpha1/graphql +status: 400 +headers: + X-Hasura-Role: merchant +query: + query: | + mutation { + insert_address(objects: [{door_no: "12-21", street: "Madhapur", city: "Hyderabad", resident_id: 1}]){ + affected_rows + } + } +response: + errors: + - path: $ + error: Check constraint violation. insert check constraint failed + code: permission-error diff --git a/server/test/testcases/insert_mutation/address_not_null_constraint_error.yaml b/server/test/testcases/insert_mutation/address_not_null_constraint_error.yaml new file mode 100644 index 0000000000000..4f1e7dc3e9bbf --- /dev/null +++ b/server/test/testcases/insert_mutation/address_not_null_constraint_error.yaml @@ -0,0 +1,19 @@ +description: Insert into order table as user role (Not Null Constraint Error) +url: /v1alpha1/graphql +status: 400 +query: + query: | + mutation { + insert_address(objects: [{street: "koramangala"}]){ + returning{ + id + street + } + affected_rows + } + } +response: + errors: + - path: $ + error: "Not-NULL violation. null value in column \"door_no\" violates not-null constraint" + code: constraint-violation diff --git a/server/test/testcases/insert_mutation/author_unique_constraint_error.yaml b/server/test/testcases/insert_mutation/author_unique_constraint_error.yaml new file mode 100644 index 0000000000000..2a6c59f7e1480 --- /dev/null +++ b/server/test/testcases/insert_mutation/author_unique_constraint_error.yaml @@ -0,0 +1,19 @@ +description: Insert into author table as user role (Unique Constraint Error) +url: /v1alpha1/graphql +status: 400 +query: + query: | + mutation { + insert_author(objects: [{name: "Author 2"}]){ + returning{ + id + name + } + affected_rows + } + } +response: + errors: + - path: $ + error: "Uniqueness violation. duplicate key value violates unique constraint \"author_name_key\"" + code: constraint-violation