Skip to content

Commit 66a5742

Browse files
committed
Add organizations and slugged routes
Get slugged routes to have proper relationships serialized Add slug validation Fix organization controller tests by adding host Fix slugged route test Make changes from code review comments Reorganize Fix bug and reorganize organization test Undo organization test reorg Auto-generate slug for organization from name on create Check that the changeset auto-generates the slug Add has_one and some temporary code Add proper creation of slugged route Simplify tests Finish up implementation Add email format validation Add explicit alias Refactor based on code review Refactor controllers and fix failing tests
1 parent 39c5ed5 commit 66a5742

24 files changed

+643
-45
lines changed

lib/code_corps.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
defmodule CodeCorps do
2+
@moduledoc false
3+
24
use Application
35

6+
alias CodeCorps.Endpoint
7+
48
# See http://elixir-lang.org/docs/stable/elixir/Application.html
59
# for more information on OTP Applications
610
def start(_type, _args) do
@@ -25,7 +29,7 @@ defmodule CodeCorps do
2529
# Tell Phoenix to update the endpoint configuration
2630
# whenever the application is updated.
2731
def config_change(changed, _new, removed) do
28-
CodeCorps.Endpoint.config_change(changed, removed)
32+
Endpoint.config_change(changed, removed)
2933
:ok
3034
end
3135
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
defmodule CodeCorps.Validators.SlugValidator do
2+
@moduledoc """
3+
Used for validating slug fields in a given changeset.
4+
"""
5+
6+
alias Ecto.Changeset
7+
8+
def validate_slug(changeset, field_name) do
9+
# Matches slugs with:
10+
# - only letters
11+
# - prefixed/suffixed underscores
12+
# - prefixed/suffixed numbers
13+
# - single inside dashes
14+
# - single/multiple inside underscores
15+
# - one character
16+
#
17+
# Prevents slugs with:
18+
# - prefixed symbols
19+
# - prefixed/suffixed dashes
20+
# - multiple consecutive dashes
21+
# - single/multiple/multiple consecutive slashes
22+
valid_slug_pattern = ~r/\A((?:(?:(?:[^-\W]-?))*)(?:(?:(?:[^-\W]-?))*)\w+)\z/
23+
24+
# Prevents slugs that conflict with reserved routes
25+
reserved_routes = ~w(
26+
about account admin android api app apps blog bug bugs cache charter
27+
comment comments contact contributor contributors cookies
28+
developer developers discover donate engineering enterprise explore
29+
facebook favorites feed followers following github help home image images
30+
integration integrations invite invitations ios issue issues jobs learn
31+
likes lists log-in log-out login logout mention mentions new news
32+
notification notifications oauth oauth_clients organization organizations
33+
ping popular post_image post_images post_like post_likes post post
34+
press pricing privacy
35+
project projects repositories role roles rules search security session
36+
sessions settings shop showcases sidekiq sign-in sign-out signin signout
37+
signup sitemap slug slugs spotlight stars status tag tags
38+
tasks team teams terms training trends trust tour twitter
39+
user_role user_roles user_skill user_skills user users watching year
40+
)
41+
42+
changeset
43+
|> Changeset.validate_format(field_name, valid_slug_pattern)
44+
|> Changeset.validate_exclusion(field_name, reserved_routes)
45+
end
46+
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ defmodule CodeCorps.Mixfile do
4444
{:comeonin, "~> 2.0"},
4545
{:mix_test_watch, "~> 0.2", only: :dev}, # Test watcher
4646
{:credo, "~> 0.4", only: [:dev, :test]}, # Code style suggestions
47+
{:inflex, "~> 1.7.0"},
4748
]
4849
end
4950

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule CodeCorps.Repo.Migrations.CreateOrganization do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:organizations) do
6+
add :name, :string
7+
add :description, :string
8+
add :slug, :string
9+
10+
timestamps()
11+
end
12+
13+
create index(:organizations, ["lower(slug)"], name: :organizations_lower_slug_index, unique: true)
14+
end
15+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule CodeCorps.Repo.Migrations.CreateSluggedRoute do
2+
use Ecto.Migration
3+
4+
def change do
5+
create table(:slugged_routes) do
6+
add :slug, :string
7+
add :organization_id, references(:organizations, on_delete: :nothing)
8+
add :user_id, references(:users, on_delete: :nothing)
9+
10+
timestamps()
11+
end
12+
13+
create index(:slugged_routes, ["lower(slug)"], name: :slugged_routes_lower_slug_index, unique: true)
14+
end
15+
end
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
defmodule CodeCorps.OrganizationControllerTest do
2+
use CodeCorps.ConnCase
3+
4+
alias CodeCorps.Organization
5+
alias CodeCorps.Repo
6+
alias CodeCorps.SluggedRoute
7+
8+
@valid_attrs %{description: "Build a better future.", name: "Code Corps"}
9+
@invalid_attrs %{}
10+
11+
setup do
12+
conn =
13+
%{build_conn | host: "api."}
14+
|> put_req_header("accept", "application/vnd.api+json")
15+
|> put_req_header("content-type", "application/vnd.api+json")
16+
17+
{:ok, conn: conn}
18+
end
19+
20+
defp relationships do
21+
%{}
22+
end
23+
24+
test "lists all entries on index", %{conn: conn} do
25+
conn = get conn, organization_path(conn, :index)
26+
assert json_response(conn, 200)["data"] == []
27+
end
28+
29+
test "shows chosen resource", %{conn: conn} do
30+
organization = Repo.insert! %Organization{}
31+
conn = get conn, organization_path(conn, :show, organization)
32+
data = json_response(conn, 200)["data"]
33+
assert data["id"] == "#{organization.id}"
34+
assert data["type"] == "organization"
35+
assert data["attributes"]["name"] == organization.name
36+
assert data["attributes"]["description"] == organization.description
37+
assert data["attributes"]["slug"] == organization.slug
38+
end
39+
40+
test "does not show resource and instead throw error when id is nonexistent", %{conn: conn} do
41+
assert_error_sent 404, fn ->
42+
get conn, user_path(conn, :show, -1)
43+
end
44+
end
45+
46+
test "creates and renders resource when data is valid", %{conn: conn} do
47+
conn = post conn, organization_path(conn, :create), %{
48+
"meta" => %{},
49+
"data" => %{
50+
"type" => "organization",
51+
"attributes" => @valid_attrs,
52+
"relationships" => relationships
53+
}
54+
}
55+
56+
organization_id = json_response(conn, 201)["data"]["id"]
57+
assert organization_id
58+
organization = Repo.get_by(Organization, @valid_attrs)
59+
assert organization
60+
slugged_route = Repo.get_by(SluggedRoute, slug: "code-corps")
61+
assert slugged_route
62+
assert organization.id == slugged_route.organization_id
63+
end
64+
65+
test "does not create resource and renders errors when data is invalid", %{conn: conn} do
66+
conn = post conn, organization_path(conn, :create), %{
67+
"meta" => %{},
68+
"data" => %{
69+
"type" => "organization",
70+
"attributes" => @invalid_attrs,
71+
"relationships" => relationships
72+
}
73+
}
74+
75+
assert json_response(conn, 422)["errors"] != %{}
76+
end
77+
78+
test "updates and renders chosen resource when data is valid", %{conn: conn} do
79+
organization = Repo.insert! %Organization{}
80+
conn = put conn, organization_path(conn, :update, organization), %{
81+
"meta" => %{},
82+
"data" => %{
83+
"type" => "organization",
84+
"id" => organization.id,
85+
"attributes" => @valid_attrs,
86+
"relationships" => relationships
87+
}
88+
}
89+
90+
assert json_response(conn, 200)["data"]["id"]
91+
assert Repo.get_by(Organization, @valid_attrs)
92+
end
93+
94+
test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
95+
organization = Repo.insert! %Organization{}
96+
conn = put conn, organization_path(conn, :update, organization), %{
97+
"meta" => %{},
98+
"data" => %{
99+
"type" => "organization",
100+
"id" => organization.id,
101+
"attributes" => @invalid_attrs,
102+
"relationships" => relationships
103+
}
104+
}
105+
106+
assert json_response(conn, 422)["errors"] != %{}
107+
end
108+
109+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule CodeCorps.SluggedRouteControllerTest do
2+
use CodeCorps.ConnCase
3+
4+
alias CodeCorps.SluggedRoute
5+
alias CodeCorps.Repo
6+
7+
@valid_attrs %{organization_id: 42, slug: "some content", user_id: 42}
8+
@invalid_attrs %{}
9+
10+
setup do
11+
conn =
12+
%{build_conn | host: "api."}
13+
|> put_req_header("accept", "application/vnd.api+json")
14+
|> put_req_header("content-type", "application/vnd.api+json")
15+
16+
{:ok, conn: conn}
17+
end
18+
19+
test "shows chosen resource", %{conn: conn} do
20+
slug = "test-slug"
21+
slugged_route = Repo.insert! %SluggedRoute{slug: slug}
22+
conn = get conn, "/#{slug}"
23+
data = json_response(conn, 200)["data"]
24+
assert data["id"] == "#{slugged_route.id}"
25+
assert data["type"] == "slugged-route"
26+
assert data["attributes"]["slug"] == slugged_route.slug
27+
assert data["attributes"]["organization_id"] == slugged_route.organization_id
28+
assert data["attributes"]["user_id"] == slugged_route.user_id
29+
end
30+
31+
test "does not show resource and instead throw error when id is nonexistent", %{conn: conn} do
32+
assert_error_sent 404, fn ->
33+
get conn, slugged_route_path(conn, :show, -1)
34+
end
35+
end
36+
37+
end

