Skip to content

Commit c148e93

Browse files
authored
Merge pull request #1047 from code-corps/1044-standardize-issues-webhook
Standardize output of CodeCorps.GitHub.Events.Issues
2 parents e29b316 + 3b02d53 commit c148e93

5 files changed

Lines changed: 97 additions & 105 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
[![Deps Status](https://beta.hexfaktor.org/badge/prod/github/code-corps/code-corps-api.svg)](https://beta.hexfaktor.org/github/code-corps/code-corps-api)
77
[![Deps Status](https://beta.hexfaktor.org/badge/all/github/code-corps/code-corps-api.svg)](https://beta.hexfaktor.org/github/code-corps/code-corps-api)
88
[![Slack Status](http://slack.codecorps.org/badge.svg)](http://slack.codecorps.org)
9-
</p>
109

1110
---
1211

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule CodeCorps.GitHub.Event.Handler do
2+
@moduledoc ~S"""
3+
Default behavior for all GitHub webhook event handlers.
4+
"""
5+
6+
alias CodeCorps.GithubEvent
7+
8+
@doc ~S"""
9+
The only entry point a GitHub webhook event handler function should contain.
10+
11+
Receives a `CodeCorps.GithubEvent` record and a payload, returns an `:ok`
12+
tuple if the process was successful, or an `:error` tuple, where the second
13+
element is an atom, if it failed.
14+
"""
15+
@callback handle(GithubEvent.t, map) :: {:ok, any} | {:error, atom}
16+
end
Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
defmodule CodeCorps.GitHub.Event.Issues do
22
@moduledoc ~S"""
3-
In charge of dealing with "Issues" GitHub Webhook events
3+
In charge of handling a GitHub Webhook payload for the Issues event type
44
5-
https://developer.github.com/v3/activity/events/types/#issuesevent
5+
[https://developer.github.com/v3/activity/events/types/#issuesevent](https://developer.github.com/v3/activity/events/types/#issuesevent)
66
"""
77

8+
@behaviour CodeCorps.GitHub.Event.Handler
9+
810
alias CodeCorps.{
911
GithubEvent,
1012
GitHub.Event.Common.RepoFinder,
@@ -16,63 +18,70 @@ defmodule CodeCorps.GitHub.Event.Issues do
1618
}
1719
alias Ecto.Multi
1820

19-
@typep outcome :: {:ok, list(Task.t)} |
20-
{:error, :not_fully_implemented} |
21-
{:error, :unexpected_payload} |
22-
{:error, :unexpected_action} |
23-
{:error, :unmatched_repository}
24-
25-
@implemented_actions ~w(opened closed edited reopened)
26-
@unimplemented_actions ~w(assigned unassigned milestoned demilestoned labeled unlabeled)
21+
@type outcome :: {:ok, list(Task.t)} |
22+
{:error, :not_fully_implemented} |
23+
{:error, :unexpected_action} |
24+
{:error, :unexpected_payload} |
25+
{:error, :repository_not_found} |
26+
{:error, :validation_error_on_inserting_user} |
27+
{:error, :multiple_github_users_matched_same_cc_user} |
28+
{:error, :validation_error_on_syncing_tasks} |
29+
{:error, :unexpected_transaction_outcome}
2730

2831
@doc ~S"""
2932
Handles the "Issues" GitHub webhook
3033
3134
The process is as follows
3235
- validate the payload is structured as expected
33-
- try and find the appropriate `GithubRepo` record.
34-
- for each `ProjectGithubRepo` belonging to that `Project`
35-
- find or initialize a new `Task`
36-
- try and find a `User`, associate `Task` with user
37-
- commit the change as an insert or update action
36+
- validate the action is properly supported
37+
- match payload with affected `CodeCorps.GithubRepo` record using `CodeCorps.GitHub.Event.Common.RepoFinder`
38+
- match with a `CodeCorps.User` using `CodeCorps.GitHub.Event.Issues.UserLinker`
39+
- for each `CodeCorps.ProjectGithubRepo` belonging to matched repo
40+
- match and update, or create a `CodeCorps.Task` on the associated `CodeCorps.Project`
3841
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
42+
If the process runs all the way through, the function will return an `:ok`
43+
tuple with a list of affected (created or updated) tasks.
4544
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.
45+
If it fails, it will instead return an `:error` tuple, where the second
46+
element is the atom indicating a reason.
5147
"""
5248
@spec handle(GithubEvent.t, map) :: outcome
53-
def handle(%GithubEvent{action: action}, payload) when action in @implemented_actions do
54-
case payload |> Validator.valid? do
55-
true -> do_handle(payload)
56-
false -> {:error, :unexpected_payload}
57-
end
49+
def handle(%GithubEvent{}, %{} = payload) do
50+
Multi.new
51+
|> Multi.run(:payload, fn _ -> payload |> validate_payload() end)
52+
|> Multi.run(:action, fn _ -> payload |> validate_action() end)
53+
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
54+
|> Multi.run(:user, fn _ -> UserLinker.find_or_create_user(payload) end)
55+
|> Multi.run(:tasks, fn %{repo: github_repo, user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
56+
|> Repo.transaction
57+
|> marshall_result()
5858
end
59-
def handle(%GithubEvent{action: action}, _payload) when action in @unimplemented_actions do
60-
{:error, :not_fully_implemented}
61-
end
62-
def handle(%GithubEvent{action: _action}, _payload), do: {:error, :unexpected_action}
6359

64-
@spec do_handle(map) :: {:ok, list(Task.t)} | {:error, :unmatched_repository}
65-
defp do_handle(%{} = payload) do
66-
multi =
67-
Multi.new
68-
|> Multi.run(:repo, fn _ -> RepoFinder.find_repo(payload) end)
69-
|> Multi.run(:user, fn _ -> UserLinker.find_or_create_user(payload) end)
70-
|> Multi.run(:tasks, fn %{repo: github_repo, user: user} -> TaskSyncer.sync_all(github_repo, user, payload) end)
60+
@spec marshall_result(tuple) :: tuple
61+
defp marshall_result({:ok, %{tasks: tasks}}), do: {:ok, tasks}
62+
defp marshall_result({:error, :payload, :invalid, _steps}), do: {:error, :unexpected_payload}
63+
defp marshall_result({:error, :action, :not_fully_implemented, _steps}), do: {:error, :not_fully_implemented}
64+
defp marshall_result({:error, :action, :unexpected_action, _steps}), do: {:error, :unexpected_action}
65+
defp marshall_result({:error, :repo, :unmatched_project, _steps}), do: {:ok, []}
66+
defp marshall_result({:error, :repo, :unmatched_repository, _steps}), do: {:error, :repository_not_found}
67+
defp marshall_result({:error, :user, %Ecto.Changeset{}, _steps}), do: {:error, :validation_error_on_inserting_user}
68+
defp marshall_result({:error, :user, :multiple_users, _steps}), do: {:error, :multiple_github_users_matched_same_cc_user}
69+
defp marshall_result({:error, :tasks, {_tasks, _errors}, _steps}), do: {:error, :validation_error_on_syncing_tasks}
70+
defp marshall_result({:error, _errored_step, _error_response, _steps}), do: {:error, :unexpected_transaction_outcome}
71+
72+
@implemented_actions ~w(opened closed edited reopened)
73+
@unimplemented_actions ~w(assigned unassigned milestoned demilestoned labeled unlabeled)
74+
75+
@spec validate_action(map) :: {:ok, :implemented} | {:error, :not_fully_implemented | :unexpected_action}
76+
defp validate_action(%{"action" => action}) when action in @implemented_actions, do: {:ok, :implemented}
77+
defp validate_action(%{"action" => action}) when action in @unimplemented_actions, do: {:error, :not_fully_implemented}
78+
defp validate_action(_payload), do: {:error, :unexpected_action}
7179

72-
case Repo.transaction(multi) do
73-
{:ok, %{tasks: tasks}} -> {:ok, tasks}
74-
{:error, :repo, :unmatched_project, _steps} -> {:ok, []}
75-
{:error, _errored_step, error_response, _steps} -> {:error, error_response}
80+
@spec validate_payload(map) :: {:ok, :valid} | {:error, :invalid}
81+
defp validate_payload(%{} = payload) do
82+
case payload |> Validator.valid? do
83+
true -> {:ok, :valid}
84+
false -> {:error, :invalid}
7685
end
7786
end
7887
end

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule CodeCorps.GitHub.Event.Issues.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/issues_test.exs

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
1414
}
1515

1616
describe "handle/2" do
17-
@payload load_event_fixture("issues_opened")
17+
@payload load_event_fixture("issues_opened") |> Map.put("action", "foo")
1818

1919
test "returns error if action of the event is wrong" do
2020
event = build(:github_event, action: "foo", type: "issues")
@@ -83,7 +83,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
8383
end
8484

8585
test "with unmatched user, returns error if unmatched repository" do
86-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
86+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
8787
refute Repo.one(User)
8888
end
8989

@@ -142,7 +142,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
142142
%{"issue" => %{"user" => %{"id" => user_github_id}}} = @payload
143143
insert(:user, github_id: user_github_id)
144144

145-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
145+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
146146
end
147147

148148
test "returns error if payload is wrong" do
@@ -217,7 +217,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
217217
end
218218

219219
test "with unmatched user, returns error if unmatched repository" do
220-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
220+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
221221
refute Repo.one(User)
222222
end
223223

@@ -276,7 +276,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
276276
%{"issue" => %{"user" => %{"id" => user_github_id}}} = @payload
277277
insert(:user, github_id: user_github_id)
278278

279-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
279+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
280280
end
281281

282282
test "returns error if payload is wrong" do
@@ -351,7 +351,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
351351
end
352352

353353
test "with unmatched user, returns error if unmatched repository" do
354-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
354+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
355355
refute Repo.one(User)
356356
end
357357

@@ -410,7 +410,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
410410
%{"issue" => %{"user" => %{"id" => user_github_id}}} = @payload
411411
insert(:user, github_id: user_github_id)
412412

413-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
413+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
414414
end
415415

416416
test "returns error if payload is wrong" do
@@ -485,7 +485,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
485485
end
486486

487487
test "with unmatched user, returns error if unmatched repository" do
488-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
488+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
489489
refute Repo.one(User)
490490
end
491491

@@ -544,7 +544,7 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
544544
%{"issue" => %{"user" => %{"id" => user_github_id}}} = @payload
545545
insert(:user, github_id: user_github_id)
546546

547-
assert Issues.handle(@event, @payload) == {:error, :unmatched_repository}
547+
assert Issues.handle(@event, @payload) == {:error, :repository_not_found}
548548
end
549549

550550
test "returns error if payload is wrong" do
@@ -560,57 +560,24 @@ defmodule CodeCorps.GitHub.Event.IssuesTest do
560560
end
561561
end
562562

563-
describe "handle/2 for Issues::assigned" do
564-
@payload %{}
565-
566-
test "is not implemented" do
567-
event = build(:github_event, action: "assigned", type: "issues")
568-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
569-
end
570-
end
571-
572-
describe "handle/2 for Issues::unassigned" do
573-
@payload %{}
574-
575-
test "is not implemented" do
576-
event = build(:github_event, action: "unassigned", type: "issues")
577-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
578-
end
579-
end
580-
581-
describe "handle/2 for Issues::labeled" do
582-
@payload %{}
583-
584-
test "is not implemented" do
585-
event = build(:github_event, action: "labeled", type: "issues")
586-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
587-
end
588-
end
589-
590-
describe "handle/2 for Issues::unlabeled" do
591-
@payload %{}
592-
593-
test "is not implemented" do
594-
event = build(:github_event, action: "unlabeled", type: "issues")
595-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
596-
end
597-
end
598-
599-
describe "handle/2 for Issues::milestoned" do
600-
@payload %{}
563+
@unimplemented_actions ~w(assigned unassigned labeled unlabeled milestoned demilestoned)
601564

602-
test "is not implemented" do
603-
event = build(:github_event, action: "milestoned", type: "issues")
604-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
605-
end
606-
end
565+
@unimplemented_actions |> Enum.each(fn action ->
566+
describe "handle/2 for Issues::#{action}" do
567+
@payload %{
568+
"action" => action,
569+
"issue" => %{
570+
"id" => 1, "title" => "foo", "body" => "bar", "state" => "baz",
571+
"user" => %{"id" => "bat"}
572+
},
573+
"repository" => %{"id" => 2}
574+
}
607575

608-
describe "handle/2 for Issues::demilestoned" do
609-
@payload %{}
576+
@event build(:github_event, action: action, type: "issues")
610577

611-
test "is not implemented" do
612-
event = build(:github_event, action: "demilestoned", type: "issues")
613-
assert Issues.handle(event, @payload) == {:error, :not_fully_implemented}
578+
test "is not implemented" do
579+
assert Issues.handle(@event, @payload) == {:error, :not_fully_implemented}
580+
end
614581
end
615-
end
582+
end)
616583
end

0 commit comments

Comments
 (0)