Skip to content

Commit 560f057

Browse files
hashrocketeervnegrisoloGabriel Reis
committed
Allow developer to delete their own TIL post
Co-authored-by: Vinicius Negrisolo <vinicius.negrisolo@hashrocket.com> Co-authored-by: Gabriel Reis <gabriel.reis@hashrocket.com>
1 parent e81a77e commit 560f057

6 files changed

Lines changed: 215 additions & 12 deletions

File tree

lib/tilex/posts.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ defmodule Tilex.Posts do
4444
end
4545
end
4646

47+
def delete_post(post) do
48+
Repo.delete(post)
49+
end
50+
4751
def published(query \\ Post) do
4852
from(p in query, where: not is_nil(p.published_at) and p.published_at <= fragment("now()"))
4953
end

lib/tilex_web/controllers/post_controller.ex

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ defmodule TilexWeb.PostController do
1111
alias Tilex.Posts
1212

1313
plug(:load_channels when action in [:new, :create, :edit, :update])
14-
plug(:extract_slug when action in [:show, :edit, :update])
14+
plug(:extract_slug when action in [:show, :edit, :update, :delete])
1515

1616
plug(
1717
Guardian.Plug.EnsureAuthenticated,
1818
[error_handler: __MODULE__]
19-
when action in ~w(new create edit update)a
19+
when action in ~w(new create edit update delete)a
2020
)
2121

2222
@behaviour Guardian.Plug.ErrorHandler
@@ -196,6 +196,30 @@ defmodule TilexWeb.PostController do
196196
end
197197
end
198198

199+
def delete(conn, _params) do
200+
current_user = Guardian.Plug.current_resource(conn)
201+
202+
post =
203+
case current_user.admin do
204+
false -> assoc(current_user, :posts)
205+
true -> Post
206+
end
207+
|> Repo.get_by!(slug: conn.assigns.slug)
208+
|> Repo.preload([:developer])
209+
210+
case Posts.delete_post(post) do
211+
{:ok, _post} ->
212+
conn
213+
|> put_flash(:info, "Post deleted successfully")
214+
|> redirect(to: Routes.developer_path(conn, :show, post.developer))
215+
216+
{:error, _changeset} ->
217+
conn
218+
|> put_flash(:error, "Failed to delete post")
219+
|> redirect(to: Routes.post_path(conn, :show, post))
220+
end
221+
end
222+
199223
defp load_channels(conn, _) do
200224
query =
201225
Channel

lib/tilex_web/templates/shared/post.html.eex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
<li>
4646
<%= link("edit", to: Routes.post_path(@conn, :edit, @post), class: "post__permalink") %>
4747
</li>
48+
<li>
49+
<%= link("delete", to: Routes.post_path(@conn, :delete, @post), method: :delete, class: "post__delete-link", data: [confirm: "Are you sure you want to delete this post?"]) %>
50+
</li>
4851
<% end %>
4952
<li>
5053
<%= link to: "#", class: "js-like-action post__like-link", id: @post.slug do %>

test/controllers/post_controller_test.exs

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
defmodule Tilex.PostControllerTest do
22
use TilexWeb.ConnCase, async: true
3-
alias Tilex.Factory
3+
44
alias Tilex.Auth
5+
alias Tilex.Blog.Post
6+
alias Tilex.Factory
7+
alias Tilex.Repo
58

69
test "lists all entries on index", %{conn: conn} do
710
conn = get(conn, Routes.post_path(conn, :index))
@@ -55,7 +58,7 @@ defmodule Tilex.PostControllerTest do
5558
post(conn, Routes.post_path(conn, :create, params))
5659

5760
til =
58-
Tilex.Blog.Post
61+
Post
5962
|> Tilex.Repo.all()
6063
|> List.first()
6164

@@ -79,7 +82,7 @@ defmodule Tilex.PostControllerTest do
7982
post(conn, Routes.post_path(conn, :create, params))
8083

8184
til =
82-
Tilex.Blog.Post
85+
Post
8386
|> Tilex.Repo.all()
8487
|> List.first()
8588

@@ -102,7 +105,7 @@ defmodule Tilex.PostControllerTest do
102105
post(conn, Routes.post_path(conn, :create, params))
103106

