Skip to content

Commit 4126b07

Browse files
authored
Merge pull request #1048 from code-corps/1045-standardize-issue-comment-webhook
Standardized output for CodeCorps.GitHub.Event.IssueComment
2 parents c148e93 + b8d7336 commit 4126b07

File tree

3 files changed

+78
-53
lines changed

3 files changed

+78
-53
lines changed
Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
defmodule CodeCorps.GitHub.Event.IssueComment do
22
@moduledoc ~S"""
3-
In charge of dealing with "IssueComment" GitHub Webhook events
4-
5-
https://developer.github.com/v3/activity/events/types/#issuecommentevent
3+
In charge of handling a GitHub Webhook payload for the IssueComment event type
4+
[https://developer.github.com/v3/activity/events/types/#issuecommentevent](https://developer.github.com/v3/activity/events/types/#issuecommentevent)
65
"""
76

7+
@behaviour CodeCorps.GitHub.Event.Handler
8+
89
alias CodeCorps.{
910
Comment,
1011
GithubEvent,
@@ -18,63 +19,86 @@ defmodule CodeCorps.GitHub.Event.IssueComment do
1819
}
1920
alias Ecto.Multi
2021

21-
@typep outcome :: {:ok, list(Comment.t)} |
22-
{:error, :unexpected_payload} |
23-
{:error, :unexpected_action} |
24-
{:error, :unmatched_repository}
22+
@type outcome :: {:ok, list(Comment.t)} |
23+
{:error, :unexpected_action} |
24+
{:error, :unexpected_payload} |
25+
{:error, :repository_not_found} |
26+
{:error, :validation_error_on_inserting_user_for_task} |
27+
{:error, :multiple_github_users_matched_same_cc_user_for_task} |
28+
{:error, :validation_error_on_inserting_user_for_comment} |
29+
{:error, :multiple_github_users_matched_same_cc_user_for_comment} |
30+
{:error, :validation_error_on_syncing_tasks} |
31+
{:error, :validation_error_on_syncing_comments} |
32+
{:error, :unexpected_transaction_outcome}
2533

