fix: Return 204 No Content without Content-Type for PUT

This commit is contained in:
Wolfgang Walther
2021-12-01 09:19:47 +01:00
committed by Wolfgang Walther
parent 1cb00d3c62
commit 0e20b47875
15 changed files with 373 additions and 193 deletions
+1
View File
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther - #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
- #2058, Return 204 No Content without Content-Type for PUT - @wolfgangwalther
## [9.0.0] - 2021-11-25 ## [9.0.0] - 2021-11-25
+1 -1
View File
@@ -374,7 +374,7 @@ handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
if iPreferRepresentation == Full then if iPreferRepresentation == Full then
response HTTP.status200 (contentTypeHeaders context) (LBS.fromStrict resBody) response HTTP.status200 (contentTypeHeaders context) (LBS.fromStrict resBody)
else else
response HTTP.status204 (contentTypeHeaders context) mempty response HTTP.status204 [] mempty
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleDelete identifier context@(RequestContext _ _ ApiRequest{..} _) = do handleDelete identifier context@(RequestContext _ _ ApiRequest{..} _) = do
+45 -20
View File
@@ -7,18 +7,23 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "Deleting" $ do describe "Deleting" $ do
context "existing record" $ do context "existing record" $ do
it "succeeds with 204 and deletion count" $ it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] "" request methodDelete "/items?id=eq.1"
`shouldRespondWith` "" []
{ matchStatus = 204 ""
, matchHeaders = ["Content-Range" <:> "*/*"] `shouldRespondWith`
} ""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
it "returns the deleted item and count if requested" $ it "returns the deleted item and count if requested" $
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] "" request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""
@@ -28,16 +33,24 @@ spec =
} }
it "ignores ?select= when return not set or return=minimal" $ do it "ignores ?select= when return not set or return=minimal" $ do
request methodDelete "/items?id=eq.3&select=id" [] "" request methodDelete "/items?id=eq.3&select=id"
`shouldRespondWith` "" []
{ matchStatus = 204 ""
, matchHeaders = ["Content-Range" <:> "*/*"] `shouldRespondWith`
} ""
request methodDelete "/items?id=eq.3&select=id" [("Prefer", "return=minimal")] "" { matchStatus = 204
`shouldRespondWith` "" , matchHeaders = [ matchHeaderAbsent hContentType
{ matchStatus = 204 , "Content-Range" <:> "*/*" ]
, matchHeaders = ["Content-Range" <:> "*/*"] }
} request methodDelete "/items?id=eq.3&select=id"
[("Prefer", "return=minimal")]
""
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
it "returns the deleted item and shapes the response" $ it "returns the deleted item and shapes the response" $
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] "" request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
@@ -84,9 +97,21 @@ spec =
} }
it "suceeds deleting the row with no explicit select when using return=minimal" $ it "suceeds deleting the row with no explicit select when using return=minimal" $
request methodDelete "/app_users?id=eq.2" [("Prefer", "return=minimal")] mempty request methodDelete "/app_users?id=eq.2"
`shouldRespondWith` 204 [("Prefer", "return=minimal")]
mempty
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "suceeds deleting the row with no explicit select by default" $ it "suceeds deleting the row with no explicit select by default" $
request methodDelete "/app_users?id=eq.3" [] mempty request methodDelete "/app_users?id=eq.3"
`shouldRespondWith` 204 []
mempty
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
+8 -2
View File
@@ -19,8 +19,14 @@ spec :: SpecWith ((), Application)
spec = describe "OpenAPI Ignore Privileges" $ do spec = describe "OpenAPI Ignore Privileges" $ do
it "root path returns a valid openapi spec" $ do it "root path returns a valid openapi spec" $ do
validateOpenApiResponse [("Accept", "application/openapi+json")] validateOpenApiResponse [("Accept", "application/openapi+json")]
request methodHead "/" (acceptHdrs "application/openapi+json") "" request methodHead "/"
`shouldRespondWith` "" { matchStatus = 200 } (acceptHdrs "application/openapi+json")
""
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8" ]
}
describe "table" $ do describe "table" $ do
+103 -52
View File
@@ -45,20 +45,30 @@ spec actualPgVersion = do
} }
it "ignores &select when return not set or using return=minimal" $ do it "ignores &select when return not set or using return=minimal" $ do
request methodPost "/menagerie?select=integer,varchar" [] request methodPost "/menagerie?select=integer,varchar"
[json| [{ []
"integer": 15, "double": 3.14159, "varchar": "testing!" [json| [{
, "boolean": false, "date": "1900-01-01", "money": "$3.99" "integer": 15, "double": 3.14159, "varchar": "testing!",
, "enum": "foo" "boolean": false, "date": "1900-01-01", "money": "$3.99",
}] |] `shouldRespondWith` "" "enum": "foo"
{ matchStatus = 201 } }] |]
request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=minimal")] `shouldRespondWith`
[json| [{ ""
"integer": 16, "double": 3.14159, "varchar": "testing!" { matchStatus = 201
, "boolean": false, "date": "1900-01-01", "money": "$3.99" , matchHeaders = [matchHeaderAbsent hContentType]
, "enum": "foo" }
}] |] `shouldRespondWith` "" request methodPost "/menagerie?select=integer,varchar"
{ matchStatus = 201 } [("Prefer", "return=minimal")]
[json| [{
"integer": 16, "double": 3.14159, "varchar": "testing!",
"boolean": false, "date": "1900-01-01", "money": "$3.99",
"enum": "foo"
}] |]
`shouldRespondWith`
""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType]
}
context "non uniform json array" $ do context "non uniform json array" $ do
it "rejects json array that isn't exclusivily composed of objects" $ it "rejects json array that isn't exclusivily composed of objects" $
@@ -113,29 +123,41 @@ spec actualPgVersion = do
context "requesting headers only representation" $ do context "requesting headers only representation" $ do
it "should not throw and return location header when selecting without PK" $ it "should not throw and return location header when selecting without PK" $
request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")] request methodPost "/projects?select=name,client_id"
[json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` "" [("Prefer", "return=headers-only")]
{ matchStatus = 201 [json|{"id":11,"name":"New Project","client_id":2}|]
, matchHeaders = [ "Location" <:> "/projects?id=eq.11" `shouldRespondWith`
, "Content-Range" <:> "*/*" ] ""
}
when (actualPgVersion >= pgVersion110) $
it "should not throw and return location header for partitioned tables when selecting without PK" $
request methodPost "/car_models" [("Prefer", "return=headers-only")]
[json|{"name":"Enzo","year":2021}|] `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021" , matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/projects?id=eq.11"
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
when (actualPgVersion >= pgVersion110) $
it "should not throw and return location header for partitioned tables when selecting without PK" $
request methodPost "/car_models"
[("Prefer", "return=headers-only")]
[json|{"name":"Enzo","year":2021}|]
`shouldRespondWith`
""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"
, "Content-Range" <:> "*/*" ]
}
context "requesting no representation" $ context "requesting no representation" $
it "should not throw and return no location header when selecting without PK" $ it "should not throw and return no location header when selecting without PK" $
request methodPost "/projects?select=name,client_id" [] request methodPost "/projects?select=name,client_id"
[json|{"id":12,"name":"New Project","client_id":2}|] `shouldRespondWith` "" []
{ matchStatus = 201 [json|{"id":12,"name":"New Project","client_id":2}|]
, matchHeaders = [ matchHeaderAbsent hLocation ] `shouldRespondWith`
} ""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
}
context "from an html form" $ context "from an html form" $
it "accepts disparate json types" $ do it "accepts disparate json types" $ do
@@ -145,7 +167,9 @@ spec actualPgVersion = do
"boolean=false&date=1900-01-01&money=$3.99&enum=foo") "boolean=false&date=1900-01-01&money=$3.99&enum=foo")
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 } { matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType ]
}
context "with no pk supplied" $ do context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" $ context "into a table with auto-incrementing pk" $
@@ -157,12 +181,14 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
[json|""|] [json|""|]
request methodPost "/auto_incrementing_pk" [("Prefer", "return=headers-only")] request methodPost "/auto_incrementing_pk"
[("Prefer", "return=headers-only")]
[json| { "non_nullable_string":"not null"} |] [json| { "non_nullable_string":"not null"} |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Location" <:> "/auto_incrementing_pk?id=eq.2" ] , matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/auto_incrementing_pk?id=eq.2" ]
} }
context "into a table with simple pk" $ context "into a table with simple pk" $
@@ -180,11 +206,13 @@ spec actualPgVersion = do
context "into a table with no pk" $ do context "into a table with no pk" $ do
it "succeeds with 201 but no location header" $ do it "succeeds with 201 but no location header" $ do
post "/no_pk" [json| { "a":"foo", "b":"bar" } |] post "/no_pk"
[json| { "a":"foo", "b":"bar" } |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchHeaderAbsent hLocation] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
} }
it "returns full details of inserted record if asked" $ do it "returns full details of inserted record if asked" $ do
@@ -231,11 +259,14 @@ spec actualPgVersion = do
let bulkData = [json| [ {"k1":21, "k2":"hello world"} let bulkData = [json| [ {"k1":21, "k2":"hello world"}
, {"k1":22, "k2":"bye for now"}] , {"k1":22, "k2":"bye for now"}]
|] |]
request methodPost "/compound_pk" [] bulkData request methodPost "/compound_pk"
[]
bulkData
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchHeaderAbsent hLocation] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
} }
context "with invalid json payload" $ context "with invalid json payload" $
@@ -258,7 +289,13 @@ spec actualPgVersion = do
context "with valid json payload" $ context "with valid json payload" $
it "succeeds and returns 201 created" $ it "succeeds and returns 201 created" $
post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201 post "/simple_pk"
[json| { "k":"k1", "extra":"e1" } |]
`shouldRespondWith`
""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType]
}
context "attempting to insert a row with the same primary key" $ context "attempting to insert a row with the same primary key" $
it "fails returning a 409 Conflict" $ it "fails returning a 409 Conflict" $
@@ -293,20 +330,26 @@ spec actualPgVersion = do
context "empty objects" $ do context "empty objects" $ do
it "successfully inserts a row with all-default columns" $ do it "successfully inserts a row with all-default columns" $ do
post "/items" "{}" `shouldRespondWith` "" post "/items"
{ matchStatus = 201 [json|{}|]
, matchHeaders = [] `shouldRespondWith`
} ""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType]
}
post "/items" "[{}]" `shouldRespondWith` "" post "/items" "[{}]" `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [] , matchHeaders = []
} }
it "successfully inserts two rows with all-default columns" $ it "successfully inserts two rows with all-default columns" $
post "/items" "[{}, {}]" `shouldRespondWith` "" post "/items"
{ matchStatus = 201 [json|[{}, {}]|]
, matchHeaders = [] `shouldRespondWith`
} ""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "successfully inserts a row with all-default columns with prefer=rep" $ do it "successfully inserts a row with all-default columns with prefer=rep" $ do
-- reset pk sequence first to make test repeatable -- reset pk sequence first to make test repeatable
@@ -455,7 +498,10 @@ spec actualPgVersion = do
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
"" ""
`shouldRespondWith` `shouldRespondWith`
204 ""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
describe "Row level permission" $ describe "Row level permission" $
it "set user_id when inserting rows" $ do it "set user_id when inserting rows" $ do
@@ -523,7 +569,9 @@ spec actualPgVersion = do
[json| { "v":"some value" } |] [json| { "v":"some value" } |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 } { matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType]
}
describe "Inserting into VIEWs" $ do describe "Inserting into VIEWs" $ do
context "requesting no representation" $ context "requesting no representation" $
@@ -533,7 +581,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hLocation ] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
} }
context "requesting header only representation" $ do context "requesting header only representation" $ do
@@ -543,7 +592,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Location" <:> "/compound_pk_view?k1=eq.1&k2=eq.test" , matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/compound_pk_view?k1=eq.1&k2=eq.test"
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -553,6 +603,7 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [ "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null" , matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null"
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
+7 -2
View File
@@ -22,8 +22,13 @@ spec :: PgVersion -> SpecWith ((), Application)
spec actualPgVersion = describe "OpenAPI" $ do spec actualPgVersion = describe "OpenAPI" $ do
it "root path returns a valid openapi spec" $ do it "root path returns a valid openapi spec" $ do
validateOpenApiResponse [("Accept", "application/openapi+json")] validateOpenApiResponse [("Accept", "application/openapi+json")]
request methodHead "/" (acceptHdrs "application/openapi+json") "" request methodHead "/"
`shouldRespondWith` "" { matchStatus = 200 } (acceptHdrs "application/openapi+json") ""
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"]
}
it "should respond to openapi request on none root path with 415" $ it "should respond to openapi request on none root path with 415" $
request methodGet "/items" request methodGet "/items"
+16 -6
View File
@@ -47,23 +47,33 @@ spec =
context "count=estimated" $ do context "count=estimated" $ do
it "uses the query planner guess when query rows > maxRows" $ it "uses the query planner guess when query rows > maxRows" $
request methodHead "/getallprojects_view" [("Prefer", "count=estimated")] "" request methodHead "/getallprojects_view"
[("Prefer", "count=estimated")]
""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/2019"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-1/2019" ]
} }
it "gives exact count when query rows <= maxRows" $ it "gives exact count when query rows <= maxRows" $
request methodHead "/getallprojects_view?id=lt.3" [("Prefer", "count=estimated")] "" request methodHead "/getallprojects_view?id=lt.3"
[("Prefer", "count=estimated")]
""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchHeaders = ["Content-Range" <:> "0-1/2"] } { matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-1/2" ]
}
it "only uses the query planner guess if it's indeed greater than the exact count" $ it "only uses the query planner guess if it's indeed greater than the exact count" $
request methodHead "/get_projects_above_view" [("Prefer", "count=estimated")] "" request methodHead "/get_projects_above_view"
[("Prefer", "count=estimated")]
""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/3"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-1/3" ]
} }
+27 -17
View File
@@ -109,11 +109,10 @@ spec = do
context "when I don't want the count" $ do context "when I don't want the count" $ do
it "returns range Content-Range with /*" $ it "returns range Content-Range with /*" $
request methodGet "/menagerie" request methodGet "/menagerie"
[("Prefer", "count=none")] "" [("Prefer", "count=none")] ""
`shouldRespondWith` "[]" `shouldRespondWith`
{ matchStatus = 200 [json|[]|]
, matchHeaders = ["Content-Range" <:> "*/*"] { matchHeaders = ["Content-Range" <:> "*/*"] }
}
it "returns range Content-Range with range/*" $ it "returns range Content-Range with range/*" $
request methodGet "/items?order=id" request methodGet "/items?order=id"
@@ -163,11 +162,15 @@ spec = do
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"] , matchHeaders = ["Content-Range" <:> "2-4/*"]
} }
request methodHead "/items?select=id&order=id.asc&limit=3&offset=2" [] mempty request methodHead "/items?select=id&order=id.asc&limit=3&offset=2"
`shouldRespondWith` "" []
{ matchStatus = 200 mempty
, matchHeaders = ["Content-Range" <:> "2-4/*"] `shouldRespondWith`
} ""
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "2-4/*" ]
}
it "succeeds if offset equals 0 as a no-op" $ it "succeeds if offset equals 0 as a no-op" $
get "/items?select=id&offset=0&order=id" get "/items?select=id&offset=0&order=id"
@@ -231,7 +234,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/15"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-14/15" ]
} }
request methodHead "/child_entities" request methodHead "/child_entities"
@@ -240,7 +244,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-5/6" ]
} }
request methodHead "/getallprojects_view" request methodHead "/getallprojects_view"
@@ -249,7 +254,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-4/2019"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-4/2019" ]
} }
it "ignores limit/offset on the planned count" $ do it "ignores limit/offset on the planned count" $ do
@@ -259,7 +265,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "3-4/15"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "3-4/15" ]
} }
request methodHead "/child_entities?limit=2" request methodHead "/child_entities?limit=2"
@@ -268,7 +275,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/6"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-1/6" ]
} }
request methodHead "/getallprojects_view?limit=2" request methodHead "/getallprojects_view?limit=2"
@@ -277,7 +285,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-1/2019"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-1/2019" ]
} }
it "works with two levels" $ it "works with two levels" $
@@ -287,7 +296,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-5/6"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-5/6" ]
} }
context "with range headers" $ do context "with range headers" $ do
+7 -4
View File
@@ -7,7 +7,8 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper
-- two helpers functions to make sure that each test can setup and cleanup properly -- two helpers functions to make sure that each test can setup and cleanup properly
@@ -18,7 +19,8 @@ postItem =
[json|{"id":0}|] [json|{"id":0}|]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 } { matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType] }
-- removes Items left over from POST, PUT, and PATCH -- removes Items left over from POST, PUT, and PATCH
deleteItems = deleteItems =
@@ -27,7 +29,8 @@ deleteItems =
"" ""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 } { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] }
preferDefault = [("Prefer", "return=representation")] preferDefault = [("Prefer", "return=representation")]
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")] preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
@@ -52,7 +55,7 @@ shouldRespondToReads reqHeaders respHeaders = do
"" ""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchHeaders = respHeaders } { matchHeaders = matchContentTypeJson : respHeaders }
it "responds to GET on RPC" $ do it "responds to GET on RPC" $ do
request methodGet "/rpc/search?id=1" request methodGet "/rpc/search?id=1"
+22 -10
View File
@@ -7,7 +7,8 @@ import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Protolude hiding (get, put) import Protolude hiding (get, put)
import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
@@ -15,9 +16,11 @@ spec =
it "succeeds setting the headers on POST" $ it "succeeds setting the headers on POST" $
post "/items" post "/items"
[json|[{"id": 11111}]|] [json|[{"id": 11111}]|]
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = [ matchHeaderAbsent hContentType
, "X-Custom-Header" <:> "mykey=myval" ]
} }
it "succeeds setting the headers on GET and HEAD" $ do it "succeeds setting the headers on GET and HEAD" $ do
@@ -33,29 +36,37 @@ spec =
"" ""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchHeaders = ["Cache-Control" <:> "no-cache, no-store, must-revalidate"] } { matchHeaders = [ matchContentTypeJson
, "Cache-Control" <:> "no-cache, no-store, must-revalidate" ]
}
request methodHead "/projects" request methodHead "/projects"
[("Accept", "text/csv")] [("Accept", "text/csv")]
"" ""
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchHeaders = ["Content-Disposition" <:> "attachment; filename=projects.csv"] } { matchHeaders = [ "Content-Type" <:> "text/csv; charset=utf-8"
, "Content-Disposition" <:> "attachment; filename=projects.csv" ]
}
it "succeeds setting the headers on PATCH" $ it "succeeds setting the headers on PATCH" $
patch "/items?id=eq.1" patch "/items?id=eq.1"
[json|[{"id": 11111}]|] [json|[{"id": 11111}]|]
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = [ matchHeaderAbsent hContentType
, "X-Custom-Header" <:> "mykey=myval" ]
} }
it "succeeds setting the headers on PUT" $ it "succeeds setting the headers on PUT" $
put "/items?id=eq.1" put "/items?id=eq.1"
[json|[{"id": 1}]|] [json|[{"id": 1}]|]
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = [ matchHeaderAbsent hContentType
, "X-Custom-Header" <:> "mykey=myval" ]
} }
it "succeeds setting the headers on DELETE" $ it "succeeds setting the headers on DELETE" $
@@ -63,7 +74,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["X-Custom-Header" <:> "mykey=myval"] , matchHeaders = [ matchHeaderAbsent hContentType
, "X-Custom-Header" <:> "mykey=myval" ]
} }
it "can override the Content-Type header" $ do it "can override the Content-Type header" $ do
request methodHead "/clients?id=eq.1" request methodHead "/clients?id=eq.1"
+15 -10
View File
@@ -38,10 +38,12 @@ spec actualPgVersion =
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = ["Content-Range" <:> "0-0/*"]
} }
request methodHead "/rpc/getitemrange?min=2&max=4" request methodHead "/rpc/getitemrange?min=2&max=4"
(rangeHdrs (ByteRangeFromTo 0 0)) "" (rangeHdrs (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-0/*" ]
} }
it "includes total count if requested" $ do it "includes total count if requested" $ do
@@ -59,10 +61,12 @@ spec actualPgVersion =
, matchHeaders = ["Content-Range" <:> "0-0/2"] , matchHeaders = ["Content-Range" <:> "0-0/2"]
} }
request methodHead "/rpc/getitemrange?min=2&max=4" request methodHead "/rpc/getitemrange?min=2&max=4"
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) "" (rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 206 { matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"] , matchHeaders = [ matchContentTypeJson
, "Content-Range" <:> "0-0/2" ]
} }
it "returns proper json" $ do it "returns proper json" $ do
@@ -89,7 +93,8 @@ spec actualPgVersion =
} }
request methodHead "/rpc/getitemrange?min=2&max=4" request methodHead "/rpc/getitemrange?min=2&max=4"
(acceptHdrs "text/csv") "" (acceptHdrs "text/csv") ""
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"] , matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
} }
@@ -328,8 +333,7 @@ spec actualPgVersion =
post "/rpc/ret_void" post "/rpc/ret_void"
[json|{}|] [json|{}|]
`shouldRespondWith` `shouldRespondWith`
"null" [json|null|]
{ matchHeaders = [matchContentTypeJson] }
it "returns null for an integer with null value" $ it "returns null for an integer with null value" $
post "/rpc/ret_null" post "/rpc/ret_null"
@@ -1081,7 +1085,8 @@ spec actualPgVersion =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Location" <:> "/stuff?id=eq.2&overriden=true"] , matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/stuff?id=eq.2&overriden=true" ]
} }
-- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535 -- On https://github.com/PostgREST/postgrest/issues/1427#issuecomment-595907535
+7 -4
View File
@@ -11,7 +11,6 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
describe "Requesting singular json object" $ do describe "Requesting singular json object" $ do
@@ -61,7 +60,9 @@ spec =
[json| { address: "C Street" } |] [json| { address: "C Street" } |]
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 } { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "raises an error for multiple rows" $ do it "raises an error for multiple rows" $ do
request methodPatch "/addresses" request methodPatch "/addresses"
@@ -128,9 +129,11 @@ spec =
request methodPost "/addresses" request methodPost "/addresses"
[("Prefer", "return=minimal"), singular] [("Prefer", "return=minimal"), singular]
[json| [ { id: 103, address: "xxx" } ] |] [json| [ { id: 103, address: "xxx" } ] |]
`shouldRespondWith` "" `shouldRespondWith`
""
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
} }
it "raises an error when attempting to create multiple entities" $ do it "raises an error when attempting to create multiple entities" $ do
+6 -2
View File
@@ -7,7 +7,8 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application) spec :: SpecWith ((), Application)
spec = spec =
@@ -31,4 +32,7 @@ spec =
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
"" ""
`shouldRespondWith` `shouldRespondWith`
204 ""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
+101 -62
View File
@@ -22,11 +22,12 @@ spec = do
context "on an empty table" $ context "on an empty table" $
it "indicates no records found to update by returning 404" $ it "indicates no records found to update by returning 404" $
request methodPatch "/empty_table" [] request methodPatch "/empty_table" []
[json| { "extra":20 } |] [json| { "extra":20 } |]
`shouldRespondWith` "" `shouldRespondWith`
{ matchStatus = 404, ""
matchHeaders = [] { matchStatus = 404,
} matchHeaders = [matchHeaderAbsent hContentType]
}
context "with invalid json payload" $ context "with invalid json payload" $
it "fails with 400 and error" $ it "fails with 400 and error" $
@@ -53,7 +54,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["Content-Range" <:> "0-0/*"] , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-0/*" ]
} }
it "returns empty array when no rows updated and return=rep" $ it "returns empty array when no rows updated and return=rep" $
@@ -89,7 +91,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = ["Content-Range" <:> "0-1/*" , matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ] , "Preference-Applied" <:> "tx=commit" ]
} }
@@ -103,7 +106,10 @@ spec = do
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
[json| { b: "0" } |] [json| { b: "0" } |]
`shouldRespondWith` `shouldRespondWith`
204 ""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "can set a column to NULL" $ do it "can set a column to NULL" $ do
request methodPatch "/no_pk?a=eq.1" request methodPatch "/no_pk?a=eq.1"
@@ -169,63 +175,86 @@ spec = do
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "ignores ?select= when return not set or return=minimal" $ do it "ignores ?select= when return not set or return=minimal" $ do
request methodPatch "/items?id=eq.1&select=id" [] [json| { id:1 } |] request methodPatch "/items?id=eq.1&select=id"
`shouldRespondWith` "" [] [json| { id:1 } |]
{ `shouldRespondWith`
matchStatus = 204, ""
matchHeaders = ["Content-Range" <:> "0-0/*"] { matchStatus = 204
} , matchHeaders = [ matchHeaderAbsent hContentType
request methodPatch "/items?id=eq.1&select=id" [("Prefer", "return=minimal")] [json| { id:1 } |] , "Content-Range" <:> "0-0/*" ]
`shouldRespondWith` "" }
{ request methodPatch "/items?id=eq.1&select=id"
matchStatus = 204, [("Prefer", "return=minimal")]
matchHeaders = ["Content-Range" <:> "0-0/*"] [json| { id:1 } |]
} `shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "0-0/*" ]
}
context "when patching with an empty body" $ do context "when patching with an empty body" $ do
it "makes no updates and returns 204 without return= and without ?select=" $ do it "makes no updates and returns 204 without return= and without ?select=" $ do
request methodPatch "/items" [] [json| {} |] request methodPatch "/items"
`shouldRespondWith` "" []
{ [json| {} |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
request methodPatch "/items" [] [json| [] |] request methodPatch "/items"
`shouldRespondWith` "" []
{ [json| [] |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
request methodPatch "/items" [] [json| [{}] |] request methodPatch "/items"
`shouldRespondWith` "" []
{ [json| [{}] |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
it "makes no updates and returns 204 without return= and with ?select=" $ do it "makes no updates and returns 204 without return= and with ?select=" $ do
request methodPatch "/items?select=id" [] [json| {} |] request methodPatch "/items?select=id"
`shouldRespondWith` "" []
{ [json| {} |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
request methodPatch "/items?select=id" [] [json| [] |] request methodPatch "/items?select=id"
`shouldRespondWith` "" []
{ [json| [] |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
request methodPatch "/items?select=id" [] [json| [{}] |] request methodPatch "/items?select=id"
`shouldRespondWith` "" []
{ [json| [{}] |]
matchStatus = 204, `shouldRespondWith`
matchHeaders = ["Content-Range" <:> "*/*"] ""
} { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Range" <:> "*/*" ]
}
it "makes no updates and returns 200 with return=rep and without ?select=" $ it "makes no updates and returns 200 with return=rep and without ?select=" $
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |] request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]
@@ -339,11 +368,21 @@ spec = do
context "table with limited privileges" $ do context "table with limited privileges" $ do
it "succeeds updating row and gives a 204 when using return=minimal" $ it "succeeds updating row and gives a 204 when using return=minimal" $
request methodPatch "/app_users?id=eq.1" [("Prefer", "return=minimal")] request methodPatch "/app_users?id=eq.1"
[json| { "password": "passxyz" } |] [("Prefer", "return=minimal")]
`shouldRespondWith` 204 [json| { "password": "passxyz" } |]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "can update without return=minimal and no explicit select" $ it "can update without return=minimal and no explicit select" $
request methodPatch "/app_users?id=eq.1" [] request methodPatch "/app_users?id=eq.1"
[json| { "password": "passabc" } |] []
`shouldRespondWith` 204 [json| { "password": "passabc" } |]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
+7 -1
View File
@@ -422,6 +422,12 @@ spec actualPgVersion =
} }
it "works with PUT" $ do it "works with PUT" $ do
put "/UnitTest?idUnitTest=eq.1" [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204 put "/UnitTest?idUnitTest=eq.1"
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith` get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] [json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|]