diff --git a/assets/js/app.js b/assets/js/app.js index 696c2c133fc3792e4afb9e8c252311b90ad2ff84..f6a937eb5dd613180ca2f7b1afe59983b64e428c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -5,6 +5,7 @@ import topbar from "../vendor/topbar"; import "../css/app.css"; import "./favorites"; import "./status"; +import "./interest"; let csrfToken = document .querySelector("meta[name='csrf-token']") diff --git a/assets/js/interest.js b/assets/js/interest.js new file mode 100644 index 0000000000000000000000000000000000000000..f3873f907e1062d0224f549937fcc88cb81449e5 --- /dev/null +++ b/assets/js/interest.js @@ -0,0 +1,85 @@ +document.addEventListener("DOMContentLoaded", initializeInterestButtons); + +function initializeInterestButtons() { + const menuButtons = document.querySelectorAll(".interest-menu-button"); + + menuButtons.forEach((button) => { + button.addEventListener("click", (e) => { + e.stopPropagation(); + const menu = button.nextElementSibling; + menu.classList.toggle("hidden"); + }); + + const options = button.nextElementSibling.querySelectorAll(".interest-option"); + options.forEach((option) => { + option.addEventListener("click", handleInterestOptionClick); + }); + }); +} + +async function handleInterestOptionClick(e) { + e.preventDefault(); + const button = this.closest('.relative').querySelector('.interest-menu-button'); + const reference = button.dataset.propertyReference; + const status = this.dataset.status; + const isNotInterested = status === "not_interested"; + + try { + const response = await fetch(`/api/properties/${reference}/interest`, { + method: isNotInterested ? "POST" : "DELETE", + headers: { + "Content-Type": "application/json", + "x-csrf-token": document.querySelector("meta[name='csrf-token']").content, + }, + }); + + const data = await response.json(); + + if (response.ok) { + const menu = this.closest('.interest-menu'); + const ticks = menu.querySelectorAll(".interest-tick"); + ticks.forEach(tick => tick.classList.add("invisible")); + this.querySelector(".interest-tick").classList.remove("invisible"); + + menu.classList.add("hidden"); + + // Fetch and update content + const propertiesContainer = document.getElementById("properties"); + propertiesContainer.innerHTML = await fetch(window.location.pathname) + .then(response => response.text()) + .then(html => { + const doc = new DOMParser().parseFromString(html, 'text/html'); + return doc.getElementById("properties").innerHTML; + }); + + // Initialize both sets of buttons + document.dispatchEvent(new Event('DOMContentLoaded')); + + showFlashMessage("success", data.message); + } else { + showFlashMessage("error", data.error); + } + } catch (error) { + console.error("Error:", error); + showFlashMessage("error", "Something went wrong"); + } +} + +document.addEventListener("click", () => { + document.querySelectorAll(".interest-menu").forEach(menu => { + menu.classList.add("hidden"); + }); +}); + +function showFlashMessage(type, message) { + const flashContainer = document.createElement("div"); + flashContainer.className = `flash-message ${type}`; + flashContainer.textContent = message; + + document.body.appendChild(flashContainer); + + setTimeout(() => { + flashContainer.style.animation = "fadeOut 0.3s ease-out"; + flashContainer.addEventListener("animationend", () => flashContainer.remove()); + }, 3000); +} \ No newline at end of file diff --git a/features/config.exs b/features/config.exs index 2872eabdc3d6c785677d5cce5de32f5aa7a2ce3c..50d545e8a3bb43570f374f7c2f82f5b6201993b7 100644 --- a/features/config.exs +++ b/features/config.exs @@ -64,4 +64,8 @@ defmodule WhiteBreadConfig do suite name: "FR-18 Display Similar Properties on Listing View", context: SimilarPropertiesContext, feature_paths: ["features/similar_properties_view.feature"] + + suite name: "FR-16: Mark Property as Not Interested", + context: NotInterestedPropertiesContext, + feature_paths: ["features/not_interested_properties.feature"] end diff --git a/features/contexts/not_interested_properties_context.exs b/features/contexts/not_interested_properties_context.exs new file mode 100644 index 0000000000000000000000000000000000000000..742cc9ee2df949dc64362f2320fab724f8d5c5d9 --- /dev/null +++ b/features/contexts/not_interested_properties_context.exs @@ -0,0 +1,198 @@ +defmodule NotInterestedPropertiesContext do + use WhiteBread.Context + use Hound.Helpers + import Ecto.Query + alias PropTrackr.Accounts + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + alias PropTrackr.Properties.Property + alias PropTrackr.NotInterested + + 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) + + random_user = List.first(table) + owner = List.last(table) + + { + :ok, + state + |> Map.put(:random_email, random_user[:email]) + |> Map.put(:random_password, random_user[:password]) + |> Map.put(:owner_email, owner[:email]) + |> Map.put(:owner_password, owner[:password]) + } + end + + and_ ~r/^the following properties exist$/, fn state, %{ table_data: table } -> + owner = Repo.get_by(User, email: state[:owner_email]) + + advertisements = + table + |> Enum.map(fn details -> details |> Map.put(:reference, Ecto.UUID.generate()) end) + |> Enum.map(fn details -> {Ecto.build_assoc(owner, :properties, details), details} end) + |> Enum.map(fn {assoc, details} -> Property.changeset(assoc, details) end) + |> Enum.map(fn changeset -> Repo.insert!(changeset) end) + + { + :ok, + state + |> Map.put(:advertisements, advertisements) + } + end + + and_ ~r/^I am logged in as random user$/, fn state -> + setup_session(state[:random_email], state[:random_password]) + {:ok, state} + end + + and_ ~r/^I am logged in as owner$/, fn state -> + setup_session(state[:owner_email], state[:owner_password]) + {:ok, state} + end + + and_ ~r/^the property "(?<argument_one>[^"]+)" is marked as not interested$/, + fn state, %{argument_one: argument_one} -> + current_user = Repo.get_by(User, email: state[:random_email]) + property = Enum.find(state.advertisements, fn advert -> + advert.title == argument_one + end) + + not_interested_changeset = %NotInterested{} + |> NotInterested.changeset(%{ + user_id: current_user.id, + property_id: property.id + }) + + Repo.insert!(not_interested_changeset) + + {:ok, state} + end + + when_ ~r/^I visit the home page$/, fn state -> + navigate_to("/") + {:ok, state} + end + + and_ ~r/^I click interest menu for "(?<argument_one>[^"]+)"$/, + fn state, %{argument_one: argument_one} -> + property = Enum.find(state.advertisements, fn advert -> + advert.title == argument_one + end) + + # Update selector to match your HTML + # Change this line in ManagePropertyInterestContext + menu_button = find_element(:css, ".interest-menu-button[data-property-reference='#{property.reference}']") + click(menu_button) + :timer.sleep(500) + + {:ok, state} + end + + and_ ~r/^I click "(?<option>[^"]+)" option$/, fn state, %{option: option} -> + option_button = find_element(:css, ".interest-option[data-status='#{String.downcase(option) |> String.replace(" ", "_")}']") + click(option_button) + :timer.sleep(500) + + {:ok, state} + end + + then_ ~r/^Property should move to bottom of the list$/, fn state -> + properties = find_all_elements(:css, "#properties > div") + last_property = List.last(properties) + property = List.first(state.advertisements) + + title_element = find_within_element(last_property, :css, "h2") + title_text = visible_text(title_element) + + assert title_text == property.title + {:ok, state} + end + + then_ ~r/^Property should move to its original position$/, fn state -> + properties = find_all_elements(:css, "#properties > div") + first_property = List.first(properties) + property = List.first(state.advertisements) + + title_element = find_within_element(first_property, :css, "h2") + title_text = visible_text(title_element) + + assert title_text == property.title + {:ok, state} + end + + then_ ~r/^I should see success message for marking as not interested$/, fn state -> + assert visible_in_page?(~r/Marked as not interested/i) + {:ok, state} + end + + then_ ~r/^I should see success message for marking as interested$/, fn state -> + assert visible_in_page?(~r/Marked as interested/i) + {:ok, state} + end + + then_ ~r/^I should not see interest menu for "(?<argument_one>[^"]+)"$/, + fn state, %{argument_one: argument_one} -> + property = Enum.find(state.advertisements, fn advert -> + advert.title == argument_one + end) + + elements = find_all_elements(:css, ".interest-menu-button[data-property-reference='#{property.reference}']") + + assert Enum.empty?(elements), + "Expected no interest menu for property '#{argument_one}' but found #{length(elements)} menu(s)" + + {:ok, state} + end + + # Add these to ManagePropertyInterestContext + + and_ ~r/^Property should be added to database$/, fn state -> + property = List.first(state.advertisements) + current_user = Repo.get_by(User, email: state[:random_email]) + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^current_user.id and n.property_id == ^property.id) + + assert not_interested != nil, + "Expected property to be marked as not interested in database" + + navigate_to("/logout") + {:ok, state} + end + + and_ ~r/^Property should be deleted from database$/, fn state -> + property = List.first(state.advertisements) + current_user = Repo.get_by(User, email: state[:random_email]) + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^current_user.id and n.property_id == ^property.id) + + assert not_interested == nil, + "Expected property to be removed from not interested in database" + + navigate_to("/logout") + {: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 + end diff --git a/features/not_interested_properties.feature b/features/not_interested_properties.feature new file mode 100644 index 0000000000000000000000000000000000000000..b6c009ed2f2aea15458cff64e384f95c3f294cb8 --- /dev/null +++ b/features/not_interested_properties.feature @@ -0,0 +1,55 @@ +Feature: Property Interest Management + + Scenario: User can mark property as not interested + 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 | + | Property | Owner | 2000-01-01 | 111 | property.owner@gmail.com | password | password | + And the following properties exist + | title | description | type | property_type | state | location | room_count | area | floor | floor_count | price | + | Really cool property | Selling this really really house | sell | house | available | London | 3 | 100.0 | 2 | 5 | 500000 | + And I am logged in as random user + When I visit the home page + And I click interest menu for "Really cool property" + And I click "Not interested" option + Then Property should move to bottom of the list + And I should see success message for marking as not interested + And Property should be added to database + + Scenario: User can mark not interested property as interested again + 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 | + | Property | Owner | 2000-01-01 | 111 | property.owner@gmail.com | password | password | + And the following properties exist + | title | description | type | property_type | state | location | room_count | area | floor | floor_count | price | + | Really cool property | Selling this really really house | sell | house | available | London | 3 | 100.0 | 2 | 5 | 500000 | + And I am logged in as random user + And the property "Really cool property" is marked as not interested + When I visit the home page + And I click interest menu for "Really cool property" + And I click "Interested" option + Then Property should move to its original position + And I should see success message for marking as interested + And Property should be deleted from database + + Scenario: User cannot mark own property as not interested + Given there exists following accounts + | name | surname | birth_date | phone_number | email | password | confirm_password | + | Property | Owner | 2000-01-01 | 111 | property.owner@gmail.com | password | password | + And the following properties exist + | title | description | type | property_type | state | location | room_count | area | floor | floor_count | price | + | Really cool property | Selling this really really house | sell | house | available | London | 3 | 100.0 | 2 | 5 | 500000 | + And I am logged in as owner + When I visit the home page + Then I should not see interest menu for "Really cool property" + + Scenario: Unauthenticated user cannot mark property as not interested + Given there exists following accounts + | name | surname | birth_date | phone_number | email | password | confirm_password | + | Property | Owner | 2000-01-01 | 111 | property.owner@gmail.com | password | password | + And the following properties exist + | title | description | type | property_type | state | location | room_count | area | floor | floor_count | price | + | Really cool property | Selling this really really house | sell | house | available | London | 3 | 100.0 | 2 | 5 | 500000 | + When I visit the home page + Then I should not see interest menu for "Really cool property" \ No newline at end of file diff --git a/lib/proptrackr/properties/not_interested.ex b/lib/proptrackr/properties/not_interested.ex new file mode 100644 index 0000000000000000000000000000000000000000..528df1217b6795a29fa6cc012f57bbeb3398b9bd --- /dev/null +++ b/lib/proptrackr/properties/not_interested.ex @@ -0,0 +1,17 @@ +defmodule PropTrackr.NotInterested do + use Ecto.Schema + import Ecto.Changeset + + schema "not_interested" do + belongs_to :user, PropTrackr.Accounts.User + belongs_to :property, PropTrackr.Properties.Property + timestamps() + end + + def changeset(not_interested, attrs \\ %{}) do + not_interested + |> cast(attrs, [:user_id, :property_id]) + |> validate_required([:user_id, :property_id]) + |> unique_constraint([:user_id, :property_id]) + end +end diff --git a/lib/proptrackr_web/controllers/interest_controller.ex b/lib/proptrackr_web/controllers/interest_controller.ex new file mode 100644 index 0000000000000000000000000000000000000000..4632e1ba456839c18f632b538e40088396200ec7 --- /dev/null +++ b/lib/proptrackr_web/controllers/interest_controller.ex @@ -0,0 +1,81 @@ +defmodule PropTrackrWeb.InterestController do + use PropTrackrWeb, :controller + + import Ecto.Query + alias PropTrackr.Repo + alias PropTrackr.Properties.Property + alias PropTrackr.NotInterested + + def create(conn, %{"reference" => reference}) do + current_user = conn.assigns.current_user + + if current_user == nil do + conn + |> put_status(:unauthorized) + |> json(%{error: "You must be logged in"}) + else + case Repo.one(from p in Property, where: p.reference == ^reference) do + nil -> + conn + |> put_status(:not_found) + |> json(%{error: "Property not found"}) + + property -> + if property.user_id == current_user.id do + conn + |> put_status(:forbidden) + |> json(%{error: "Cannot mark own property as not interested"}) + else + changeset = %NotInterested{} + |> NotInterested.changeset(%{ + user_id: current_user.id, + property_id: property.id + }) + + case Repo.insert(changeset) do + {:ok, _} -> json(conn, %{message: "Marked as not interested"}) + {:error, _} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: "Already marked as not interested"}) + end + end + end + end + end + + def delete(conn, %{"reference" => reference}) do + current_user = conn.assigns.current_user + + if current_user == nil do + conn + |> put_status(:unauthorized) + |> json(%{error: "You must be logged in"}) + else + case Repo.one(from p in Property, where: p.reference == ^reference) do + nil -> + conn + |> put_status(:not_found) + |> json(%{error: "Property not found"}) + + property -> + case Repo.one(from n in NotInterested, + where: n.user_id == ^current_user.id and n.property_id == ^property.id) do + nil -> + conn + |> put_status(:not_found) + |> json(%{error: "Already marked as interested"}) + + not_interested -> + case Repo.delete(not_interested) do + {:ok, _} -> json(conn, %{message: "Marked as interested"}) + {:error, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Failed to update"}) + end + end + end + end + end +end diff --git a/lib/proptrackr_web/controllers/properties_controller.ex b/lib/proptrackr_web/controllers/properties_controller.ex index 33c03a6fb8414fe9c34746ac73421d9d8ea6f239..e25d93d04d868ec2360ecf74ec145290a5fd0ab9 100644 --- a/lib/proptrackr_web/controllers/properties_controller.ex +++ b/lib/proptrackr_web/controllers/properties_controller.ex @@ -6,16 +6,28 @@ defmodule PropTrackrWeb.PropertiesController do alias PropTrackr.Properties.{Property, Photo} alias PropTrackr.Favorites.Favorite alias PropTrackr.Uploads + alias PropTrackr.NotInterested def index(conn, _params) do - - properties = Repo.all( - from p in Property, - where: p.state == :available, - order_by: [asc: p.inserted_at], - preload: [:photos], - # preload: [photos: ^from(ph in Photo, order_by: [asc: ph.order])], - select: p) + current_user = conn.assigns.current_user + current_user_id = current_user && current_user.id + + properties = from(p in Property, + left_join: ni in NotInterested, + on: ni.property_id == p.id and ni.user_id == ^(current_user_id || 0), + where: p.state == :available, + order_by: [asc: not is_nil(ni.id), asc: p.inserted_at], + preload: [:photos], + select: p) + |> Repo.all() + + not_interested = if current_user do + Repo.all(from n in NotInterested, + where: n.user_id == ^current_user.id, + select: n.property_id) + else + [] + end favorites = case conn.assigns.current_user do nil -> [] @@ -29,7 +41,8 @@ defmodule PropTrackrWeb.PropertiesController do render(conn, "index.html", properties: properties, - favorites: favorites + favorites: favorites, + not_interested: not_interested ) end diff --git a/lib/proptrackr_web/controllers/properties_html/index.html.heex b/lib/proptrackr_web/controllers/properties_html/index.html.heex index 43f687abaa7c1e30aac5e5dda12143f4c902b305..3c41d52a211995911209a41753ce3f9c781bdb2b 100644 --- a/lib/proptrackr_web/controllers/properties_html/index.html.heex +++ b/lib/proptrackr_web/controllers/properties_html/index.html.heex @@ -37,14 +37,35 @@ <div class="flex justify-between items-start"> <h2 class="font-bold"><%= property.title %></h2> <%= if @conn.assigns[:current_user] && @conn.assigns.current_user.id != property.user_id do %> - <button - type="button" - class="favorite-button text-2xl" - data-property-reference={property.reference} - data-favorited={if property.id in @favorites, do: "true", else: "false"} - > - <%= if property.id in @favorites, do: "âک…", else: "âک†" %> - </button> + <div class="flex items-center gap-4"> + <button + type="button" + class="favorite-button text-2xl" + data-property-reference={property.reference} + data-favorited={if property.id in @favorites, do: "true", else: "false"} + > + <%= if property.id in @favorites, do: "âک…", else: "âک†" %> + </button> + <div class="relative"> + <button type="button" class="interest-menu-button" data-property-reference={property.reference}> + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6"> + <circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/> + </svg> + </button> + <div class="interest-menu hidden absolute right-0 mt-2 bg-white border rounded shadow-lg z-10"> + <div class="py-1"> + <button class="interest-option w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center gap-2" data-status="interested"> + <span class={"interest-tick #{if property.id in @not_interested, do: "invisible", else: "visible"}"}>✓</span> + Interested + </button> + <button class="interest-option w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center gap-2 whitespace-nowrap" data-status="not_interested"> + <span class={"interest-tick #{if property.id in @not_interested, do: "visible", else: "invisible"}"}>✓</span> + Not interested + </button> + </div> + </div> + </div> + </div> <% end %> </div> <p><%= property.description %></p> diff --git a/lib/proptrackr_web/router.ex b/lib/proptrackr_web/router.ex index 2bc6c7892a90ae4cd301c45c6665d4c9f58334dc..6e6b915686cb23132be8d9108ca814fa27b49077 100644 --- a/lib/proptrackr_web/router.ex +++ b/lib/proptrackr_web/router.ex @@ -54,6 +54,8 @@ defmodule PropTrackrWeb.Router do post "/properties/:reference/favorite", FavoriteController, :create delete "/properties/:reference/favorite", FavoriteController, :delete post "/properties/:reference/status", PropertiesController, :update_status + post "/properties/:reference/interest", InterestController, :create + delete "/properties/:reference/interest", InterestController, :delete end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20241128095131_add_not_interested.exs b/priv/repo/migrations/20241128095131_add_not_interested.exs new file mode 100644 index 0000000000000000000000000000000000000000..51c98ad19333587f2472be06906a91a99ffee301 --- /dev/null +++ b/priv/repo/migrations/20241128095131_add_not_interested.exs @@ -0,0 +1,13 @@ +defmodule PropTrackr.Repo.Migrations.AddNotInterested do + use Ecto.Migration + + def change do + create table(:not_interested) do + add :user_id, references(:users, on_delete: :delete_all) + add :property_id, references(:properties, on_delete: :delete_all) + timestamps() + end + + create unique_index(:not_interested, [:user_id, :property_id]) + end +end diff --git a/test/proptrackr_web/controllers/interest_controller_test.exs b/test/proptrackr_web/controllers/interest_controller_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..63d80c49d193ee4c60ace3ed5bcf9c9ab973354a --- /dev/null +++ b/test/proptrackr_web/controllers/interest_controller_test.exs @@ -0,0 +1,133 @@ +defmodule PropTrackrWeb.InterestControllerTest do + use PropTrackrWeb.ConnCase + alias PropTrackr.Accounts.User + alias PropTrackr.Properties.Property + alias PropTrackr.NotInterested + alias PropTrackr.Repo + import Ecto.Query + + setup do + owner = %User{ + name: "Property", surname: "Owner", + birth_date: "2000-01-01", phone_number: "111", + email: "property.owner@gmail.com", + password: "password", confirm_password: "password" + } |> Repo.insert!() + + random_user = %User{ + name: "Random", surname: "User", + birth_date: "2000-01-01", phone_number: "000", + email: "random.user@gmail.com", + password: "password", confirm_password: "password" + } |> Repo.insert!() + + property = %Property{ + reference: Ecto.UUID.generate(), + title: "Test Property", description: "Test Description", + type: :sell, property_type: :house, state: :available, + location: "Test Location", room_count: 3, area: 100.0, + floor: 2, floor_count: 5, price: 500000.0, + user_id: owner.id + } |> Repo.insert!() + + {:ok, %{owner: owner, random_user: random_user, property: property}} + end + + test "authenticated user can mark property as not interested", %{conn: conn, random_user: user, property: property} do + conn = conn |> setup_session(user) + conn = post(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 200)["message"] == "Marked as not interested" + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^user.id and n.property_id == ^property.id) + assert not_interested != nil + end + + test "authenticated user cannot mark same property as not interested twice", %{conn: conn, random_user: user, property: property} do + Repo.insert!(%NotInterested{user_id: user.id, property_id: property.id}) + + conn = conn |> setup_session(user) + conn = post(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 422)["error"] == "Already marked as not interested" + + count = Repo.one(from n in NotInterested, + where: n.user_id == ^user.id and n.property_id == ^property.id, + select: count(n.id)) + assert count == 1 + end + + test "owner cannot mark own property as not interested", %{conn: conn, owner: owner, property: property} do + conn = conn |> setup_session(owner) + conn = post(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 403)["error"] == "Cannot mark own property as not interested" + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^owner.id and n.property_id == ^property.id) + assert not_interested == nil + end + + test "unauthenticated user cannot mark property as not interested", %{conn: conn, property: property} do + initial_count = Repo.one(from n in NotInterested, select: count(n.id)) + conn = post(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 401)["error"] == "You must be logged in" + final_count = Repo.one(from n in NotInterested, select: count(n.id)) + assert initial_count == final_count + end + + test "cannot mark non-existent property as not interested", %{conn: conn, random_user: user} do + conn = conn |> setup_session(user) + conn = post(conn, ~p"/api/properties/non-existent/interest") + + assert json_response(conn, 404)["error"] == "Property not found" + end + + test "authenticated user can mark property as interested again", %{conn: conn, random_user: user, property: property} do + Repo.insert!(%NotInterested{user_id: user.id, property_id: property.id}) + + conn = conn |> setup_session(user) + conn = delete(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 200)["message"] == "Marked as interested" + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^user.id and n.property_id == ^property.id) + assert not_interested == nil + end + + test "authenticated user cannot mark interested property as interested again", %{conn: conn, random_user: user, property: property} do + conn = conn |> setup_session(user) + conn = delete(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 404)["error"] == "Already marked as interested" + end + + test "unauthenticated user cannot mark property as interested", %{conn: conn, random_user: user, property: property} do + Repo.insert!(%NotInterested{user_id: user.id, property_id: property.id}) + initial_count = Repo.one(from n in NotInterested, select: count(n.id)) + + conn = delete(conn, ~p"/api/properties/#{property.reference}/interest") + + assert json_response(conn, 401)["error"] == "You must be logged in" + final_count = Repo.one(from n in NotInterested, select: count(n.id)) + assert initial_count == final_count + end + + test "deleting property removes associated not interested marks", %{conn: conn, random_user: user, property: property} do + Repo.insert!(%NotInterested{user_id: user.id, property_id: property.id}) + Repo.delete!(property) + + not_interested = Repo.one(from n in NotInterested, + where: n.user_id == ^user.id and n.property_id == ^property.id) + assert not_interested == nil + 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