this repo has no description

Add tests for HTTP endpoints

hauleth.dev a94b9d1e 125482fe

verified
Changed files
+185 -28
config
lib
test
esl_hn_web
support
+3
config/test.exs
···
"6iR9shI35kN7Xr5bOLgBVMHXTZQS49Gwu82WW4rsr0uhaia7D+NjfNrhhvcOp4rr",
server: false
+
# Disable main refresher in tests
+
config :esl_hn, refresh: 0
+
# Print only warnings and errors during test
config :logger, level: :warning
+5 -3
lib/esl_hn.ex
···
if it comes from the database, an external API or others.
"""
+
alias EslHn.Cache
+
def all(page \\ 1, per_page \\ 10) do
skip = per_page * (page - 1)
-
ids = EslHn.Cache.get(EslHn, :index, [])
+
ids = Cache.get(EslHn, :index, [])
ids
|> Enum.drop(skip)
|> Enum.take(per_page)
-
|> Enum.map(&story/1)
+
|> Enum.map(&Cache.get(EslHn, &1))
end
def story(id) do
-
EslHn.Cache.get(EslHn, id)
+
Cache.fetch(EslHn, id)
end
def broadcast_new(stories) do
+6
lib/esl_hn/cache.ex
···
:ok
end
+
def flush(tid) do
+
:ets.delete_all_objects(tid)
+
+
:ok
+
end
+
@impl true
def init(opts) do
{name, opts} =
+11 -7
lib/esl_hn/refresher.ex
···
{mod, tid} = Access.fetch!(opts, :cache)
req_opts = Access.get(opts, :req_opts, [])
-
{:ok,
-
%{
-
refresh: refresh,
-
ids: MapSet.new(),
-
cache: {mod, tid},
-
req_opts: req_opts
-
}, {:continue, :refresh}}
+
if refresh > 0 do
+
{:ok,
+
%{
+
refresh: refresh,
+
ids: MapSet.new(),
+
cache: {mod, tid},
+
req_opts: req_opts
+
}, {:continue, :refresh}}
+
else
+
:ignore
+
end
end
@impl true
+18 -2
lib/esl_hn_web/api/controller.ex
···
render(conn, items: EslHn.all(page))
end
-
def show(conn, %{"id" => _id}) do
-
render(conn, item: %EslHn.Hn.Story{})
+
def show(conn, %{"id" => id}) do
+
with {:ok, id} <- try_int(id),
+
{:ok, story} <- EslHn.story(id) do
+
render(conn, item: story)
+
else
+
_ ->
+
conn
+
|> put_status(:not_found)
+
|> put_view(EslHnWeb.Error.JSON)
+
|> render(:"404", %{})
+
end
end
defp get_page(nil), do: 1
defp get_page(input) do
String.to_integer(input)
+
end
+
+
defp try_int(value) do
+
case Integer.parse(value) do
+
{int, ""} -> {:ok, int}
+
_ -> :error
+
end
end
end
+1
lib/esl_hn_web/router.ex
···
pipe_through :api
get "/", API.Controller, :index
+
get "/:id", API.Controller, :show
end
if Application.compile_env(:esl_hn, :dev_routes) do
+120
test/esl_hn_web/api/controller_test.exs
···
+
defmodule EslHnWeb.Api.ControllerTest do
+
use EslHnWeb.ConnCase, async: false
+
use ExUnitProperties
+
+
import EslHn.Test.Data
+
+
alias EslHn.Cache
+
+
setup do
+
on_exit(fn -> Cache.flush(EslHn) end)
+
end
+
+
describe "GET /" do
+
test "with empty cache returns empty list", %{conn: conn} do
+
resp =
+
conn
+
|> get(~p"/")
+
|> json_response(200)
+
+
assert resp == []
+
end
+
+
property "with some stories in index these are fetched", %{conn: conn} do
+
check all(stories <- list_of(story(), max_length: 10)) do
+
ids = Enum.map(stories, & &1.id)
+
Cache.write(EslHn, :index, ids)
+
+
Cache.write_all(EslHn, Enum.map(stories, &{&1.id, &1}))
+
+
resp_ids =
+
conn
+
|> get(~p"/")
+
|> json_response(200)
+
|> Enum.map(& &1["id"])
+
+
assert Enum.all?(ids, &(&1 in resp_ids))
+
end
+
end
+
+
property "return at most 10 elements at once", %{conn: conn} do
+
check all(stories <- list_of(story())) do
+
ids = Enum.map(stories, & &1.id)
+
Cache.write(EslHn, :index, ids)
+
+
Cache.write_all(EslHn, Enum.map(stories, &{&1.id, &1}))
+
+
resp =
+
conn
+
|> get(~p"/")
+
|> json_response(200)
+
+
assert length(resp) <= 10
+
end
+
end
+
+
property "if more than 10 elements there are stories on second page", %{
+
conn: conn
+
} do
+
check all(stories <- list_of(story(), min_length: 11)) do
+
ids = Enum.map(stories, & &1.id)
+
Cache.write(EslHn, :index, ids)
+
+
Cache.write_all(EslHn, Enum.map(stories, &{&1.id, &1}))
+
+
resp =
+
conn
+
|> get(~p"/?page=2")
+
|> json_response(200)
+
+
assert length(resp) in 1..10
+
end
+
end
+
+
property "if requested page is outside of the possible range, returns empty list",
+
%{
+
conn: conn
+
} do
+
check all(stories <- list_of(story(), max_length: 50)) do
+
ids = Enum.map(stories, & &1.id)
+
Cache.write(EslHn, :index, ids)
+
+
Cache.write_all(EslHn, Enum.map(stories, &{&1.id, &1}))
+
+
resp =
+
conn
+
|> get(~p"/?page=2137")
+
|> json_response(200)
+
+
assert resp == []
+
end
+
end
+
end
+
+
describe "GET /:id" do
+
test "for non-existent story returns 404", %{conn: conn} do
+
conn
+
|> get(~p"/2137")
+
|> json_response(404)
+
end
+
+
test "for non-integer story ID returns 404", %{conn: conn} do
+
conn
+
|> get(~p"/foo-bar")
+
|> json_response(404)
+
end
+
+
test "return data for existing story", %{conn: conn} do
+
Cache.write_all(EslHn, [
+
{2137, %EslHn.Hn.Story{id: 2137, title: "Foo"}}
+
])
+
+
resp =
+
conn
+
|> get(~p"/2137")
+
|> json_response(200)
+
+
assert "Foo" == resp["title"]
+
end
+
end
+
end
+2 -16
test/esl_hn_web/api/json_test.exs
···
use ExUnit.Case, async: true
use ExUnitProperties
-
alias EslHn.Hn.Story
+
import EslHn.Test.Data
@subject EslHnWeb.API.JSON
doctest @subject
-
-
defp story do
-
gen all(
-
title <- string(:utf8, min_length: 1),
-
score <- positive_integer()
-
) do
-
%Story{
-
id: System.unique_integer([:positive]),
-
title: title,
-
score: score,
-
url: "https://example.com/#{URI.encode(title)}"
-
}
-
end
-
end
describe "index/1" do
test "for empty list renders empty list" do
···
describe "show/1" do
property "encoded title is the same as input title" do
-
check all story <- story() do
+
check all(story <- story()) do
assert story.title == @subject.show(%{item: story}).title
end
end
+19
test/support/data.ex
···
+
defmodule EslHn.Test.Data do
+
use ExUnitProperties
+
+
alias EslHn.Hn.Story
+
+
def story do
+
gen all(
+
title <- string(:utf8, min_length: 1),
+
score <- positive_integer()
+
) do
+
%Story{
+
id: System.unique_integer([:positive]),
+
title: title,
+
score: score,
+
url: "https://example.com/#{URI.encode(title)}"
+
}
+
end
+
end
+
end