Skip to content

Commit f531647

Browse files
authored
Merge pull request #997 from code-corps/980-fix-email-overwriting-by-github
Fix email overwriting by GitHub
2 parents f7b2fbe + af8117a commit f531647

7 files changed

Lines changed: 220 additions & 99 deletions

File tree

lib/code_corps/accounts/accounts.ex

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,103 @@ defmodule CodeCorps.Accounts do
22
@moduledoc ~S"""
33
Main entry-point for managing accounts.
44
5-
All actions to acounts should go through here.
5+
All actions to accounts should go through here.
66
"""
77

88
alias CodeCorps.{
9+
Accounts.Changesets,
10+
Comment,
911
GitHub.Adapters,
12+
GithubAppInstallation,
1013
User,
11-
Repo
14+
Repo,
15+
Task
1216
}
13-
alias Ecto.Changeset
17+
alias Ecto.{Changeset, Multi}
18+
19+
import Ecto.Query
1420

1521
@doc ~S"""
1622
Creates a user record using attributes from a GitHub payload.
1723
"""
1824
@spec create_from_github(map) :: {:ok, User.t} | {:error, Changeset.t}
1925
def create_from_github(%{} = attrs) do
2026
%User{}
21-
|> create_from_github_changeset(attrs)
27+
|> Changesets.create_from_github_changeset(attrs)
2228
|> Repo.insert
2329
end
2430

2531
@doc ~S"""
26-
Casts a changeset used for creating a user account from a github user payload
32+
Updates a user record using attributes from a GitHub payload along with the
33+
access token.
2734
"""
28-
@spec create_from_github_changeset(struct, map) :: Changeset.t
29-
def create_from_github_changeset(struct, %{} = params) do
30-
struct
31-
|> Changeset.change(params |> Adapters.User.from_github_user())
32-
|> Changeset.put_change(:context, "github")
33-
|> Changeset.unique_constraint(:email)
34-
|> Changeset.validate_inclusion(:type, ["bot", "user"])
35+
@spec update_from_github_oauth(User.t, map, String.t) :: {:ok, User.t} | {:error, Changeset.t}
36+
def update_from_github_oauth(%User{} = user, %{} = params, access_token) do
37+
params =
38+
params
39+
|> Adapters.User.from_github_user()
40+
|> Map.put(:github_auth_token, access_token)
41+
42+
changeset = user |> Changesets.update_from_github_oauth_changeset(params)
43+
44+
multi =
45+
Multi.new
46+
|> Multi.update(:user, changeset)
47+
|> Multi.run(:installations, fn %{user: %User{} = user} -> user |> associate_installations() end)
48+
|> Multi.run(:tasks, fn %{user: %User{} = user} -> user |> associate_tasks() end)
49+
|> Multi.run(:comments, fn %{user: %User{} = user} -> user |> associate_comments() end)
50+
51+
case Repo.transaction(multi) do
52+
{:ok, %{user: %User{} = user, installations: installations}} ->
53+
{:ok, user |> Map.put(:github_app_installations, installations)}
54+
{:error, :user, %Changeset{} = changeset, _actions_done} ->
55+
{:error, changeset}
56+
end
57+
end
58+
59+
@spec associate_installations(User.t) :: {:ok, list(GithubAppInstallation.t)}
60+
defp associate_installations(%User{id: user_id, github_id: github_id}) do
61+
updates = [set: [user_id: user_id]]
62+
update_options = [returning: true]
63+
64+
GithubAppInstallation
65+
|> where([i], i.sender_github_id == ^github_id)
66+
|> where([i], is_nil(i.user_id))
67+
|> Repo.update_all(updates, update_options)
68+
|> (fn {_count, installations} -> {:ok, installations} end).()
69+
end
70+
71+
@spec associate_tasks(User.t) :: {:ok, list(Task.t)}
72+
defp associate_tasks(%User{id: user_id, github_id: github_id}) do
73+
updates = [set: [user_id: user_id]]
74+
update_options = [returning: true]
75+
76+
existing_user_ids =
77+
User
78+
|> where(github_id: ^github_id)
79+
|> select([u], u.id)
80+
|> Repo.all
81+
82+
Task
83+
|> where([t], t.user_id in ^existing_user_ids)
84+
|> Repo.update_all(updates, update_options)
85+
|> (fn {_count, tasks} -> {:ok, tasks} end).()
86+
end
87+
88+
@spec associate_comments(User.t) :: {:ok, list(Comment.t)}
89+
defp associate_comments(%User{id: user_id, github_id: github_id}) do
90+
updates = [set: [user_id: user_id]]
91+
update_options = [returning: true]
92+
93+
existing_user_ids =
94+
User
95+
|> where(github_id: ^github_id)
96+
|> select([u], u.id)
97+
|> Repo.all
98+
99+
Comment
100+
|> where([c], c.user_id in ^existing_user_ids)
101+
|> Repo.update_all(updates, update_options)
102+
|> (fn {_count, comments} -> {:ok, comments} end).()
35103
end
36104
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
defmodule CodeCorps.Accounts.Changesets do
2+
@moduledoc ~S"""
3+
Changesets for Code Corps accounts.
4+
"""
5+
6+
alias CodeCorps.GitHub.Adapters
7+
alias Ecto.Changeset
8+
9+
@doc ~S"""
10+
Casts a changeset used for creating a user account from a github user payload
11+
"""
12+
@spec create_from_github_changeset(struct, map) :: Changeset.t
13+
def create_from_github_changeset(struct, %{} = params) do
14+
struct
15+
|> Changeset.change(params |> Adapters.User.from_github_user())
16+
|> Changeset.put_change(:sign_up_context, "github")
17+
|> Changeset.unique_constraint(:email)
18+
|> Changeset.validate_inclusion(:type, ["bot", "user"])
19+
end
20+
21+
@doc ~S"""
22+
Casts a changeset used for creating a user account from a github user payload
23+
"""
24+
@spec update_from_github_oauth_changeset(struct, map) :: Changeset.t
25+
def update_from_github_oauth_changeset(struct, %{} = params) do
26+
struct
27+
|> Changeset.cast(params, [:github_auth_token, :github_avatar_url, :github_id, :github_username, :type])
28+
|> ensure_email_without_overwriting(params)
29+
|> Changeset.unique_constraint(:email)
30+
|> Changeset.validate_required([:github_auth_token, :github_avatar_url, :github_id, :github_username, :type])
31+
end
32+
33+
@spec ensure_email_without_overwriting(Changeset.t, map) :: Changeset.t
34+
defp ensure_email_without_overwriting(%Changeset{} = changeset, %{"email" => new_email} = _params) do
35+
case changeset |> Changeset.get_field(:email) do
36+
nil -> changeset |> Changeset.put_change(:email, new_email)
37+
_email -> changeset
38+
end
39+
end
40+
defp ensure_email_without_overwriting(%Changeset{} = changeset, _params), do: changeset
41+
end

