678 lines
29 KiB
Haskell
678 lines
29 KiB
Haskell
module Feature.InsertSpec where
|
|
|
|
import qualified Data.Aeson as JSON
|
|
|
|
import Control.Monad (replicateM_, void)
|
|
import Data.List (lookup)
|
|
import Data.Maybe (fromJust)
|
|
import Network.Wai (Application)
|
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
|
import Test.Hspec hiding (pendingWith)
|
|
import Test.Hspec.Wai.Matcher (bodyEquals)
|
|
import TestTypes (CompoundPK (..), IncPK (..))
|
|
|
|
import Network.HTTP.Types
|
|
import Test.Hspec.Wai
|
|
import Test.Hspec.Wai.JSON
|
|
import Text.Heredoc
|
|
|
|
import PostgREST.Types (PgVersion, pgVersion112)
|
|
import Protolude hiding (get)
|
|
import SpecHelper
|
|
|
|
spec :: PgVersion -> SpecWith ((), Application)
|
|
spec actualPgVersion = do
|
|
describe "Posting new record" $ do
|
|
context "disparate json types" $ do
|
|
it "accepts disparate json types" $ do
|
|
p <- post "/menagerie"
|
|
[json| {
|
|
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
|
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
|
|
, "enum": "foo"
|
|
} |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` ""
|
|
simpleStatus p `shouldBe` created201
|
|
-- should not have content type set when body is empty
|
|
lookup hContentType (simpleHeaders p) `shouldBe` Nothing
|
|
|
|
it "filters columns in result using &select" $
|
|
request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=representation")]
|
|
[json| [{
|
|
"integer": 14, "double": 3.14159, "varchar": "testing!"
|
|
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
|
|
, "enum": "foo"
|
|
}] |] `shouldRespondWith` [str|[{"integer":14,"varchar":"testing!"}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "non uniform json array" $ do
|
|
it "rejects json array that isn't exclusivily composed of objects" $
|
|
post "/articles"
|
|
[json| [{"id": 100, "body": "xxxxx"}, 123, "xxxx", {"id": 111, "body": "xxxx"}] |]
|
|
`shouldRespondWith`
|
|
[json| {"message":"All object keys must match"} |]
|
|
{ matchStatus = 400
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
it "rejects json array that has objects with different keys" $
|
|
post "/articles"
|
|
[json| [{"id": 100, "body": "xxxxx"}, {"id": 111, "body": "xxxx", "owner": "me"}] |]
|
|
`shouldRespondWith`
|
|
[json| {"message":"All object keys must match"} |]
|
|
{ matchStatus = 400
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "requesting full representation" $ do
|
|
it "includes related data after insert" $
|
|
request methodPost "/projects?select=id,name,clients(id,name)"
|
|
[("Prefer", "return=representation"), ("Prefer", "count=exact")]
|
|
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` [str|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [ matchContentTypeJson
|
|
, "Location" <:> "/projects?id=eq.6"
|
|
, "Content-Range" <:> "*/1" ]
|
|
}
|
|
|
|
it "can rename and cast the selected columns" $
|
|
request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text"
|
|
[("Prefer", "return=representation")]
|
|
[str|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith`
|
|
[str|[{"pId":"7","pName":"New Project","cId":"2"}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [ matchContentTypeJson
|
|
, "Location" <:> "/projects?id=eq.7"
|
|
, "Content-Range" <:> "*/*" ]
|
|
}
|
|
|
|
context "from an html form" $
|
|
it "accepts disparate json types" $ do
|
|
p <- request methodPost "/menagerie"
|
|
[("Content-Type", "application/x-www-form-urlencoded")]
|
|
("integer=7&double=2.71828&varchar=forms+are+fun&" <>
|
|
"boolean=false&date=1900-01-01&money=$3.99&enum=foo")
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` ""
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
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
|
|
|
|
context "into a table with simple pk" $
|
|
it "fails with 400 and error" $
|
|
post "/simple_pk" [json| { "extra":"foo"} |]
|
|
`shouldRespondWith`
|
|
[json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]
|
|
{ matchStatus = 400
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "into a table with no pk" $ do
|
|
it "succeeds with 201 and a link including all fields" $ do
|
|
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` ""
|
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
it "returns full details of inserted record if asked" $ do
|
|
p <- request methodPost "/no_pk"
|
|
[("Prefer", "return=representation")]
|
|
[json| { "a":"bar", "b":"baz" } |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` [json| [{ "a":"bar", "b":"baz" }] |]
|
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
it "returns empty array when no items inserted, and return=rep" $ do
|
|
p <- request methodPost "/no_pk"
|
|
[("Prefer", "return=representation")]
|
|
[json| [] |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` [json| [] |]
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
it "can post nulls" $ do
|
|
p <- request methodPost "/no_pk"
|
|
[("Prefer", "return=representation")]
|
|
[json| { "a":null, "b":"foo" } |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` [json| [{ "a":null, "b":"foo" }] |]
|
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=is.null&b=eq.foo"
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
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
|
|
|
|
context "with bulk insert" $
|
|
it "returns 201 but no location header" $ do
|
|
let bulkData = [json| [ {"k1":21, "k2":"hello world"}
|
|
, {"k1":22, "k2":"bye for now"}]
|
|
|]
|
|
p <- request methodPost "/compound_pk" [] bulkData
|
|
liftIO $ do
|
|
simpleStatus p `shouldBe` created201
|
|
lookup hLocation (simpleHeaders p) `shouldBe` Nothing
|
|
|
|
context "with invalid json payload" $
|
|
it "fails with 400 and error" $
|
|
post "/simple_pk" "}{ x = 2"
|
|
`shouldRespondWith`
|
|
[json|{"message":"Error in $: Failed reading: not a valid json value"}|]
|
|
{ matchStatus = 400
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "with valid json payload" $
|
|
it "succeeds and returns 201 created" $
|
|
post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201
|
|
|
|
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" } |]
|
|
`shouldRespondWith`
|
|
[json|{"hint":null,"details":"Key (k)=(k1) already exists.","code":"23505","message":"duplicate key value violates unique constraint \"contacts_pkey\""}|]
|
|
{ matchStatus = 409
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "attempting to insert a row with conflicting unique constraint" $
|
|
it "fails returning a 409 Conflict" $
|
|
post "/withUnique" [json| { "uni":"nodup", "extra":"e2" } |] `shouldRespondWith` 409
|
|
|
|
context "jsonb" $ do
|
|
it "serializes nested object" $ do
|
|
let inserted = [json| { "data": { "foo":"bar" } } |]
|
|
location = "/json?data=eq.%7B%22foo%22%3A%22bar%22%7D"
|
|
request methodPost "/json"
|
|
[("Prefer", "return=representation")]
|
|
inserted
|
|
`shouldRespondWith` [str|[{"data":{"foo":"bar"}}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Location" <:> location]
|
|
}
|
|
|
|
it "serializes nested array" $ do
|
|
let inserted = [json| { "data": [1,2,3] } |]
|
|
location = "/json?data=eq.%5B1%2C2%2C3%5D"
|
|
request methodPost "/json"
|
|
[("Prefer", "return=representation")]
|
|
inserted
|
|
`shouldRespondWith` [str|[{"data":[1,2,3]}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Location" <:> location]
|
|
}
|
|
|
|
context "empty objects" $ do
|
|
it "successfully inserts a row with all-default columns" $ do
|
|
post "/items" "{}" `shouldRespondWith` ""
|
|
{ matchStatus = 201
|
|
, matchHeaders = []
|
|
}
|
|
post "/items" "[{}]" `shouldRespondWith` ""
|
|
{ matchStatus = 201
|
|
, matchHeaders = []
|
|
}
|
|
|
|
it "successfully inserts two rows with all-default columns" $
|
|
post "/items" "[{}, {}]" `shouldRespondWith` ""
|
|
{ matchStatus = 201
|
|
, matchHeaders = []
|
|
}
|
|
|
|
context "POST with ?columns parameter" $ do
|
|
it "ignores json keys not included in ?columns" $ do
|
|
request methodPost "/articles?columns=id,body" [("Prefer", "return=representation")]
|
|
[json| {"id": 200, "body": "xxx", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith`
|
|
[json|[{"id": 200, "body": "xxx", "owner": "postgrest_test_anonymous"}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [] }
|
|
request methodPost "/articles?columns=id,body&select=id,body" [("Prefer", "return=representation")]
|
|
[json| [
|
|
{"id": 201, "body": "yyy", "smth": "here", "other": "stuff", "fake_id": 13},
|
|
{"id": 202, "body": "zzz", "garbage": "%%$&", "kkk": "jjj"},
|
|
{"id": 203, "body": "aaa", "hey": "ho"} ]|] `shouldRespondWith`
|
|
[json|[
|
|
{"id": 201, "body": "yyy"},
|
|
{"id": 202, "body": "zzz"},
|
|
{"id": 203, "body": "aaa"} ]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [] }
|
|
|
|
-- TODO parse columns error message needs to be improved
|
|
it "disallows blank ?columns" $
|
|
post "/articles?columns="
|
|
[json|[
|
|
{"id": 204, "body": "yyy"},
|
|
{"id": 205, "body": "zzz"}]|]
|
|
`shouldRespondWith`
|
|
[json| {"details":"unexpected end of input expecting field name (* or [a..z0..9_])","message":"\"failed to parse columns parameter ()\" (line 1, column 1)"} |]
|
|
{ matchStatus = 400
|
|
, matchHeaders = []
|
|
}
|
|
|
|
it "disallows array elements that are not json objects" $
|
|
post "/articles?columns=id,body"
|
|
[json|[
|
|
{"id": 204, "body": "yyy"},
|
|
333,
|
|
"asdf",
|
|
{"id": 205, "body": "zzz"}]|] `shouldRespondWith`
|
|
[json|{
|
|
"code": "22023",
|
|
"details": null,
|
|
"hint": null,
|
|
"message": "argument of json_populate_recordset must be an array of objects"}|]
|
|
{ matchStatus = 400
|
|
, matchHeaders = []
|
|
}
|
|
|
|
describe "CSV insert" $ do
|
|
context "disparate csv types" $
|
|
it "succeeds with multipart response" $ do
|
|
pendingWith "Decide on what to do with CSV insert"
|
|
let inserted = [str|integer,double,varchar,boolean,date,money,enum
|
|
|13,3.14159,testing!,false,1900-01-01,$3.99,foo
|
|
|12,0.1,a string,true,1929-10-01,12,bar
|
|
|]
|
|
request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted
|
|
`shouldRespondWith` ResponseMatcher
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
|
, matchBody = bodyEquals inserted
|
|
}
|
|
|
|
context "requesting full representation" $ do
|
|
it "returns full details of inserted record" $
|
|
request methodPost "/no_pk"
|
|
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
|
|
"a,b\nbar,baz"
|
|
`shouldRespondWith` "a,b\nbar,baz"
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
|
|
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
|
|
}
|
|
|
|
it "can post nulls" $
|
|
request methodPost "/no_pk"
|
|
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
|
|
"a,b\nNULL,foo"
|
|
`shouldRespondWith` "a,b\n,foo"
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
|
|
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
|
|
}
|
|
|
|
it "only returns the requested column header with its associated data" $
|
|
request methodPost "/projects?select=id"
|
|
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
|
|
"id,name,client_id\n8,Xenix,1\n9,Windows NT,1"
|
|
`shouldRespondWith` "id\n8\n9"
|
|
{ matchStatus = 201
|
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
|
|
"Content-Range" <:> "*/*"]
|
|
}
|
|
|
|
context "with wrong number of columns" $
|
|
it "fails for too few" $
|
|
request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
|
|
`shouldRespondWith`
|
|
[json|{"message":"All lines must have same number of fields"}|]
|
|
{ matchStatus = 400
|
|
, matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "with unicode values" $
|
|
it "succeeds and returns usable location header" $ do
|
|
let payload = [json| { "a":"圍棋", "b":"¥" } |]
|
|
p <- request methodPost "/no_pk"
|
|
[("Prefer", "return=representation")]
|
|
payload
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` "["<>payload<>"]"
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
let Just location = lookup hLocation $ simpleHeaders p
|
|
r <- get location
|
|
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
|
|
|
|
describe "Patching record" $ do
|
|
context "to unknown uri" $
|
|
it "indicates no table found by returning 404" $
|
|
request methodPatch "/fake" []
|
|
[json| { "real": false } |]
|
|
`shouldRespondWith` 404
|
|
|
|
context "on an empty table" $
|
|
it "indicates no records found to update by returning 404" $
|
|
request methodPatch "/empty_table" []
|
|
[json| { "extra":20 } |]
|
|
`shouldRespondWith` ""
|
|
{ matchStatus = 404,
|
|
matchHeaders = []
|
|
}
|
|
|
|
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
|
|
|
|
-- check it really got updated
|
|
g' <- get "/items?id=eq.42"
|
|
liftIO $ simpleHeaders g'
|
|
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*"
|
|
-- put value back for other tests
|
|
void $ request methodPatch "/items?id=eq.42" [] [json| { "id":2 } |]
|
|
|
|
it "returns empty array when no rows updated and return=rep" $
|
|
request methodPatch "/items?id=eq.999999"
|
|
[("Prefer", "return=representation")] [json| { "id":999999 } |]
|
|
`shouldRespondWith` "[]"
|
|
{
|
|
matchStatus = 404,
|
|
matchHeaders = []
|
|
}
|
|
|
|
it "gives a 404 when no rows updated" $
|
|
request methodPatch "/items?id=eq.99999999" []
|
|
[json| { "id": 42 } |]
|
|
`shouldRespondWith` 404
|
|
|
|
it "returns updated object as array when return=rep" $
|
|
request methodPatch "/items?id=eq.2"
|
|
[("Prefer", "return=representation")] [json| { "id":2 } |]
|
|
`shouldRespondWith` [str|[{"id":2}]|]
|
|
{ matchStatus = 200,
|
|
matchHeaders = ["Content-Range" <:> "0-0/*"]
|
|
}
|
|
|
|
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/\\*"
|
|
|
|
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] }
|
|
|
|
context "filtering by a computed column" $ do
|
|
it "is successful" $
|
|
request methodPatch
|
|
"/items?is_first=eq.true"
|
|
[("Prefer", "return=representation")]
|
|
[json| { id: 100 } |]
|
|
`shouldRespondWith` [json| [{ id: 100 }] |]
|
|
{ matchStatus = 200,
|
|
matchHeaders = [matchContentTypeJson, "Content-Range" <:> "0-0/*"]
|
|
}
|
|
|
|
it "indicates no records updated by returning 404" $
|
|
request methodPatch
|
|
"/items?always_true=eq.false"
|
|
[("Prefer", "return=representation")]
|
|
[json| { id: 100 } |]
|
|
`shouldRespondWith` "[]"
|
|
{ matchStatus = 404,
|
|
matchHeaders = []
|
|
}
|
|
|
|
it "can provide a representation" $ do
|
|
_ <- post "/items"
|
|
[json| { id: 1 } |]
|
|
request methodPatch
|
|
"/items?id=eq.1"
|
|
[("Prefer", "return=representation")]
|
|
[json| { id: 99 } |]
|
|
`shouldRespondWith` [json| [{id:99}] |]
|
|
{ matchHeaders = [matchContentTypeJson] }
|
|
-- put value back for other tests
|
|
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
|
|
|
|
it "makes no updates and returns 204, when patching with an empty json object/array" $ do
|
|
request methodPatch "/items" [] [json| {} |]
|
|
`shouldRespondWith` ""
|
|
{
|
|
matchStatus = 204,
|
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
|
}
|
|
|
|
request methodPatch "/items" [] [json| [] |]
|
|
`shouldRespondWith` ""
|
|
{
|
|
matchStatus = 204,
|
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
|
}
|
|
|
|
request methodPatch "/items" [] [json| [{}] |]
|
|
`shouldRespondWith` ""
|
|
{
|
|
matchStatus = 204,
|
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
|
}
|
|
|
|
it "makes no updates and returns 200, when patching with an empty json object and return=rep" $
|
|
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]
|
|
`shouldRespondWith` "[]"
|
|
{
|
|
matchStatus = 200,
|
|
matchHeaders = ["Content-Range" <:> "*/*"]
|
|
}
|
|
|
|
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
|
|
|
|
context "PATCH with ?columns parameter" $ do
|
|
it "ignores json keys not included in ?columns" $
|
|
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 = [] }
|
|
|
|
it "ignores json keys and gives 404 if no record updated" $
|
|
request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]
|
|
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 404
|
|
|
|
describe "Row level permission" $
|
|
it "set user_id when inserting rows" $ do
|
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
|
|
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
|
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
|
|
|
|
p1 <- request methodPost "/authors_only"
|
|
[ auth, ("Prefer", "return=representation") ]
|
|
[json| { "secret": "nyancat" } |]
|
|
liftIO $ do
|
|
simpleBody p1 `shouldBe` [str|[{"owner":"jdoe","secret":"nyancat"}]|]
|
|
simpleStatus p1 `shouldBe` created201
|
|
|
|
p2 <- request methodPost "/authors_only"
|
|
-- jwt token for jroe
|
|
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]
|
|
[json| { "secret": "lolcat", "owner": "hacker" } |]
|
|
liftIO $ do
|
|
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
|
|
simpleStatus p2 `shouldBe` created201
|
|
|
|
context "tables with self reference foreign keys" $ do
|
|
it "embeds parent after insert" $
|
|
request methodPost "/web_content?select=id,name,parent_content:p_web_id(name)"
|
|
[("Prefer", "return=representation")]
|
|
[json|{"id":6, "name":"wot", "p_web_id":4}|]
|
|
`shouldRespondWith`
|
|
[json|[{"id":6,"name":"wot","parent_content":{"name":"wut"}}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]
|
|
}
|
|
|
|
it "embeds childs after update" $
|
|
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
|
|
[("Prefer", "return=representation")]
|
|
[json|{"name": "tardis-patched"}|]
|
|
`shouldRespondWith`
|
|
[json|
|
|
[ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
|
|]
|
|
{ matchStatus = 200,
|
|
matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
it "embeds parent, childs and grandchilds after update" $
|
|
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"
|
|
[("Prefer", "return=representation")]
|
|
[json|{"name": "tardis-patched-2"}|]
|
|
`shouldRespondWith`
|
|
[json| [
|
|
{
|
|
"id": 0,
|
|
"name": "tardis-patched-2",
|
|
"parent_content": { "name": "wat" },
|
|
"web_content": [
|
|
{ "name": "fezz", "web_content": [ { "name": "wut" } ] },
|
|
{ "name": "foo", "web_content": [] },
|
|
{ "name": "bar", "web_content": [] }
|
|
]
|
|
}
|
|
] |]
|
|
{ matchStatus = 200,
|
|
matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
it "embeds childs after update without explicitly including the id in the ?select" $
|
|
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
|
|
[("Prefer", "return=representation")]
|
|
[json|{"name": "tardis-patched"}|]
|
|
`shouldRespondWith`
|
|
[json|
|
|
[ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
|
|]
|
|
{ matchStatus = 200,
|
|
matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
it "embeds an M2M relationship plus parent after update" $
|
|
request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"
|
|
[("Prefer", "return=representation")]
|
|
[json|{"name": "Kevin Malone"}|]
|
|
`shouldRespondWith`
|
|
[json|[
|
|
{
|
|
"name": "Kevin Malone",
|
|
"tasks": [
|
|
{ "name": "Design w7", "project": { "name": "Windows 7" } },
|
|
{ "name": "Code w7", "project": { "name": "Windows 7" } },
|
|
{ "name": "Design w10", "project": { "name": "Windows 10" } },
|
|
{ "name": "Code w10", "project": { "name": "Windows 10" } }
|
|
]
|
|
}
|
|
]|]
|
|
{ matchStatus = 200,
|
|
matchHeaders = [matchContentTypeJson]
|
|
}
|
|
|
|
context "table with limited privileges" $ do
|
|
it "succeeds inserting if correct select is applied" $
|
|
request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")]
|
|
[json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [str|[{"article_id":2,"user_id":1}]|]
|
|
{ matchStatus = 201
|
|
, matchHeaders = []
|
|
}
|
|
|
|
it "fails inserting if more columns are selected" $
|
|
request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")]
|
|
[json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith` (
|
|
if actualPgVersion >= pgVersion112 then
|
|
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
|
|
else
|
|
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
|
|
)
|
|
{ matchStatus = 401
|
|
, matchHeaders = []
|
|
}
|
|
|
|
it "fails inserting if select is not specified" $
|
|
request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
|
|
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` (
|
|
if actualPgVersion >= pgVersion112 then
|
|
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
|
|
else
|
|
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
|
|
)
|
|
{ matchStatus = 401
|
|
, matchHeaders = []
|
|
}
|
|
|
|
it "can insert in a table with no select and return=minimal" $ do
|
|
p <- request methodPost "/insertonly"
|
|
[("Prefer", "return=minimal")]
|
|
[json| { "v":"some value" } |]
|
|
liftIO $ do
|
|
simpleBody p `shouldBe` ""
|
|
simpleStatus p `shouldBe` created201
|
|
|
|
it "succeeds updating row and gives a 204 when using return=minimal" $
|
|
request methodPatch "/app_users?id=eq.1" [("Prefer", "return=minimal")]
|
|
[json| { "password": "passxyz" } |]
|
|
`shouldRespondWith` 204
|
|
|
|
it "can update without return=minimal and no explicit select" $
|
|
request methodPatch "/app_users?id=eq.1" []
|
|
[json| { "password": "passabc" } |]
|
|
`shouldRespondWith` 204
|