Plani: a plani only takes the owner's foci, ankyra and sources

b4aa1b1 · Gabe Morell · 2026-11-04 22:43

10 files +86 -0
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum/configuration/plani.ex
+9 −0
@@ -14,6 +14,9 @@ defmodule RoomSanctum.Configuration.Plani do
14 14 use Ecto.Schema
15 15 import Ecto.Changeset
16 16
17 + alias RoomSanctum.Accounts.RabbitUser
18 + alias RoomSanctum.Configuration.{Foci, Ownership, Source}
19 +
17 20 schema "cfg_plani" do
18 21 field :name, :string
19 22 belongs_to :user, RoomSanctum.Accounts.User
@@ -102,6 +105,12 @@ defmodule RoomSanctum.Configuration.Plani do
102 105 |> validate_home_tint()
103 106 |> foreign_key_constraint(:user_id)
104 107 |> foreign_key_constraint(:home_foci_id)
108 + # The form only offers the owner's own. Anything else would ask another
109 + # user's sources, follow their client's position, or call their foci home.
110 + |> Ownership.validate_owned(:home_foci_id, Foci)
111 + |> Ownership.validate_owned(:home_foci_ids, Foci)
112 + |> Ownership.validate_owned(:ankyra_id, RabbitUser)
113 + |> Ownership.validate_owned(:sources, Source)
105 114 end
106 115
107 116 # A tint that is not one of the offered colours renders as an unstyled dot
added apps/room_sanctum/test/room_sanctum_web/live/plani_owned_ids_test.exs
+77 −0
@@ -0,0 +1,77 @@
1 +defmodule RoomSanctumWeb.PlaniOwnedIdsTest do
2 + @moduledoc """
3 + A Plani asks its owner's sources around wherever their client is, falling
4 + back to their foci. The form only offers those; a forged id of anyone
5 + else's is refused with a changeset error and nothing is saved.
6 + """
7 + use RoomSanctumWeb.ConnCase
8 +
9 + import Phoenix.LiveViewTest
10 + import RoomSanctum.DataCase, only: [errors_on: 1]
11 + import RoomSanctum.OwnershipFixtures
12 +
13 + alias RoomSanctum.{Configuration, Repo}
14 + alias RoomSanctum.Configuration.Plani
15 +
16 + setup :users_rabbit!
17 +
18 + setup %{conn: conn} do
19 + me = owner()
20 + %{conn: log_in_user(conn, me), me: me, them: owner()}
21 + end
22 +
23 + defp submit(conn, fields) do
24 + {:ok, live, _html} = live(conn, Routes.plani_index_path(conn, :new))
25 +
26 + live
27 + |> form("#plani-form")
28 + |> render_submit(%{"plani" => Map.put(fields, "name", "Pocket")})
29 + end
30 +
31 + defp mine(ctx) do
32 + foci = foci_for(ctx.me)
33 +
34 + %{
35 + "home_foci_id" => to_string(foci.id),
36 + "home_foci_ids" => [to_string(foci.id)],
37 + "ankyra_id" => to_string(ankyra_for(ctx.me).id),
38 + "sources" => [to_string(source_for(ctx.me).id)]
39 + }
40 + end
41 +
42 + test "the form saves the owner's own foci, ankyra and sources", ctx do
43 + own = mine(ctx)
44 + submit(ctx.conn, own)
45 +
46 + assert [%Plani{} = saved] = Repo.all(Plani)
47 + assert to_string(saved.ankyra_id) == own["ankyra_id"]
48 + assert Enum.map(saved.sources, &to_string/1) == own["sources"]
49 + end
50 +
51 + test "the form refuses each of another user's records", ctx do
52 + own = mine(ctx)
53 + their_foci = to_string(foci_for(ctx.them).id)
54 +
55 + forged = [
56 + %{"home_foci_id" => their_foci},
57 + %{"home_foci_ids" => own["home_foci_ids"] ++ [their_foci]},
58 + %{"ankyra_id" => to_string(ankyra_for(ctx.them).id)},
59 + %{"sources" => own["sources"] ++ [to_string(source_for(ctx.them).id)]}
60 + ]
61 +
62 + for fields <- forged do
63 + submit(ctx.conn, Map.merge(own, fields))
64 + assert Repo.all(Plani) == [], "saved with #{inspect(fields)}"
65 + end
66 + end
67 +
68 + test "an existing plani cannot be pointed at another user's source", ctx do
69 + plani = plani_for(ctx.me)
70 +
71 + assert {:error, changeset} =
72 + Configuration.update_plani(plani, %{sources: [source_for(ctx.them).id]})
73 +
74 + assert %{sources: [_]} = errors_on(changeset)
75 + assert {:ok, _} = Configuration.update_plani(plani, %{sources: [source_for(ctx.me).id]})
76 + end
77 +end

Parents: e70ed4d