lib/code_corps/github/user.ex

Lines changed: 3 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ defmodule CodeCorps.GitHub.User do
33
Used to perform user actions on the github API
44
"""
55

6-
alias CodeCorps.{Comment, GitHub, GithubAppInstallation, Repo, Task, User}
7-
alias CodeCorps.GitHub.Adapters.User, as: UserAdapter
8-
alias Ecto.{Changeset, Multi}
6+
alias CodeCorps.{Accounts, GitHub, User}
7+
alias Ecto.{Changeset}
98

10-
import Ecto.Query
119

1210
@single_endpoint "user"
1311

@@ -42,73 +40,7 @@ defmodule CodeCorps.GitHub.User do
4240
@spec do_connect(User.t, map, String.t) :: {:ok, User.t} | {:error, Changeset.t}
4341
defp do_connect(%User{} = user, %{} = user_payload, access_token)
4442
when is_binary(access_token) do
45-
46-
changeset =
47-
user
48-
|> Changeset.change(user_payload |> UserAdapter.from_github_user)
49-
|> Changeset.put_change(:github_auth_token, access_token)
50-
|> Changeset.validate_required([:github_auth_token, :github_avatar_url, :github_id, :github_username])
51-
52-
multi =
53-
Multi.new
54-
|> Multi.update(:user, changeset)
55-
|> Multi.run(:installations, fn %{user: %User{} = user} -> user |> associate_installations() end)
56-
|> Multi.run(:tasks, fn %{user: %User{} = user} -> user |> associate_tasks() end)
57-
|> Multi.run(:comments, fn %{user: %User{} = user} -> user |> associate_comments() end)
58-
59-
case Repo.transaction(multi) do
60-
{:ok, %{user: %User{} = user, installations: installations}} ->
61-
{:ok, user |> Map.put(:github_app_installations, installations)}
62-
{:error, :user, %Changeset{} = changeset, _actions_done} ->
63-
{:error, changeset}
64-
end
65-
end
66-
67-
68-
@spec associate_installations(User.t) :: {:ok, list(GithubAppInstallation.t)}
69-
defp associate_installations(%User{id: user_id, github_id: github_id}) do
70-
updates = [set: [user_id: user_id]]
71-
update_options = [returning: true]
72-
73-
GithubAppInstallation
74-
|> where([i], i.sender_github_id == ^github_id)
75-
|> where([i], is_nil(i.user_id))
76-
|> Repo.update_all(updates, update_options)
77-
|> (fn {_count, installations} -> {:ok, installations} end).()
78-
end
79-
80-
@spec associate_tasks(User.t) :: {:ok, list(Task.t)}
81-
defp associate_tasks(%User{id: user_id, github_id: github_id}) do
82-
updates = [set: [user_id: user_id]]
83-
update_options = [returning: true]
84-
85-
existing_user_ids =
86-
User
87-
|> where(github_id: ^github_id)
88-
|> select([u], u.id)
89-
|> Repo.all
90-
91-
Task
92-
|> where([t], t.user_id in ^existing_user_ids)
93-
|> Repo.update_all(updates, update_options)
94-
|> (fn {_count, tasks} -> {:ok, tasks} end).()
95-
end
96-
97-
@spec associate_comments(User.t) :: {:ok, list(Comment.t)}
98-
defp associate_comments(%User{id: user_id, github_id: github_id}) do
99-
updates = [set: [user_id: user_id]]
100-
update_options = [returning: true]
101-
102-
existing_user_ids =
103-
User
104-
|> where(github_id: ^github_id)
105-
|> select([u], u.id)
106-
|> Repo.all
107-
108-
Comment
109-
|> where([c], c.user_id in ^existing_user_ids)
110-
|> Repo.update_all(updates, update_options)
111-
|> (fn {_count, comments} -> {:ok, comments} end).()
43+
Accounts.update_from_github_oauth(user, user_payload, access_token)
11244
end
11345

11446
@doc ~S"""

