Skip to content

Commit 726363a

Browse files
authored
Merge pull request #671 from code-corps/670-task-skill-model-policies-endpoints
TaskSkill model, policies and endpoints
2 parents 60c06de + fcc3f5d commit 726363a

File tree

14 files changed

+484
-15
lines changed

14 files changed

+484
-15
lines changed

lib/code_corps/helpers/policy.ex

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ defmodule CodeCorps.Helpers.Policy do
66

77
import Ecto.Query
88

9-
alias CodeCorps.{Organization, OrganizationMembership, Project, Repo, StripeConnectAccount, User}
9+
alias CodeCorps.{
10+
Organization, OrganizationMembership,
11+
Project, Repo, StripeConnectAccount,
12+
TaskSkill, Task, User, UserTask
13+
}
1014
alias Ecto.Changeset
1115

1216
@doc """
@@ -15,6 +19,7 @@ defmodule CodeCorps.Helpers.Policy do
1519
1620
Returns `CodeCorps.OrganizationMembership`
1721
"""
22+
@spec get_membership(nil | Changeset.t | Project.t | Organization.t | StripeConnectAccount.t, User.t) :: nil | OrganizationMembership.t
1823
def get_membership(nil, %User{}), do: nil
1924
def get_membership(%Changeset{changes: %{organization_id: organization_id}}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
2025
def get_membership(%Project{organization_id: organization_id}, %User{id: user_id}), do: do_get_membership(organization_id, user_id)
@@ -31,6 +36,7 @@ defmodule CodeCorps.Helpers.Policy do
3136
3237
Returns `CodeCorps.Project`
3338
"""
39+
@spec get_project(struct | Changeset.t | any) :: Project.t
3440
def get_project(%{project_id: project_id}), do: Project |> Repo.get(project_id)
3541
def get_project(%Changeset{changes: %{project_id: project_id}}), do: Project |> Repo.get(project_id)
3642
def get_project(_), do: nil
@@ -40,25 +46,45 @@ defmodule CodeCorps.Helpers.Policy do
4046
4147
Returns `:string`
4248
"""
49+
@spec get_role(nil | OrganizationMembership.t | Changeset.t) :: String.t
4350
def get_role(nil), do: nil
4451
def get_role(%OrganizationMembership{role: role}), do: role
4552
def get_role(%Changeset{} = changeset), do: changeset |> Changeset.get_field(:role)
4653

4754
@doc """
4855
Determines if provided string is equal to "owner"
4956
"""
57+
@spec owner?(String.t) :: boolean
5058
def owner?("owner"), do: true
5159
def owner?(_), do: false
5260

5361
@doc """
5462
Determines if provided string is equal to one of `["admin", "owner"]`
5563
"""
64+
@spec admin_or_higher?(String.t) :: boolean
5665
def admin_or_higher?(role) when role in ["admin", "owner"], do: true
5766
def admin_or_higher?(_), do: false
5867

5968
@doc """
6069
Determines if provided string is equal to one of `["contributor", "admin", "owner"]`
6170
"""
71+
@spec contributor_or_higher?(String.t) :: boolean
6272
def contributor_or_higher?(role) when role in ["contributor", "admin", "owner"], do: true
6373
def contributor_or_higher?(_), do: false
74+
75+
@doc """
76+
Retrieves task from associated record
77+
"""
78+
@spec get_task(Changeset.t | TaskSkill.t | UserTask.t) :: Task.t
79+
def get_task(%TaskSkill{task_id: task_id}), do: Repo.get(Task, task_id)
80+
def get_task(%UserTask{task_id: task_id}), do: Repo.get(Task, task_id)
81+
def get_task(%Changeset{changes: %{task_id: task_id}}), do: Repo.get(Task, task_id)
82+
83+
@doc """
84+
Determines if the provided task was authored by the provided user
85+
"""
86+
@spec task_authored_by?(Task.t, User.t) :: boolean
87+
def task_authored_by?(%Task{user_id: author_id}, %User{id: user_id}), do: user_id == author_id
88+
89+
6490
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule CodeCorps.Repo.Migrations.CreateTaskSkill do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def change do
7+
create table(:task_skills) do
8+
add :skill_id, references(:skills), null: false
9+
add :task_id, references(:tasks), null: false
10+
11+
timestamps()
12+
end
13+
14+
create index :task_skills, [:task_id, :skill_id], unique: true
15+
end
16+
end
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
defmodule CodeCorps.TaskSkillControllerTest do
2+
@moduledoc false
3+
4+
use CodeCorps.ApiCase, resource_name: :task_skill
5+
6+
describe "index" do
7+
test "lists all entries on index", %{conn: conn} do
8+
[task_skill_1, task_skill_2] = insert_pair(:task_skill)
9+
10+
conn
11+
|> request_index
12+
|> json_response(200)
13+
|> assert_ids_from_response([task_skill_1.id, task_skill_2.id])
14+
end
15+
16+
test "filters resources on index", %{conn: conn} do
17+
[task_skill_1, task_skill_2 | _] = insert_list(3, :task_skill)
18+
19+
path = "task-skills/?filter[id]=#{task_skill_1.id},#{task_skill_2.id}"
20+
21+
conn
22+
|> get(path)
23+
|> json_response(200)
24+
|> assert_ids_from_response([task_skill_1.id, task_skill_2.id])
25+
end
26+
end
27+
28+
describe "show" do
29+
test "shows chosen resource", %{conn: conn} do
30+
skill = insert(:skill)
31+
task = insert(:task)
32+
task_skill = insert(:task_skill, task: task, skill: skill)
33+
34+
conn
35+
|> request_show(task_skill)
36+
|> json_response(200)
37+
|> Map.get("data")
38+
|> assert_result_id(task_skill.id)
39+
end
40+
41+
test "renders 404 error when id is nonexistent", %{conn: conn} do
42+
assert conn |> request_show(:not_found) |> json_response(404)
43+
end
44+
end
45+
46+
describe "create" do
47+
@tag :authenticated
48+
test "creates and renders resource when data is valid", %{conn: conn, current_user: current_user} do
49+
task = insert(:task, user: current_user)
50+
skill = insert(:skill)
51+
52+
attrs = %{task: task, skill: skill}
53+
assert conn |> request_create(attrs) |> json_response(201)
54+
end
55+
56+
@tag :authenticated
57+
test "renders 422 error when data is invalid", %{conn: conn, current_user: current_user} do
58+
task = insert(:task, user: current_user)
59+
60+
invalid_attrs = %{task: task, skill: nil}
61+
assert conn |> request_create(invalid_attrs) |> json_response(422)
62+
end
63+
64+
test "renders 401 when unauthenticated", %{conn: conn} do
65+
assert conn |> request_create |> json_response(401)
66+
end
67+
68+
@tag :authenticated
69+
test "renders 403 when not authorized", %{conn: conn} do
70+
task = insert(:task)
71+
skill = insert(:skill)
72+
attrs = %{task: task, skill: skill}
73+
74+
assert conn |> request_create(attrs) |> json_response(403)
75+
end
76+
end
77+
78+
describe "delete" do
79+
@tag :authenticated
80+
test "deletes chosen resource", %{conn: conn, current_user: current_user} do
81+
task = insert(:task, user: current_user)
82+
task_skill = insert(:task_skill, task: task)
83+
84+
assert conn |> request_delete(task_skill) |> response(204)
85+
end
86+
87+
test "renders 401 when unauthenticated", %{conn: conn} do
88+
assert conn |> request_delete |> json_response(401)
89+
end
90+
91+
@tag :authenticated
92+
test "renders 403 when not authorized", %{conn: conn} do
93+
assert conn |> request_delete |> json_response(403)
94+
end
95+
96+
@tag :authenticated
97+
test "renders 404 when id is nonexistent on delete", %{conn: conn} do
98+
assert conn |> request_delete(:not_found) |> json_response(404)
99+
end
100+
end
101+
end

test/models/task_skill_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule CodeCorps.TaskSkillTest do
2+
@moduledoc false
3+
4+
use CodeCorps.ModelCase
5+
6+
alias CodeCorps.TaskSkill
7+
8+
describe "create_changeset/2" do
9+
@required_attrs ~w(task_id skill_id)
10+
11+
test "requires #{@required_attrs}" do
12+
changeset = TaskSkill.create_changeset(%TaskSkill{}, %{})
13+
14+
assert_validation_triggered(changeset, :task_id, :required)
15+
assert_validation_triggered(changeset, :skill_id, :required)
16+
end
17+
18+
test "ensures associated Task record exists" do
19+
skill = insert(:skill)
20+
changeset = TaskSkill.create_changeset(%TaskSkill{}, %{task_id: -1, skill_id: skill.id})
21+
22+
{:error, response_changeset} = Repo.insert(changeset)
23+
assert_error_message(response_changeset, :task, "does not exist")
24+
end
25+
26+
test "ensures associated Skill record exists" do
27+
task = insert(:task)
28+
changeset = TaskSkill.create_changeset(%TaskSkill{}, %{task_id: task.id, skill_id: -1})
29+
30+
{:error, response_changeset} = Repo.insert(changeset)
31+
assert_error_message(response_changeset, :skill, "does not exist")
32+
end
33+
34+
test "ensures uniqueness of Skill/Task combination" do
35+
task_skill = insert(:task_skill)
36+
37+
changeset = TaskSkill.create_changeset(%TaskSkill{}, %{task_id: task_skill.task_id, skill_id: task_skill.skill_id})
38+
39+
{:error, response_changeset} = Repo.insert(changeset)
40+
assert_error_message(response_changeset, :skill, "has already been taken")
41+
end
42+
end
43+
end
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
defmodule CodeCorps.TaskSkillPolicyTest do
2+
@moduledoc false
3+
4+
use CodeCorps.PolicyCase
5+
6+
import CodeCorps.TaskSkillPolicy, only: [create?: 2, delete?: 2]
7+
import CodeCorps.TaskSkill, only: [create_changeset: 2]
8+
9+
alias CodeCorps.TaskSkill
10+
11+
defp generate_data_for(role) do
12+
{user, project} = insert_user_and_project(role)
13+
14+
task = case role do
15+
"author" -> insert(:task, project: project, user: user)
16+
_ -> insert(:task, project: project)
17+
end
18+
19+
{user, task}
20+
end
21+
22+
defp insert_user_and_project(role) do
23+
user = insert(:user)
24+
organization = insert(:organization)
25+
project = insert(:project, organization: organization)
26+
27+
insert_membership(user, organization, role)
28+
29+
{user, project}
30+
end
31+
32+
defp insert_membership(_, _, role) when role in ~w(non-member author), do: nil
33+
defp insert_membership(user, organization, role) do
34+
insert(:organization_membership, organization: organization, member: user, role: role)
35+
end
36+
37+
describe "create?" do
38+
test "returns false when user is not member of organization" do
39+
{user, task} = generate_data_for("non-member")
40+
41+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
42+
refute create?(user, changeset)
43+
end
44+
45+
test "returns false when user is pending member of organization" do
46+
{user, task} = generate_data_for("pending")
47+
48+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
49+
refute create?(user, changeset)
50+
end
51+
52+
test "returns true when user is contributor of organization" do
53+
{user, task} = generate_data_for("contributor")
54+
55+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
56+
assert create?(user, changeset)
57+
end
58+
59+
test "returns true when user is admin of organization" do
60+
{user, task} = generate_data_for("admin")
61+
62+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
63+
assert create?(user, changeset)
64+
end
65+
66+
test "returns true when user is owner of organization" do
67+
{user, task} = generate_data_for("owner")
68+
69+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
70+
assert create?(user, changeset)
71+
end
72+
73+
test "returns true when user is author of task" do
74+
{user, task} = generate_data_for("author")
75+
76+
changeset = %TaskSkill{} |> create_changeset(%{task_id: task.id})
77+
78+
assert create?(user, changeset)
79+
end
80+
end
81+
82+
describe "delete?" do
83+
test "returns false when user is not member of organization" do
84+
{user, task} = generate_data_for("non-member")
85+
86+
task_skill = insert(:task_skill, task: task)
87+
88+
refute delete?(user, task_skill)
89+
end
90+
91+
test "returns false when user is pending member of organization" do
92+
{user, task} = generate_data_for("pending")
93+
94+
task_skill = insert(:task_skill, task: task)
95+
96+
refute delete?(user, task_skill)
97+
end
98+
99+
test "returns true when user is contributor of organization" do
100+
{user, task} = generate_data_for("contributor")
101+
102+
task_skill = insert(:task_skill, task: task)
103+
104+
assert delete?(user, task_skill)
105+
end
106+
107+
test "returns true when user is admin of organization" do
108+
{user, task} = generate_data_for("admin")
109+
110+
task_skill = insert(:task_skill, task: task)
111+
112+
assert delete?(user, task_skill)
113+
end
114+
115+
test "returns true when user is owner of organization" do
116+
{user, task} = generate_data_for("owner")
117+
118+
task_skill = insert(:task_skill, task: task)
119+
120+
assert delete?(user, task_skill)
121+
end
122+
123+
test "returns true when user is author of task" do
124+
{user, task} = generate_data_for("author")
125+
126+
task_skill = insert(:task_skill, task: task)
127+
128+
assert delete?(user, task_skill)
129+
end
130+
end
131+
end

test/support/factories.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule CodeCorps.Factories do
2+
@moduledoc false
3+
24
# with Ecto
35
use ExMachina.Ecto, repo: CodeCorps.Repo
46

@@ -64,6 +66,13 @@ defmodule CodeCorps.Factories do
6466
}
6567
end
6668

69+
def task_skill_factory do
70+
%CodeCorps.TaskSkill{
71+
skill: build(:skill),
72+
task: build(:task)
73+
}
74+
end
75+
6776
def project_factory do
6877
%CodeCorps.Project{
6978
title: sequence(:title, &"Project #{&1}"),
@@ -94,6 +103,7 @@ defmodule CodeCorps.Factories do
94103
}
95104
end
96105

106+
@spec set_password(CodeCorps.User.t, String.t) :: CodeCorps.User.t
97107
def set_password(user, password) do
98108
hashed_password = Comeonin.Bcrypt.hashpwsalt(password)
99109
%{user | encrypted_password: hashed_password}

0 commit comments

Comments
 (0)