2634
@doc ~S"""
27-
Handles the "Issues" GitHub webhook
35+
Handles the "IssueComment" GitHub webhook
2836
2937
The process is as follows
3038
- validate the payload is structured as expected
31-
- try and find the appropriate `GithubRepo` record.
32-
- for each `ProjectGithubRepo` belonging to that `Project`
33-
- find or initialize a `Task`
34-
- if initializing, associate with existing, or create `User`
35-
- find or initialize a `Comment`
36-
- if initializing, associate with existing, or create `User`
37-
- commit the change as an insert or update action
39+
- validate the action is properly supported
40+
- match payload with affected `CodeCorps.GithubRepo` record using `CodeCorps.GitHub.Event.Common.RepoFinder`
41+
- match issue part of the payload with a `CodeCorps.User` using `CodeCorps.GitHub.Event.Issues.UserLinker`
42+
- match comment part of the payload with a `CodeCorps.User` using `CodeCorps.GitHub.Event.IssueComment.UserLinker`
43+
- for each `CodeCorps.ProjectGithubRepo` belonging to matched repo
44+
- match and update, or create a `CodeCorps.Task` on the associated `CodeCorps.Project`
45+
- match and update, or create a `CodeCorps.Comment` associated to `CodeCorps.Task`
3846
39-
Depending on the success of the process, the function will return one of
40-
- `{:ok, list_of_tasks}`
41-
- `{:error, :not_fully_implemented}` - while we're aware of this action, we have not implemented support for it yet
42-
- `{:error, :unexpected_payload}` - the payload was not as expected
43-
- `{:error, :unexpected_action}` - the action was not of type we are aware of
44-
- `{:error, :unmatched_repository}` - the repository for this issue was not found
47+
If the process runs all the way through, the function will return an `:ok`
48+
tuple with a list of affected (created or updated) comments.
4549
46-
Note that it is also possible to have a matched GithubRepo, but with that
47-
record not having any ProjectGithubRepo children. The outcome of that case
48-
should NOT be an errored event, since it simply means that the GithubRepo
49-
was not linked to a Project by the Project owner. This is allowed and
50-
relatively common.
50+
If it fails, it will instead return an `:error` tuple, where the second
51+
element is the atom indicating a reason.
5152
"""
5253
@spec handle(GithubEvent.t, map) :: outcome
53-
def handle(%GithubEvent{action: action}, payload) when action in ~w(created edited deleted) do
54-
case payload |> IssueComment.Validator.valid? do
55-
true -> do_handle(payload)
56-
false -> {:error, :unexpected_payload}
57-
end
54+
def handle(%GithubEvent{}, payload) do
55+
Multi.new
56+
|> Multi.run(:payload, fn _ -> payload |> validate_payload() end)
57+
|> Multi.run(:action, fn _ -> payload |> validate_action() end)
58+
|> Multi.append(payload |> operational_multi())
59+
|> Repo.transaction
60+
|> marshall_result()
61+
end
62+
63+
@spec operational_multi(map) :: Multi.t
64+
defp operational_multi(%{"action" => action} = payload) when action in ~w(created edited) do
65+
Multi.new
66+
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
67+
|> Multi.run(:issue_user, fn _ -> Issues.UserLinker.find_or_create_user(payload) end)
68+
|> Multi.run(:comment_user, fn _ -> IssueComment.UserLinker.find_or_create_user(payload) end)
69+
|> Multi.run(:tasks, fn %{repo: github_repo, issue_user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
70+
|> Multi.run(:comments, fn %{tasks: tasks, comment_user: user} -> CommentSyncer.sync_all(tasks, user, payload) end)
71+
end
72+
defp operational_multi(%{"action" => "deleted"} = payload) do
73+
Multi.new
74+
|> Multi.run(:comments, fn _ -> CommentDeleter.delete_all(payload) end)
5875
end
59-
def handle(%GithubEvent{action: _action}, _payload), do: {:error, :unexpected_action}
76+
defp operational_multi(%{}), do: Multi.new
6077

61-
@spec do_handle(map) :: {:ok, list(Comment.t)} | {:error, :unmatched_repository}
62-
defp do_handle(%{"action" => action} = payload) when action in ~w(created edited) do
63-
multi =
64-
Multi.new
65-
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
66-
|> Multi.run(:issue_user, fn _ -> Issues.UserLinker.find_or_create_user(payload) end)
67-
|> Multi.run(:comment_user, fn _ -> IssueComment.UserLinker.find_or_create_user(payload) end)
68-
|> Multi.run(:tasks, fn %{repo: github_repo, issue_user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
69-
|> Multi.run(:comments, fn %{tasks: tasks, comment_user: user} -> CommentSyncer.sync_all(tasks, user, payload) end)
78+
@spec marshall_result(tuple) :: tuple
79+
defp marshall_result({:ok, %{comments: comments}}), do: {:ok, comments}
80+
defp marshall_result({:error, :payload, :invalid, _steps}), do: {:error, :unexpected_payload}
81+
defp marshall_result({:error, :action, :unexpected_action, _steps}), do: {:error, :unexpected_action}
82+
defp marshall_result({:error, :repo, :unmatched_project, _steps}), do: {:ok, []}
83+
defp marshall_result({:error, :repo, :unmatched_repository, _steps}), do: {:error, :repository_not_found}
84+
defp marshall_result({:error, :issue_user, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_user_for_task}
85+
defp marshall_result({:error, :issue_user, :multiple_users, _steps}), do: {:error, :multiple_github_users_matched_same_cc_user_for_task}
86+
defp marshall_result({:error, :comment_user, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_user_for_comment}
87+
defp marshall_result({:error, :comment_user, :multiple_users, _steps}), do: {:error, :multiple_github_users_matched_same_cc_user_for_comment}
88+
defp marshall_result({:error, :tasks, {_tasks, _errors}, _steps}), do: {:error, :validation_error_on_syncing_tasks}
89+
defp marshall_result({:error, :comments, {_comments, _errors}, _steps}), do: {:error, :validation_error_on_syncing_comments}
90+
defp marshall_result({:error, _errored_step, _error_response, _steps}), do: {:error, :unexpected_transaction_outcome}
7091

71-
case Repo.transaction(multi) do
72-
{:ok, %{comments: comments}} -> {:ok, comments}
73-
{:error, :repo, :unmatched_project, _steps} -> {:ok, []}
74-
{:error, _errored_step, error_response, _steps} -> {:error, error_response}
92+
@spec validate_payload(map) :: {:ok, :valid} | {:error, :invalid}
93+
defp validate_payload(%{} = payload) do
94+
case payload |> IssueComment.Validator.valid? do
95+
true -> {:ok, :valid}
96+
false -> {:error, :invalid}
7597
end
7698
end
77-
defp do_handle(%{"action" => "deleted"} = payload) do
78-
CommentDeleter.delete_all(payload)
79-
end
99+
100+
@implemented_actions ~w(created edited deleted)
101+
@spec validate_action(map) :: {:ok, :implemented} | {:error, :unexpected_action}
102+
defp validate_action(%{"action" => action}) when action in @implemented_actions, do: {:ok, :implemented}
103+
defp validate_action(%{}), do: {:error, :unexpected_action}
80104
end

lib/code_corps/github/event/issue_comment/validator.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule CodeCorps.GitHub.Event.IssueComment.Validator do
1111
"""
1212
@spec valid?(map) :: boolean
1313
def valid?(%{
14+
"action" => _,
1415
"issue" => %{
1516
"id" => _, "title" => _, "body" => _, "state" => _,
1617
"user" => %{"id" => _}

test/lib/code_corps/github/event/issue_comment_test.exs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule CodeCorps.GitHub.Event.IssueCommentTest do
1515
}
1616

1717
describe "handle/2" do
18-
@payload load_event_fixture("issue_comment_created")
18+
@payload load_event_fixture("issue_comment_created") |> Map.put("action", "foo")
1919

2020
test "returns error if action of the event is wrong" do
2121
event = build(:github_event, action: "foo", type: "issue_comment")
@@ -96,7 +96,7 @@ defmodule CodeCorps.GitHub.Event.IssueCommentTest do
9696
end
9797

9898
test "with unmatched both users, returns error if unmatched repository" do
99-
assert IssueComment.handle(@event, @payload) == {:error, :unmatched_repository}
99+
assert IssueComment.handle(@event, @payload) == {:error, :repository_not_found}
100100
refute Repo.one(User)
101101
end
102102

@@ -180,7 +180,7 @@ defmodule CodeCorps.GitHub.Event.IssueCommentTest do
180180

181181
_issue_user = insert(:user, github_id: issue_user_github_id)
182182

183-
assert IssueComment.handle(@event, @payload) == {:error, :unmatched_repository}
183+
assert IssueComment.handle(@event, @payload) == {:error, :repository_not_found}
184184
end
185185

186186
test "with unmatched issue user, matched comment_user, passes with no changes made if no matching projects" do
@@ -255,7 +255,7 @@ defmodule CodeCorps.GitHub.Event.IssueCommentTest do
255255

256256
_comment_user = insert(:user, github_id: comment_user_github_id)
257257

258-
assert IssueComment.handle(@event, @payload) == {:error, :unmatched_repository}
258+
assert IssueComment.handle(@event, @payload) == {:error, :repository_not_found}
259259
end
260260

261261
test "with matched issue and comment_user, passes with no changes made if no matching projects" do
@@ -350,7 +350,7 @@ defmodule CodeCorps.GitHub.Event.IssueCommentTest do
350350
insert(:user, github_id: comment_user_github_id)
351351
insert(:user, github_id: issue_user_github_id)
352352

353-
assert IssueComment.handle(@event, @payload) == {:error, :unmatched_repository}
353+
assert IssueComment.handle(@event, @payload) == {:error, :repository_not_found}
354354
end
355355

356356
test "returns error if payload is wrong" do

0 commit comments

Comments
 (0)