104107
til =
105-
Tilex.Blog.Post
108+
Post
106109
|> Tilex.Repo.all()
107110
|> List.first()
108111

@@ -133,7 +136,7 @@ defmodule Tilex.PostControllerTest do
133136
put(conn, Routes.post_path(conn, :update, til.slug, params))
134137

135138
til =
136-
Tilex.Blog.Post
139+
Post
137140
|> Tilex.Repo.get(til.id)
138141

139142
assert til.title == "New Title"
@@ -162,7 +165,7 @@ defmodule Tilex.PostControllerTest do
162165
put(conn, Routes.post_path(conn, :update, til.slug, params))
163166

164167
til =
165-
Tilex.Blog.Post
168+
Post
166169
|> Tilex.Repo.get(til.id)
167170

168171
assert til.title == "New Title"
@@ -190,18 +193,82 @@ defmodule Tilex.PostControllerTest do
190193
put(conn, Routes.post_path(conn, :update, til.slug, params))
191194

192195
til =
193-
Tilex.Blog.Post
196+
Post
194197
|> Tilex.Repo.get(til.id)
195198

196199
assert til.title == "New Title"
197200
assert til.max_likes == 1
198201
end
202+
203+
test "author can delete their own post", %{
204+
conn: conn,
205+
current_user: current_user
206+
} do
207+
post = Factory.insert!(:post, developer: current_user)
208+
209+
conn = delete(conn, Routes.post_path(conn, :delete, post.slug))
210+
211+
assert redirected_to(conn) == Routes.developer_path(conn, :show, current_user)
212+
assert get_flash(conn, :info) == "Post deleted successfully"
213+
assert Repo.all(Post) == []
214+
end
215+
216+
test "author cannot delete another developer's post", %{
217+
conn: conn
218+
} do
219+
other_user = Factory.insert!(:developer, username: "other-dev")
220+
post = Factory.insert!(:post, developer: other_user)
221+
post_id = post.id
222+
223+
assert_raise Ecto.NoResultsError, fn ->
224+
delete(conn, Routes.post_path(conn, :delete, post.slug))
225+
end
226+
227+
assert [%Post{id: ^post_id}] = Repo.all(Post)
228+
end
199229
end
200230

201-
defp authenticated_conn(%{conn: conn}) do
202-
current_user = Factory.insert!(:developer, email: "current@example.com", username: "current")
203-
channel = Factory.insert!(:channel, name: "git")
231+
describe "when authenticated as admin" do
232+
setup :authenticated_admin_conn
204233

