No count by default (#700)
* WIP: disable counting total table size by default * Remove commented tests since PUT is no longer supported * Fix tests for invalid range which require count * Do not report count in PATCH response if not asked * Return count of deleted items only if asked * Return count for bulk insert when requested * Changelog entry
This commit is contained in:
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Use HTTP 400 for raise\_exception - @begriffs
|
||||
- Remove non-OpenAPI schema description - @begriffs
|
||||
- Use comma rather than semicolon to separate Prefer header values - @begriffs
|
||||
- Omit total query count by default - @begriffs
|
||||
|
||||
## [0.3.2.0] - 2016-06-10
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ userApiRequest schema req reqBody =
|
||||
, iPayload = relevantPayload
|
||||
, iPreferRepresentation = representation
|
||||
, iPreferSingular = singular
|
||||
, iPreferCount = not $ singular || hasPrefer "count=none"
|
||||
, iPreferCount = not singular && hasPrefer "count=exact"
|
||||
, iFilters = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", k /= "offset", not (endingIn ["order", "limit"] k) ]
|
||||
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||
|
||||
+17
-8
@@ -141,13 +141,20 @@ app dbStructure conf apiRequest =
|
||||
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == CTTextCSV) payload
|
||||
row <- H.query uniform stm
|
||||
let (_, _, fs, body) = extractQueryResult row
|
||||
header =
|
||||
if null fs then []
|
||||
else [(hLocation, "/" <> toS table <> renderLocationFields fs)]
|
||||
headers = catMaybes [
|
||||
if null fs
|
||||
then Nothing
|
||||
else Just (hLocation, "/" <> toS table <> renderLocationFields fs)
|
||||
, if iPreferRepresentation apiRequest == Full
|
||||
then Just $ ctToHeader contentType
|
||||
else Nothing
|
||||
, Just . contentRangeH 1 0 $
|
||||
toInteger <$> if shouldCount then Just (V.length rows) else Nothing
|
||||
]
|
||||
|
||||
return $ if iPreferRepresentation apiRequest == Full
|
||||
then responseLBS status201 (ctToHeader contentType : header) (toS body)
|
||||
else responseLBS status201 header ""
|
||||
return . responseLBS status201 headers $
|
||||
if iPreferRepresentation apiRequest == Full
|
||||
then toS body else ""
|
||||
|
||||
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) ->
|
||||
serves [CTApplicationJSON, CTTextCSV] (iAccepts apiRequest) $ \contentType ->
|
||||
@@ -165,7 +172,8 @@ app dbStructure conf apiRequest =
|
||||
'plurality=singular specified, but more than one object would be updated';
|
||||
END $$;
|
||||
|]
|
||||
let r = contentRangeH 0 (toInteger $ queryTotal-1) (toInteger <$> Just queryTotal)
|
||||
let r = contentRangeH 0 (toInteger $ queryTotal-1)
|
||||
(toInteger <$> if shouldCount then Just queryTotal else Nothing)
|
||||
s = case () of _ | queryTotal == 0 -> status404
|
||||
| iPreferRepresentation apiRequest == Full -> status200
|
||||
| otherwise -> status204
|
||||
@@ -183,7 +191,8 @@ app dbStructure conf apiRequest =
|
||||
stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == CTTextCSV) fakeload
|
||||
row <- H.query emptyUniform stm
|
||||
let (_, queryTotal, _, body) = extractQueryResult row
|
||||
r = contentRangeH 1 0 (toInteger <$> Just queryTotal)
|
||||
r = contentRangeH 1 0 $
|
||||
toInteger <$> if shouldCount then Just queryTotal else Nothing
|
||||
return $ if queryTotal == 0
|
||||
then notFound
|
||||
else if iPreferRepresentation apiRequest == Full
|
||||
|
||||
@@ -16,11 +16,11 @@ spec =
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing
|
||||
, matchStatus = 204
|
||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "returns the deleted item" $
|
||||
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation")] ""
|
||||
it "returns the deleted item and count if requested" $
|
||||
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":2}]|]
|
||||
, matchStatus = 200
|
||||
@@ -31,14 +31,14 @@ spec =
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":2,"name":"Two"}]|]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
it "can embed (parent) entities" $
|
||||
request methodDelete "/tasks?id=eq.8&select=id,name,project{id}" [("Prefer", "return=representation")] ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "actually clears items ouf the db" $ do
|
||||
@@ -47,7 +47,7 @@ spec =
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":15}]|]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
context "known route, unknown record" $
|
||||
|
||||
@@ -48,11 +48,14 @@ spec = do
|
||||
}
|
||||
|
||||
it "includes related data after insert" $
|
||||
request methodPost "/projects?select=id,name,clients{id,name}" [("Prefer", "return=representation")]
|
||||
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` ResponseMatcher {
|
||||
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
|
||||
, matchStatus = 201
|
||||
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", "Location" <:> "/projects?id=eq.6"]
|
||||
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
|
||||
, "Location" <:> "/projects?id=eq.6"
|
||||
, "Content-Range" <:> "*/1" ]
|
||||
}
|
||||
|
||||
context "from an html form" $
|
||||
@@ -258,81 +261,6 @@ spec = do
|
||||
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
|
||||
|
||||
|
||||
describe "Putting record" $ do
|
||||
|
||||
context "to unknown uri" $
|
||||
it "gives a 404" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
request methodPut "/fake" []
|
||||
[json| { "real": false } |]
|
||||
`shouldRespondWith` 404
|
||||
|
||||
context "to a known uri" $ do
|
||||
context "without a fully-specified primary key" $
|
||||
it "is not an allowed operation" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
request methodPut "/compound_pk?k1=eq.12" []
|
||||
[json| { "k1":12, "k2":42 } |]
|
||||
`shouldRespondWith` 405
|
||||
|
||||
context "with a fully-specified primary key" $ do
|
||||
|
||||
context "not specifying every column in the table" $
|
||||
it "is rejected for lack of idempotence" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||
[json| { "k1":12, "k2":42 } |]
|
||||
`shouldRespondWith` 400
|
||||
|
||||
context "specifying every column in the table" $ do
|
||||
it "can create a new record" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||
[json| { "k1":12, "k2":42, "extra":3 } |]
|
||||
liftIO $ do
|
||||
simpleBody p `shouldBe` ""
|
||||
simpleStatus p `shouldBe` status204
|
||||
|
||||
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
|
||||
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
|
||||
liftIO $ do
|
||||
length rows `shouldBe` 1
|
||||
let record = head rows
|
||||
compoundK1 record `shouldBe` 12
|
||||
compoundK2 record `shouldBe` "42"
|
||||
compoundExtra record `shouldBe` Just 3
|
||||
|
||||
it "can update an existing record" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||
[json| { "k1":12, "k2":42, "extra":4 } |]
|
||||
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||
[json| { "k1":12, "k2":42, "extra":5 } |]
|
||||
|
||||
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
|
||||
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
|
||||
liftIO $ do
|
||||
length rows `shouldBe` 1
|
||||
let record = head rows
|
||||
compoundExtra record `shouldBe` Just 5
|
||||
|
||||
context "with an auto-incrementing primary key"$
|
||||
|
||||
it "succeeds with 204" $ do
|
||||
pendingWith "Decide on PUT usefullness"
|
||||
request methodPut "/auto_incrementing_pk?id=eq.1" []
|
||||
[json| {
|
||||
"id":1,
|
||||
"nullable_string":"hi",
|
||||
"non_nullable_string":"bye",
|
||||
"inserted_at": "2020-11-11"
|
||||
} |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing,
|
||||
matchStatus = 204,
|
||||
matchHeaders = []
|
||||
}
|
||||
|
||||
describe "Patching record" $ do
|
||||
|
||||
context "to unknown uri" $
|
||||
@@ -351,19 +279,19 @@ spec = do
|
||||
it "can update a single item" $ do
|
||||
g <- get "/items?id=eq.42"
|
||||
liftIO $ simpleHeaders g
|
||||
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
|
||||
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*"
|
||||
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
|
||||
pure p `shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing,
|
||||
matchStatus = 204,
|
||||
matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||
matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
liftIO $
|
||||
lookup hContentType (simpleHeaders p) `shouldBe` Nothing
|
||||
|
||||
g' <- get "/items?id=eq.42"
|
||||
liftIO $ simpleHeaders g'
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*"
|
||||
|
||||
it "can update multiple items" $ do
|
||||
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||
@@ -375,7 +303,7 @@ spec = do
|
||||
[json| { non_nullable_string: "c" } |]
|
||||
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
|
||||
liftIO $ simpleHeaders g
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-9/\\*"
|
||||
|
||||
it "can set a column to NULL" $ do
|
||||
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
|
||||
|
||||
@@ -16,8 +16,8 @@ spec =
|
||||
get "/items"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/15"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "respects additional client limiting" $ do
|
||||
@@ -25,22 +25,22 @@ spec =
|
||||
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-0/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "0-0/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "limit works on all levels" $
|
||||
get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "limit is not applied to parent embeds" $
|
||||
get "/tasks?select=id,project{id}&id=gt.5"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
|
||||
+25
-15
@@ -27,7 +27,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
it "matches with equality using not operator" $
|
||||
@@ -35,7 +35,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [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}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-13/14"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-13/*"]
|
||||
}
|
||||
|
||||
it "matches with more than one condition using not operator" $
|
||||
@@ -46,13 +46,13 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":14},{"id":15}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
get "/items?id=not.gt.2&order=id.asc"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "matches items IN" $
|
||||
@@ -60,7 +60,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "matches items NOT IN" $
|
||||
@@ -68,7 +68,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "matches items NOT IN using not operator" $
|
||||
@@ -76,7 +76,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "matches nulls using not operator" $
|
||||
@@ -319,14 +319,14 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
it "by a column desc" $
|
||||
get "/items?id=lte.2&order=id.desc"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":2},{"id":1}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "by a column asc with nulls last" $
|
||||
@@ -336,7 +336,7 @@ spec = do
|
||||
{"a":"2","b":"0"},
|
||||
{"a":null,"b":null}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "by a column desc with nulls first" $
|
||||
@@ -346,7 +346,7 @@ spec = do
|
||||
{"a":"2","b":"0"},
|
||||
{"a":"1","b":"0"}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "by a column desc with nulls last" $
|
||||
@@ -356,7 +356,7 @@ spec = do
|
||||
{"a":"1","b":"0"},
|
||||
{"a":null,"b":null}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
|
||||
it "without other constraints" $
|
||||
@@ -466,7 +466,17 @@ spec = do
|
||||
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":3}] |]
|
||||
, matchStatus = 206
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
it "includes total count if requested" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrsWithCount (ByteRangeFromTo 0 0))
|
||||
[json| { "min": 2, "max": 4 } |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":3}] |]
|
||||
, matchStatus = 206 -- it now knows the response is partial
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||
}
|
||||
|
||||
@@ -488,8 +498,8 @@ spec = do
|
||||
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json|[{"id":3},{"id":4}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "1-2/3"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "1-2/*"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
+30
-31
@@ -28,8 +28,7 @@ spec = do
|
||||
|
||||
context "when I don't want the count" $ do
|
||||
it "returns range Content-Range with */* for empty range" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
[("Prefer", "count=none")] emptyRange
|
||||
request methodPost "/rpc/getitemrange" [] emptyRange
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [] |]
|
||||
, matchStatus = 200
|
||||
@@ -37,8 +36,7 @@ spec = do
|
||||
}
|
||||
|
||||
it "returns range Content-Range with range/*" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
[("Prefer", "count=none")] defaultRange
|
||||
request methodPost "/rpc/getitemrange" [] defaultRange
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [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
|
||||
@@ -53,8 +51,8 @@ spec = do
|
||||
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-1/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "0-1/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "understands open-ended ranges" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
@@ -67,7 +65,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just "[]"
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "*/0"]
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "allows one-item requests" $ do
|
||||
@@ -75,16 +73,16 @@ spec = do
|
||||
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-0/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "0-0/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "handles ranges beyond collection length via truncation" $ do
|
||||
r <- request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "10-14/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "10-14/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
context "of invalid range" $ do
|
||||
it "fails with 416 for offside range" $
|
||||
@@ -94,7 +92,7 @@ spec = do
|
||||
|
||||
it "refuses a range with nonzero start when there are no items" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrs $ ByteRangeFromTo 1 2) emptyRange
|
||||
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing
|
||||
, matchStatus = 416
|
||||
@@ -103,12 +101,13 @@ spec = do
|
||||
|
||||
it "refuses a range requesting start past last item" $
|
||||
request methodPost "/rpc/getitemrange"
|
||||
(rangeHdrs $ ByteRangeFromTo 100 199) defaultRange
|
||||
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing
|
||||
, matchStatus = 416
|
||||
, matchHeaders = ["Content-Range" <:> "*/15"]
|
||||
}
|
||||
|
||||
describe "GET /items" $ do
|
||||
context "without range headers" $ do
|
||||
context "with response under server size limit" $
|
||||
@@ -149,30 +148,30 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"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/15"]
|
||||
, matchHeaders = ["Content-Range" <:> "0-14/*"]
|
||||
}
|
||||
it "top level limit with parameter" $
|
||||
get "/items?select=id&order=id.asc&limit=3"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":1},{"id":2},{"id":3}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/15"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||
}
|
||||
it "headers override get parameters" $
|
||||
request methodGet "/items?select=id&order=id.asc&limit=3"
|
||||
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":1},{"id":2}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/15"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "limit works on all levels" $
|
||||
get "/clients?select=id,projects{id,tasks{id}}&order=id.asc&limit=1&projects.order=id.asc&projects.limit=1&projects.tasks.order=id.asc&projects.tasks.limit=2"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]}]}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
it "fails on offset specified below level 1" $
|
||||
@@ -183,8 +182,8 @@ spec = do
|
||||
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [str|[{"id":3},{"id":4},{"id":5}]|]
|
||||
, matchStatus = 206
|
||||
, matchHeaders = ["Content-Range" <:> "2-4/15"]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "2-4/*"]
|
||||
}
|
||||
|
||||
context "with range headers" $ do
|
||||
@@ -195,8 +194,8 @@ spec = do
|
||||
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-1/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "0-1/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "understands open-ended ranges" $
|
||||
request methodGet "/items"
|
||||
@@ -209,7 +208,7 @@ spec = do
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just "[]"
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "*/0"]
|
||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||
}
|
||||
|
||||
it "allows one-item requests" $ do
|
||||
@@ -217,16 +216,16 @@ spec = do
|
||||
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "0-0/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "0-0/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
it "handles ranges beyond collection length via truncation" $ do
|
||||
r <- request methodGet "/items"
|
||||
(rangeHdrs $ ByteRangeFromTo 10 100) ""
|
||||
liftIO $ do
|
||||
simpleHeaders r `shouldSatisfy`
|
||||
matchHeader "Content-Range" "10-14/15"
|
||||
simpleStatus r `shouldBe` partialContent206
|
||||
matchHeader "Content-Range" "10-14/*"
|
||||
simpleStatus r `shouldBe` ok200
|
||||
|
||||
context "of invalid range" $ do
|
||||
it "fails with 416 for offside range" $
|
||||
@@ -236,7 +235,7 @@ spec = do
|
||||
|
||||
it "refuses a range with nonzero start when there are no items" $
|
||||
request methodGet "/menagerie"
|
||||
(rangeHdrs $ ByteRangeFromTo 1 2) ""
|
||||
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing
|
||||
, matchStatus = 416
|
||||
@@ -245,7 +244,7 @@ spec = do
|
||||
|
||||
it "refuses a range requesting start past last item" $
|
||||
request methodGet "/items"
|
||||
(rangeHdrs $ ByteRangeFromTo 100 199) ""
|
||||
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Nothing
|
||||
, matchStatus = 416
|
||||
|
||||
@@ -84,6 +84,9 @@ loadFixture name =
|
||||
rangeHdrs :: ByteRange -> [Header]
|
||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||
|
||||
rangeHdrsWithCount :: ByteRange -> [Header]
|
||||
rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r
|
||||
|
||||
acceptHdrs :: BS.ByteString -> [Header]
|
||||
acceptHdrs mime = [(hAccept, mime)]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user