neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Visions: query_ids only take the owner's own queries
5a0c3c4 · Gabe Morell · 2026-11-04 21:46
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum/configuration/vision.ex
+19
−7
@@ -1,6 +1,10 @@
| 1 | 1 | defmodule RoomSanctum.Configuration.Vision do |
| 2 | 2 | use Ecto.Schema |
| 3 | 3 | import Ecto.Changeset |
| 4 | + import Ecto.Query, only: [from: 2] | |
| 5 | + | |
| 6 | + alias RoomSanctum.Configuration.Query | |
| 7 | + alias RoomSanctum.Repo | |
| 4 | 8 | |
| 5 | 9 | schema "cfg_visions" do |
| 6 | 10 | belongs_to :user, RoomSanctum.Accounts.User |
@@ -74,16 +78,24 @@ defmodule RoomSanctum.Configuration.Vision do
| 74 | 78 | end |
| 75 | 79 | end |
| 76 | 80 | |
| 81 | + # A vision can only point at its owner's queries -- the form's picker only | |
| 82 | + # offers those. Only the ids this change adds are checked: delete_query/1 | |
| 83 | + # leaves its id behind on any vision it was on, so a stored vision can | |
| 84 | + # already hold ids with nothing behind them, and it has to stay editable. | |
| 77 | 85 | defp validate_queries_have_valid_ids(changeset) do |
| 78 | − case get_field(changeset, :query_ids) do | |
| 79 | − [_ | _] -> | |
| 80 | − # Validate that all query IDs exist and belong to the user | |
| 81 | − changeset | |
| 82 | − | |
| 83 | − _ -> | |
| 84 | − changeset | |
| 86 | + with [_ | _] = ids <- get_change(changeset, :query_ids), | |
| 87 | + [_ | _] = added <- Enum.uniq(ids) -- (changeset.data.query_ids || []), | |
| 88 | + user_id when not is_nil(user_id) <- get_field(changeset, :user_id), | |
| 89 | + [_ | _] <- added -- owned_query_ids(added, user_id) do | |
| 90 | + add_error(changeset, :query_ids, "must be queries you own") | |
| 91 | + else | |
| 92 | + _ -> changeset | |
| 85 | 93 | end |
| 86 | 94 | end |
| 95 | + | |
| 96 | + defp owned_query_ids(ids, user_id) do | |
| 97 | + Repo.all(from q in Query, where: q.id in ^ids and q.user_id == ^user_id, select: q.id) | |
| 98 | + end | |
| 87 | 99 | end |
| 88 | 100 | |
| 89 | 101 | defmodule RoomSanctum.Configuration.Vision.Schema do |
added
apps/room_sanctum/test/room_sanctum/vision_query_ids_test.exs
+103
−0
@@ -0,0 +1,103 @@
| 1 | +defmodule RoomSanctum.VisionQueryIdsTest do | |
| 2 | + @moduledoc """ | |
| 3 | + A vision can only point at its owner's queries -- the form's picker only | |
| 4 | + offers those -- and `query_ids` is what every "which visions is this query | |
| 5 | + on" lookup reads, so an id that is someone else's, or nobody's, does not get | |
| 6 | + in. | |
| 7 | + """ | |
| 8 | + use RoomSanctum.DataCase | |
| 9 | + | |
| 10 | + alias RoomSanctum.{Accounts, Configuration} | |
| 11 | + | |
| 12 | + defp user do | |
| 13 | + {:ok, user} = | |
| 14 | + Accounts.register_user(%{ | |
| 15 | + email: "seer#{System.unique_integer([:positive])}@example.com", | |
| 16 | + password: "hello world!hello world!" | |
| 17 | + }) | |
| 18 | + | |
| 19 | + user | |
| 20 | + end | |
| 21 | + | |
| 22 | + defp query(user) do | |
| 23 | + {:ok, source} = | |
| 24 | + Configuration.create_source(%{ | |
| 25 | + name: "Markets", | |
| 26 | + notes: "", | |
| 27 | + type: :bourse, | |
| 28 | + enabled: true, | |
| 29 | + user_id: user.id, | |
| 30 | + config: %{"__type__" => "bourse"} | |
| 31 | + }) | |
| 32 | + | |
| 33 | + {:ok, query} = | |
| 34 | + Configuration.create_query(%{ | |
| 35 | + name: "AAPL", | |
| 36 | + source_id: source.id, | |
| 37 | + user_id: user.id, | |
| 38 | + query: %{"__type__" => "bourse", "symbol" => "AAPL"} | |
| 39 | + }) | |
| 40 | + | |
| 41 | + query | |
| 42 | + end | |
| 43 | + | |
| 44 | + test "the owner's own queries are accepted" do | |
| 45 | + me = user() | |
| 46 | + q = query(me) | |
| 47 | + | |
| 48 | + assert {:ok, vision} = | |
| 49 | + Configuration.create_vision(%{name: "Mine", user_id: me.id, query_ids: [q.id]}) | |
| 50 | + | |
| 51 | + assert vision.query_ids == [q.id] | |
| 52 | + end | |
| 53 | + | |
| 54 | + test "another user's query is refused" do | |
| 55 | + me = user() | |
| 56 | + theirs = query(user()) | |
| 57 | + | |
| 58 | + assert {:error, changeset} = | |
| 59 | + Configuration.create_vision(%{name: "Mine", user_id: me.id, query_ids: [theirs.id]}) | |
| 60 | + | |
| 61 | + assert %{query_ids: [_]} = errors_on(changeset) | |
| 62 | + end | |
| 63 | + | |
| 64 | + test "an id with no query behind it is refused" do | |
| 65 | + me = user() | |
| 66 | + q = query(me) | |
| 67 | + | |
| 68 | + assert {:error, changeset} = | |
| 69 | + Configuration.create_vision(%{ | |
| 70 | + name: "Mine", | |
| 71 | + user_id: me.id, | |
| 72 | + query_ids: [q.id, q.id + 100_000] | |
| 73 | + }) | |
| 74 | + | |
| 75 | + assert %{query_ids: [_]} = errors_on(changeset) | |
| 76 | + end | |
| 77 | + | |
| 78 | + test "adding to a vision checks only what is being added" do | |
| 79 | + me = user() | |
| 80 | + q = query(me) | |
| 81 | + | |
| 82 | + {:ok, vision} = | |
| 83 | + Configuration.create_vision(%{name: "Mine", user_id: me.id, query_ids: [q.id]}) | |
| 84 | + | |
| 85 | + # delete_query/1 leaves the id behind on any vision it was pinned to, so | |
| 86 | + # stored visions can already hold ids with nothing behind them. They have | |
| 87 | + # to stay editable. | |
| 88 | + {:ok, _} = Configuration.delete_query(q) | |
| 89 | + assert {:ok, renamed} = Configuration.update_vision_ni(vision, %{name: "Renamed"}) | |
| 90 | + | |
| 91 | + other = query(me) | |
| 92 | + | |
| 93 | + assert {:ok, _} = | |
| 94 | + Configuration.update_vision_ni(renamed, %{query_ids: renamed.query_ids ++ [other.id]}) | |
| 95 | + | |
| 96 | + theirs = query(user()) | |
| 97 | + | |
| 98 | + assert {:error, _} = | |
| 99 | + Configuration.update_vision_ni(renamed, %{ | |
| 100 | + query_ids: renamed.query_ids ++ [theirs.id] | |
| 101 | + }) | |
| 102 | + end | |
| 103 | +end |
Parents: c7ea72b