test/controllers/user_controller_test.exs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule CodeCorps.UserControllerTest do
33

44
alias CodeCorps.User
55
alias CodeCorps.Repo
6+
alias CodeCorps.SluggedRoute
67

78
@valid_attrs %{
89
@@ -22,7 +23,8 @@ defmodule CodeCorps.UserControllerTest do
2223
}
2324

2425
setup do
25-
conn = %{build_conn | host: "api."}
26+
conn =
27+
%{build_conn | host: "api."}
2628
|> put_req_header("accept", "application/vnd.api+json")
2729
|> put_req_header("content-type", "application/vnd.api+json")
2830

@@ -68,7 +70,11 @@ defmodule CodeCorps.UserControllerTest do
6870

6971
id = json_response(conn, 201)["data"]["id"]
7072
assert id
71-
assert Repo.get(User, id)
73+
user = Repo.get(User, id)
74+
assert user
75+
slugged_route = Repo.get_by(SluggedRoute, slug: "testuser")
76+
assert slugged_route
77+
assert user.id == slugged_route.user_id
7278
end
7379

7480
test "does not create resource and renders errors when data is invalid", %{conn: conn} do
@@ -101,7 +107,6 @@ defmodule CodeCorps.UserControllerTest do
101107
id = json_response(conn, 200)["data"]["id"]
102108
assert id
103109
user = Repo.get(User, id)
104-
assert user.username == "testuser"
105110
assert user.email == "[email protected]"
106111
assert user.first_name == "Test"
107112
assert user.last_name == "User"
@@ -125,19 +130,11 @@ defmodule CodeCorps.UserControllerTest do
125130
assert json["errors"] != %{}
126131
errors = json["errors"]
127132
assert errors["email"] == ["can't be blank"]
128-
assert errors["username"] == ["can't be blank"]
129133
assert errors["twitter"] == ["has invalid format"]
130134
assert errors["website"] == ["has invalid format"]
131135
end
132136
end
133137

134-
test "deletes chosen resource", %{conn: conn} do
135-
user = insert_user()
136-
conn = delete conn, user_path(conn, :delete, user)
137-
assert response(conn, 204)
138-
refute Repo.get(User, user.id)
139-
end
140-
141138
describe "email_available" do
142139
test "returns valid and available when email is valid and available", %{conn: conn} do
143140
resp = get conn, user_path(conn, :email_available, %{email: "[email protected]"})

0 commit comments

Comments
 (0)