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:
@@ -33,13 +33,24 @@ spec actualPgVersion =
|
||||
{"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
|
||||
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
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`
|
||||
[json|[
|
||||
{"id": 1, "child_entities": [ { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]}, { "id": 2, "grandchild_entities": []}]},
|
||||
{"id": 2, "child_entities": [ { "id": 3, "grandchild_entities": []} ]},
|
||||
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
|
||||
]|] { matchHeaders = [matchContentTypeJson] }
|
||||
get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))"
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{"id": 1, "child_entities": [
|
||||
{ "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]},
|
||||
{ "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
|
||||
it "can be nested inside the same expression" $
|
||||
@@ -210,12 +221,15 @@ spec actualPgVersion =
|
||||
context "used with POST" $
|
||||
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"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|[{"id":4,"name":"entity 4","parent_id":1},
|
||||
{"id":5,"name":"entity 5","parent_id":2},
|
||||
{"id":6,"name":"entity 6","parent_id":3}]|] `shouldRespondWith`
|
||||
[json|[{"id": 4, "entities":null}, {"id": 5, "entities": {"id": 2}}, {"id": 6, "entities": {"id": 3}}]|]
|
||||
{ matchStatus = 201, matchHeaders = [matchContentTypeJson] }
|
||||
[("Prefer", "return=representation")]
|
||||
[json|[
|
||||
{"id":7,"name":"entity 4","parent_id":1},
|
||||
{"id":8,"name":"entity 5","parent_id":2},
|
||||
{"id":9,"name":"entity 6","parent_id":3}
|
||||
]|]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
|
||||
{ matchStatus = 201 }
|
||||
|
||||
context "used with PATCH" $
|
||||
it "succeeds when using and/or params" $
|
||||
@@ -228,9 +242,10 @@ spec actualPgVersion =
|
||||
context "used with DELETE" $
|
||||
it "succeeds when using and/or params" $
|
||||
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
||||
[("Prefer", "return=representation")] "" `shouldRespondWith`
|
||||
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
[("Prefer", "return=representation")]
|
||||
""
|
||||
`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" $
|
||||
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||
|
||||
@@ -57,14 +57,6 @@ spec =
|
||||
, 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" $
|
||||
it "includes [] body if return=rep" $
|
||||
request methodDelete "/items?id=eq.101"
|
||||
|
||||
@@ -194,9 +194,9 @@ spec =
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can request two parents with fks" $
|
||||
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/articleStars?select=createdAt,article(id),user(name)&limit=1"
|
||||
`shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"id": 1},"user":{"name": "Angela Martin"}}]|]
|
||||
|
||||
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`
|
||||
|
||||
+53
-36
@@ -135,18 +135,21 @@ spec actualPgVersion = do
|
||||
|
||||
context "with no pk supplied" $ do
|
||||
context "into a table with auto-incrementing pk" $
|
||||
it "succeeds with 201 and link" $ do
|
||||
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
|
||||
liftIO $ do
|
||||
simpleBody p `shouldBe` ""
|
||||
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/auto_incrementing_pk\\?id=eq\\.[0-9]+"
|
||||
simpleStatus p `shouldBe` created201
|
||||
let Just location = lookup hLocation $ simpleHeaders p
|
||||
r <- get location
|
||||
let [record] = fromJust (JSON.decode $ simpleBody r :: Maybe [IncPK])
|
||||
liftIO $ do
|
||||
incStr record `shouldBe` "not null"
|
||||
incNullableStr record `shouldBe` Nothing
|
||||
it "succeeds with 201 and location header" $ do
|
||||
-- reset pk sequence first to make test repeatable
|
||||
request methodPost "/rpc/reset_sequence"
|
||||
[("Prefer", "tx=commit")]
|
||||
[json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|]
|
||||
`shouldRespondWith`
|
||||
[json|""|]
|
||||
|
||||
post "/auto_incrementing_pk"
|
||||
[json| { "non_nullable_string":"not null"} |]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = [ "Location" <:> "/auto_incrementing_pk?id=eq.2" ]
|
||||
}
|
||||
|
||||
context "into a table with simple pk" $
|
||||
it "fails with 400 and error" $
|
||||
@@ -197,21 +200,14 @@ spec actualPgVersion = do
|
||||
|
||||
context "with compound pk supplied" $
|
||||
it "builds response location header appropriately" $ do
|
||||
let inserted = [json| { "k1":12, "k2":"Rock & R+ll" } |]
|
||||
expectedObj = CompoundPK 12 "Rock & R+ll" Nothing
|
||||
expectedLoc = "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll"
|
||||
p <- request methodPost "/compound_pk"
|
||||
[("Prefer", "return=representation")]
|
||||
inserted
|
||||
liftIO $ do
|
||||
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
|
||||
request methodPost "/compound_pk"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "k1":12, "k2":"Rock & R+ll" } |]
|
||||
`shouldRespondWith`
|
||||
[json|[ { "k1":12, "k2":"Rock & R+ll", "extra": null } ]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = [ "Location" <:> "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll" ]
|
||||
}
|
||||
|
||||
context "with bulk insert" $
|
||||
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" $
|
||||
it "fails returning a 409 Conflict" $
|
||||
post "/simple_pk"
|
||||
[json| { "k":"k1", "extra":"e1" } |]
|
||||
[json| { "k":"xyyx", "extra":"e1" } |]
|
||||
`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 }
|
||||
|
||||
context "attempting to insert a row with conflicting unique constraint" $
|
||||
@@ -293,16 +289,30 @@ spec actualPgVersion = do
|
||||
, 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")] "{}"
|
||||
`shouldRespondWith` [json|[{ id: 20 }]|]
|
||||
{ matchStatus = 201,
|
||||
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")] "{}"
|
||||
`shouldRespondWith` [json|[{ id: 21 }]|]
|
||||
`shouldRespondWith` [json|[{ id: 20 }]|]
|
||||
{ matchStatus = 201,
|
||||
matchHeaders = []
|
||||
}
|
||||
@@ -411,15 +421,22 @@ spec actualPgVersion = do
|
||||
it "succeeds and returns usable location header" $ do
|
||||
let payload = [json| { "k":"圍棋", "extra":"¥" } |]
|
||||
p <- request methodPost "/simple_pk?select=extra,k"
|
||||
[("Prefer", "return=representation")]
|
||||
payload
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=representation")]
|
||||
payload
|
||||
liftIO $ do
|
||||
simpleBody p `shouldBe` "["<>payload<>"]"
|
||||
simpleStatus p `shouldBe` created201
|
||||
|
||||
let Just location = lookup hLocation $ simpleHeaders p
|
||||
r <- get (location <> "&select=extra,k")
|
||||
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
|
||||
get location
|
||||
`shouldRespondWith`
|
||||
[json|[ { "k":"圍棋", "extra":"¥" } ]|]
|
||||
|
||||
request methodDelete location
|
||||
[("Prefer", "tx=commit")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
204
|
||||
|
||||
describe "Row level permission" $
|
||||
it "set user_id when inserting rows" $ do
|
||||
|
||||
@@ -202,12 +202,11 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
||||
|
||||
context "Patching record, in a nonempty table" $
|
||||
it "can set a json column to escaped value" $ do
|
||||
_ <- post "/json_table" [json| { data: {"escaped":"bar"} } |]
|
||||
request methodPatch "/json_table?data->>escaped=eq.bar"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "data": { "escaped":" \"bar" } } |]
|
||||
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
|
||||
{ matchStatus = 200 , matchHeaders = [] }
|
||||
request methodPatch "/json_table?data->>id=eq.3"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "data": { "id":" \"escaped" } } |]
|
||||
`shouldRespondWith`
|
||||
[json| [{ "data": { "id":" \"escaped" } }] |]
|
||||
|
||||
when (actualPgVersion >= pgVersion95) $
|
||||
context "json array negative index" $ do
|
||||
|
||||
@@ -77,33 +77,37 @@ spec actualPgVersion =
|
||||
|
||||
context "Inserting tables on different schemas" $ do
|
||||
it "succeeds inserting on default schema and returning it" $
|
||||
request methodPost "/children" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
|
||||
}
|
||||
request methodPost "/children"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|{"id": 0, "name": "child v1-1", "parent_id": 1}|]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id": 0, "name": "child v1-1", "parent_id": 1}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = ["Content-Profile" <:> "v1"]
|
||||
}
|
||||
|
||||
it "succeeds inserting on the v1 schema and returning its parent" $
|
||||
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]
|
||||
[json|{"name": "child v1-2", "parent_id": 2}|]
|
||||
request methodPost "/children?select=id,parent(*)"
|
||||
[("Prefer", "return=representation"), ("Content-Profile", "v1")]
|
||||
[json|{"id": 0, "name": "child v1-2", "parent_id": 2}|]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]
|
||||
}
|
||||
[json|[{"id": 0, "parent": {"id": 2, "name": "parent v1-2"}}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = ["Content-Profile" <:> "v1"]
|
||||
}
|
||||
|
||||
it "succeeds inserting on the v2 schema and returning its parent" $
|
||||
request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]
|
||||
[json|{"name": "child v2-3", "parent_id": 3}|]
|
||||
request methodPost "/children?select=id,parent(*)"
|
||||
[("Prefer", "return=representation"), ("Content-Profile", "v2")]
|
||||
[json|{"id": 0, "name": "child v2-3", "parent_id": 3}|]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
|
||||
}
|
||||
[json|[{"id": 0, "parent": {"id": 3, "name": "parent v2-3"}}]|]
|
||||
{
|
||||
matchStatus = 201
|
||||
, matchHeaders = ["Content-Profile" <:> "v2"]
|
||||
}
|
||||
|
||||
it "fails when inserting on an unknown schema" $
|
||||
request methodPost "/children" [("Content-Profile", "unknown")]
|
||||
@@ -180,18 +184,12 @@ spec actualPgVersion =
|
||||
}
|
||||
|
||||
it "succeeds on deleting on the v2 schema" $ do
|
||||
request methodDelete "/children?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""
|
||||
`shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|]
|
||||
{
|
||||
matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
|
||||
}
|
||||
request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""
|
||||
`shouldRespondWith` "[]"
|
||||
{
|
||||
matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
|
||||
}
|
||||
request methodDelete "/children?id=eq.1"
|
||||
[("Content-Profile", "v2"), ("Prefer", "return=representation")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]
|
||||
{ matchHeaders = ["Content-Profile" <:> "v2"] }
|
||||
|
||||
when (actualPgVersion >= pgVersion96) $
|
||||
it "succeeds on PUT on the v2 schema" $
|
||||
|
||||
@@ -15,59 +15,56 @@ spec :: SpecWith ((), Application)
|
||||
spec =
|
||||
describe "Requesting many items with server limits(max-rows) enabled" $ do
|
||||
it "restricts results" $
|
||||
get "/items"
|
||||
`shouldRespondWith` [json| [{"id":1},{"id":2}] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
get "/items?order=id"
|
||||
`shouldRespondWith`
|
||||
[json| [{"id":1},{"id":2}] |]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-1/*"] }
|
||||
|
||||
it "respects additional client limiting" $ do
|
||||
r <- request methodGet "/items"
|
||||
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-0/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
request methodGet "/items"
|
||||
(rangeHdrs $ ByteRangeFromTo 0 0)
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| [{"id":1}] |]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-0/*"] }
|
||||
|
||||
it "works on all levels" $
|
||||
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}]}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-1/*"] }
|
||||
|
||||
it "succeeds in getting parent embeds despite the limit, see #647" $
|
||||
get "/tasks?select=id,project:projects(id)&id=gt.5"
|
||||
`shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-1/*"] }
|
||||
|
||||
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"
|
||||
`shouldRespondWith` [json|[{"id":6,"project":null}, {"id":7,"project":null}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":6,"project":null}, {"id":7,"project":null}]|]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-1/*"] }
|
||||
|
||||
context "count=estimated" $ do
|
||||
it "uses the query planner guess when query rows > maxRows" $
|
||||
request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2019"]
|
||||
}
|
||||
|
||||
it "gives exact count when query rows <= maxRows" $
|
||||
request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchHeaders = ["Content-Range" <:> "0-1/2"] }
|
||||
|
||||
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")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
||||
}
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ spec actualPgVersion = do
|
||||
{ matchHeaders = ["Content-Range" <:> "0-0/*"] }
|
||||
|
||||
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}] |]
|
||||
{ matchHeaders = ["Content-Range" <:> "0-13/*"] }
|
||||
|
||||
@@ -401,9 +401,9 @@ spec actualPgVersion = do
|
||||
get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200
|
||||
|
||||
it "can request two parents" $
|
||||
get "/articleStars?select=createdAt,article:articles(owner),user:users(name)&limit=1" `shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/articleStars?select=createdAt,article:articles(id),user:users(name)&limit=1"
|
||||
`shouldRespondWith`
|
||||
[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" $
|
||||
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
|
||||
|
||||
+97
-63
@@ -33,9 +33,11 @@ spec = do
|
||||
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
|
||||
|
||||
it "returns range Content-Range with range/*" $
|
||||
request methodPost "/rpc/getitemrange" [] defaultRange
|
||||
`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/*"] }
|
||||
post "/rpc/getitemrange?order=id"
|
||||
defaultRange
|
||||
`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 "of acceptable range" $ do
|
||||
@@ -168,20 +170,16 @@ spec = do
|
||||
}
|
||||
|
||||
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`
|
||||
[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/*"]
|
||||
}
|
||||
[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/*"] }
|
||||
|
||||
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`
|
||||
[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/*"]
|
||||
}
|
||||
[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/*"] }
|
||||
|
||||
it "fails if limit equals 0" $
|
||||
get "/items?select=id&limit=0"
|
||||
@@ -199,62 +197,98 @@ spec = do
|
||||
|
||||
context "when count=planned" $ do
|
||||
it "obtains a filtered range" $ do
|
||||
request methodGet "/items?select=id&id=gt.8" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` [json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-6/8"]
|
||||
}
|
||||
request methodGet "/child_entities?select=id&id=gt.3" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` [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"]
|
||||
}
|
||||
request methodGet "/items?select=id&id=gt.8"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":9}, {"id":10}, {"id":11}, {"id":12}, {"id":13}, {"id":14}, {"id":15}]|]
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-6/8"]
|
||||
}
|
||||
|
||||
request methodGet "/child_entities?select=id&id=gt.3"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[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
|
||||
request methodHead "/items" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-14/15"]
|
||||
}
|
||||
request methodHead "/child_entities" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-5/6"]
|
||||
}
|
||||
request methodHead "/getallprojects_view" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-4/2019"]
|
||||
}
|
||||
request methodHead "/items"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-14/15"]
|
||||
}
|
||||
|
||||
request methodHead "/child_entities"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ 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
|
||||
request methodHead "/items?limit=2&offset=3" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "3-4/15"]
|
||||
}
|
||||
request methodHead "/child_entities?limit=2" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ 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"]
|
||||
}
|
||||
request methodHead "/items?limit=2&offset=3"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "3-4/15"]
|
||||
}
|
||||
|
||||
request methodHead "/child_entities?limit=2"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ 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" $
|
||||
request methodHead "/child_entities?select=*,entities(*)" [("Prefer", "count=planned")] ""
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-5/6"]
|
||||
}
|
||||
request methodHead "/child_entities?select=*,entities(*)"
|
||||
[("Prefer", "count=planned")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-5/6"]
|
||||
}
|
||||
|
||||
context "with range headers" $ do
|
||||
context "of acceptable range" $ do
|
||||
|
||||
@@ -15,7 +15,7 @@ import SpecHelper
|
||||
-- creates Item to work with for PATCH and DELETE
|
||||
postItem =
|
||||
request methodPost "/items"
|
||||
[("Prefer", "resolution=ignore-duplicates")]
|
||||
[("Prefer", "tx=commit"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json|{"id":0}|]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
@@ -23,7 +23,9 @@ postItem =
|
||||
|
||||
-- removes Items left over from POST, PUT, and PATCH
|
||||
deleteItems =
|
||||
delete "/items?id=lte.0"
|
||||
request methodDelete "/items?id=lte.0"
|
||||
[("Prefer", "tx=commit")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 204 }
|
||||
@@ -175,9 +177,8 @@ shouldNotPersistMutations reqHeaders respHeaders = do
|
||||
allowed :: SpecWith ((), Application)
|
||||
allowed = describe "tx-allow-override = true" $ do
|
||||
describe "without Prefer tx" $ do
|
||||
-- TODO: Change this to default to rollback for whole test-suite
|
||||
preferDefault `shouldRespondToReads` withoutPreferenceApplied
|
||||
preferDefault `shouldPersistMutations` withoutPreferenceApplied
|
||||
preferDefault `shouldNotPersistMutations` withoutPreferenceApplied
|
||||
|
||||
describe "Prefer tx=commit" $ do
|
||||
preferCommit `shouldRespondToReads` withPreferenceCommitApplied
|
||||
|
||||
@@ -11,62 +11,79 @@ import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Text.Heredoc
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude hiding (get, put)
|
||||
import SpecHelper
|
||||
|
||||
spec :: SpecWith ((), Application)
|
||||
spec =
|
||||
describe "GUC headers on all methods via pre-request" $ do
|
||||
it "succeeds setting the headers on POST" $
|
||||
request methodPost "/items" [] [json|[{"id": 11111}]|]
|
||||
post "/items"
|
||||
[json|[{"id": 11111}]|]
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
|
||||
it "succeeds setting the headers on GET and HEAD" $ do
|
||||
request methodGet "/items?id=eq.11111" [("User-Agent", "MSIE 6.0")] mempty
|
||||
`shouldRespondWith` [json|[{"id": 11111}]|]
|
||||
{matchHeaders = [
|
||||
matchContentTypeJson,
|
||||
"Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
|
||||
request methodGet "/items?id=eq.1"
|
||||
[("User-Agent", "MSIE 6.0")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"id": 1}]|]
|
||||
{ matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }
|
||||
|
||||
request methodHead "/items?id=eq.11111" [("User-Agent", "MSIE 7.0")] mempty
|
||||
`shouldRespondWith` ""
|
||||
{matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"]}
|
||||
request methodHead "/items?id=eq.1"
|
||||
[("User-Agent", "MSIE 7.0")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] }
|
||||
|
||||
request methodHead "/projects" [("Accept", "text/csv")] mempty
|
||||
`shouldRespondWith` ""
|
||||
{matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"]}
|
||||
request methodHead "/projects"
|
||||
[("Accept", "text/csv")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"] }
|
||||
|
||||
it "succeeds setting the headers on PATCH" $
|
||||
request methodPatch "/items?id=eq.11111" [] [json|[{"id": 11111}]|]
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
patch "/items?id=eq.1"
|
||||
[json|[{"id": 11111}]|]
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
|
||||
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` ""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
|
||||
it "succeeds setting the headers on DELETE" $
|
||||
request methodDelete "/items?id=eq.11111" [] mempty
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
delete "/items?id=eq.1"
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"]
|
||||
}
|
||||
it "can override the Content-Type header" $ do
|
||||
request methodHead "/clients?id=eq.1" [] mempty
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/custom+json"]
|
||||
}
|
||||
request methodHead "/rpc/getallprojects" [] mempty
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/custom+json"]
|
||||
}
|
||||
request methodHead "/clients?id=eq.1"
|
||||
[]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/custom+json"]
|
||||
}
|
||||
request methodHead "/rpc/getallprojects"
|
||||
[]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "application/custom+json"]
|
||||
}
|
||||
|
||||
+29
-13
@@ -457,12 +457,24 @@ spec actualPgVersion =
|
||||
`shouldRespondWith` 405
|
||||
|
||||
it "executes the proc exactly once per request" $ do
|
||||
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||
[json|1|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
|
||||
[json|2|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
-- callcounter is persistent even with rollback, because it uses a sequence
|
||||
-- reset counter first to make test repeatable
|
||||
request methodPost "/rpc/reset_sequence"
|
||||
[("Prefer", "tx=commit")]
|
||||
[json|{"name": "callcounter_count", "value": 1}|]
|
||||
`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
|
||||
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"]}
|
||||
|
||||
it "can override the Location header on a trigger" $
|
||||
request methodPost "/stuff" [] [json|[{"id": 1, "name": "stuff 1"}]|]
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Location" <:> "/stuff?id=eq.1&overriden=true"]
|
||||
}
|
||||
post "/stuff"
|
||||
[json|[{"id": 2, "name": "stuff 2"}]|]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Location" <:> "/stuff?id=eq.2&overriden=true"]
|
||||
}
|
||||
|
||||
-- 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.
|
||||
@@ -935,8 +949,10 @@ spec actualPgVersion =
|
||||
}
|
||||
|
||||
it "can override the status through trigger" $
|
||||
request methodPatch "/stuff?id=eq.1" [] [json|[{"name": "updated stuff 1"}]|]
|
||||
`shouldRespondWith` 205
|
||||
patch "/stuff?id=eq.1"
|
||||
[json|[{"name": "updated stuff 1"}]|]
|
||||
`shouldRespondWith`
|
||||
205
|
||||
|
||||
it "fails when setting invalid status guc" $
|
||||
get "/rpc/send_bad_status"
|
||||
|
||||
+119
-89
@@ -15,8 +15,7 @@ import SpecHelper
|
||||
spec :: SpecWith ((), Application)
|
||||
spec =
|
||||
describe "Requesting singular json object" $ do
|
||||
let pgrstObj = "application/vnd.pgrst.object+json"
|
||||
singular = ("Accept", pgrstObj)
|
||||
let singular = ("Accept", "application/vnd.pgrst.object+json")
|
||||
|
||||
context "with GET request" $ do
|
||||
it "fails for zero rows" $
|
||||
@@ -49,48 +48,52 @@ spec =
|
||||
|
||||
context "when updating rows" $ do
|
||||
it "works for one row with return=rep" $ do
|
||||
post "/addresses" [json| { id: 97, address: "A Street" } |]
|
||||
request methodPatch "/addresses?id=eq.97"
|
||||
[("Prefer", "return=representation"), singular]
|
||||
[json| { address: "B Street" } |]
|
||||
request methodPatch "/addresses?id=eq.1"
|
||||
[("Prefer", "return=representation"), singular]
|
||||
[json| { address: "B Street" } |]
|
||||
`shouldRespondWith`
|
||||
[json|{"id":97,"address":"B Street"}|]
|
||||
[json|{"id":1,"address":"B Street"}|]
|
||||
{ matchHeaders = [matchContentTypeSingular] }
|
||||
|
||||
it "works for one row with return=minimal" $
|
||||
request methodPatch
|
||||
"/addresses?id=eq.97"
|
||||
[("Prefer", "return=minimal"), singular]
|
||||
[json| { address: "C Street" } |]
|
||||
request methodPatch "/addresses?id=eq.1"
|
||||
[("Prefer", "return=minimal"), singular]
|
||||
[json| { address: "C Street" } |]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 204 }
|
||||
|
||||
it "raises an error for multiple rows" $ do
|
||||
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
|
||||
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
|
||||
p <- request methodPatch "/addresses?id=gt.0"
|
||||
[singular]
|
||||
[json| { address: "zzz" } |]
|
||||
liftIO $ do
|
||||
simpleStatus p `shouldBe` notAcceptable406
|
||||
isErrorFormat (simpleBody p) `shouldBe` True
|
||||
request methodPatch "/addresses"
|
||||
[("Prefer", "tx=commit"), singular]
|
||||
[json| { address: "zzz" } |]
|
||||
`shouldRespondWith`
|
||||
[json|{"details":"Results contain 4 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 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
|
||||
_ <- post "/addresses" [json| { id: 100, address: "xxx" } |]
|
||||
_ <- post "/addresses" [json| { id: 101, address: "yyy" } |]
|
||||
p <- request methodPatch "/addresses?id=gt.0"
|
||||
[("Prefer", "return=representation"), singular]
|
||||
[json| { address: "zzz" } |]
|
||||
liftIO $ do
|
||||
simpleStatus p `shouldBe` notAcceptable406
|
||||
isErrorFormat (simpleBody p) `shouldBe` True
|
||||
request methodPatch "/addresses"
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
|
||||
[json| { address: "zzz" } |]
|
||||
`shouldRespondWith`
|
||||
[json|{"details":"Results contain 4 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 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" $
|
||||
request methodPatch "/items?id=gt.0&id=lt.0"
|
||||
@@ -123,51 +126,60 @@ spec =
|
||||
|
||||
it "works for one row with return=minimal" $ do
|
||||
request methodPost "/addresses"
|
||||
[("Prefer", "return=minimal"), singular]
|
||||
[json| [ { id: 103, address: "xxx" } ] |]
|
||||
[("Prefer", "return=minimal"), singular]
|
||||
[json| [ { id: 103, address: "xxx" } ] |]
|
||||
`shouldRespondWith` ""
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
-- and the element should exist
|
||||
get "/addresses?id=eq.103"
|
||||
`shouldRespondWith` [json|[{"id":103,"address":"xxx"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = []
|
||||
}
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "raises an error when attempting to create multiple entities" $ do
|
||||
p <- request methodPost
|
||||
"/addresses"
|
||||
[singular]
|
||||
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
|
||||
liftIO $ simpleStatus p `shouldBe` notAcceptable406
|
||||
request methodPost "/addresses"
|
||||
[("Prefer", "tx=commit"), singular]
|
||||
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
|
||||
`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" ]
|
||||
}
|
||||
|
||||
-- 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
|
||||
p <- request methodPost
|
||||
"/addresses"
|
||||
[("Prefer", "return=representation"), singular]
|
||||
[json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]
|
||||
liftIO $ simpleStatus p `shouldBe` notAcceptable406
|
||||
request methodPost "/addresses"
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular]
|
||||
[json| [ { id: 202, address: "xxx" }, { id: 203, address: "yyy" } ] |]
|
||||
`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" ]
|
||||
}
|
||||
|
||||
-- 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
|
||||
request methodPost "/addresses"
|
||||
[("Prefer", "return=minimal"), singular]
|
||||
[json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=minimal"), singular]
|
||||
[json| [ { id: 204, address: "xxx" }, { id: 205, address: "yyy" } ] |]
|
||||
`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]
|
||||
}
|
||||
[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
|
||||
get "/addresses?id=eq.204" `shouldRespondWith` "[]"
|
||||
get "/addresses?id=eq.204"
|
||||
`shouldRespondWith`
|
||||
"[]"
|
||||
|
||||
it "raises an error when creating zero entities" $
|
||||
request methodPost "/addresses"
|
||||
@@ -203,28 +215,40 @@ spec =
|
||||
liftIO $ simpleBody p `shouldBe` ""
|
||||
|
||||
it "raises an error when attempting to delete multiple entities" $ do
|
||||
let firstItems = "/items?id=gt.0&id=lt.6"
|
||||
request methodDelete firstItems
|
||||
[singular] ""
|
||||
`shouldRespondWith` 406
|
||||
request methodDelete "/items?id=gt.0&id=lt.6"
|
||||
[("Prefer", "tx=commit"), singular]
|
||||
""
|
||||
`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
|
||||
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5}] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-4/*"]
|
||||
}
|
||||
-- the rows should still exist
|
||||
get "/items?id=gt.0&id=lt.6&order=id"
|
||||
`shouldRespondWith`
|
||||
[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
|
||||
let firstItems = "/items?id=gt.5&id=lt.11"
|
||||
request methodDelete firstItems
|
||||
[("Prefer", "return=representation"), singular] ""
|
||||
`shouldRespondWith` 406
|
||||
request methodDelete "/items?id=gt.5&id=lt.11"
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=representation"), singular] ""
|
||||
`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
|
||||
get "/items?id=gt.5&id=lt.11"
|
||||
`shouldRespondWith` [json| [{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-4/*"]
|
||||
}
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-4/*"]
|
||||
}
|
||||
|
||||
it "raises an error when deleting zero entities" $
|
||||
request methodDelete "/items?id=lt.0"
|
||||
@@ -277,18 +301,24 @@ spec =
|
||||
, matchHeaders = [matchContentTypeSingular]
|
||||
}
|
||||
|
||||
it "executes the proc exactly once per request" $ do
|
||||
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
||||
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|]
|
||||
|
||||
request methodPost "/rpc/setprojects" [singular]
|
||||
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
|
||||
it "fails for multiple rows with rolled back changes" $ do
|
||||
post "/rpc/getproject?select=id,name"
|
||||
[json| {"id": 1} |]
|
||||
`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]
|
||||
}
|
||||
[json|[{"id":1,"name":"Windows 7"}]|]
|
||||
|
||||
-- should not actually have executed the function
|
||||
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
|
||||
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7"}]|]
|
||||
request methodPost "/rpc/setprojects"
|
||||
[("Prefer", "tx=commit"), singular]
|
||||
[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"}]|]
|
||||
|
||||
@@ -2,6 +2,7 @@ module Feature.UnicodeSpec where
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
@@ -16,9 +17,19 @@ spec =
|
||||
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
`shouldRespondWith` "[]"
|
||||
|
||||
void $ post "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
[json| { "هویت": 1 } |]
|
||||
request methodPost "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
[("Prefer", "tx=commit"), ("Prefer", "return=representation")]
|
||||
[json| { "هویت": 1 } |]
|
||||
`shouldRespondWith`
|
||||
[json| [{ "هویت": 1 }] |]
|
||||
{ matchStatus = 201 }
|
||||
|
||||
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
`shouldRespondWith` [json| [{ "هویت": 1 }] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
`shouldRespondWith`
|
||||
[json| [{ "هویت": 1 }] |]
|
||||
|
||||
request methodDelete "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
[("Prefer", "tx=commit")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
204
|
||||
|
||||
+62
-42
@@ -51,22 +51,31 @@ spec = do
|
||||
|
||||
context "in a nonempty table" $ do
|
||||
it "can update a single item" $ do
|
||||
g <- get "/items?id=eq.42"
|
||||
liftIO $ simpleHeaders g
|
||||
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*"
|
||||
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
|
||||
pure p `shouldRespondWith` ""
|
||||
{ matchStatus = 204,
|
||||
matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
liftIO $ lookup hContentType (simpleHeaders p) `shouldBe` Nothing
|
||||
get "/items?id=eq.42"
|
||||
`shouldRespondWith`
|
||||
[json|[]|]
|
||||
|
||||
request methodPatch "/items?id=eq.2"
|
||||
[("Prefer", "tx=commit")]
|
||||
[json| { "id":42 } |]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"
|
||||
, "Preference-Applied" <:> "tx=commit" ]
|
||||
}
|
||||
|
||||
-- check it really got updated
|
||||
g' <- get "/items?id=eq.42"
|
||||
liftIO $ simpleHeaders g'
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*"
|
||||
get "/items?id=eq.42"
|
||||
`shouldRespondWith`
|
||||
[json|[ { "id": 42 } ]|]
|
||||
|
||||
-- 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" $
|
||||
request methodPatch "/items?id=eq.999999"
|
||||
@@ -91,23 +100,38 @@ spec = do
|
||||
}
|
||||
|
||||
it "can update multiple items" $ do
|
||||
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||
[json| { non_nullable_string: "a" } |]
|
||||
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||
[json| { non_nullable_string: "b" } |]
|
||||
_ <- request methodPatch
|
||||
"/auto_incrementing_pk?non_nullable_string=eq.a" []
|
||||
[json| { non_nullable_string: "c" } |]
|
||||
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
|
||||
liftIO $ simpleHeaders g
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-9/\\*"
|
||||
get "/no_pk?select=a&b=eq.1"
|
||||
`shouldRespondWith`
|
||||
[json|[]|]
|
||||
|
||||
request methodPatch "/no_pk?b=eq.0"
|
||||
[("Prefer", "tx=commit")]
|
||||
[json| { b: "1" } |]
|
||||
`shouldRespondWith`
|
||||
""
|
||||
{ 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
|
||||
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
|
||||
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |]
|
||||
get "/no_pk?a=eq.keepme" `shouldRespondWith`
|
||||
[json| [{ a: "keepme", b: null }] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
request methodPatch "/no_pk?a=eq.1"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { b: null } |]
|
||||
`shouldRespondWith`
|
||||
[json| [{ a: "1", b: null }] |]
|
||||
|
||||
context "filtering by a computed column" $ do
|
||||
it "is successful" $
|
||||
@@ -250,23 +274,19 @@ spec = do
|
||||
|
||||
context "with unicode values" $
|
||||
it "succeeds and returns values intact" $ do
|
||||
void $ request methodPost "/no_pk" []
|
||||
[json| { "a":"patchme", "b":"patchme" } |]
|
||||
let payload = [json| { "a":"圍棋", "b":"¥" } |]
|
||||
p <- request methodPatch "/no_pk?a=eq.patchme&b=eq.patchme"
|
||||
[("Prefer", "return=representation")] payload
|
||||
liftIO $ do
|
||||
simpleBody p `shouldBe` "["<>payload<>"]"
|
||||
simpleStatus p `shouldBe` ok200
|
||||
request methodPatch "/no_pk?a=eq.1"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "a":"圍棋", "b":"¥" } |]
|
||||
`shouldRespondWith`
|
||||
[json|[ { "a":"圍棋", "b":"¥" } ]|]
|
||||
|
||||
context "PATCH with ?columns parameter" $ do
|
||||
it "ignores json keys not included in ?columns" $ do
|
||||
post "/articles?columns=id,body" [json| {"id": 200} |]
|
||||
request methodPatch "/articles?id=eq.200&columns=body" [("Prefer", "return=representation")]
|
||||
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`
|
||||
[json|[{"id": 200, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [] }
|
||||
request methodPatch "/articles?id=eq.1&columns=body"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |]
|
||||
`shouldRespondWith`
|
||||
[json|[{"id": 1, "body": "Some real content", "owner": "postgrest_test_anonymous"}]|]
|
||||
|
||||
it "ignores json keys and gives 404 if no record updated" $
|
||||
request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]
|
||||
|
||||
+131
-78
@@ -100,30 +100,38 @@ spec =
|
||||
}
|
||||
|
||||
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")]
|
||||
[json| [
|
||||
{ "unique_key": 1, "value": "B" },
|
||||
{ "unique_key": 2, "value": "C" },
|
||||
{ "unique_key": 3, "value": "D" }
|
||||
]|] `shouldRespondWith` [json| [
|
||||
{ "unique_key": 3, "value": "D" }
|
||||
]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||
}
|
||||
request methodPost "/single_unique?on_conflict=unique_key"
|
||||
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json| [
|
||||
{ "unique_key": 1, "value": "B" },
|
||||
{ "unique_key": 2, "value": "C" },
|
||||
{ "unique_key": 3, "value": "D" }
|
||||
]|]
|
||||
`shouldRespondWith`
|
||||
[json| [
|
||||
{ "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" $
|
||||
request methodPost "/compound_unique?on_conflict=key1,key2" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json| [
|
||||
{ "key1": 1, "key2": 1, "value": "B" },
|
||||
{ "key1": 1, "key2": 2, "value": "C" },
|
||||
{ "key1": 1, "key2": 3, "value": "D" }
|
||||
]|] `shouldRespondWith` [json| [
|
||||
{ "key1": 1, "key2": 3, "value": "D" }
|
||||
]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||
}
|
||||
request methodPost "/compound_unique?on_conflict=key1,key2"
|
||||
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json| [
|
||||
{ "key1": 1, "key2": 1, "value": "B" },
|
||||
{ "key1": 1, "key2": 2, "value": "C" },
|
||||
{ "key1": 1, "key2": 3, "value": "D" }
|
||||
]|]
|
||||
`shouldRespondWith`
|
||||
[json| [
|
||||
{ "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
|
||||
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
@@ -233,58 +241,93 @@ spec =
|
||||
|
||||
context "Inserting row" $ do
|
||||
it "succeeds on table with single pk col" $ do
|
||||
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` "[]"
|
||||
put "/tiobe_pls?name=eq.Go" [json| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 204
|
||||
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
-- assert that the next request will indeed be an insert
|
||||
get "/tiobe_pls?name=eq.Go"
|
||||
`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
|
||||
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
||||
`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
|
||||
-- assert that the next request will indeed be an insert
|
||||
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
||||
`shouldRespondWith`
|
||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
[json|[]|]
|
||||
|
||||
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
|
||||
get "/only_pk?id=eq.10" `shouldRespondWith` "[]"
|
||||
put "/only_pk?id=eq.10" [json|[ { "id": 10 } ]|] `shouldRespondWith` 204
|
||||
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
-- assert that the next request will indeed be an insert
|
||||
get "/only_pk?id=eq.10"
|
||||
`shouldRespondWith`
|
||||
[json|[]|]
|
||||
|
||||
request methodPut "/only_pk?id=eq.10"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|[ { "id": 10 } ]|]
|
||||
`shouldRespondWith`
|
||||
[json|[ { "id": 10 } ]|]
|
||||
|
||||
context "Updating row" $ do
|
||||
it "succeeds on table with single pk col" $ do
|
||||
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json|[ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
put "/tiobe_pls?name=eq.Go" [json| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204
|
||||
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 } ] |]
|
||||
-- assert that the next request will indeed be an update
|
||||
get "/tiobe_pls?name=eq.Java"
|
||||
`shouldRespondWith`
|
||||
[json|{ "name": "Go", "rank": 19 }|]
|
||||
{ matchStatus = 200 , matchHeaders = [matchContentTypeSingular] }
|
||||
[json|[ { "name": "Java", "rank": 1 } ]|]
|
||||
|
||||
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
|
||||
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`
|
||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||
`shouldRespondWith` 204
|
||||
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
|
||||
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$24,000.00", "company": "One-Up Realty", "occupation": "Author" } ]|]
|
||||
|
||||
request methodPut "/employees?first_name=eq.Frances M.&last_name=eq.Roe"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||
`shouldRespondWith`
|
||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||
|
||||
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] }
|
||||
put "/only_pk?id=eq.10" [json|[ { "id": 10 } ]|] `shouldRespondWith` 204
|
||||
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
-- assert that the next request will indeed be an update
|
||||
get "/only_pk?id=eq.1"
|
||||
`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" $
|
||||
request methodPut "/tiobe_pls?name=eq.Ruby"
|
||||
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
|
||||
@@ -292,28 +335,38 @@ spec =
|
||||
`shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] }
|
||||
|
||||
context "with a camel case pk column" $ do
|
||||
it "works with POST and merge-duplicates/ignore-duplicates headers" $ do
|
||||
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||
[json| [
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|] `shouldRespondWith` [json|[
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||
}
|
||||
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json| [
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|] `shouldRespondWith` [json|[]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||
}
|
||||
it "works with POST and merge-duplicates" $ do
|
||||
request methodPost "/UnitTest"
|
||||
[("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||
[json|[
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|]
|
||||
`shouldRespondWith`
|
||||
[json|[
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates"]
|
||||
}
|
||||
|
||||
it "works with POST and ignore-duplicates headers" $ do
|
||||
request methodPost "/UnitTest"
|
||||
[("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
|
||||
put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204
|
||||
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
|
||||
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]
|
||||
|
||||
+9
-16
@@ -96,39 +96,32 @@ main = do
|
||||
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
|
||||
multipleSchemaApp = appDbs testMultipleSchemaCfg
|
||||
|
||||
let reset, analyze :: IO ()
|
||||
reset = resetDb testDbConn
|
||||
let analyze :: IO ()
|
||||
analyze = do
|
||||
analyzeTable testDbConn "items"
|
||||
analyzeTable testDbConn "child_entities"
|
||||
|
||||
specs = uncurry describe <$> [
|
||||
("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion)
|
||||
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
||||
("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec actualPgVersion)
|
||||
, ("Feature.AuthSpec" , Feature.AuthSpec.spec actualPgVersion)
|
||||
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.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.OpenApiSpec" , Feature.OpenApiSpec.spec)
|
||||
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec)
|
||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
||||
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
|
||||
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
||||
, ("Feature.RollbackAllowedSpec" , Feature.RollbackSpec.allowed)
|
||||
, ("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)
|
||||
]
|
||||
|
||||
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
|
||||
-- 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
|
||||
|
||||
-- we analyze to get accurate results from EXPLAIN
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configRawMediaTypes = []
|
||||
, configJWKS = parseSecret <$> secret
|
||||
, configLogLevel = LogCrit
|
||||
, configTxRollbackAll = False
|
||||
, configTxRollbackAll = True
|
||||
, configTxAllowOverride = True
|
||||
, configDbPrepared = True
|
||||
}
|
||||
|
||||
Vendored
+12
@@ -359,6 +359,9 @@ TRUNCATE TABLE child_entities CASCADE;
|
||||
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 (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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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');
|
||||
|
||||
Vendored
-2
@@ -144,8 +144,6 @@ GRANT USAGE ON SEQUENCE
|
||||
, items_id_seq
|
||||
, callcounter_count
|
||||
, leak_id_seq
|
||||
, v1.children_id_seq
|
||||
, v2.children_id_seq
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
-- Privileges for non anonymous users
|
||||
|
||||
Vendored
+13
-3
@@ -365,6 +365,16 @@ CREATE FUNCTION callcounter() RETURNS bigint
|
||||
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: -
|
||||
--
|
||||
@@ -1825,8 +1835,8 @@ create table v1.parents (
|
||||
);
|
||||
|
||||
create table v1.children (
|
||||
id serial primary key
|
||||
, name text
|
||||
id int primary key
|
||||
, name text
|
||||
, parent_id int
|
||||
, constraint parent foreign key(parent_id)
|
||||
references v1.parents(id)
|
||||
@@ -1843,7 +1853,7 @@ create table v2.parents (
|
||||
);
|
||||
|
||||
create table v2.children (
|
||||
id serial primary key
|
||||
id int primary key
|
||||
, name text
|
||||
, parent_id int
|
||||
, constraint parent foreign key(parent_id)
|
||||
|
||||
Reference in New Issue
Block a user