diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f6d12..7cbcce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Fix stored XSS vulnerability in custom embed iframes via input sanitization with attribute whitelisting - Fix XSS vulnerability in URL link formatting by escaping user-submitted URLs - Fix IDOR on form export endpoint by adding authorization check +- Fix cross-event IDOR on polls, quizzes, forms, embeds, and posts by enforcing event-scoped resource access in context layer - Fix atom exhaustion DoS by replacing `String.to_atom/1` on user input with explicit whitelists (8 locations) - Add rate limiting on authentication endpoints using Hammer 7.0 diff --git a/lib/claper/embeds.ex b/lib/claper/embeds.ex index ebbf059..376ceed 100644 --- a/lib/claper/embeds.ex +++ b/lib/claper/embeds.ex @@ -59,6 +59,23 @@ defmodule Claper.Embeds do def get_embed!(id, preload \\ []), do: Repo.get!(Embed, id) |> Repo.preload(preload) + @doc """ + Gets a single embed scoped to the given event. + + Returns `nil` if the embed does not exist or does not belong to the event. + """ + def get_embed_for_event(id, event_id, preload \\ []) do + from(e in Embed, + join: pf in assoc(e, :presentation_file), + where: e.id == ^id and pf.event_id == ^event_id + ) + |> Repo.one() + |> case do + nil -> nil + embed -> Repo.preload(embed, preload) + end + end + @doc """ Gets a single embed for a given position. diff --git a/lib/claper/forms.ex b/lib/claper/forms.ex index 2ec8931..f71f276 100644 --- a/lib/claper/forms.ex +++ b/lib/claper/forms.ex @@ -61,6 +61,23 @@ defmodule Claper.Forms do def get_form!(id, preload \\ []), do: Repo.get!(Form, id) |> Repo.preload(preload) + @doc """ + Gets a single form scoped to the given event. + + Returns `nil` if the form does not exist or does not belong to the event. + """ + def get_form_for_event(id, event_id, preload \\ []) do + from(f in Form, + join: pf in assoc(f, :presentation_file), + where: f.id == ^id and pf.event_id == ^event_id + ) + |> Repo.one() + |> case do + nil -> nil + form -> Repo.preload(form, preload) + end + end + @doc """ Gets a single form for a given position. @@ -280,6 +297,20 @@ defmodule Claper.Forms do def get_form_submit_by_id!(id, preload \\ []), do: Repo.get_by!(FormSubmit, id: id) |> Repo.preload(preload) + @doc """ + Gets a single FormSubmit scoped to the given event. + + Returns `nil` if the FormSubmit does not exist or does not belong to the event. + """ + def get_form_submit_for_event(id, event_id) do + from(fs in FormSubmit, + join: f in assoc(fs, :form), + join: pf in assoc(f, :presentation_file), + where: fs.id == ^id and pf.event_id == ^event_id + ) + |> Repo.one() + end + @doc """ Creates or update a FormSubmit. diff --git a/lib/claper/polls.ex b/lib/claper/polls.ex index baa2a0d..d3ac0b0 100644 --- a/lib/claper/polls.ex +++ b/lib/claper/polls.ex @@ -72,6 +72,34 @@ defmodule Claper.Polls do ) |> set_percentages() + @doc """ + Gets a single poll scoped to the given event. + + Returns `nil` if the poll does not exist or does not belong to the event. + """ + def get_poll_for_event(id, event_id) do + from(p in Poll, + join: pf in assoc(p, :presentation_file), + where: p.id == ^id and pf.event_id == ^event_id + ) + |> Repo.one() + |> case do + nil -> + nil + + poll -> + poll + |> Repo.preload( + poll_opts: + from( + o in PollOpt, + order_by: [asc: o.id] + ) + ) + |> set_percentages() + end + end + @doc """ Gets a single poll for a given position. diff --git a/lib/claper/posts.ex b/lib/claper/posts.ex index 9691288..f28e643 100644 --- a/lib/claper/posts.ex +++ b/lib/claper/posts.ex @@ -120,6 +120,22 @@ defmodule Claper.Posts do """ def get_post!(id, preload \\ []), do: Repo.get_by!(Post, uuid: id) |> Repo.preload(preload) + @doc """ + Gets a single post scoped to the given event. + + Returns `nil` if the post does not exist or does not belong to the event. + """ + def get_post_for_event(uuid, event_id, preload \\ []) do + from(p in Post, + where: p.uuid == ^uuid and p.event_id == ^event_id + ) + |> Repo.one() + |> case do + nil -> nil + post -> Repo.preload(post, preload) + end + end + @doc """ Creates a post. diff --git a/lib/claper/quizzes.ex b/lib/claper/quizzes.ex index 012b0d4..e2d9537 100644 --- a/lib/claper/quizzes.ex +++ b/lib/claper/quizzes.ex @@ -70,6 +70,23 @@ defmodule Claper.Quizzes do |> set_percentages() end + @doc """ + Gets a single quiz scoped to the given event. + + Returns `nil` if the quiz does not exist or does not belong to the event. + """ + def get_quiz_for_event(id, event_id, preload \\ []) do + from(q in Quiz, + join: pf in assoc(q, :presentation_file), + where: q.id == ^id and pf.event_id == ^event_id + ) + |> Repo.one() + |> case do + nil -> nil + quiz -> quiz |> Repo.preload(preload) |> set_percentages() + end + end + @doc """ Gets a single quiz for a given position. diff --git a/lib/claper_web/live/embed_live/form_component.ex b/lib/claper_web/live/embed_live/form_component.ex index 8bb7a7e..dad4569 100644 --- a/lib/claper_web/live/embed_live/form_component.ex +++ b/lib/claper_web/live/embed_live/form_component.ex @@ -24,10 +24,14 @@ defmodule ClaperWeb.EmbedLive.FormComponent do @impl true def handle_event("delete", %{"id" => id}, socket) do - embed = Embeds.get_embed!(id) - {:ok, _} = Embeds.delete_embed(socket.assigns.event_uuid, embed) + case Embeds.get_embed_for_event(id, socket.assigns.presentation_file.event_id) do + nil -> + {:noreply, socket} - {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + embed -> + {:ok, _} = Embeds.delete_embed(socket.assigns.event_uuid, embed) + {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + end end @impl true diff --git a/lib/claper_web/live/event_live/manage.ex b/lib/claper_web/live/event_live/manage.ex index ab5951f..cd03b67 100644 --- a/lib/claper_web/live/event_live/manage.ex +++ b/lib/claper_web/live/event_live/manage.ex @@ -76,6 +76,8 @@ defmodule ClaperWeb.EventLive.Manage do defp leader?(_socket, _event), do: false + defp event_id(%{assigns: %{event: event}}), do: event.id + @impl true def handle_info(%{event: "presence_diff"}, %{assigns: %{event: event}} = socket) do attendees = Presence.list("event:#{event.uuid}") @@ -329,127 +331,175 @@ defmodule ClaperWeb.EventLive.Manage do end def handle_event("poll-set-active", %{"id" => id}, socket) do - with poll <- Polls.get_poll!(id), :ok <- Claper.Interactions.enable_interaction(poll) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, poll} - ) + case Polls.get_poll_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, poll) - |> interactions_at_position(socket.assigns.state.position)} + poll -> + with :ok <- Claper.Interactions.enable_interaction(poll) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, poll} + ) + + {:noreply, + socket + |> assign(:current_interaction, poll) + |> interactions_at_position(socket.assigns.state.position)} + end end end def handle_event("form-set-active", %{"id" => id}, socket) do - with form <- Forms.get_form!(id), :ok <- Claper.Interactions.enable_interaction(form) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, form} - ) + case Forms.get_form_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, form) - |> interactions_at_position(socket.assigns.state.position)} + form -> + with :ok <- Claper.Interactions.enable_interaction(form) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, form} + ) + + {:noreply, + socket + |> assign(:current_interaction, form) + |> interactions_at_position(socket.assigns.state.position)} + end end end def handle_event("embed-set-active", %{"id" => id}, socket) do - with embed <- Embeds.get_embed!(id), :ok <- Claper.Interactions.enable_interaction(embed) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, embed} - ) + case Embeds.get_embed_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, embed) - |> interactions_at_position(socket.assigns.state.position)} + embed -> + with :ok <- Claper.Interactions.enable_interaction(embed) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, embed} + ) + + {:noreply, + socket + |> assign(:current_interaction, embed) + |> interactions_at_position(socket.assigns.state.position)} + end end end def handle_event("poll-set-inactive", %{"id" => id}, socket) do - with poll <- Polls.get_poll!(id), {:ok, _} <- Claper.Interactions.disable_interaction(poll) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, nil} - ) - end + case Polls.get_poll_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, nil) - |> interactions_at_position(socket.assigns.state.position)} + poll -> + with {:ok, _} <- Claper.Interactions.disable_interaction(poll) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, nil} + ) + end + + {:noreply, + socket + |> assign(:current_interaction, nil) + |> interactions_at_position(socket.assigns.state.position)} + end end def handle_event("form-set-inactive", %{"id" => id}, socket) do - with form <- Forms.get_form!(id), {:ok, _} <- Claper.Interactions.disable_interaction(form) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, nil} - ) - end + case Forms.get_form_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, nil) - |> interactions_at_position(socket.assigns.state.position)} + form -> + with {:ok, _} <- Claper.Interactions.disable_interaction(form) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, nil} + ) + end + + {:noreply, + socket + |> assign(:current_interaction, nil) + |> interactions_at_position(socket.assigns.state.position)} + end end def handle_event("embed-set-inactive", %{"id" => id}, socket) do - with embed <- Embeds.get_embed!(id), - {:ok, _} <- Claper.Interactions.disable_interaction(embed) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, nil} - ) - end + case Embeds.get_embed_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, nil) - |> interactions_at_position(socket.assigns.state.position)} + embed -> + with {:ok, _} <- Claper.Interactions.disable_interaction(embed) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, nil} + ) + end + + {:noreply, + socket + |> assign(:current_interaction, nil) + |> interactions_at_position(socket.assigns.state.position)} + end end @impl true def handle_event("quiz-set-active", %{"id" => id}, socket) do - with quiz <- Quizzes.get_quiz!(id, [:quiz_questions, quiz_questions: :quiz_question_opts]), - :ok <- Claper.Interactions.enable_interaction(quiz) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, quiz} - ) + case Quizzes.get_quiz_for_event(id, event_id(socket), [ + :quiz_questions, + quiz_questions: :quiz_question_opts + ]) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, quiz) - |> interactions_at_position(socket.assigns.state.position)} + quiz -> + with :ok <- Claper.Interactions.enable_interaction(quiz) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, quiz} + ) + + {:noreply, + socket + |> assign(:current_interaction, quiz) + |> interactions_at_position(socket.assigns.state.position)} + end end end def handle_event("quiz-set-inactive", %{"id" => id}, socket) do - with quiz <- Quizzes.get_quiz!(id), - {:ok, _} <- Claper.Interactions.disable_interaction(quiz) do - Phoenix.PubSub.broadcast( - Claper.PubSub, - "event:#{socket.assigns.event.uuid}", - {:current_interaction, nil} - ) - end + case Quizzes.get_quiz_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - socket - |> assign(:current_interaction, nil) - |> interactions_at_position(socket.assigns.state.position)} + quiz -> + with {:ok, _} <- Claper.Interactions.disable_interaction(quiz) do + Phoenix.PubSub.broadcast( + Claper.PubSub, + "event:#{socket.assigns.event.uuid}", + {:current_interaction, nil} + ) + end + + {:noreply, + socket + |> assign(:current_interaction, nil) + |> interactions_at_position(socket.assigns.state.position)} + end end @impl true @@ -465,8 +515,10 @@ defmodule ClaperWeb.EventLive.Manage do @impl true def handle_event("pin", %{"id" => id}, socket) do - post = Claper.Posts.get_post!(id, [:event]) - pin(post, socket) + case Claper.Posts.get_post_for_event(id, event_id(socket), [:event]) do + nil -> {:noreply, socket} + post -> pin(post, socket) + end end @impl true @@ -669,21 +721,26 @@ defmodule ClaperWeb.EventLive.Manage do @impl true def handle_event("delete", %{"id" => id}, socket) do - post = Claper.Posts.get_post!(id, [:event]) - {:ok, _} = Claper.Posts.delete_post(post) + case Claper.Posts.get_post_for_event(id, event_id(socket), [:event]) do + nil -> + {:noreply, socket} - updated_socket = - if post.pinned do - stream(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid), - reset: true - ) + post -> + {:ok, _} = Claper.Posts.delete_post(post) - stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true) - else - stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true) - end + updated_socket = + if post.pinned do + stream(socket, :pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid), + reset: true + ) - {:noreply, updated_socket} + stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true) + else + stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true) + end + + {:noreply, updated_socket} + end end @impl true @@ -697,16 +754,21 @@ defmodule ClaperWeb.EventLive.Manage do end @impl true - def handle_event("delete-form-submit", %{"event-id" => event_id, "id" => id}, socket) do - form = Claper.Forms.get_form_submit_by_id!(id) - {:ok, _} = Claper.Forms.delete_form_submit(event_id, form) + def handle_event("delete-form-submit", %{"id" => id}, socket) do + case Claper.Forms.get_form_submit_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, - assign( - socket, - :form_submits, - list_form_submits(socket, socket.assigns.event.presentation_file.id) - )} + form_submit -> + {:ok, _} = Claper.Forms.delete_form_submit(socket.assigns.event.uuid, form_submit) + + {:noreply, + assign( + socket, + :form_submits, + list_form_submits(socket, socket.assigns.event.presentation_file.id) + )} + end end @impl true @@ -762,18 +824,26 @@ defmodule ClaperWeb.EventLive.Manage do @impl true def handle_event("delete-poll", %{"id" => id}, socket) do - poll = Polls.get_poll!(id) - {:ok, _} = Polls.delete_poll(socket.assigns.event.uuid, poll) + case Polls.get_poll_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, socket} + poll -> + {:ok, _} = Polls.delete_poll(socket.assigns.event.uuid, poll) + {:noreply, socket} + end end @impl true def handle_event("delete-quiz", %{"id" => id}, socket) do - quiz = Quizzes.get_quiz!(id) - {:ok, _} = Quizzes.delete_quiz(socket.assigns.event.uuid, quiz) + case Quizzes.get_quiz_for_event(id, event_id(socket)) do + nil -> + {:noreply, socket} - {:noreply, socket} + quiz -> + {:ok, _} = Quizzes.delete_quiz(socket.assigns.event.uuid, quiz) + {:noreply, socket} + end end @impl true @@ -826,13 +896,19 @@ defmodule ClaperWeb.EventLive.Manage do end defp apply_action(socket, :edit_poll, %{"id" => id}) do - poll = Polls.get_poll!(id) + case Polls.get_poll_for_event(id, event_id(socket)) do + nil -> + socket + |> put_flash(:error, gettext("Resource not found")) + |> push_navigate(to: ~p"/e/#{socket.assigns.event.code}/manage") - socket - |> assign(:create, "poll") - |> assign(:interaction_modal, true) - |> assign(:create_action, :edit) - |> assign(:poll, poll) + poll -> + socket + |> assign(:create, "poll") + |> assign(:interaction_modal, true) + |> assign(:create_action, :edit) + |> assign(:poll, poll) + end end defp apply_action(socket, :add_form, _params) do @@ -859,23 +935,35 @@ defmodule ClaperWeb.EventLive.Manage do end defp apply_action(socket, :edit_form, %{"id" => id}) do - form = Forms.get_form!(id) + case Forms.get_form_for_event(id, event_id(socket)) do + nil -> + socket + |> put_flash(:error, gettext("Resource not found")) + |> push_navigate(to: ~p"/e/#{socket.assigns.event.code}/manage") - socket - |> assign(:create, "form") - |> assign(:interaction_modal, true) - |> assign(:create_action, :edit) - |> assign(:form, form) + form -> + socket + |> assign(:create, "form") + |> assign(:interaction_modal, true) + |> assign(:create_action, :edit) + |> assign(:form, form) + end end defp apply_action(socket, :edit_embed, %{"id" => id}) do - embed = Embeds.get_embed!(id) + case Embeds.get_embed_for_event(id, event_id(socket)) do + nil -> + socket + |> put_flash(:error, gettext("Resource not found")) + |> push_navigate(to: ~p"/e/#{socket.assigns.event.code}/manage") - socket - |> assign(:create, "embed") - |> assign(:interaction_modal, true) - |> assign(:create_action, :edit) - |> assign(:embed, embed) + embed -> + socket + |> assign(:create, "embed") + |> assign(:interaction_modal, true) + |> assign(:create_action, :edit) + |> assign(:embed, embed) + end end defp apply_action(socket, :add_quiz, _params) do @@ -900,13 +988,22 @@ defmodule ClaperWeb.EventLive.Manage do end defp apply_action(socket, :edit_quiz, %{"id" => id}) do - quiz = Quizzes.get_quiz!(id, [:quiz_questions, quiz_questions: :quiz_question_opts]) + case Quizzes.get_quiz_for_event(id, event_id(socket), [ + :quiz_questions, + quiz_questions: :quiz_question_opts + ]) do + nil -> + socket + |> put_flash(:error, gettext("Resource not found")) + |> push_navigate(to: ~p"/e/#{socket.assigns.event.code}/manage") - socket - |> assign(:create, "quiz") - |> assign(:interaction_modal, true) - |> assign(:create_action, :edit) - |> assign(:quiz, quiz) + quiz -> + socket + |> assign(:create, "quiz") + |> assign(:interaction_modal, true) + |> assign(:create_action, :edit) + |> assign(:quiz, quiz) + end end defp pin(post, socket) do diff --git a/lib/claper_web/live/event_live/manage.html.heex b/lib/claper_web/live/event_live/manage.html.heex index 573a0e6..0f1ec08 100644 --- a/lib/claper_web/live/event_live/manage.html.heex +++ b/lib/claper_web/live/event_live/manage.html.heex @@ -1398,7 +1398,6 @@ to: "#", phx_click: "delete-form-submit", phx_value_id: submission.id, - phx_value_event_id: @event.uuid, data: [confirm: gettext("This cannot be undone, confirm ?")] )} diff --git a/lib/claper_web/live/form_live/form_component.ex b/lib/claper_web/live/form_live/form_component.ex index a2f4271..9bd76e8 100644 --- a/lib/claper_web/live/form_live/form_component.ex +++ b/lib/claper_web/live/form_live/form_component.ex @@ -17,10 +17,14 @@ defmodule ClaperWeb.FormLive.FormComponent do @impl true def handle_event("delete", %{"id" => id}, socket) do - form = Forms.get_form!(id) - {:ok, _} = Forms.delete_form(socket.assigns.event_uuid, form) + case Forms.get_form_for_event(id, socket.assigns.presentation_file.event_id) do + nil -> + {:noreply, socket} - {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + form -> + {:ok, _} = Forms.delete_form(socket.assigns.event_uuid, form) + {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + end end @impl true diff --git a/lib/claper_web/live/poll_live/form_component.ex b/lib/claper_web/live/poll_live/form_component.ex index 79aafa6..4dddda3 100644 --- a/lib/claper_web/live/poll_live/form_component.ex +++ b/lib/claper_web/live/poll_live/form_component.ex @@ -17,10 +17,14 @@ defmodule ClaperWeb.PollLive.FormComponent do @impl true def handle_event("delete", %{"id" => id}, socket) do - poll = Polls.get_poll!(id) - {:ok, _} = Polls.delete_poll(socket.assigns.event_uuid, poll) + case Polls.get_poll_for_event(id, socket.assigns.presentation_file.event_id) do + nil -> + {:noreply, socket} - {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + poll -> + {:ok, _} = Polls.delete_poll(socket.assigns.event_uuid, poll) + {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + end end @impl true diff --git a/lib/claper_web/live/quiz_live/quiz_component.ex b/lib/claper_web/live/quiz_live/quiz_component.ex index 46d56e4..b5281b2 100644 --- a/lib/claper_web/live/quiz_live/quiz_component.ex +++ b/lib/claper_web/live/quiz_live/quiz_component.ex @@ -17,10 +17,17 @@ defmodule ClaperWeb.QuizLive.QuizComponent do @impl true def handle_event("delete", %{"id" => id}, socket) do - quiz = Quizzes.get_quiz!(id, [:quiz_questions, quiz_questions: :quiz_question_opts]) - {:ok, _} = Quizzes.delete_quiz(socket.assigns.event.uuid, quiz) + case Quizzes.get_quiz_for_event(id, socket.assigns.event.id, [ + :quiz_questions, + quiz_questions: :quiz_question_opts + ]) do + nil -> + {:noreply, socket} - {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + quiz -> + {:ok, _} = Quizzes.delete_quiz(socket.assigns.event.uuid, quiz) + {:noreply, socket |> push_navigate(to: socket.assigns.return_to)} + end end @impl true diff --git a/test/claper/embeds_test.exs b/test/claper/embeds_test.exs index c026bf7..39f00b8 100644 --- a/test/claper/embeds_test.exs +++ b/test/claper/embeds_test.exs @@ -138,5 +138,26 @@ defmodule Claper.EmbedsTest do embed = embed_fixture(%{presentation_file_id: presentation_file.id}) assert %Ecto.Changeset{} = Embeds.change_embed(embed) end + + test "get_embed_for_event/3 returns embed when it belongs to the event" do + presentation_file = presentation_file_fixture() + embed = embed_fixture(%{presentation_file_id: presentation_file.id}) + + fetched_embed = Embeds.get_embed_for_event(embed.id, presentation_file.event_id) + assert fetched_embed.id == embed.id + end + + test "get_embed_for_event/3 returns nil when embed belongs to a different event" do + presentation_file_a = presentation_file_fixture() + presentation_file_b = presentation_file_fixture() + embed = embed_fixture(%{presentation_file_id: presentation_file_a.id}) + + assert is_nil(Embeds.get_embed_for_event(embed.id, presentation_file_b.event_id)) + end + + test "get_embed_for_event/3 returns nil for nonexistent embed id" do + presentation_file = presentation_file_fixture() + assert is_nil(Embeds.get_embed_for_event(-1, presentation_file.event_id)) + end end end diff --git a/test/claper/forms_test.exs b/test/claper/forms_test.exs index 63f1ccb..86295e7 100644 --- a/test/claper/forms_test.exs +++ b/test/claper/forms_test.exs @@ -102,10 +102,63 @@ defmodule Claper.FormsTest do form = form_fixture(%{presentation_file_id: presentation_file.id}) assert %Ecto.Changeset{} = Forms.change_form(form) end + + test "get_form_for_event/3 returns form when it belongs to the event" do + presentation_file = presentation_file_fixture() + form = form_fixture(%{presentation_file_id: presentation_file.id}) + + fetched_form = Forms.get_form_for_event(form.id, presentation_file.event_id) + assert fetched_form.id == form.id + end + + test "get_form_for_event/3 returns nil when form belongs to a different event" do + presentation_file_a = presentation_file_fixture() + presentation_file_b = presentation_file_fixture() + form = form_fixture(%{presentation_file_id: presentation_file_a.id}) + + assert is_nil(Forms.get_form_for_event(form.id, presentation_file_b.event_id)) + end + + test "get_form_for_event/3 returns nil for nonexistent form id" do + presentation_file = presentation_file_fixture() + assert is_nil(Forms.get_form_for_event(-1, presentation_file.event_id)) + end end describe "form_submits" do - import Claper.{FormsFixtures, PresentationsFixtures} + import Claper.{FormsFixtures, PresentationsFixtures, AccountsFixtures} + + test "get_form_submit_for_event/2 returns form submit when it belongs to the event" do + presentation_file = presentation_file_fixture() + form = form_fixture(%{presentation_file_id: presentation_file.id}) + user = user_fixture() + + {:ok, form_submit} = + Forms.create_form_submit(%{ + form_id: form.id, + user_id: user.id, + response: %{"Name" => "Test"} + }) + + fetched = Forms.get_form_submit_for_event(form_submit.id, presentation_file.event_id) + assert fetched.id == form_submit.id + end + + test "get_form_submit_for_event/2 returns nil when form submit belongs to a different event" do + presentation_file_a = presentation_file_fixture() + presentation_file_b = presentation_file_fixture() + form = form_fixture(%{presentation_file_id: presentation_file_a.id}) + user = user_fixture() + + {:ok, form_submit} = + Forms.create_form_submit(%{ + form_id: form.id, + user_id: user.id, + response: %{"Name" => "Test"} + }) + + assert is_nil(Forms.get_form_submit_for_event(form_submit.id, presentation_file_b.event_id)) + end test "get_form_submit/2 returns the form_submit with given id and user id" do form_submit = form_submit_fixture() diff --git a/test/claper/polls_test.exs b/test/claper/polls_test.exs index a901283..33664a0 100644 --- a/test/claper/polls_test.exs +++ b/test/claper/polls_test.exs @@ -106,6 +106,27 @@ defmodule Claper.PollsTest do poll = poll_fixture(%{presentation_file_id: presentation_file.id}) assert %Ecto.Changeset{} = Polls.change_poll(poll) end + + test "get_poll_for_event/2 returns poll when it belongs to the event" do + presentation_file = presentation_file_fixture() + poll = poll_fixture(%{presentation_file_id: presentation_file.id}) + + fetched_poll = Polls.get_poll_for_event(poll.id, presentation_file.event_id) + assert fetched_poll.id == poll.id + end + + test "get_poll_for_event/2 returns nil when poll belongs to a different event" do + presentation_file_a = presentation_file_fixture() + presentation_file_b = presentation_file_fixture() + poll = poll_fixture(%{presentation_file_id: presentation_file_a.id}) + + assert is_nil(Polls.get_poll_for_event(poll.id, presentation_file_b.event_id)) + end + + test "get_poll_for_event/2 returns nil for nonexistent poll id" do + presentation_file = presentation_file_fixture() + assert is_nil(Polls.get_poll_for_event(-1, presentation_file.event_id)) + end end describe "poll_opts" do diff --git a/test/claper/posts_test.exs b/test/claper/posts_test.exs index f368990..c70627f 100644 --- a/test/claper/posts_test.exs +++ b/test/claper/posts_test.exs @@ -52,6 +52,27 @@ defmodule Claper.PostsTest do assert {:ok, %Post{}} = Posts.delete_post(post) assert_raise Ecto.NoResultsError, fn -> Posts.get_post!(post.uuid) end end + + test "get_post_for_event/3 returns post when it belongs to the event" do + event = event_fixture() + post = post_fixture(%{event: event}, [:event]) + + fetched_post = Posts.get_post_for_event(post.uuid, event.id, [:event]) + assert fetched_post.id == post.id + end + + test "get_post_for_event/3 returns nil when post belongs to a different event" do + event_a = event_fixture() + event_b = event_fixture() + post = post_fixture(%{event: event_a}) + + assert is_nil(Posts.get_post_for_event(post.uuid, event_b.id)) + end + + test "get_post_for_event/3 returns nil for nonexistent post uuid" do + event = event_fixture() + assert is_nil(Posts.get_post_for_event(Ecto.UUID.generate(), event.id)) + end end describe "reactions" do diff --git a/test/claper/quizzes_test.exs b/test/claper/quizzes_test.exs index 4b90422..f52049d 100644 --- a/test/claper/quizzes_test.exs +++ b/test/claper/quizzes_test.exs @@ -196,6 +196,27 @@ defmodule Claper.QuizzesTest do assert length(responses) == 1 end + test "get_quiz_for_event/3 returns quiz when it belongs to the event" do + presentation_file = presentation_file_fixture() + quiz = quiz_fixture(%{presentation_file: presentation_file}) + + fetched_quiz = Quizzes.get_quiz_for_event(quiz.id, presentation_file.event_id) + assert fetched_quiz.id == quiz.id + end + + test "get_quiz_for_event/3 returns nil when quiz belongs to a different event" do + presentation_file_a = presentation_file_fixture() + presentation_file_b = presentation_file_fixture() + quiz = quiz_fixture(%{presentation_file: presentation_file_a}) + + assert is_nil(Quizzes.get_quiz_for_event(quiz.id, presentation_file_b.event_id)) + end + + test "get_quiz_for_event/3 returns nil for nonexistent quiz id" do + presentation_file = presentation_file_fixture() + assert is_nil(Quizzes.get_quiz_for_event(-1, presentation_file.event_id)) + end + test "submit_quiz/4 with user and duplicate opts deduplicates by id" do quiz = quiz_fixture() user = user_fixture()