234+
test "admin can delete any post", %{
235+
conn: conn,
236+
current_user: _admin
237+
} do
238+
other_user = Factory.insert!(:developer, username: "other-dev")
239+
post = Factory.insert!(:post, developer: other_user)
240+
241+
conn = delete(conn, Routes.post_path(conn, :delete, post.slug))
242+
243+
assert redirected_to(conn) == Routes.developer_path(conn, :show, other_user)
244+
assert get_flash(conn, :info) == "Post deleted successfully"
245+
assert Repo.all(Post) == []
246+
end
247+
end
248+
249+
test "unauthenticated user cannot delete post", %{conn: conn} do
250+
post = Factory.insert!(:post)
251+
post_id = post.id
252+
253+
conn = delete(conn, Routes.post_path(conn, :delete, post.slug))
254+
255+
assert html_response(conn, 302)
256+
assert get_flash(conn, :info) == "Authentication required"
257+
assert [%Post{id: ^post_id}] = Repo.all(Post)
258+
end
259+
260+
defp authenticated_conn(context) do
261+
Factory.insert!(:developer, email: "current@example.com", username: "current")
262+
|> do_authenticated_conn(context)
263+
end
264+
265+
defp authenticated_admin_conn(context) do
266+
Factory.insert!(:developer, email: "admin@example.com", username: "admin", admin: true)
267+
|> do_authenticated_conn(context)
268+
end
269+
270+
defp do_authenticated_conn(current_user, %{conn: conn}) do
271+
channel = Factory.insert!(:channel, name: "git")
205272
conn = Auth.Guardian.Plug.sign_in(conn, current_user)
206273
{:ok, conn: conn, current_user: current_user, channel: channel}
207274
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
defmodule DeveloperDeletesPostTest do
2+
use Tilex.IntegrationCase, async: false
3+
4+
alias Tilex.Blog.Post
5+
alias Tilex.Integration.Pages.PostShowPage
6+
alias Tilex.Repo
7+
8+
feature "developer can delete their own post", %{session: session} do
9+
developer = Factory.insert!(:developer)
10+
post = Factory.insert!(:post, developer: developer)
11+
12+
session
13+
|> sign_in(developer)
14+
|> PostShowPage.navigate(post)
15+
|> click_and_confirm(Query.link("delete"))
16+
|> assert_flash(:info, "Post deleted successfully")
17+
|> assert_path(developer_path(TilexWeb.Endpoint, :show, developer))
18+
19+
assert Repo.all(Post) == []
20+
end
21+
22+
feature "admin can delete other developer's post", %{session: session} do
23+
developer = Factory.insert!(:developer, username: "regular-dev")
24+
admin = Factory.insert!(:developer, username: "admin-user", admin: true)
25+
post = Factory.insert!(:post, developer: developer)
26+
27+
session
28+
|> sign_in(admin)
29+
|> PostShowPage.navigate(post)
30+
|> click_and_confirm(Query.link("delete"))
31+
|> assert_flash(:info, "Post deleted successfully")
32+
|> assert_path(developer_path(TilexWeb.Endpoint, :show, developer))
33+
34+
assert Repo.all(Post) == []
35+
end
36+
37+
feature "non-owner cannot see delete button", %{session: session} do
38+
developer = Factory.insert!(:developer, username: "post-owner")
39+
other_developer = Factory.insert!(:developer, username: "other-dev")
40+
post = Factory.insert!(:post, developer: developer)
41+
42+
session
43+
|> sign_in(other_developer)
44+
|> PostShowPage.navigate(post)
45+
|> refute_has(Query.link("delete"))
46+
end
47+
end

test/support/integration_case.ex

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
defmodule Tilex.IntegrationCase do
22
use ExUnit.CaseTemplate
33

4+
alias Wallaby.Browser
5+
alias Wallaby.Element
6+
alias Wallaby.Query
7+
48
using do
59
quote do
610
use Wallaby.Feature
@@ -34,4 +38,58 @@ defmodule Tilex.IntegrationCase do
3438
end
3539
end
3640
end
41+
42+
def click_and_confirm(session, query) do
43+
Browser.accept_confirm(session, &Browser.click(&1, query))
44+
session
45+
end
46+
47+
def assert_path(session, path) do
48+
retry!(session, fn ->
49+
assert Browser.current_path(session) == path
50+
session
51+
end)
52+
end
53+
54+
def assert_contains(session, query, expected_text) do
55+
retry!(session, fn ->
56+
texts = session |> Browser.all(query) |> Enum.map(&Element.text/1)
57+
58+
assert Enum.any?(texts, &String.contains?(&1, expected_text)),
59+
"Unable to find contains text: '#{expected_text}', instead found '#{texts}'"
60+
61+
session
62+
end)
63+
end
64+
65+
def assert_texts(session, query, expected_texts) do
66+
retry!(session, fn ->
67+
texts = session |> Browser.all(query) |> Enum.map(&Element.text/1)
68+
69+
assert texts == expected_texts,
70+
"Unable to find text: '#{expected_texts}', instead found '#{texts}'"
71+
72+
session
73+
end)
74+
end
75+
76+
def assert_flash(session, level, message) when level in [:info, :success] do
77+
assert_contains(session, Query.css(".alert-#{level}"), message)
78+
end
79+
80+
@waits [10, 20, 30, 50, 80, 130]
81+
def retry!(session, func, waits \\ @waits) do
82+
func.()
83+
session
84+
rescue
85+
error ->
86+
case waits do
87+
[] ->
88+
reraise(error, __STACKTRACE__)
89+
90+
[wait | waits] ->
91+
Process.sleep(wait)
92+
retry!(session, func, waits)
93+
end
94+
end
3795
end

0 commit comments

Comments
 (0)