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
|
- Use HTTP 400 for raise\_exception - @begriffs
|
||||||
- Remove non-OpenAPI schema description - @begriffs
|
- Remove non-OpenAPI schema description - @begriffs
|
||||||
- Use comma rather than semicolon to separate Prefer header values - @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
|
## [0.3.2.0] - 2016-06-10
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ userApiRequest schema req reqBody =
|
|||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferSingular = singular
|
, 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) ]
|
, 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
|
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, 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
|
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == CTTextCSV) payload
|
||||||
row <- H.query uniform stm
|
row <- H.query uniform stm
|
||||||
let (_, _, fs, body) = extractQueryResult row
|
let (_, _, fs, body) = extractQueryResult row
|
||||||
header =
|
headers = catMaybes [
|
||||||
if null fs then []
|
if null fs
|
||||||
else [(hLocation, "/" <> toS table <> renderLocationFields 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
|
return . responseLBS status201 headers $
|
||||||
then responseLBS status201 (ctToHeader contentType : header) (toS body)
|
if iPreferRepresentation apiRequest == Full
|
||||||
else responseLBS status201 header ""
|
then toS body else ""
|
||||||
|
|
||||||
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) ->
|
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) ->
|
||||||
serves [CTApplicationJSON, CTTextCSV] (iAccepts apiRequest) $ \contentType ->
|
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';
|
'plurality=singular specified, but more than one object would be updated';
|
||||||
END $$;
|
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
|
s = case () of _ | queryTotal == 0 -> status404
|
||||||
| iPreferRepresentation apiRequest == Full -> status200
|
| iPreferRepresentation apiRequest == Full -> status200
|
||||||
| otherwise -> status204
|
| otherwise -> status204
|
||||||
@@ -183,7 +191,8 @@ app dbStructure conf apiRequest =
|
|||||||
stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == CTTextCSV) fakeload
|
stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == CTTextCSV) fakeload
|
||||||
row <- H.query emptyUniform stm
|
row <- H.query emptyUniform stm
|
||||||
let (_, queryTotal, _, body) = extractQueryResult row
|
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
|
return $ if queryTotal == 0
|
||||||
then notFound
|
then notFound
|
||||||
else if iPreferRepresentation apiRequest == Full
|
else if iPreferRepresentation apiRequest == Full
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ spec =
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing
|
matchBody = Nothing
|
||||||
, matchStatus = 204
|
, matchStatus = 204
|
||||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "returns the deleted item" $
|
it "returns the deleted item and count if requested" $
|
||||||
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation")] ""
|
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":2}]|]
|
matchBody = Just [str|[{"id":2}]|]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
@@ -31,14 +31,14 @@ spec =
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":2,"name":"Two"}]|]
|
matchBody = Just [str|[{"id":2,"name":"Two"}]|]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
it "can embed (parent) entities" $
|
it "can embed (parent) entities" $
|
||||||
request methodDelete "/tasks?id=eq.8&select=id,name,project{id}" [("Prefer", "return=representation")] ""
|
request methodDelete "/tasks?id=eq.8&select=id,name,project{id}" [("Prefer", "return=representation")] ""
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
|
matchBody = Just [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/1"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "actually clears items ouf the db" $ do
|
it "actually clears items ouf the db" $ do
|
||||||
@@ -47,7 +47,7 @@ spec =
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":15}]|]
|
matchBody = Just [str|[{"id":15}]|]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "known route, unknown record" $
|
context "known route, unknown record" $
|
||||||
|
|||||||
@@ -48,11 +48,14 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "includes related data after insert" $
|
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 {
|
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
|
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
|
||||||
, matchStatus = 201
|
, 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" $
|
context "from an html form" $
|
||||||
@@ -258,81 +261,6 @@ spec = do
|
|||||||
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
|
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
|
describe "Patching record" $ do
|
||||||
|
|
||||||
context "to unknown uri" $
|
context "to unknown uri" $
|
||||||
@@ -351,19 +279,19 @@ spec = do
|
|||||||
it "can update a single item" $ do
|
it "can update a single item" $ do
|
||||||
g <- get "/items?id=eq.42"
|
g <- get "/items?id=eq.42"
|
||||||
liftIO $ simpleHeaders g
|
liftIO $ simpleHeaders g
|
||||||
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
|
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*"
|
||||||
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
|
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
|
||||||
pure p `shouldRespondWith` ResponseMatcher {
|
pure p `shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing,
|
matchBody = Nothing,
|
||||||
matchStatus = 204,
|
matchStatus = 204,
|
||||||
matchHeaders = ["Content-Range" <:> "0-0/1"]
|
matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
liftIO $
|
liftIO $
|
||||||
lookup hContentType (simpleHeaders p) `shouldBe` Nothing
|
lookup hContentType (simpleHeaders p) `shouldBe` Nothing
|
||||||
|
|
||||||
g' <- get "/items?id=eq.42"
|
g' <- get "/items?id=eq.42"
|
||||||
liftIO $ simpleHeaders g'
|
liftIO $ simpleHeaders g'
|
||||||
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
|
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*"
|
||||||
|
|
||||||
it "can update multiple items" $ do
|
it "can update multiple items" $ do
|
||||||
replicateM_ 10 $ post "/auto_incrementing_pk"
|
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||||
@@ -375,7 +303,7 @@ spec = do
|
|||||||
[json| { non_nullable_string: "c" } |]
|
[json| { non_nullable_string: "c" } |]
|
||||||
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
|
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
|
||||||
liftIO $ simpleHeaders g
|
liftIO $ simpleHeaders g
|
||||||
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
|
`shouldSatisfy` matchHeader "Content-Range" "0-9/\\*"
|
||||||
|
|
||||||
it "can set a column to NULL" $ do
|
it "can set a column to NULL" $ do
|
||||||
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
|
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ spec =
|
|||||||
get "/items"
|
get "/items"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/15"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "respects additional client limiting" $ do
|
it "respects additional client limiting" $ do
|
||||||
@@ -25,22 +25,22 @@ spec =
|
|||||||
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-0/15"
|
matchHeader "Content-Range" "0-0/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "limit works on all levels" $
|
it "limit works on all levels" $
|
||||||
get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc"
|
get "/users?select=id,tasks{id}&order=id.asc&tasks.order=id.asc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
|
matchBody = Just [str|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "limit is not applied to parent embeds" $
|
it "limit is not applied to parent embeds" $
|
||||||
get "/tasks?select=id,project{id}&id=gt.5"
|
get "/tasks?select=id,project{id}&id=gt.5"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
matchBody = Just [str|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/3"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-15
@@ -27,7 +27,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":5}] |]
|
matchBody = Just [json| [{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches with equality using not operator" $
|
it "matches with equality using not operator" $
|
||||||
@@ -35,7 +35,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`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}] |]
|
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
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-13/14"]
|
, matchHeaders = ["Content-Range" <:> "0-13/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches with more than one condition using not operator" $
|
it "matches with more than one condition using not operator" $
|
||||||
@@ -46,13 +46,13 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":14},{"id":15}] |]
|
matchBody = Just [json| [{"id":14},{"id":15}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
get "/items?id=not.gt.2&order=id.asc"
|
get "/items?id=not.gt.2&order=id.asc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches items IN" $
|
it "matches items IN" $
|
||||||
@@ -60,7 +60,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches items NOT IN" $
|
it "matches items NOT IN" $
|
||||||
@@ -68,7 +68,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches items NOT IN using not operator" $
|
it "matches items NOT IN using not operator" $
|
||||||
@@ -76,7 +76,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "matches nulls using not operator" $
|
it "matches nulls using not operator" $
|
||||||
@@ -319,14 +319,14 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
it "by a column desc" $
|
it "by a column desc" $
|
||||||
get "/items?id=lte.2&order=id.desc"
|
get "/items?id=lte.2&order=id.desc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":2},{"id":1}] |]
|
matchBody = Just [json| [{"id":2},{"id":1}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "by a column asc with nulls last" $
|
it "by a column asc with nulls last" $
|
||||||
@@ -336,7 +336,7 @@ spec = do
|
|||||||
{"a":"2","b":"0"},
|
{"a":"2","b":"0"},
|
||||||
{"a":null,"b":null}] |]
|
{"a":null,"b":null}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "by a column desc with nulls first" $
|
it "by a column desc with nulls first" $
|
||||||
@@ -346,7 +346,7 @@ spec = do
|
|||||||
{"a":"2","b":"0"},
|
{"a":"2","b":"0"},
|
||||||
{"a":"1","b":"0"}] |]
|
{"a":"1","b":"0"}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "by a column desc with nulls last" $
|
it "by a column desc with nulls last" $
|
||||||
@@ -356,7 +356,7 @@ spec = do
|
|||||||
{"a":"1","b":"0"},
|
{"a":"1","b":"0"},
|
||||||
{"a":null,"b":null}] |]
|
{"a":null,"b":null}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "without other constraints" $
|
it "without other constraints" $
|
||||||
@@ -466,7 +466,17 @@ spec = do
|
|||||||
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [{"id":3}] |]
|
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"]
|
, 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| {} |]
|
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json|[{"id":3},{"id":4}]|]
|
matchBody = Just [json|[{"id":3},{"id":4}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "1-2/3"]
|
, matchHeaders = ["Content-Range" <:> "1-2/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+30
-31
@@ -28,8 +28,7 @@ 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 */* for empty range" $
|
it "returns range Content-Range with */* for empty range" $
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange" [] emptyRange
|
||||||
[("Prefer", "count=none")] emptyRange
|
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| [] |]
|
matchBody = Just [json| [] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
@@ -37,8 +36,7 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "returns range Content-Range with range/*" $
|
it "returns range Content-Range with range/*" $
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange" [] defaultRange
|
||||||
[("Prefer", "count=none")] defaultRange
|
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`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}] |]
|
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
|
, matchStatus = 200
|
||||||
@@ -53,8 +51,8 @@ spec = do
|
|||||||
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
|
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-1/15"
|
matchHeader "Content-Range" "0-1/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "understands open-ended ranges" $
|
it "understands open-ended ranges" $
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange"
|
||||||
@@ -67,7 +65,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[]"
|
matchBody = Just "[]"
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/0"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "allows one-item requests" $ do
|
it "allows one-item requests" $ do
|
||||||
@@ -75,16 +73,16 @@ spec = do
|
|||||||
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
|
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-0/15"
|
matchHeader "Content-Range" "0-0/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "handles ranges beyond collection length via truncation" $ do
|
it "handles ranges beyond collection length via truncation" $ do
|
||||||
r <- request methodPost "/rpc/getitemrange"
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
|
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "10-14/15"
|
matchHeader "Content-Range" "10-14/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
context "of invalid range" $ do
|
context "of invalid range" $ do
|
||||||
it "fails with 416 for offside range" $
|
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" $
|
it "refuses a range with nonzero start when there are no items" $
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 1 2) emptyRange
|
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing
|
matchBody = Nothing
|
||||||
, matchStatus = 416
|
, matchStatus = 416
|
||||||
@@ -103,12 +101,13 @@ spec = do
|
|||||||
|
|
||||||
it "refuses a range requesting start past last item" $
|
it "refuses a range requesting start past last item" $
|
||||||
request methodPost "/rpc/getitemrange"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 100 199) defaultRange
|
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing
|
matchBody = Nothing
|
||||||
, matchStatus = 416
|
, matchStatus = 416
|
||||||
, matchHeaders = ["Content-Range" <:> "*/15"]
|
, matchHeaders = ["Content-Range" <:> "*/15"]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "GET /items" $ do
|
describe "GET /items" $ do
|
||||||
context "without range headers" $ do
|
context "without range headers" $ do
|
||||||
context "with response under server size limit" $
|
context "with response under server size limit" $
|
||||||
@@ -149,30 +148,30 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`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}]|]
|
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
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-14/15"]
|
, matchHeaders = ["Content-Range" <:> "0-14/*"]
|
||||||
}
|
}
|
||||||
it "top level limit with parameter" $
|
it "top level limit with parameter" $
|
||||||
get "/items?select=id&order=id.asc&limit=3"
|
get "/items?select=id&order=id.asc&limit=3"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":1},{"id":2},{"id":3}]|]
|
matchBody = Just [str|[{"id":1},{"id":2},{"id":3}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-2/15"]
|
, matchHeaders = ["Content-Range" <:> "0-2/*"]
|
||||||
}
|
}
|
||||||
it "headers override get parameters" $
|
it "headers override get parameters" $
|
||||||
request methodGet "/items?select=id&order=id.asc&limit=3"
|
request methodGet "/items?select=id&order=id.asc&limit=3"
|
||||||
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":1},{"id":2}]|]
|
matchBody = Just [str|[{"id":1},{"id":2}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/15"]
|
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "limit works on all levels" $
|
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"
|
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 {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]}]}]|]
|
matchBody = Just [str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]}]}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "fails on offset specified below level 1" $
|
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"
|
get "/items?select=id&order=id.asc&limit=3&offset=2"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [str|[{"id":3},{"id":4},{"id":5}]|]
|
matchBody = Just [str|[{"id":3},{"id":4},{"id":5}]|]
|
||||||
, matchStatus = 206
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "2-4/15"]
|
, matchHeaders = ["Content-Range" <:> "2-4/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with range headers" $ do
|
context "with range headers" $ do
|
||||||
@@ -195,8 +194,8 @@ spec = do
|
|||||||
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
(rangeHdrs $ ByteRangeFromTo 0 1) ""
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-1/15"
|
matchHeader "Content-Range" "0-1/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "understands open-ended ranges" $
|
it "understands open-ended ranges" $
|
||||||
request methodGet "/items"
|
request methodGet "/items"
|
||||||
@@ -209,7 +208,7 @@ spec = do
|
|||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[]"
|
matchBody = Just "[]"
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/0"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "allows one-item requests" $ do
|
it "allows one-item requests" $ do
|
||||||
@@ -217,16 +216,16 @@ spec = do
|
|||||||
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
(rangeHdrs $ ByteRangeFromTo 0 0) ""
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-0/15"
|
matchHeader "Content-Range" "0-0/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "handles ranges beyond collection length via truncation" $ do
|
it "handles ranges beyond collection length via truncation" $ do
|
||||||
r <- request methodGet "/items"
|
r <- request methodGet "/items"
|
||||||
(rangeHdrs $ ByteRangeFromTo 10 100) ""
|
(rangeHdrs $ ByteRangeFromTo 10 100) ""
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "10-14/15"
|
matchHeader "Content-Range" "10-14/*"
|
||||||
simpleStatus r `shouldBe` partialContent206
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
context "of invalid range" $ do
|
context "of invalid range" $ do
|
||||||
it "fails with 416 for offside range" $
|
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" $
|
it "refuses a range with nonzero start when there are no items" $
|
||||||
request methodGet "/menagerie"
|
request methodGet "/menagerie"
|
||||||
(rangeHdrs $ ByteRangeFromTo 1 2) ""
|
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing
|
matchBody = Nothing
|
||||||
, matchStatus = 416
|
, matchStatus = 416
|
||||||
@@ -245,7 +244,7 @@ spec = do
|
|||||||
|
|
||||||
it "refuses a range requesting start past last item" $
|
it "refuses a range requesting start past last item" $
|
||||||
request methodGet "/items"
|
request methodGet "/items"
|
||||||
(rangeHdrs $ ByteRangeFromTo 100 199) ""
|
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing
|
matchBody = Nothing
|
||||||
, matchStatus = 416
|
, matchStatus = 416
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ loadFixture name =
|
|||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||||
|
|
||||||
|
rangeHdrsWithCount :: ByteRange -> [Header]
|
||||||
|
rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r
|
||||||
|
|
||||||
acceptHdrs :: BS.ByteString -> [Header]
|
acceptHdrs :: BS.ByteString -> [Header]
|
||||||
acceptHdrs mime = [(hAccept, mime)]
|
acceptHdrs mime = [(hAccept, mime)]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user