Skip to content
Snippets Groups Projects
Commit a03c70fd authored by shyngys's avatar shyngys
Browse files

Merge branch '18-fr-08-create-property-advertisement' into 'main'

Resolve "FR-08: Create Property Advertisement" and "FR-07: View Properties (Unauthenticated)"

Closes #17 and #18

See merge request !13
parents 265bbc34 4993a12d
No related branches found
No related tags found
1 merge request!13Resolve "FR-08: Create Property Advertisement" and "FR-07: View Properties (Unauthenticated)"
Pipeline #44687 passed
Showing
with 480 additions and 28 deletions
Feature: FR-08 Property Advertisement Creation
Scenario: Authenticated user should be able to create a property advertisement
Given there exists following accounts
| name | surname | birth_date | phone_number | email | password | confirm_password |
| Existing | Account | 2000-01-01 | 000 | existing.account@gmail.com | password | password |
Given I want to insert the following data
| title | description | type | property_type | location | room_count | area | floor | floor_count | price |
| Apartment | Small apartment | rent | apartment | Tartu, Estonia | 1 | 13 | 2 | 4 | 430 |
And I am logged in
And I want to create a new property advertisement
And I fill in the property advertisement form with valid data
When I click submit
Then I should see a success message
And I should be redirected to the property advertisement page
Scenario: Authenticated user should be shown an error if the property advertisement form is filled incorrectly
Given there exists following accounts
| name | surname | birth_date | phone_number | email | password | confirm_password |
| Existing | Account | 2000-01-01 | 000 | existing.account@gmail.com | password | password |
Given I want to insert the following data
| title | description | type | property_type | location | room_count | area | floor | floor_count | price |
| Apartment | Small apartment | rent | apartment | Tartu, Estonia | 1 | 13 | 2 | 4 | 430 |
And I am logged in
And I want to create a new property advertisement
And I fill in the property advertisement form with invalid data
When I click submit
Then I should see error messages
And I should still see the new property advertisement form
Scenario: Unauthenticated user should not be able to create an advertisement
Given I am not logged in
And I would like to create a new property advertisement
Then I should not see a link to create a new property advertisement
And I should be redirected back to home page when I try to access the link
Feature: FR-07 Property Advertisement List (Unauthenticated)
Scenario: Unauthenticated user should see the list of properties
Given there exists such advertisements
| title | description | type | property_type | location | room_count | area | floor | floor_count | price |
| Apartment | Small apartment in Tartu for rent | rent | apartment | Tartu, Estonia | 1 | 13 | 2 | 4 | 430 |
| Nice house | Cool house in Tallinn, pls buy | sell | house | Tallinn, Estonia | 12 | 200 | 1 | | 640000 |
And I am not logged in
When I visit the property advertisement list page
Then I should see the list of all properties
Scenario: Unauthenticated user should see a message that there is no advertisements if the database is empty
Given there exists such advertisements
| title | description | type | property_type | location | room_count | area | floor | floor_count | price |
And I am not logged in
When I visit the property advertisement list page
Then I should see a message that there is no advertisements
defmodule WhiteBreadConfig do
use WhiteBread.SuiteConfiguration
suite name: "User Registration Features",
suite name: "FR-01 User Registration Features",
context: UserRegistrationContext,
feature_paths: ["features/user_registration.feature"]
suite name: "User Login Features",
suite name: "FR-02 User Login Features",
context: UserLoginContext,
feature_paths: ["features/user_login.feature"]
suite name: "User Logout Features",
suite name: "FR-03 User Logout Features",
context: UserLogoutContext,
feature_paths: ["features/user_logout.feature"]
suite name: "Password Change Features",
suite name: "FR-04 Password Change Features",
context: PasswordChangeContext,
feature_paths: ["features/password_change.feature"]
suite name: "User Profile Features",
suite name: "FR-30 User Profile Features",
context: UserProfileContext,
feature_paths: ["features/user_profile.feature"]
suite name: "FR-08 Property Advertisement Creation Features",
context: AdvertisementCreationContext,
feature_paths: ["features/advertisement_creation.feature"]
suite name: "FR-07 Property Advertisement List Features",
context: AdvertisementListContext,
feature_paths: ["features/advertisement_list.feature"]
end
defmodule AdvertisementCreationContext do
use WhiteBread.Context
use Hound.Helpers
alias PropTrackr.Accounts
alias PropTrackr.Repo
alias PropTrackr.Accounts.User
scenario_starting_state fn _state ->
Ecto.Adapters.SQL.Sandbox.checkout(PropTrackr.Repo)
Ecto.Adapters.SQL.Sandbox.mode(PropTrackr.Repo, {:shared, self()})
Hound.start_session()
%{}
end
scenario_finalize fn _status, _state ->
Ecto.Adapters.SQL.Sandbox.checkin(PropTrackr.Repo)
Hound.end_session()
end
given_ ~r/^there exists following accounts$/, fn state, %{table_data: table} ->
table
|> Enum.map(fn user_details -> User.changeset(%User{}, user_details) end)
|> Enum.each(fn changeset -> Repo.insert!(changeset) end)
existing_user = List.first(table)
{
:ok,
state
|> Map.put(:email, existing_user[:email])
|> Map.put(:password, existing_user[:password])
}
end
given_ ~r/^I want to insert the following data$/, fn state, %{table_data: table} ->
data = List.first(table)
{
:ok,
state
|> Map.put(:data, data)
}
end
and_ ~r/^I am logged in$/, fn state ->
setup_session(state[:email], state[:password])
{:ok, state}
end
and_ ~r/^I want to create a new property advertisement$/, fn state ->
navigate_to("/")
click({:id, "add_advertisement"})
{:ok, state}
end
and_ ~r/^I fill in the property advertisement form with valid data$/, fn state ->
fill_field({:id, "title"}, state[:data][:title])
fill_field({:id, "description"}, state[:data][:description])
select_dropdown("type", state[:data][:type])
select_dropdown("property_type", state[:data][:property_type])
fill_field({:id, "location"}, state[:data][:location])
fill_field({:id, "area"}, state[:data][:area])
fill_field({:id, "room_count"}, state[:data][:room_count])
fill_field({:id, "floor"}, state[:data][:floor])
fill_field({:id, "floor_count"}, state[:data][:floor_count])
fill_field({:id, "price"}, state[:data][:price])
{:ok, state}
end
when_ ~r/^I click submit$/, fn state ->
click({:id, "submit_button"})
{:ok, state}
end
then_ ~r/^I should see a success message$/, fn state ->
assert visible_in_page? ~r/Property created successfully!/
{:ok, state}
end
and_ ~r/^I should be redirected to the property advertisement page$/, fn state ->
paths = String.split(current_path(), "/")
# https://stackoverflow.com/a/6640851
regex = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
assert Enum.at(paths, -1) =~ regex
{:ok, state}
end
and_ ~r/^I fill in the property advertisement form with invalid data$/, fn state ->
fill_field({:id, "title"}, "wrong")
fill_field({:id, "description"}, "invalid_obvs")
# Note: These can't be set invalid from the UI
select_dropdown("type", state[:data][:type])
select_dropdown("property_type", state[:data][:property_type])
fill_field({:id, "location"}, "Tartu, Estonia")
fill_field({:id, "area"}, "-10")
fill_field({:id, "room_count"}, "1")
fill_field({:id, "floor"}, "1")
fill_field({:id, "floor_count"}, "0")
fill_field({:id, "price"}, "-100")
{:ok, state}
end
then_ ~r/^I should see error messages$/, fn state ->
assert visible_in_page? ~r/must be greater than 0/
assert visible_in_page? ~r/Floor count must be greater than floor/
assert visible_in_page? ~r/must be greater than 0/
{:ok, state}
end
and_ ~r/^I should still see the new property advertisement form$/, fn state ->
assert visible_in_page? ~r/Add a new property listing/
{:ok, state}
end
given_ ~r/^I am not logged in$/, fn state ->
navigate_to("/logout")
{:ok, state}
end
and_ ~r/^I would like to create a new property advertisement$/, fn state ->
navigate_to("/")
{:ok, state}
end
then_ ~r/^I should not see a link to create a new property advertisement$/, fn state ->
assert not visible_in_page? ~r/Add a new property listing/
{:ok, state}
end
and_ ~r/^I should be redirected back to home page when I try to access the link$/, fn state ->
assert current_path() == "/"
{:ok, state}
end
defp setup_session(email, password) do
navigate_to("/login")
fill_field({:id, "email"}, email)
fill_field({:id, "password"}, password)
click({:id, "login_button"})
end
# https://stackoverflow.com/a/49861811
defp select_dropdown(drop_down_id, option) do
find_element(:css, "##{drop_down_id} option[value='#{option}']") |> click()
end
end
defmodule AdvertisementListContext do
use WhiteBread.Context
use Hound.Helpers
alias PropTrackr.Accounts
alias PropTrackr.Repo
alias PropTrackr.Properties.Property
scenario_starting_state fn _state ->
Ecto.Adapters.SQL.Sandbox.checkout(PropTrackr.Repo)
Ecto.Adapters.SQL.Sandbox.mode(PropTrackr.Repo, {:shared, self()})
Hound.start_session()
%{}
end
scenario_finalize fn _status, _state ->
Ecto.Adapters.SQL.Sandbox.checkin(PropTrackr.Repo)
Hound.end_session()
end
given_ ~r/^there exists such advertisements$/, fn state, %{table_data: table} ->
advertisements =
table
|> Enum.map(fn details -> details |> Map.put(:reference, Ecto.UUID.generate()) end)
|> Enum.map(fn details -> Property.changeset(%Property{}, details) end)
|> Enum.map(fn changeset -> Repo.insert!(changeset) end)
{
:ok,
state
|> Map.put(:advertisements, advertisements)
}
end
and_ ~r/^I am not logged in$/, fn state ->
navigate_to("/logout")
{:ok, state}
end
when_ ~r/^I visit the property advertisement list page$/, fn state ->
navigate_to("/")
{:ok, state}
end
then_ ~r/^I should see the list of all properties$/, fn state ->
advertisements = state[:advertisements]
advertisements |> Enum.each(fn advertisement ->
assert visible_in_page? ~r/#{advertisement.title}/
assert visible_in_page? ~r/#{advertisement.description}/
assert visible_in_page? ~r/#{advertisement.location}/
assert visible_in_page? ~r/#{advertisement.price}/
assert visible_in_page? ~r/#{advertisement.room_count}/
assert visible_in_page? ~r/#{advertisement.area}/
end)
{:ok, state}
end
then_ ~r/^I should see a message that there is no advertisements$/, fn state ->
assert visible_in_page? ~r/No advertisements at the moment/
{:ok, state}
end
end
Feature: Password Change
Feature: FR-04 Password Change
Scenario: Authenticated user can change their password
Given there exists following accounts
......@@ -47,3 +47,4 @@ Feature: Password Change
When I click change password
Then I should receive an error message stating that the new password is the same as the old password
And I should be shown the change password form again
Feature: User Login
Feature: FR-02 User Login
Scenario: Unauthenticated user can login with valid credentials (AC1, AC4)
Given there exists following accounts
......
Feature: User Logout
Feature: FR-03 User Logout
Scenario: Authenticated user can logout successfully (AC1, AC2)
Given there exists following accounts
| name | surname | birth_date | phone_number | email | password | confirm_password |
......@@ -13,4 +13,5 @@ Feature: User Logout
Given I am not logged in
When I try to access the logout functionality
Then I should see an error message
And I should be redirected to the home page
\ No newline at end of file
And I should be redirected to the home page
Feature: User Registration
Feature: FR-01 User Registration
Scenario: Unauthenticated user registers successfully (AC1, AC5, AC6, AC7)
Given I am on the registration page
......@@ -11,4 +11,5 @@ Feature: User Registration
Given I am on the registration page
When I fill in the registration form with mismatched passwords
And I submit the form
Then I should see an error message Passwords do not match
\ No newline at end of file
Then I should see an error message Passwords do not match
......@@ -33,6 +33,35 @@ defmodule PropTrackr.Properties.Property do
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:title, :description, :reference, :type, :property_type, :state, :location, :room_count, :area, :floor, :floor_count, :price, :user_id])
|> validate_required([])
|> validate_required([:title, :description, :reference, :type, :property_type, :state, :location, :room_count, :area, :floor, :price])
|> validate_number(:room_count, greater_than: 0)
|> validate_number(:area, greater_than: 0)
|> validate_number(:floor, greater_than: 0)
|> validate_number(:price, greater_than: 0)
|> validate_length(:title, min: 5)
|> validate_length(:description, min: 20)
|> validate_total_floors()
end
defp validate_total_floors(changeset) do
property_type = get_field(changeset, :property_type)
if property_type != :house do
floor = get_field(changeset, :floor)
floor_count = get_field(changeset, :floor_count)
cond do
floor_count < floor ->
changeset
|> add_error(:floor_count, "Floor count must be greater than floor")
floor_count < 1 ->
changeset
|> add_error(:floor_count, "Floor count must be greater than 0")
true ->
changeset
end
else
changeset
end
end
end
......@@ -22,20 +22,32 @@ defmodule PropTrackrWeb.PropertiesController do
def create(conn, %{"property" => property}) do
current_user = conn.assigns.current_user
values_to_atoms = [:type, :property_type, :state]
property_assoc = Ecto.build_assoc(current_user, :properties, Enum.map(property, fn {key, value} -> {String.to_atom(key), value} end))
property_changeset = Property.changeset(property_assoc, property)
|> Ecto.Changeset.put_change(:reference, Ecto.UUID.generate())
case Repo.insert(property_changeset) do
{:ok, property} ->
conn
|> put_flash(:info, "Property created successfully.")
|> redirect(to: ~p"/properties/#{property.reference}")
{:error, changeset} ->
render conn, "new.html", changeset: changeset
if current_user == nil do
conn
|> put_flash(:error, "You are not logged in!")
|> redirect(to: "/")
else
property = property |> Map.put("reference", Ecto.UUID.generate())
property_assoc = Ecto.build_assoc(current_user, :properties, Enum.map(property, fn {key, value} -> {String.to_atom(key), value} end))
property_changeset = Property.changeset(property_assoc, property)
# Note: IDK this does not work properly
# |> Ecto.Changeset.put_change(:reference, Ecto.UUID.generate())
case Repo.insert(property_changeset) do
{:ok, property} ->
conn
|> put_flash(:info, "Property created successfully!")
|> redirect(to: ~p"/properties/#{property.reference}")
{:error, changeset} ->
render conn, "new.html", changeset: changeset
end
end
end
def show(conn, %{"id" => id}) do
# TODO: Fill this in with the appropriate code (FR-12)
render conn, "index.html", properties: []
end
end
......@@ -5,17 +5,23 @@
<%= if @conn.assigns[:current_user] do %>
<div class="flex flex-row justify-end gap-x-2">
<.link href={~p"/properties/new"}>
<.button type="button" class="text-white rounded px-4 py-2">Add a property</.button>
<.button type="button" class="text-white rounded px-4 py-2" id="add_advertisement">Add a property</.button>
</.link>
</div>
<% end %>
<%= if @properties == [] do %>
<p>No advertisements at the moment</p>
<% end %>
<div id="properties" class="flex flex-col gap-y-4 mt-20">
<%= for property <- @properties do %>
<div class="bg-white border-black border rounded px-4 py-2">
<h2 class="font-bold"><%= property.title %></h2>
<p><%= property.description %></p>
<p class="italic"><%= property.location %></p>
<div class="flex flex-row gap-x-2">
<span><%= property.price %></span>
<span><%= property.room_count %> rooms</span>
......
......@@ -12,7 +12,7 @@
<.input field={f[:area]} id="area" type="text" label="Area (m^2)" required />
<.input field={f[:room_count]} id="room_count" type="number" label="Room count" min={1} required />
<.input field={f[:floor]} id="floor" type="number" label="Floor" min={0} required />
<.input field={f[:floor]} id="floor" type="number" label="Floor" min={1} required />
<.input field={f[:floor_count]} id="floor_count" type="number" label="Floor count" min={0} required />
<.input field={f[:price]} id="price" type="text" label="Price" required />
......
......@@ -34,6 +34,7 @@ defmodule PropTrackrWeb.Router do
get "/", PropertiesController, :index
get "/properties/new", PropertiesController, :new
post "/properties", PropertiesController, :create
get "/properties/:id", PropertiesController, :show
end
# Other scopes may use custom stacks.
......
defmodule PropTrackrWeb.PropertiesControllerTest do
use PropTrackrWeb.ConnCase
alias PropTrackr.Accounts.User
alias PropTrackr.Properties.Property
alias PropTrackr.Repo
@valid_data %{
title: "Apartment",
description: "Small apartment",
type: "rent",
property_type: "apartment",
location: "Tartu, Estonia",
room_count: 1,
area: 13.0,
floor: 2,
floor_count: 4,
price: 430.0,
}
@invalid_data %{
title: "wrong",
description: "invalid_obvs",
type: "rent",
property_type: "apartment",
location: "Tartu, Estonia",
room_count: 0,
area: -1.0,
floor: -1,
floor_count: -1,
price: -1.0,
}
setup do
user = %User{
name: "Test",
surname: "User",
birth_date: "2000-01-01",
phone_number: "000",
bio: "Yo",
email: "test.user@gmail.com",
password: "testing",
confirm_password: "testing",
}
user = Repo.insert!(user)
{:ok, %{user: user}}
end
test "Authenticated user should be able to create a new property advertisement with valid data", %{ conn: conn, user: user } do
conn = conn |> setup_session(user)
conn = conn |> post("/properties", property: @valid_data)
uuidv4_regex = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
path = String.split(redirected_to(conn), "/")
assert Enum.at(path, -1) =~ uuidv4_regex
assert Enum.at(path, -2) == "properties"
advertisement_reference = Enum.at(path, -1)
conn = get conn, redirected_to(conn)
assert html_response(conn, 200) =~ "Property created successfully!"
advertisement = Repo.get_by!(Property, reference: advertisement_reference)
assert advertisement.title == @valid_data[:title]
assert advertisement.description == @valid_data[:description]
assert advertisement.type == String.to_atom(@valid_data[:type])
assert advertisement.property_type == String.to_atom(@valid_data[:property_type])
assert advertisement.location == @valid_data[:location]
assert advertisement.room_count == @valid_data[:room_count]
assert advertisement.area == @valid_data[:area]
assert advertisement.floor == @valid_data[:floor]
assert advertisement.floor_count == @valid_data[:floor_count]
assert advertisement.price == @valid_data[:price]
end
test "Authenticated user should not be able to create an advertisement with invalid data", %{ conn: conn, user: user } do
conn = conn |> setup_session(user)
conn = conn |> post("/properties", property: @invalid_data)
assert html_response(conn, 200) =~ "must be greater than 0"
end
test "Authenticated user should not be able to create an advertisement with invalid floor values", %{ conn: conn, user: user } do
conn = conn |> setup_session(user)
data = %{ @invalid_data | floor: 2, floor_count: 1 }
conn = conn |> post("/properties", property: data)
assert html_response(conn, 200) =~ "Floor count must be greater than floor"
end
test "Authenticated user should not be able to create an advertisement with invalid floor count data on a house listing", %{ conn: conn, user: user } do
conn = conn |> setup_session(user)
data = %{ @invalid_data | property_type: :house, floor: 2, floor_count: 1 }
conn = conn |> post("/properties", property: data)
# Just assert that a redirect happened, don't care about data since it does not matter here
assert redirected_to(conn) != "/properties"
end
test "Unauthenticated user should be redirected to the homepage", %{ conn: conn, user: user } do
conn = post(conn, "/properties", property: @valid_data)
assert redirected_to(conn) == "/"
end
test "Unauthenticated user should see a message when there are no advertisements", %{ conn: conn } do
conn = get conn, "/"
assert html_response(conn, 200) =~ "No advertisements at the moment"
end
test "Unauthenticated user should see a list of all properties", %{ conn: conn, user: user } do
conn = conn |> setup_session(user)
conn = conn |> post("/properties", property: @valid_data)
conn = get conn, "/"
assert html_response(conn, 200) =~ @valid_data[:title]
assert html_response(conn, 200) =~ @valid_data[:description]
assert html_response(conn, 200) =~ @valid_data[:location]
assert html_response(conn, 200) =~ Float.to_string(@valid_data[:price])
assert html_response(conn, 200) =~ Integer.to_string(@valid_data[:room_count])
assert html_response(conn, 200) =~ Float.to_string(@valid_data[:area])
end
defp setup_session(conn, user) do
conn = conn |> post("/login", email: user.email, password: user.password)
conn = get conn, redirected_to(conn)
conn
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment