Make transaction-rollback=true the default for test-suite (#1663)

Change test-suite to use db-tx-rollback-all = true by default
This commit is contained in:
Wolfgang Walther
2020-12-03 18:54:33 +01:00
committed by GitHub
parent 10a70d4e52
commit 609c9aead8
21 changed files with 708 additions and 495 deletions
+30 -15
View File
@@ -33,13 +33,24 @@ spec actualPgVersion =
{"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []}, {"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []} {"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
it "can do logic on the third level" $ it "can do logic on the third level" $
get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))" `shouldRespondWith` get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"
[json|[ `shouldRespondWith`
{"id": 1, "child_entities": [ { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]}, { "id": 2, "grandchild_entities": []}]}, [json|[
{"id": 2, "child_entities": [ { "id": 3, "grandchild_entities": []} ]}, {"id": 1, "child_entities": [
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []} { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},
]|] { matchHeaders = [matchContentTypeJson] } { "id": 2, "grandchild_entities": []},
{ "id": 4, "grandchild_entities": []},
{ "id": 5, "grandchild_entities": []}
]},
{"id": 2, "child_entities": [
{ "id": 3, "grandchild_entities": []},
{ "id": 6, "grandchild_entities": []}
]},
{"id": 3, "child_entities": []},
{"id": 4, "child_entities": []}
]|]
context "and/or params combined" $ do context "and/or params combined" $ do
it "can be nested inside the same expression" $ it "can be nested inside the same expression" $
@@ -210,12 +221,15 @@ spec actualPgVersion =
context "used with POST" $ context "used with POST" $
it "includes related data with filters" $ it "includes related data with filters" $
request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id" request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json|[{"id":4,"name":"entity 4","parent_id":1}, [json|[
{"id":5,"name":"entity 5","parent_id":2}, {"id":7,"name":"entity 4","parent_id":1},
{"id":6,"name":"entity 6","parent_id":3}]|] `shouldRespondWith` {"id":8,"name":"entity 5","parent_id":2},
[json|[{"id": 4, "entities":null}, {"id": 5, "entities": {"id": 2}}, {"id": 6, "entities": {"id": 3}}]|] {"id":9,"name":"entity 6","parent_id":3}
{ matchStatus = 201, matchHeaders = [matchContentTypeJson] } ]|]
`shouldRespondWith`
[json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
{ matchStatus = 201 }
context "used with PATCH" $ context "used with PATCH" $
it "succeeds when using and/or params" $ it "succeeds when using and/or params" $
@@ -228,9 +242,10 @@ spec actualPgVersion =
context "used with DELETE" $ context "used with DELETE" $
it "succeeds when using and/or params" $ it "succeeds when using and/or params" $
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name" request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
[("Prefer", "return=representation")] "" `shouldRespondWith` [("Prefer", "return=representation")]
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|] ""
{ matchHeaders = [matchContentTypeJson] } `shouldRespondWith`
[json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]
it "can query columns that begin with and/or reserved words" $ it "can query columns that begin with and/or reserved words" $
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200 get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
-8
View File
@@ -57,14 +57,6 @@ spec =
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` [json|[{"id":15}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "known route, no records matched" $ context "known route, no records matched" $
it "includes [] body if return=rep" $ it "includes [] body if return=rep" $
request methodDelete "/items?id=eq.101" request methodDelete "/items?id=eq.101"
+3 -3
View File
@@ -194,9 +194,9 @@ spec =
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can request two parents with fks" $ it "can request two parents with fks" $
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith` get "/articleStars?select=createdAt,article(id),user(name)&limit=1"
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|] `shouldRespondWith`
{ matchHeaders = [matchContentTypeJson] } [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]
it "can specify a view!fk" $ it "can specify a view!fk" $
get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith` get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith`
+53 -36
View File
@@ -135,18 +135,21 @@ spec actualPgVersion = do
context "with no pk supplied" $ do context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" $ context "into a table with auto-incrementing pk" $
it "succeeds with 201 and link" $ do it "succeeds with 201 and location header" $ do
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |] -- reset pk sequence first to make test repeatable
liftIO $ do request methodPost "/rpc/reset_sequence"
simpleBody p `shouldBe` "" [("Prefer", "tx=commit")]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/auto_incrementing_pk\\?id=eq\\.[0-9]+" [json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|]
simpleStatus p `shouldBe` created201 `shouldRespondWith`
let Just location = lookup hLocation $ simpleHeaders p [json|""|]
r <- get location
let [record] = fromJust (JSON.decode $ simpleBody r :: Maybe [IncPK]) post "/auto_incrementing_pk"
liftIO $ do [json| { "non_nullable_string":"not null"} |]
incStr record `shouldBe` "not null" `shouldRespondWith`
incNullableStr record `shouldBe` Nothing ""
{ matchStatus = 201
, matchHeaders = [ "Location" <:> "/auto_incrementing_pk?id=eq.2" ]
}
context "into a table with simple pk" $ context "into a table with simple pk" $
it "fails with 400 and error" $ it "fails with 400 and error" $
@@ -197,21 +200,14 @@ spec actualPgVersion = do
context "with compound pk supplied" $ context "with compound pk supplied" $
it "builds response location header appropriately" $ do it "builds response location header appropriately" $ do
let inserted = [json| { "k1":12, "k2":"Rock & R+ll" } |] request methodPost "/compound_pk"
expectedObj = CompoundPK 12 "Rock & R+ll" Nothing [("Prefer", "return=representation")]
expectedLoc = "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll" [json| { "k1":12, "k2":"Rock & R+ll" } |]
p <- request methodPost "/compound_pk" `shouldRespondWith`
[("Prefer", "return=representation")] [json|[ { "k1":12, "k2":"Rock & R+ll", "extra": null } ]|]
inserted { matchStatus = 201
liftIO $ do , matchHeaders = [ "Location" <:> "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll" ]
JSON.decode (simpleBody p) `shouldBe` Just [expectedObj] }
simpleStatus p `shouldBe` created201
lookup hLocation (simpleHeaders p) `shouldBe` Just expectedLoc
r <- get expectedLoc
liftIO $ do
JSON.decode (simpleBody r) `shouldBe` Just [expectedObj]
simpleStatus r `shouldBe` ok200
context "with bulk insert" $ context "with bulk insert" $
it "returns 201 but no location header" $ do it "returns 201 but no location header" $ do
@@ -248,9 +244,9 @@ spec actualPgVersion = do
context "attempting to insert a row with the same primary key" $ context "attempting to insert a row with the same primary key" $
it "fails returning a 409 Conflict" $ it "fails returning a 409 Conflict" $
post "/simple_pk" post "/simple_pk"
[json| { "k":"k1", "extra":"e1" } |] [json| { "k":"xyyx", "extra":"e1" } |]
`shouldRespondWith` `shouldRespondWith`
[json|{"hint":null,"details":"Key (k)=(k1) already exists.","code":"23505","message":"duplicate key value violates unique constraint \"simple_pk_pkey\""}|] [json|{"hint":null,"details":"Key (k)=(xyyx) already exists.","code":"23505","message":"duplicate key value violates unique constraint \"simple_pk_pkey\""}|]
{ matchStatus = 409 } { matchStatus = 409 }
context "attempting to insert a row with conflicting unique constraint" $ context "attempting to insert a row with conflicting unique constraint" $
@@ -293,16 +289,30 @@ spec actualPgVersion = do
, matchHeaders = [] , matchHeaders = []
} }
it "successfully inserts a row with all-default columns with prefer=rep" $ it "successfully inserts a row with all-default columns with prefer=rep" $ do
-- reset pk sequence first to make test repeatable
request methodPost "/rpc/reset_sequence"
[("Prefer", "tx=commit")]
[json|{"name": "items_id_seq", "value": 20}|]
`shouldRespondWith`
[json|""|]
request methodPost "/items" [("Prefer", "return=representation")] "{}" request methodPost "/items" [("Prefer", "return=representation")] "{}"
`shouldRespondWith` [json|[{ id: 20 }]|] `shouldRespondWith` [json|[{ id: 20 }]|]
{ matchStatus = 201, { matchStatus = 201,
matchHeaders = [] matchHeaders = []
} }
it "successfully inserts a row with all-default columns with prefer=rep and &select=" $ it "successfully inserts a row with all-default columns with prefer=rep and &select=" $ do
-- reset pk sequence first to make test repeatable
request methodPost "/rpc/reset_sequence"
[("Prefer", "tx=commit")]
[json|{"name": "items_id_seq", "value": 20}|]
`shouldRespondWith`
[json|""|]
request methodPost "/items?select=id" [("Prefer", "return=representation")] "{}" request methodPost "/items?select=id" [("Prefer", "return=representation")] "{}"
`shouldRespondWith` [json|[{ id: 21 }]|] `shouldRespondWith` [json|[{ id: 20 }]|]
{ matchStatus = 201, { matchStatus = 201,
matchHeaders = [] matchHeaders = []
} }
@@ -411,15 +421,22 @@ spec actualPgVersion = do
it "succeeds and returns usable location header" $ do it "succeeds and returns usable location header" $ do
let payload = [json| { "k":"圍棋", "extra":"" } |] let payload = [json| { "k":"圍棋", "extra":"" } |]
p <- request methodPost "/simple_pk?select=extra,k" p <- request methodPost "/simple_pk?select=extra,k"
[("Prefer", "return=representation")] [("Prefer", "tx=commit"), ("Prefer", "return=representation")]
payload payload
liftIO $ do liftIO $ do
simpleBody p `shouldBe` "["<>payload<>"]" simpleBody p `shouldBe` "["<>payload<>"]"
simpleStatus p `shouldBe` created201 simpleStatus p `shouldBe` created201
let Just location = lookup hLocation $ simpleHeaders p let Just location = lookup hLocation $ simpleHeaders p
r <- get (location <> "&select=extra,k") get location
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]" `shouldRespondWith`
[json|[ { "k":"圍棋", "extra":"" } ]|]
request methodDelete location
[("Prefer", "tx=commit")]
""
`shouldRespondWith`
204
describe "Row level permission" $ describe "Row level permission" $
it "set user_id when inserting rows" $ do it "set user_id when inserting rows" $ do
+5 -6
View File
@@ -202,12 +202,11 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
context "Patching record, in a nonempty table" $ context "Patching record, in a nonempty table" $
it "can set a json column to escaped value" $ do it "can set a json column to escaped value" $ do
_ <- post "/json_table" [json| { data: {"escaped":"bar"} } |] request methodPatch "/json_table?data->>id=eq.3"
request methodPatch "/json_table?data->>escaped=eq.bar" [("Prefer", "return=representation")]
[("Prefer", "return=representation")] [json| { "data": { "id":" \"escaped" } } |]
[json| { "data": { "escaped":" \"bar" } } |] `shouldRespondWith`
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |] [json| [{ "data": { "id":" \"escaped" } }] |]
{ matchStatus = 200 , matchHeaders = [] }
when (actualPgVersion >= pgVersion95) $ when (actualPgVersion >= pgVersion95) $
context "json array negative index" $ do context "json array negative index" $ do
+31 -33
View File
@@ -77,33 +77,37 @@ spec actualPgVersion =
context "Inserting tables on different schemas" $ do context "Inserting tables on different schemas" $ do
it "succeeds inserting on default schema and returning it" $ it "succeeds inserting on default schema and returning it" $
request methodPost "/children" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|] request methodPost "/children"
`shouldRespondWith` [("Prefer", "return=representation")]
[json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|] [json|{"id": 0, "name": "child v1-1", "parent_id": 1}|]
{ `shouldRespondWith`
matchStatus = 201 [json|[{"id": 0, "name": "child v1-1", "parent_id": 1}]|]
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"] {
} matchStatus = 201
, matchHeaders = ["Content-Profile" <:> "v1"]
}
it "succeeds inserting on the v1 schema and returning its parent" $ it "succeeds inserting on the v1 schema and returning its parent" $
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")] request methodPost "/children?select=id,parent(*)"
[json|{"name": "child v1-2", "parent_id": 2}|] [("Prefer", "return=representation"), ("Content-Profile", "v1")]
[json|{"id": 0, "name": "child v1-2", "parent_id": 2}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|] [json|[{"id": 0, "parent": {"id": 2, "name": "parent v1-2"}}]|]
{ {
matchStatus = 201 matchStatus = 201
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"] , matchHeaders = ["Content-Profile" <:> "v1"]
} }
it "succeeds inserting on the v2 schema and returning its parent" $ it "succeeds inserting on the v2 schema and returning its parent" $
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")] request methodPost "/children?select=id,parent(*)"
[json|{"name": "child v2-3", "parent_id": 3}|] [("Prefer", "return=representation"), ("Content-Profile", "v2")]
[json|{"id": 0, "name": "child v2-3", "parent_id": 3}|]
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|] [json|[{"id": 0, "parent": {"id": 3, "name": "parent v2-3"}}]|]
{ {
matchStatus = 201 matchStatus = 201
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"] , matchHeaders = ["Content-Profile" <:> "v2"]
} }
it "fails when inserting on an unknown schema" $ it "fails when inserting on an unknown schema" $
request methodPost "/children" [("Content-Profile", "unknown")] request methodPost "/children" [("Content-Profile", "unknown")]
@@ -180,18 +184,12 @@ spec actualPgVersion =
} }
it "succeeds on deleting on the v2 schema" $ do it "succeeds on deleting on the v2 schema" $ do
request methodDelete "/children?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] "" request methodDelete "/children?id=eq.1"
`shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|] [("Content-Profile", "v2"), ("Prefer", "return=representation")]
{ ""
matchStatus = 200 `shouldRespondWith`
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"] [json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]
} { matchHeaders = ["Content-Profile" <:> "v2"] }
request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""
`shouldRespondWith` "[]"
{
matchStatus = 200
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
}
when (actualPgVersion >= pgVersion96) $ when (actualPgVersion >= pgVersion96) $
it "succeeds on PUT on the v2 schema" $ it "succeeds on PUT on the v2 schema" $
+32 -35
View File
@@ -15,59 +15,56 @@ spec :: SpecWith ((), Application)
spec = spec =
describe "Requesting many items with server limits(max-rows) enabled" $ do describe "Requesting many items with server limits(max-rows) enabled" $ do
it "restricts results" $ it "restricts results" $
get "/items" get "/items?order=id"
`shouldRespondWith` [json| [{"id":1},{"id":2}] |] `shouldRespondWith`
{ matchStatus = 200 [json| [{"id":1},{"id":2}] |]
, matchHeaders = ["Content-Range" <:> "0-1/*"] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
}
it "respects additional client limiting" $ do it "respects additional client limiting" $ do
r <- request methodGet "/items" request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 0 0) "" (rangeHdrs $ ByteRangeFromTo 0 0)
liftIO $ do ""
simpleHeaders r `shouldSatisfy` `shouldRespondWith`
matchHeader "Content-Range" "0-0/*" [json| [{"id":1}] |]
simpleStatus r `shouldBe` ok200 { matchHeaders = ["Content-Range" <:> "0-0/*"] }
it "works on all levels" $ it "works on all levels" $
get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc" get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc"
`shouldRespondWith` [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|] `shouldRespondWith`
{ matchStatus = 200 [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
, matchHeaders = ["Content-Range" <:> "0-1/*"] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
}
it "succeeds in getting parent embeds despite the limit, see #647" $ it "succeeds in getting parent embeds despite the limit, see #647" $
get "/tasks?select=id,project:projects(id)&id=gt.5" get "/tasks?select=id,project:projects(id)&id=gt.5"
`shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|] `shouldRespondWith`
{ matchStatus = 200 [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
, matchHeaders = ["Content-Range" <:> "0-1/*"] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
}
it "can offset the parent embed, being consistent with the other embed types" $ it "can offset the parent embed, being consistent with the other embed types" $
get "/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1" get "/tasks?select=id,project:projects(id)&id=gt.5&project.offset=1"
`shouldRespondWith` [json|[{"id":6,"project":null}, {"id":7,"project":null}]|] `shouldRespondWith`
{ matchStatus = 200 [json|[{"id":6,"project":null}, {"id":7,"project":null}]|]
, matchHeaders = ["Content-Range" <:> "0-1/*"] { matchHeaders = ["Content-Range" <:> "0-1/*"] }
}
context "count=estimated" $ do context "count=estimated" $ do
it "uses the query planner guess when query rows > maxRows" $ it "uses the query planner guess when query rows > maxRows" $
request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] "" request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 206 ""
, matchHeaders = ["Content-Range" <:> "0-1/2019"] { matchStatus = 206
} , matchHeaders = ["Content-Range" <:> "0-1/2019"]
}
it "gives exact count when query rows <= maxRows" $ it "gives exact count when query rows <= maxRows" $
request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] "" request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 200 ""
, matchHeaders = ["Content-Range" <:> "0-1/2"] { matchHeaders = ["Content-Range" <:> "0-1/2"] }
}
it "only uses the query planner guess if it's indeed greater than the exact count" $ it "only uses the query planner guess if it's indeed greater than the exact count" $
request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] "" request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] ""
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 206 ""
, matchHeaders = ["Content-Range" <:> "0-1/3"] { matchStatus = 206
} , matchHeaders = ["Content-Range" <:> "0-1/3"]
}
+4 -4
View File
@@ -35,7 +35,7 @@ spec actualPgVersion = do
{ matchHeaders = ["Content-Range" <:> "0-0/*"] } { matchHeaders = ["Content-Range" <:> "0-0/*"] }
it "matches with equality using not operator" $ it "matches with equality using not operator" $
get "/items?id=not.eq.5" get "/items?id=not.eq.5&order=id"
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |] `shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
{ matchHeaders = ["Content-Range" <:> "0-13/*"] } { matchHeaders = ["Content-Range" <:> "0-13/*"] }
@@ -401,9 +401,9 @@ spec actualPgVersion = do
get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200 get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200
it "can request two parents" $ it "can request two parents" $
get "/articleStars?select=createdAt,article:articles(owner),user:users(name)&limit=1" `shouldRespondWith` get "/articleStars?select=createdAt,article:articles(id),user:users(name)&limit=1"
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|] `shouldRespondWith`
{ matchHeaders = [matchContentTypeJson] } [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]
it "can detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $ it "can detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith` get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
+97 -63
View File
@@ -33,9 +33,11 @@ spec = do
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]} `shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
it "returns range Content-Range with range/*" $ it "returns range Content-Range with range/*" $
request methodPost "/rpc/getitemrange" [] defaultRange post "/rpc/getitemrange?order=id"
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |] defaultRange
{ matchHeaders = ["Content-Range" <:> "0-14/*"] } `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
{ matchHeaders = ["Content-Range" <:> "0-14/*"] }
context "with range headers" $ do context "with range headers" $ do
context "of acceptable range" $ do context "of acceptable range" $ do
@@ -168,20 +170,16 @@ spec = do
} }
it "succeeds if offset equals 0 as a no-op" $ it "succeeds if offset equals 0 as a no-op" $
get "/items?select=id&offset=0" get "/items?select=id&offset=0&order=id"
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|] [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]
{ matchStatus = 200 { matchHeaders = ["Content-Range" <:> "0-14/*"] }
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
it "succeeds if offset is negative as a no-op" $ it "succeeds if offset is negative as a no-op" $
get "/items?select=id&offset=-4" get "/items?select=id&offset=-4&order=id"
`shouldRespondWith` `shouldRespondWith`
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|] [json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}]|]
{ matchStatus = 200 { matchHeaders = ["Content-Range" <:> "0-14/*"] }
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
it "fails if limit equals 0" $ it "fails if limit equals 0" $
get "/items?select=id&limit=0" get "/items?select=id&limit=0"
@@ -199,62 +197,98 @@ spec = do
context "when count=planned" $ do context "when count=planned" $ do
it "obtains a filtered range" $ do it "obtains a filtered range" $ do
request methodGet "/items?select=id&id=gt.8" [("Prefer", "count=planned")] "" request methodGet "/items?select=id&id=gt.8"
`shouldRespondWith` [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|] [("Prefer", "count=planned")]
{ matchStatus = 206 ""
, matchHeaders = ["Content-Range" <:> "0-6/8"] `shouldRespondWith`
} [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]
request methodGet "/child_entities?select=id&id=gt.3" [("Prefer", "count=planned")] "" { matchStatus = 206
`shouldRespondWith` [json|[{"id":4}, {"id":5}, {"id":6}]|] , matchHeaders = ["Content-Range" <:> "0-6/8"]
{ matchStatus = 206 }
, matchHeaders = ["Content-Range" <:> "0-2/4"]
} request methodGet "/child_entities?select=id&id=gt.3"
request methodGet "/getallprojects_view?select=id&id=lt.3" [("Prefer", "count=planned")] "" [("Prefer", "count=planned")]
`shouldRespondWith` [json|[{"id":1}, {"id":2}]|] ""
{ matchStatus = 206 `shouldRespondWith`
, matchHeaders = ["Content-Range" <:> "0-1/673"] [json|[{"id":4}, {"id":5}, {"id":6}]|]
} { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-2/4"]
}
request methodGet "/getallprojects_view?select=id&id=lt.3"
[("Prefer", "count=planned")]
""
`shouldRespondWith`
[json|[{"id":1}, {"id":2}]|]
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/673"]
}
it "obtains the full range" $ do it "obtains the full range" $ do
request methodHead "/items" [("Prefer", "count=planned")] "" request methodHead "/items"
`shouldRespondWith` "" [("Prefer", "count=planned")]
{ matchStatus = 200 ""
, matchHeaders = ["Content-Range" <:> "0-14/15"] `shouldRespondWith`
} ""
request methodHead "/child_entities" [("Prefer", "count=planned")] "" { matchStatus = 200
`shouldRespondWith` "" , matchHeaders = ["Content-Range" <:> "0-14/15"]
{ matchStatus = 200 }
, matchHeaders = ["Content-Range" <:> "0-5/6"]
} request methodHead "/child_entities"
request methodHead "/getallprojects_view" [("Prefer", "count=planned")] "" [("Prefer", "count=planned")]
`shouldRespondWith` "" ""
{ matchStatus = 206 `shouldRespondWith`
, matchHeaders = ["Content-Range" <:> "0-4/2019"] ""
} { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"]
}
request methodHead "/getallprojects_view"
[("Prefer", "count=planned")]
""
`shouldRespondWith`
""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-4/2019"]
}
it "ignores limit/offset on the planned count" $ do it "ignores limit/offset on the planned count" $ do
request methodHead "/items?limit=2&offset=3" [("Prefer", "count=planned")] "" request methodHead "/items?limit=2&offset=3"
`shouldRespondWith` "" [("Prefer", "count=planned")]
{ matchStatus = 206 ""
, matchHeaders = ["Content-Range" <:> "3-4/15"] `shouldRespondWith`
} ""
request methodHead "/child_entities?limit=2" [("Prefer", "count=planned")] "" { matchStatus = 206
`shouldRespondWith` "" , matchHeaders = ["Content-Range" <:> "3-4/15"]
{ matchStatus = 206 }
, matchHeaders = ["Content-Range" <:> "0-1/6"]
} request methodHead "/child_entities?limit=2"
request methodHead "/getallprojects_view?limit=2" [("Prefer", "count=planned")] "" [("Prefer", "count=planned")]
`shouldRespondWith` "" ""
{ matchStatus = 206 `shouldRespondWith`
, matchHeaders = ["Content-Range" <:> "0-1/2019"] ""
} { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/6"]
}
request methodHead "/getallprojects_view?limit=2"
[("Prefer", "count=planned")]
""
`shouldRespondWith`
""
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
}
it "works with two levels" $ it "works with two levels" $
request methodHead "/child_entities?select=*,entities(*)" [("Prefer", "count=planned")] "" request methodHead "/child_entities?select=*,entities(*)"
`shouldRespondWith` "" [("Prefer", "count=planned")]
{ matchStatus = 200 ""
, matchHeaders = ["Content-Range" <:> "0-5/6"] `shouldRespondWith`
} ""
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"]
}
context "with range headers" $ do context "with range headers" $ do
context "of acceptable range" $ do context "of acceptable range" $ do
+5 -4
View File
@@ -15,7 +15,7 @@ import SpecHelper
-- creates Item to work with for PATCH and DELETE -- creates Item to work with for PATCH and DELETE
postItem = postItem =
request methodPost "/items" request methodPost "/items"
[("Prefer", "resolution=ignore-duplicates")] [("Prefer", "tx=commit"), ("Prefer", "resolution=ignore-duplicates")]
[json|{"id":0}|] [json|{"id":0}|]
`shouldRespondWith` `shouldRespondWith`
"" ""
@@ -23,7 +23,9 @@ postItem =
-- removes Items left over from POST, PUT, and PATCH -- removes Items left over from POST, PUT, and PATCH
deleteItems = deleteItems =
delete "/items?id=lte.0" request methodDelete "/items?id=lte.0"
[("Prefer", "tx=commit")]
""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 } { matchStatus = 204 }
@@ -175,9 +177,8 @@ shouldNotPersistMutations reqHeaders respHeaders = do
allowed :: SpecWith ((), Application) allowed :: SpecWith ((), Application)
allowed = describe "tx-allow-override = true" $ do allowed = describe "tx-allow-override = true" $ do
describe "without Prefer tx" $ do describe "without Prefer tx" $ do
-- TODO: Change this to default to rollback for whole test-suite
preferDefault `shouldRespondToReads` withoutPreferenceApplied preferDefault `shouldRespondToReads` withoutPreferenceApplied
preferDefault `shouldPersistMutations` withoutPreferenceApplied preferDefault `shouldNotPersistMutations` withoutPreferenceApplied
describe "Prefer tx=commit" $ do describe "Prefer tx=commit" $ do
preferCommit `shouldRespondToReads` withPreferenceCommitApplied preferCommit `shouldRespondToReads` withPreferenceCommitApplied
+57 -40
View File
@@ -11,62 +11,79 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Text.Heredoc import Text.Heredoc
import Protolude hiding (get) import Protolude hiding (get, put)
import SpecHelper import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "GUC headers on all methods via pre-request" $ do describe "GUC headers on all methods via pre-request" $ do
it "succeeds setting the headers on POST" $ it "succeeds setting the headers on POST" $
request methodPost "/items" [] [json|[{"id": 11111}]|] post "/items"
[json|[{"id": 11111}]|]
`shouldRespondWith` "" `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
} }
it "succeeds setting the headers on GET and HEAD" $ do it "succeeds setting the headers on GET and HEAD" $ do
request methodGet "/items?id=eq.11111" [("User-Agent", "MSIE 6.0")] mempty request methodGet "/items?id=eq.1"
`shouldRespondWith` [json|[{"id": 11111}]|] [("User-Agent", "MSIE 6.0")]
{matchHeaders = [ ""
matchContentTypeJson, `shouldRespondWith`
"Cache-Control" <:> "no-cache, no-store, must-revalidate"]} [json|[{"id": 1}]|]
{ matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }
request methodHead "/items?id=eq.11111" [("User-Agent", "MSIE 7.0")] mempty request methodHead "/items?id=eq.1"
`shouldRespondWith` "" [("User-Agent", "MSIE 7.0")]
{matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]} ""
`shouldRespondWith`
""
{ matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }
request methodHead "/projects" [("Accept", "text/csv")] mempty request methodHead "/projects"
`shouldRespondWith` "" [("Accept", "text/csv")]
{matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]} ""
`shouldRespondWith`
""
{ matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"] }
it "succeeds setting the headers on PATCH" $ it "succeeds setting the headers on PATCH" $
request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|] patch "/items?id=eq.1"
`shouldRespondWith` "" [json|[{"id": 11111}]|]
{ matchStatus = 204 `shouldRespondWith` ""
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] { matchStatus = 204
} , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "succeeds setting the headers on PUT" $ it "succeeds setting the headers on PUT" $
request methodPut "/items?id=eq.11111" [] [json|[{"id": 11111}]|] put "/items?id=eq.1"
[json|[{"id": 1}]|]
`shouldRespondWith` "" `shouldRespondWith` ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
} }
it "succeeds setting the headers on DELETE" $ it "succeeds setting the headers on DELETE" $
request methodDelete "/items?id=eq.11111" [] mempty delete "/items?id=eq.1"
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 204 ""
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] { matchStatus = 204
} , matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
}
it "can override the Content-Type header" $ do it "can override the Content-Type header" $ do
request methodHead "/clients?id=eq.1" [] mempty request methodHead "/clients?id=eq.1"
`shouldRespondWith` "" []
{ matchStatus = 200 ""
, matchHeaders = ["Content-Type" <:> "application/custom+json"] `shouldRespondWith`
} ""
request methodHead "/rpc/getallprojects" [] mempty { matchStatus = 200
`shouldRespondWith` "" , matchHeaders = ["Content-Type" <:> "application/custom+json"]
{ matchStatus = 200 }
, matchHeaders = ["Content-Type" <:> "application/custom+json"] request methodHead "/rpc/getallprojects"
} []
""
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/custom+json"]
}
+29 -13
View File
@@ -457,12 +457,24 @@ spec actualPgVersion =
`shouldRespondWith` 405 `shouldRespondWith` 405
it "executes the proc exactly once per request" $ do it "executes the proc exactly once per request" $ do
post "/rpc/callcounter" [json| {} |] `shouldRespondWith` -- callcounter is persistent even with rollback, because it uses a sequence
[json|1|] -- reset counter first to make test repeatable
{ matchHeaders = [matchContentTypeJson] } request methodPost "/rpc/reset_sequence"
post "/rpc/callcounter" [json| {} |] `shouldRespondWith` [("Prefer", "tx=commit")]
[json|2|] [json|{"name": "callcounter_count", "value": 1}|]
{ matchHeaders = [matchContentTypeJson] } `shouldRespondWith`
[json|""|]
-- now the test
post "/rpc/callcounter"
[json|{}|]
`shouldRespondWith`
[json|1|]
post "/rpc/callcounter"
[json|{}|]
`shouldRespondWith`
[json|2|]
context "a proc that receives no parameters" $ do context "a proc that receives no parameters" $ do
it "interprets empty string as empty json object on a post request" $ it "interprets empty string as empty json object on a post request" $
@@ -891,11 +903,13 @@ spec actualPgVersion =
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]} "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
it "can override the Location header on a trigger" $ it "can override the Location header on a trigger" $
request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|] post "/stuff"
`shouldRespondWith` "" [json|[{"id": 2, "name": "stuff 2"}]|]
{ matchStatus = 201 `shouldRespondWith`
, matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"] ""
} { matchStatus = 201
, matchHeaders = ["Location" <:> "/stuff?id=eq.2&overriden=true"]
}
-- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535 -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535
-- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests. -- it was reported that blank headers ` : ` where added and that cause proxies to fail the requests.
@@ -935,8 +949,10 @@ spec actualPgVersion =
} }
it "can override the status through trigger" $ it "can override the status through trigger" $
request methodPatch "/stuff?id=eq.1" [] [json|[{"name": "updated stuff 1"}]|] patch "/stuff?id=eq.1"
`shouldRespondWith` 205 [json|[{"name": "updated stuff 1"}]|]
`shouldRespondWith`
205
it "fails when setting invalid status guc" $ it "fails when setting invalid status guc" $
get "/rpc/send_bad_status" get "/rpc/send_bad_status"
+119 -89
View File
@@ -15,8 +15,7 @@ import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "Requesting singular json object" $ do describe "Requesting singular json object" $ do
let pgrstObj = "application/vnd.pgrst.object+json" let singular = ("Accept", "application/vnd.pgrst.object+json")
singular = ("Accept", pgrstObj)
context "with GET request" $ do context "with GET request" $ do
it "fails for zero rows" $ it "fails for zero rows" $
@@ -49,48 +48,52 @@ spec =
context "when updating rows" $ do context "when updating rows" $ do
it "works for one row with return=rep" $ do it "works for one row with return=rep" $ do
post "/addresses" [json| { id: 97, address: "A Street" } |] request methodPatch "/addresses?id=eq.1"
request methodPatch "/addresses?id=eq.97" [("Prefer", "return=representation"), singular]
[("Prefer", "return=representation"), singular] [json| { address: "B Street" } |]
[json| { address: "B Street" } |]
`shouldRespondWith` `shouldRespondWith`
[json|{"id":97,"address":"B Street"}|] [json|{"id":1,"address":"B Street"}|]
{ matchHeaders = [matchContentTypeSingular] } { matchHeaders = [matchContentTypeSingular] }
it "works for one row with return=minimal" $ it "works for one row with return=minimal" $
request methodPatch request methodPatch "/addresses?id=eq.1"
"/addresses?id=eq.97" [("Prefer", "return=minimal"), singular]
[("Prefer", "return=minimal"), singular] [json| { address: "C Street" } |]
[json| { address: "C Street" } |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 } { matchStatus = 204 }
it "raises an error for multiple rows" $ do it "raises an error for multiple rows" $ do
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |] request methodPatch "/addresses"
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |] [("Prefer", "tx=commit"), singular]
p <- request methodPatch "/addresses?id=gt.0" [json| { address: "zzz" } |]
[singular] `shouldRespondWith`
[json| { address: "zzz" } |] [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
liftIO $ do { matchStatus = 406
simpleStatus p `shouldBe` notAcceptable406 , matchHeaders = [ matchContentTypeSingular
isErrorFormat (simpleBody p) `shouldBe` True , "Preference-Applied" <:> "tx=commit" ]
}
-- the rows should not be updated, either -- the rows should not be updated, either
get "/addresses?id=eq.98" `shouldRespondWith` [json|[{"id":98,"address":"xxx"}]|] get "/addresses?id=eq.1"
`shouldRespondWith`
[json|[{"id":1,"address":"address 1"}]|]
it "raises an error for multiple rows with return=rep" $ do it "raises an error for multiple rows with return=rep" $ do
_ <- post "/addresses" [json| { id: 100, address: "xxx" } |] request methodPatch "/addresses"
_ <- post "/addresses" [json| { id: 101, address: "yyy" } |] [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
p <- request methodPatch "/addresses?id=gt.0" [json| { address: "zzz" } |]
[("Prefer", "return=representation"), singular] `shouldRespondWith`
[json| { address: "zzz" } |] [json|{"details":"Results contain 4 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
liftIO $ do { matchStatus = 406
simpleStatus p `shouldBe` notAcceptable406 , matchHeaders = [ matchContentTypeSingular
isErrorFormat (simpleBody p) `shouldBe` True , "Preference-Applied" <:> "tx=commit" ]
}
-- the rows should not be updated, either -- the rows should not be updated, either
get "/addresses?id=eq.100" `shouldRespondWith` [json|[{"id":100,"address":"xxx"}]|] get "/addresses?id=eq.1"
`shouldRespondWith`
[json|[{"id":1,"address":"address 1"}]|]
it "raises an error for zero rows" $ it "raises an error for zero rows" $
request methodPatch "/items?id=gt.0&id=lt.0" request methodPatch "/items?id=gt.0&id=lt.0"
@@ -123,51 +126,60 @@ spec =
it "works for one row with return=minimal" $ do it "works for one row with return=minimal" $ do
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 103, address: "xxx" } ] |] [json| [ { id: 103, address: "xxx" } ] |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = ["Content-Range" <:> "*/*"]
} }
-- and the element should exist
get "/addresses?id=eq.103"
`shouldRespondWith` [json|[{"id":103,"address":"xxx"}]|]
{ matchStatus = 200
, matchHeaders = []
}
it "raises an error when attempting to create multiple entities" $ do it "raises an error when attempting to create multiple entities" $ do
p <- request methodPost request methodPost "/addresses"
"/addresses" [("Prefer", "tx=commit"), singular]
[singular] [json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |] `shouldRespondWith`
liftIO $ simpleStatus p `shouldBe` notAcceptable406 [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
}
-- the rows should not exist, either -- the rows should not exist, either
get "/addresses?id=eq.200" `shouldRespondWith` "[]" get "/addresses?id=eq.200"
`shouldRespondWith`
"[]"
it "raises an error when attempting to create multiple entities with return=rep" $ do it "raises an error when attempting to create multiple entities with return=rep" $ do
p <- request methodPost request methodPost "/addresses"
"/addresses" [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
[("Prefer", "return=representation"), singular] [json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]
[json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |] `shouldRespondWith`
liftIO $ simpleStatus p `shouldBe` notAcceptable406 [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
}
-- the rows should not exist, either -- the rows should not exist, either
get "/addresses?id=eq.202" `shouldRespondWith` "[]" get "/addresses?id=eq.202"
`shouldRespondWith`
"[]"
it "raises an error regardless of return=minimal" $ do it "raises an error regardless of return=minimal" $ do
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular]
[json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |] [json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]
`shouldRespondWith` `shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] [json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406 { matchStatus = 406
, matchHeaders = [matchContentTypeSingular] , matchHeaders = [ matchContentTypeSingular
} , "Preference-Applied" <:> "tx=commit" ]
}
-- the rows should not exist, either -- the rows should not exist, either
get "/addresses?id=eq.204" `shouldRespondWith` "[]" get "/addresses?id=eq.204"
`shouldRespondWith`
"[]"
it "raises an error when creating zero entities" $ it "raises an error when creating zero entities" $
request methodPost "/addresses" request methodPost "/addresses"
@@ -203,28 +215,40 @@ spec =
liftIO $ simpleBody p `shouldBe` "" liftIO $ simpleBody p `shouldBe` ""
it "raises an error when attempting to delete multiple entities" $ do it "raises an error when attempting to delete multiple entities" $ do
let firstItems = "/items?id=gt.0&id=lt.6" request methodDelete "/items?id=gt.0&id=lt.6"
request methodDelete firstItems [("Prefer", "tx=commit"), singular]
[singular] "" ""
`shouldRespondWith` 406 `shouldRespondWith`
[json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
}
get firstItems -- the rows should still exist
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |] get "/items?id=gt.0&id=lt.6&order=id"
{ matchStatus = 200 `shouldRespondWith`
, matchHeaders = ["Content-Range" <:> "0-4/*"] [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |]
} { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-4/*"]
}
it "raises an error when attempting to delete multiple entities with return=rep" $ do it "raises an error when attempting to delete multiple entities with return=rep" $ do
let firstItems = "/items?id=gt.5&id=lt.11" request methodDelete "/items?id=gt.5&id=lt.11"
request methodDelete firstItems [("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] ""
[("Prefer", "return=representation"), singular] "" `shouldRespondWith`
`shouldRespondWith` 406 [json|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
}
get firstItems -- the rows should still exist
get "/items?id=gt.5&id=lt.11"
`shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |] `shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-4/*"] , matchHeaders = ["Content-Range" <:> "0-4/*"]
} }
it "raises an error when deleting zero entities" $ it "raises an error when deleting zero entities" $
request methodDelete "/items?id=lt.0" request methodDelete "/items?id=lt.0"
@@ -277,18 +301,24 @@ spec =
, matchHeaders = [matchContentTypeSingular] , matchHeaders = [matchContentTypeSingular]
} }
it "executes the proc exactly once per request" $ do it "fails for multiple rows with rolled back changes" $ do
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |] post "/rpc/getproject?select=id,name"
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|] [json| {"id": 1} |]
request methodPost "/rpc/setprojects" [singular]
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
`shouldRespondWith` `shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|] [json|[{"id":1,"name":"Windows 7"}]|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeSingular]
}
-- should not actually have executed the function request methodPost "/rpc/setprojects"
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |] [("Prefer", "tx=commit"), singular]
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|] [json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
`shouldRespondWith`
[json|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
{ matchStatus = 406
, matchHeaders = [ matchContentTypeSingular
, "Preference-Applied" <:> "tx=commit" ]
}
-- should rollback function
post "/rpc/getproject?select=id,name"
[json| {"id": 1} |]
`shouldRespondWith`
[json|[{"id":1,"name":"Windows 7"}]|]
+15 -4
View File
@@ -2,6 +2,7 @@ module Feature.UnicodeSpec where
import Network.Wai (Application) import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
@@ -16,9 +17,19 @@ spec =
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF" get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
`shouldRespondWith` "[]" `shouldRespondWith` "[]"
void $ post "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF" request methodPost "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
[json| { "هویت": 1 } |] [("Prefer", "tx=commit"), ("Prefer", "return=representation")]
[json| { "هویت": 1 } |]
`shouldRespondWith`
[json| [{ "هویت": 1 }] |]
{ matchStatus = 201 }
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF" get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
`shouldRespondWith` [json| [{ "هویت": 1 }] |] `shouldRespondWith`
{ matchHeaders = [matchContentTypeJson] } [json| [{ "هویت": 1 }] |]
request methodDelete "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
[("Prefer", "tx=commit")]
""
`shouldRespondWith`
204
+62 -42
View File
@@ -51,22 +51,31 @@ spec = do
context "in a nonempty table" $ do context "in a nonempty table" $ do
it "can update a single item" $ do it "can update a single item" $ do
g <- get "/items?id=eq.42" get "/items?id=eq.42"
liftIO $ simpleHeaders g `shouldRespondWith`
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*" [json|[]|]
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
pure p `shouldRespondWith` "" request methodPatch "/items?id=eq.2"
{ matchStatus = 204, [("Prefer", "tx=commit")]
matchHeaders = ["Content-Range" <:> "0-0/*"] [json| { "id":42 } |]
} `shouldRespondWith`
liftIO $ lookup hContentType (simpleHeaders p) `shouldBe` Nothing ""
{ matchStatus = 204
, matchHeaders = ["Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "tx=commit" ]
}
-- check it really got updated -- check it really got updated
g' <- get "/items?id=eq.42" get "/items?id=eq.42"
liftIO $ simpleHeaders g' `shouldRespondWith`
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*" [json|[ { "id": 42 } ]|]
-- put value back for other tests -- put value back for other tests
void $ request methodPatch "/items?id=eq.42" [] [json| { "id":2 } |] request methodPatch "/items?id=eq.42"
[("Prefer", "tx=commit")]
[json| { "id":2 } |]
`shouldRespondWith`
204
it "returns empty array when no rows updated and return=rep" $ it "returns empty array when no rows updated and return=rep" $
request methodPatch "/items?id=eq.999999" request methodPatch "/items?id=eq.999999"
@@ -91,23 +100,38 @@ spec = do
} }
it "can update multiple items" $ do it "can update multiple items" $ do
replicateM_ 10 $ post "/auto_incrementing_pk" get "/no_pk?select=a&b=eq.1"
[json| { non_nullable_string: "a" } |] `shouldRespondWith`
replicateM_ 10 $ post "/auto_incrementing_pk" [json|[]|]
[json| { non_nullable_string: "b" } |]
_ <- request methodPatch request methodPatch "/no_pk?b=eq.0"
"/auto_incrementing_pk?non_nullable_string=eq.a" [] [("Prefer", "tx=commit")]
[json| { non_nullable_string: "c" } |] [json| { b: "1" } |]
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c" `shouldRespondWith`
liftIO $ simpleHeaders g ""
`shouldSatisfy` matchHeader "Content-Range" "0-9/\\*" { matchStatus = 204
, matchHeaders = ["Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ]
}
-- check it really got updated
get "/no_pk?select=a&b=eq.1"
`shouldRespondWith`
[json|[ { a: "1" }, { a: "2" } ]|]
-- put value back for other tests
request methodPatch "/no_pk?b=eq.1"
[("Prefer", "tx=commit")]
[json| { b: "0" } |]
`shouldRespondWith`
204
it "can set a column to NULL" $ do it "can set a column to NULL" $ do
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |] request methodPatch "/no_pk?a=eq.1"
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |] [("Prefer", "return=representation")]
get "/no_pk?a=eq.keepme" `shouldRespondWith` [json| { b: null } |]
[json| [{ a: "keepme", b: null }] |] `shouldRespondWith`
{ matchHeaders = [matchContentTypeJson] } [json| [{ a: "1", b: null }] |]
context "filtering by a computed column" $ do context "filtering by a computed column" $ do
it "is successful" $ it "is successful" $
@@ -250,23 +274,19 @@ spec = do
context "with unicode values" $ context "with unicode values" $
it "succeeds and returns values intact" $ do it "succeeds and returns values intact" $ do
void $ request methodPost "/no_pk" [] request methodPatch "/no_pk?a=eq.1"
[json| { "a":"patchme", "b":"patchme" } |] [("Prefer", "return=representation")]
let payload = [json| { "a":"圍棋", "b":"" } |] [json| { "a":"圍棋", "b":"" } |]
p <- request methodPatch "/no_pk?a=eq.patchme&b=eq.patchme" `shouldRespondWith`
[("Prefer", "return=representation")] payload [json|[ { "a":"圍棋", "b":"" } ]|]
liftIO $ do
simpleBody p `shouldBe` "["<>payload<>"]"
simpleStatus p `shouldBe` ok200
context "PATCH with ?columns parameter" $ do context "PATCH with ?columns parameter" $ do
it "ignores json keys not included in ?columns" $ do it "ignores json keys not included in ?columns" $ do
post "/articles?columns=id,body" [json| {"id": 200} |] request methodPatch "/articles?id=eq.1&columns=body"
request methodPatch "/articles?id=eq.200&columns=body" [("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` [json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |]
[json|[{"id": 200, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|] `shouldRespondWith`
{ matchStatus = 200 [json|[{"id": 1, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]
, matchHeaders = [] }
it "ignores json keys and gives 404 if no record updated" $ it "ignores json keys and gives 404 if no record updated" $
request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")] request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]
+131 -78
View File
@@ -100,30 +100,38 @@ spec =
} }
it "INSERTs and ignores rows on single unique key conflict" $ it "INSERTs and ignores rows on single unique key conflict" $
request methodPost "/single_unique?on_conflict=unique_key" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] request methodPost "/single_unique?on_conflict=unique_key"
[json| [ [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
{ "unique_key": 1, "value": "B" }, [json| [
{ "unique_key": 2, "value": "C" }, { "unique_key": 1, "value": "B" },
{ "unique_key": 3, "value": "D" } { "unique_key": 2, "value": "C" },
]|] `shouldRespondWith` [json| [ { "unique_key": 3, "value": "D" }
{ "unique_key": 3, "value": "D" } ]|]
]|] `shouldRespondWith`
{ matchStatus = 201 [json| [
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] { "unique_key": 2, "value": "C" },
} { "unique_key": 3, "value": "D" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]
}
it "INSERTs and UPDATEs rows on compound unique keys conflict" $ it "INSERTs and UPDATEs rows on compound unique keys conflict" $
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] request methodPost "/compound_unique?on_conflict=key1,key2"
[json| [ [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
{ "key1": 1, "key2": 1, "value": "B" }, [json| [
{ "key1": 1, "key2": 2, "value": "C" }, { "key1": 1, "key2": 1, "value": "B" },
{ "key1": 1, "key2": 3, "value": "D" } { "key1": 1, "key2": 2, "value": "C" },
]|] `shouldRespondWith` [json| [ { "key1": 1, "key2": 3, "value": "D" }
{ "key1": 1, "key2": 3, "value": "D" } ]|]
]|] `shouldRespondWith`
{ matchStatus = 201 [json| [
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] { "key1": 1, "key2": 2, "value": "C" },
} { "key1": 1, "key2": 3, "value": "D" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]
}
it "succeeds if the table has only PK cols and no other cols" $ do it "succeeds if the table has only PK cols and no other cols" $ do
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
@@ -233,58 +241,93 @@ spec =
context "Inserting row" $ do context "Inserting row" $ do
it "succeeds on table with single pk col" $ do it "succeeds on table with single pk col" $ do
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` "[]" -- assert that the next request will indeed be an insert
put "/tiobe_pls?name=eq.Go" [json| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 204 get "/tiobe_pls?name=eq.Go"
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] } `shouldRespondWith`
[json|[]|]
request methodPut "/tiobe_pls?name=eq.Go"
[("Prefer", "return=representation")]
[json| [ { "name": "Go", "rank": 19 } ]|]
`shouldRespondWith`
[json| [ { "name": "Go", "rank": 19 } ]|]
it "succeeds on table with composite pk" $ do it "succeeds on table with composite pk" $ do
get "/employees?first_name=eq.Susan&last_name=eq.Heidt" -- assert that the next request will indeed be an insert
`shouldRespondWith` "[]"
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith` 204
get "/employees?first_name=eq.Susan&last_name=eq.Heidt" get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith` `shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|] [json|[]|]
{ matchHeaders = [matchContentTypeJson] }
request methodPut "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[("Prefer", "return=representation")]
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
it "succeeds if the table has only PK cols and no other cols" $ do it "succeeds if the table has only PK cols and no other cols" $ do
get "/only_pk?id=eq.10" `shouldRespondWith` "[]" -- assert that the next request will indeed be an insert
put "/only_pk?id=eq.10" [json|[ { "id": 10 } ]|] `shouldRespondWith` 204 get "/only_pk?id=eq.10"
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] } `shouldRespondWith`
[json|[]|]
request methodPut "/only_pk?id=eq.10"
[("Prefer", "return=representation")]
[json|[ { "id": 10 } ]|]
`shouldRespondWith`
[json|[ { "id": 10 } ]|]
context "Updating row" $ do context "Updating row" $ do
it "succeeds on table with single pk col" $ do it "succeeds on table with single pk col" $ do
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json|[ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] } -- assert that the next request will indeed be an update
put "/tiobe_pls?name=eq.Go" [json| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204 get "/tiobe_pls?name=eq.Java"
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }
it "succeeds if the payload has more than one row, but it only puts the first element" $
request methodPut "/tiobe_pls?name=eq.Go"
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
[json| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]
`shouldRespondWith` `shouldRespondWith`
[json|{ "name": "Go", "rank": 19 }|] [json|[ { "name": "Java", "rank": 1 } ]|]
{ matchStatus = 200 , matchHeaders = [matchContentTypeSingular] }
request methodPut "/tiobe_pls?name=eq.Java"
[("Prefer", "return=representation")]
[json| [ { "name": "Java", "rank": 13 } ]|]
`shouldRespondWith`
[json| [ { "name": "Java", "rank": 13 } ]|]
-- TODO: move this to SingularSpec?
it "succeeds if the payload has more than one row, but it only puts the first element" $ do
-- assert that the next request will indeed be an update
get "/tiobe_pls?name=eq.Java"
`shouldRespondWith`
[json|[ { "name": "Java", "rank": 1 } ]|]
request methodPut "/tiobe_pls?name=eq.Java"
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
[json| [ { "name": "Java", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]
`shouldRespondWith`
[json|{ "name": "Java", "rank": 19 }|]
{ matchHeaders = [matchContentTypeSingular] }
it "succeeds on table with composite pk" $ do it "succeeds on table with composite pk" $ do
get "/employees?first_name=eq.Susan&last_name=eq.Heidt" -- assert that the next request will indeed be an update
get "/employees?first_name=eq.Frances M.&last_name=eq.Roe"
`shouldRespondWith` `shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|] [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$24,000.00", "company": "One-Up Realty", "occupation": "Author" } ]|]
{ matchHeaders = [matchContentTypeJson] }
put "/employees?first_name=eq.Susan&last_name=eq.Heidt" request methodPut "/employees?first_name=eq.Frances M.&last_name=eq.Roe"
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|] [("Prefer", "return=representation")]
`shouldRespondWith` 204 [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith` `shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|] [json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "succeeds if the table has only PK cols and no other cols" $ do it "succeeds if the table has only PK cols and no other cols" $ do
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] } -- assert that the next request will indeed be an update
put "/only_pk?id=eq.10" [json|[ { "id": 10 } ]|] `shouldRespondWith` 204 get "/only_pk?id=eq.1"
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] } `shouldRespondWith`
[json|[ { "id": 1 } ]|]
request methodPut "/only_pk?id=eq.1"
[("Prefer", "return=representation")]
[json|[ { "id": 1 } ]|]
`shouldRespondWith`
[json|[ { "id": 1 } ]|]
-- TODO: move this to SingularSpec?
it "works with return=representation and vnd.pgrst.object+json" $ it "works with return=representation and vnd.pgrst.object+json" $
request methodPut "/tiobe_pls?name=eq.Ruby" request methodPut "/tiobe_pls?name=eq.Ruby"
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")] [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
@@ -292,28 +335,38 @@ spec =
`shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] } `shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] }
context "with a camel case pk column" $ do context "with a camel case pk column" $ do
it "works with POST and merge-duplicates/ignore-duplicates headers" $ do it "works with POST and merge-duplicates" $ do
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")] request methodPost "/UnitTest"
[json| [ [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" }, [json|[
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" } { "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
]|] `shouldRespondWith` [json|[ { "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" }, ]|]
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" } `shouldRespondWith`
]|] [json|[
{ matchStatus = 201 { "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson] { "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
} ]|]
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")] { matchStatus = 201
[json| [ , matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates"]
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" }, }
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|] `shouldRespondWith` [json|[]|] it "works with POST and ignore-duplicates headers" $ do
{ matchStatus = 201 request methodPost "/UnitTest"
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson] [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
} [json|[
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|]
`shouldRespondWith`
[json|[
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates"]
}
it "works with PUT" $ do it "works with PUT" $ do
put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204 put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith` get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] { matchHeaders = [matchContentTypeJson] } [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]
+9 -16
View File
@@ -96,39 +96,32 @@ main = do
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
multipleSchemaApp = appDbs testMultipleSchemaCfg multipleSchemaApp = appDbs testMultipleSchemaCfg
let reset, analyze :: IO () let analyze :: IO ()
reset = resetDb testDbConn
analyze = do analyze = do
analyzeTable testDbConn "items" analyzeTable testDbConn "items"
analyzeTable testDbConn "child_entities" analyzeTable testDbConn "child_entities"
specs = uncurry describe <$> [ specs = uncurry describe <$> [
("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion) ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion)
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec) , ("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion)
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec) , ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
, ("Feature.CorsSpec" , Feature.CorsSpec.spec) , ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion) , ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec) , ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec)
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec) , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion) , ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec) , ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
, ("Feature.RollbackAllowedSpec" , Feature.RollbackSpec.allowed) , ("Feature.RollbackAllowedSpec" , Feature.RollbackSpec.allowed)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion) , ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion) , ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
, ("Feature.UpsertSpec" , Feature.UpsertSpec.spec) , ("Feature.UpsertSpec" , Feature.UpsertSpec.spec)
] ]
mutSpecs = uncurry describe <$> [
("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
]
hspec $ do hspec $ do
-- Only certain Specs need a database reset, this should be used with care as it slows down the whole test suite.
mapM_ (afterAll_ reset . before withApp) mutSpecs
mapM_ (before withApp) specs mapM_ (before withApp) specs
-- we analyze to get accurate results from EXPLAIN -- we analyze to get accurate results from EXPLAIN
+1 -1
View File
@@ -90,7 +90,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configRawMediaTypes = [] , configRawMediaTypes = []
, configJWKS = parseSecret <$> secret , configJWKS = parseSecret <$> secret
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configTxRollbackAll = False , configTxRollbackAll = True
, configTxAllowOverride = True , configTxAllowOverride = True
, configDbPrepared = True , configDbPrepared = True
} }
+12
View File
@@ -359,6 +359,9 @@ TRUNCATE TABLE child_entities CASCADE;
INSERT INTO child_entities VALUES (1, 'child entity 1', 1); INSERT INTO child_entities VALUES (1, 'child entity 1', 1);
INSERT INTO child_entities VALUES (2, 'child entity 2', 1); INSERT INTO child_entities VALUES (2, 'child entity 2', 1);
INSERT INTO child_entities VALUES (3, 'child entity 3', 2); INSERT INTO child_entities VALUES (3, 'child entity 3', 2);
INSERT INTO child_entities VALUES (4, 'child entity 4', 1);
INSERT INTO child_entities VALUES (5, 'child entity 5', 1);
INSERT INTO child_entities VALUES (6, 'child entity 6', 2);
TRUNCATE TABLE grandchild_entities CASCADE; TRUNCATE TABLE grandchild_entities CASCADE;
INSERT INTO grandchild_entities VALUES (1, 'grandchild entity 1', 1, null, null, null); INSERT INTO grandchild_entities VALUES (1, 'grandchild entity 1', 1, null, null, null);
@@ -626,8 +629,17 @@ INSERT INTO unit_workdays VALUES(1, '2019-12-02', 1, 1, 2, 3);
TRUNCATE TABLE v1.parents CASCADE; TRUNCATE TABLE v1.parents CASCADE;
INSERT INTO v1.parents VALUES(1, 'parent v1-1'), (2, 'parent v1-2'); INSERT INTO v1.parents VALUES(1, 'parent v1-1'), (2, 'parent v1-2');
TRUNCATE TABLE v1.children CASCADE;
INSERT INTO v1.children VALUES(1, 'child v1-1', 1), (2, 'child v1-2', 2);
TRUNCATE TABLE v2.parents CASCADE; TRUNCATE TABLE v2.parents CASCADE;
INSERT INTO v2.parents VALUES(3, 'parent v2-3'), (4, 'parent v2-4'); INSERT INTO v2.parents VALUES(3, 'parent v2-3'), (4, 'parent v2-4');
TRUNCATE TABLE v2.children CASCADE;
INSERT INTO v2.children VALUES(1, 'child v2-3', 3);
TRUNCATE TABLE v2.another_table CASCADE; TRUNCATE TABLE v2.another_table CASCADE;
INSERT INTO v2.another_table VALUES(5, 'value 5'), (6, 'value 6'); INSERT INTO v2.another_table VALUES(5, 'value 5'), (6, 'value 6');
TRUNCATE TABLE private.stuff CASCADE;
INSERT INTO private.stuff (id, name) VALUES (1, 'stuff 1');
-2
View File
@@ -144,8 +144,6 @@ GRANT USAGE ON SEQUENCE
, items_id_seq , items_id_seq
, callcounter_count , callcounter_count
, leak_id_seq , leak_id_seq
, v1.children_id_seq
, v2.children_id_seq
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
-- Privileges for non anonymous users -- Privileges for non anonymous users
+13 -3
View File
@@ -365,6 +365,16 @@ CREATE FUNCTION callcounter() RETURNS bigint
SELECT nextval('test.callcounter_count'); SELECT nextval('test.callcounter_count');
$_$; $_$;
CREATE FUNCTION reset_sequence(name TEXT, value INTEGER) RETURNS void
SECURITY DEFINER
LANGUAGE plpgsql AS $_$
BEGIN
EXECUTE FORMAT($exec$
ALTER SEQUENCE %s RESTART WITH %s
$exec$, name, value);
END
$_$;
-- --
-- Name: singlejsonparam(json); Type: FUNCTION; Schema: test; Owner: - -- Name: singlejsonparam(json); Type: FUNCTION; Schema: test; Owner: -
-- --
@@ -1825,8 +1835,8 @@ create table v1.parents (
); );
create table v1.children ( create table v1.children (
id serial primary key id int primary key
, name text , name text
, parent_id int , parent_id int
, constraint parent foreign key(parent_id) , constraint parent foreign key(parent_id)
references v1.parents(id) references v1.parents(id)
@@ -1843,7 +1853,7 @@ create table v2.parents (
); );
create table v2.children ( create table v2.children (
id serial primary key id int primary key
, name text , name text
, parent_id int , parent_id int
, constraint parent foreign key(parent_id) , constraint parent foreign key(parent_id)