diff options
author | ILYA Khlopotov <iilyak@apache.org> | 2020-05-14 12:47:29 -0700 |
---|---|---|
committer | ILYA Khlopotov <iilyak@apache.org> | 2020-05-15 11:58:34 -0700 |
commit | 02e4c3e96aad6f653af9a9550c8c7a9340fc2e58 (patch) | |
tree | 94070f0dc27499cbc21eeb3818a938f5a132dd9c | |
parent | b8a13a531fd682329572e61eb17b3acdb35a6a13 (diff) | |
download | couchdb-02e4c3e96aad6f653af9a9550c8c7a9340fc2e58.tar.gz |
Add tests for pagination API
-rw-r--r-- | src/chttpd/test/exunit/pagination_test.exs | 771 |
1 files changed, 771 insertions, 0 deletions
diff --git a/src/chttpd/test/exunit/pagination_test.exs b/src/chttpd/test/exunit/pagination_test.exs index 4b12c8b2f..fcb8f9add 100644 --- a/src/chttpd/test/exunit/pagination_test.exs +++ b/src/chttpd/test/exunit/pagination_test.exs @@ -68,6 +68,52 @@ defmodule Couch.Test.Pagination do %{view_name: "all", ddoc_id: ddoc_id} end + defp all_docs(context) do + assert Map.has_key?(context, :page_size), "Please define '@describetag page_size: 4'" + + assert Map.has_key?(context, :descending), + "Please define '@describetag descending: false'" + + resp = + Couch.Session.get(context.session, "/#{context.db_name}/_all_docs", + query: %{page_size: context.page_size, descending: context.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + %{ + response: resp.body + } + end + + defp paginate(context) do + if Map.has_key?(context.response, "next") do + bookmark = context.response["next"] + pages = Map.get(context, :pages, [context.response]) + assert length(pages) < div(context.n_docs, context.page_size) + 1 + + resp = + Couch.Session.get(context.session, "/#{context.db_name}/_all_docs", + query: %{bookmark: bookmark} + ) + + context = + Map.merge(context, %{ + pages: [resp.body | pages], + response: resp.body + }) + + paginate(context) + else + context = + Map.update(context, :pages, [], fn acc -> + Enum.reverse(acc) + end) + + context + end + end + def create_db(session, db_name, opts \\ []) do retry_until(fn -> resp = Couch.Session.put(session, "/#{db_name}", opts) @@ -298,5 +344,730 @@ defmodule Couch.Test.Pagination do assert q1 == Enum.reverse(q2) assert q1 == Enum.sort(q1) end + + test "ensure we paginate starting from first query", ctx do + queries = %{ + queries: [%{descending: false}, %{descending: true}] + } + + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size}, + body: :jiffy.encode(queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + [q1, q2] = resp.body["results"] + q1 = Enum.map(q1["rows"], fn row -> row["id"] end) + q2 = Enum.map(q2["rows"], fn row -> row["id"] end) + assert ctx.page_size == length(q1) + assert q2 == [] + end + end + + describe "Pagination API (10 docs)" do + @describetag n_docs: 10 + @describetag page_size: 4 + setup [:with_session, :random_db, :with_docs] + + test ": _all_docs?page_size=4", ctx do + %{session: session, db_name: db_name} = ctx + + resp = + Couch.Session.get(session, "/#{db_name}/_all_docs", + query: %{page_size: ctx.page_size} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + end + + test ": _all_docs/queries should limit number of queries", ctx do + queries = %{ + queries: [%{}, %{}, %{}, %{}, %{}] + } + + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size}, + body: :jiffy.encode(queries) + ) + + assert resp.status_code == 400 + + assert resp.body["reason"] == + "Provided number of queries is more than given page_size" + end + + test ": _all_docs/queries should forbid `page_size` in queries", ctx do + queries = %{ + queries: [%{page_size: 3}] + } + + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size}, + body: :jiffy.encode(queries) + ) + + assert resp.status_code == 400 + + assert resp.body["reason"] == + "You cannot specify `page_size` inside the query" + end + + test ": _all_docs should forbid `page_size` and `keys`", ctx do + body = %{ + page_size: 3, + keys: [ + "002", + "004" + ] + } + + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs", + body: :jiffy.encode(body) + ) + + assert resp.status_code == 400 + + assert resp.body["reason"] == + "`page_size` is incompatible with `keys`" + end + + test ": _all_docs should limit 'skip' parameter", ctx do + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{page_size: ctx.page_size, skip: 3000} + ) + + assert resp.status_code == 400 + + assert resp.body["reason"] == + "`skip` should be an integer in range [0 .. 2000]" + end + + test ": _all_docs should forbid extra parameters when 'bookmark' is present", ctx do + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{page_size: ctx.page_size, skip: 3000, bookmark: ""} + ) + + assert resp.status_code == 400 + + assert resp.body["reason"] == + "Cannot use `bookmark` with other options" + end + end + + for descending <- [false, true] do + for n <- [4, 9] do + describe "Pagination API (10 docs) : _all_docs?page_size=#{n}&descending=#{ + descending + }" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + setup [:with_session, :random_db, :with_docs, :all_docs] + + test "should return 'next' bookmark", ctx do + body = ctx.response + assert Map.has_key?(body, "next") + end + + test "total_rows matches the length of rows array", ctx do + body = ctx.response + assert body["total_rows"] == length(body["rows"]) + end + + test "total_rows matches the requested page_size", ctx do + body = ctx.response + assert body["total_rows"] == ctx.page_size + end + + test "can use 'next' bookmark to get remaining results", ctx do + bookmark = ctx.response["next"] + + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{bookmark: bookmark} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + body = resp.body + assert body["total_rows"] == length(body["rows"]) + assert body["total_rows"] <= ctx.page_size + end + end + + describe "Pagination API (10 docs) : _all_docs?page_size=#{n}&descending=#{ + descending + } : range" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + setup [:with_session, :random_db, :with_docs] + + test "start_key is respected", ctx do + head_pos = 2 + tail_pos = ctx.n_docs - head_pos + doc_ids = Enum.map(ctx.docs, fn doc -> doc["id"] end) + + {start_pos, doc_ids} = + if ctx.descending do + {head_pos, Enum.reverse(Enum.drop(Enum.sort(doc_ids), -tail_pos))} + else + {tail_pos, Enum.drop(Enum.sort(doc_ids), tail_pos - 1)} + end + + start_key = ~s("#{docid(start_pos)}") + + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{descending: ctx.descending, start_key: start_key} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + ids = Enum.map(resp.body["rows"], fn row -> row["id"] end) + assert doc_ids == ids + end + + test "end_key is respected", ctx do + head_pos = 2 + tail_pos = ctx.n_docs - head_pos + doc_ids = Enum.map(ctx.docs, fn doc -> doc["id"] end) + + {end_pos, doc_ids} = + if ctx.descending do + {tail_pos, Enum.reverse(Enum.drop(Enum.sort(doc_ids), tail_pos - 1))} + else + {head_pos, Enum.drop(Enum.sort(doc_ids), -tail_pos)} + end + + end_key = ~s("#{docid(end_pos)}") + + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{descending: ctx.descending, end_key: end_key} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + ids = Enum.map(resp.body["rows"], fn row -> row["id"] end) + assert doc_ids == ids + end + + test "range between start_key and end_key works", ctx do + head_pos = 2 + slice_size = 3 + doc_ids = Enum.sort(Enum.map(ctx.docs, fn doc -> doc["id"] end)) + # -1 due to 0 based indexing + # -2 is due to 0 based indexing and inclusive end + slice = Enum.slice(doc_ids, (head_pos - 1)..(head_pos + slice_size - 2)) + + {start_key, end_key, doc_ids} = + if ctx.descending do + reversed = Enum.reverse(slice) + [first | _] = reversed + [last | _] = slice + {~s("#{first}"), ~s("#{last}"), reversed} + else + [first | _] = slice + [last | _] = Enum.reverse(slice) + {~s("#{first}"), ~s("#{last}"), slice} + end + + assert length(doc_ids) == slice_size + + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{descending: ctx.descending, start_key: start_key, end_key: end_key} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + ids = Enum.map(resp.body["rows"], fn row -> row["id"] end) + assert doc_ids == ids + end + end + end + end + + for descending <- [false, true] do + for n <- [4, 9] do + describe "Pagination API (10 docs) : _all_docs?page_size=#{n}&descending=#{ + descending + } : pages" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + setup [:with_session, :random_db, :with_docs, :all_docs, :paginate] + + test "final page doesn't include 'next' bookmark", ctx do + assert not Map.has_key?(ctx.response, "next") + assert ctx.response["total_rows"] == rem(ctx.n_docs, ctx.page_size) + end + + test "each but last page has page_size rows", ctx do + pages = Enum.drop(ctx.pages, -1) + + assert Enum.all?(pages, fn resp -> + length(resp["rows"]) == ctx.page_size + end) + end + + test "sum of rows on all pages is equal to number of documents", ctx do + pages = ctx.pages + n = Enum.reduce(pages, 0, fn resp, acc -> acc + length(resp["rows"]) end) + assert n == ctx.n_docs + end + + test "the rows are correctly sorted", ctx do + pages = ctx.pages + + ids = + Enum.reduce(pages, [], fn resp, acc -> + acc ++ Enum.map(resp["rows"], fn row -> row["id"] end) + end) + + if ctx.descending do + assert Enum.reverse(Enum.sort(ids)) == ids + else + assert Enum.sort(ids) == ids + end + end + end + end + end + + for n <- 10..11 do + describe "Pagination API (10 docs) : _all_docs?page_size=#{n}" do + @describetag n_docs: 10 + @describetag descending: false + @describetag page_size: n + setup [:with_session, :random_db, :with_docs, :all_docs] + + test "should not return 'next' bookmark", ctx do + body = ctx.response + assert not Map.has_key?(body, "next") + end + + test "total_rows matches the length of rows array", ctx do + body = ctx.response + assert body["total_rows"] == length(body["rows"]) + end + + test "total_rows less than the requested page_size", ctx do + body = ctx.response + assert body["total_rows"] <= ctx.page_size + end + end + end + + for descending <- [false, true] do + for n <- [4, 9] do + describe "Pagination API (10 docs) : _all_docs/queries?page_size=#{n}&descending=#{ + descending + } : pages" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + + @describetag queries: %{ + queries: [ + %{ + descending: true + }, + %{ + limit: n + 1, + skip: 2 + } + ] + } + + setup [:with_session, :random_db, :with_docs] + + test "one of the results contains 'next' bookmark", ctx do + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + assert Enum.any?(results, fn result -> Map.has_key?(result, "next") end) + end + + test "each 'next' bookmark is working", ctx do + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + bookmarks = + results + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> Map.get(result, "next") end) + + assert [] != bookmarks + + Enum.each(bookmarks, fn bookmark -> + resp = + Couch.Session.get(ctx.session, "/#{ctx.db_name}/_all_docs", + query: %{bookmark: bookmark} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert [] != resp.body["rows"] + end) + + assert Enum.any?(results, fn result -> Map.has_key?(result, "next") end) + end + + test "can post bookmarks to queries", ctx do + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + queries = + results + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> %{bookmark: Map.get(result, "next")} end) + + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + body: :jiffy.encode(%{queries: queries}) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + Enum.each(resp.body["results"], fn result -> + assert [] != result["rows"] + end) + end + + test "respect request page_size", ctx do + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + Enum.each(results ++ resp.body["results"], fn result -> + assert length(result["rows"]) <= ctx.page_size + end) + end + + test "independent page_size in the bookmark", ctx do + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + queries = + resp.body["results"] + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> %{bookmark: Map.get(result, "next")} end) + + resp = + Couch.Session.post(ctx.session, "/#{ctx.db_name}/_all_docs/queries", + body: :jiffy.encode(%{queries: queries}) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + Enum.each(resp.body["results"], fn result -> + assert length(result["rows"]) > ctx.page_size + end) + end + end + end + end + + for descending <- [false, true] do + for n <- [4, 9] do + describe "Pagination API (10 docs) : /{db}/_design/{ddoc}/_view?page_size=#{n}&descending=#{ + descending + }" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + setup [:with_session, :random_db, :with_view, :with_docs] + + test "should return 'next' bookmark", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert Map.has_key?(resp.body, "next") + end + + test "total_rows matches the length of rows array", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + body = resp.body + assert body["total_rows"] == length(body["rows"]) + end + + test "total_rows matches the requested page_size", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert resp.body["total_rows"] == ctx.page_size + end + + test "can use 'next' bookmark to get remaining results", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + bookmark = resp.body["next"] + + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{bookmark: bookmark} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + body = resp.body + assert body["total_rows"] == length(body["rows"]) + assert body["total_rows"] <= ctx.page_size + end + end + end + end + + for n <- 10..11 do + describe "Pagination API (10 docs) : /{db}/_design/{ddoc}/_view?page_size=#{n}" do + @describetag n_docs: 10 + @describetag descending: false + @describetag page_size: n + setup [:with_session, :random_db, :with_view, :with_docs] + + test "should not return 'next' bookmark", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert not Map.has_key?(resp.body, "next") + end + + test "total_rows matches the length of rows array", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + body = resp.body + assert body["total_rows"] == length(body["rows"]) + end + + test "total_rows less than the requested page_size", ctx do + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{page_size: ctx.page_size, descending: ctx.descending} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert resp.body["total_rows"] <= ctx.page_size + end + end + end + + for descending <- [false, true] do + for n <- [4, 9] do + describe "Pagination API (10 docs) : /{db}/_design/{ddoc}/_view/queries?page_size=#{ + n + }&descending=#{descending} : pages" do + @describetag n_docs: 10 + @describetag descending: descending + @describetag page_size: n + + @describetag queries: %{ + queries: [ + %{ + descending: true + }, + %{ + limit: n + 1, + skip: 2 + } + ] + } + setup [:with_session, :random_db, :with_view, :with_docs] + + test "one of the results contains 'next' bookmark", ctx do + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + assert Enum.any?(results, fn result -> Map.has_key?(result, "next") end) + end + + test "each 'next' bookmark is working", ctx do + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + bookmarks = + results + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> Map.get(result, "next") end) + + assert [] != bookmarks + + Enum.each(bookmarks, fn bookmark -> + resp = + Couch.Session.get( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}", + query: %{bookmark: bookmark} + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + assert [] != resp.body["rows"] + end) + + assert Enum.any?(results, fn result -> Map.has_key?(result, "next") end) + end + + test "can post bookmarks to queries", ctx do + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + queries = + results + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> %{bookmark: Map.get(result, "next")} end) + + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + body: :jiffy.encode(%{queries: queries}) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + Enum.each(resp.body["results"], fn result -> + assert [] != result["rows"] + end) + end + + test "respect request page_size", ctx do + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + results = resp.body["results"] + + Enum.each(results ++ resp.body["results"], fn result -> + assert length(result["rows"]) <= ctx.page_size + end) + end + + test "independent page_size in the bookmark", ctx do + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + query: %{page_size: ctx.page_size, descending: ctx.descending}, + body: :jiffy.encode(ctx.queries) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + queries = + resp.body["results"] + |> Enum.filter(fn result -> Map.has_key?(result, "next") end) + |> Enum.map(fn result -> %{bookmark: Map.get(result, "next")} end) + + resp = + Couch.Session.post( + ctx.session, + "/#{ctx.db_name}/_design/#{ctx.ddoc_id}/_view/#{ctx.view_name}/queries", + body: :jiffy.encode(%{queries: queries}) + ) + + assert resp.status_code == 200, "got error #{inspect(resp.body)}" + + Enum.each(resp.body["results"], fn result -> + assert length(result["rows"]) > ctx.page_size + end) + end + end + end end end |