From af75988dd4df735f3ca1fdf98a5016490147a0e0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 4 Sep 2016 10:31:24 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/PostgREST/ApiRequest.hs | 2 +- src/PostgREST/App.hs | 25 ++++++--- test/Feature/DeleteSpec.hs | 12 ++--- test/Feature/InsertSpec.hs | 90 ++++---------------------------- test/Feature/QueryLimitedSpec.hs | 16 +++--- test/Feature/QuerySpec.hs | 40 ++++++++------ test/Feature/RangeSpec.hs | 61 +++++++++++----------- test/SpecHelper.hs | 3 ++ 9 files changed, 100 insertions(+), 150 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e3e897a..1d9519712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index ef291ce02..ccdd18a18 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -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 ] diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index c23c0c077..cca8392a5 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -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 diff --git a/test/Feature/DeleteSpec.hs b/test/Feature/DeleteSpec.hs index b82b85537..4a22e9ad6 100644 --- a/test/Feature/DeleteSpec.hs +++ b/test/Feature/DeleteSpec.hs @@ -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" $ diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index e36860cc5..6916fce05 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -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" } |] diff --git a/test/Feature/QueryLimitedSpec.hs b/test/Feature/QueryLimitedSpec.hs index 311fb83c0..5300706ca 100644 --- a/test/Feature/QueryLimitedSpec.hs +++ b/test/Feature/QueryLimitedSpec.hs @@ -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/*"] } diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index b5b9092f9..a318967db 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -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/*"] } diff --git a/test/Feature/RangeSpec.hs b/test/Feature/RangeSpec.hs index 3e7bc6d9c..04f3eb625 100644 --- a/test/Feature/RangeSpec.hs +++ b/test/Feature/RangeSpec.hs @@ -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 diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 5043947a9..4fd5be8ea 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -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)]