priv/repo/seeds.exs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
alias CodeCorps.{
22
Category, Organization, ProjectUser, Project, ProjectCategory, ProjectSkill,
3-
Repo, Role, Skill, SluggedRoute, Task, User, UserCategory, UserRole,
4-
UserSkill
3+
Repo, Role, Skill, Task, User, UserCategory, UserRole, UserSkill
54
}
65

76
users = [

test/lib/code_corps/accounts/accounts_test.exs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,38 @@ defmodule CodeCorps.AccountsTest do
1414
|> Accounts.create_from_github
1515

1616
assert user.id
17-
assert user.context == "github"
17+
assert user.sign_up_context == "github"
1818
assert user.type == "user"
1919
end
2020

21-
test "returns changeset if there was a validation error" do
21+
test "validates the uniqueness of email" do
2222
%{"email" => email} = payload = TestHelpers.load_endpoint_fixture("user")
23-
# email must be unique, so if a user with email already exists, this
24-
# triggers a validation error
23+
24+
# Ensure a user exists so there's a duplicate email
2525
insert(:user, email: email)
2626

27-
{:error, %Changeset{} = changeset} = payload |> Accounts.create_from_github
27+
{:error, %Changeset{} = changeset} =
28+
payload
29+
|> Accounts.create_from_github
30+
2831
assert changeset.errors[:email] == {"has already been taken", []}
2932
end
3033
end
3134

32-
describe "create_from_github_changeset/1" do
33-
test "validates inclusion of type" do
34-
params = %{"email" => "test@email.com", "type" => "Organization"}
35-
changeset = Accounts.create_from_github_changeset(%User{}, params)
36-
assert changeset.errors[:type] == {"is invalid", [validation: :inclusion]}
35+
describe "update_from_github_oauth/3" do
36+
test "updates proper user from provided payload" do
37+
user = insert(:user)
38+
params = TestHelpers.load_endpoint_fixture("user")
39+
token = "random_token"
40+
41+
{:ok, %User{} = user} =
42+
user
43+
|> Accounts.update_from_github_oauth(params, token)
44+
45+
assert user.id
46+
assert user.github_auth_token == token
47+
assert user.sign_up_context == "default"
48+
assert user.type == "user"
3749
end
3850
end
3951
end
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
defmodule CodeCorps.Accounts.ChangesetsTest do
2+
@moduledoc false
3+
4+
use CodeCorps.DbAccessCase
5+
6+
alias CodeCorps.{Accounts, User}
7+
8+
describe "create_from_github_changeset/1" do
9+
test "validates inclusion of type" do
10+
params = %{"email" => "test@email.com", "type" => "Organization"}
11+
12+
changeset =
13+
%User{}
14+
|> Accounts.Changesets.create_from_github_changeset(params)
15+
16+
assert changeset.errors[:type] == {"is invalid", [validation: :inclusion]}
17+
end
18+
end
19+
20+
describe "update_from_github_oauth_changeset/2" do
21+
test "ensures an email is not overridden when the user has an email" do
22+
user = insert(:user, email: "original@email.com")
23+
params = %{"email" => "new@email.com"}
24+
25+
changeset =
26+
user
27+
|> Accounts.Changesets.update_from_github_oauth_changeset(params)
28+
29+
refute changeset.changes[:email]
30+
end
31+
32+
test "ensures an email is not set to nil" do
33+
user = insert(:user, email: "original@email.com")
34+
params = %{"email" => nil}
35+
36+
changeset =
37+
user
38+
|> Accounts.Changesets.update_from_github_oauth_changeset(params)
39+
40+
refute changeset.changes[:email]
41+
end
42+
43+
test "ensures an email is set when initially nil" do
44+
user = insert(:user, email: nil)
45+
params = %{"email" => "new@email.com"}
46+
47+
changeset =
48+
user
49+
|> Accounts.Changesets.update_from_github_oauth_changeset(params)
50+
51+
assert changeset.changes[:email]
52+
end
53+
54+
test "works without email params" do
55+
user = insert(:user)
56+
params = %{}
57+
58+
changeset =
59+
user
60+
|> Accounts.Changesets.update_from_github_oauth_changeset(params)
61+
62+
refute changeset.errors[:email]
63+
end
64+
end
65+
end

0 commit comments

Comments
 (0)