Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: port garden_channel_test.exs to ChannelCase

Replace manually constructed Phoenix.Socket structs with real
socket connections via ChannelCase. Add rejection tests for
nonexistent garden and mismatched local_sid. Replay test now
exercises the full join → handle_info pipeline using assert_push.

SOW-73

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+104 -59
+104 -59
apps/sower/test/sower_web/channels/garden_channel_test.exs
··· 1 1 defmodule SowerWeb.GardenChannelTest do 2 - use Sower.DataCase, async: true 2 + use SowerWeb.ChannelCase, async: true 3 3 4 - import Sower.AccountsFixtures 5 - import Sower.OrchestrationFixtures 6 - import Sower.SeedFixtures 4 + describe "join/3" do 5 + test "joins with matching local_sid and assigns garden" do 6 + %{socket: socket, garden: garden} = connect_and_join_garden() 7 7 8 - alias Phoenix.Socket.Broadcast 9 - alias Sower.Accounts.AccessToken 10 - alias Sower.Orchestration 11 - alias SowerWeb.GardenChannel 8 + assert socket.assigns.garden.id == garden.id 9 + end 12 10 13 - describe "join/3" do 14 - test "schedules replay when garden joins with matching local sid" do 11 + test "accepts legacy agent: topic prefix" do 15 12 user = user_fixture() 16 13 Sower.Repo.put_org_id(user.org_id) 17 14 18 - garden = garden_fixture(%{sid: "garden_join_replay_1", local_sid: "garden_local_1"}) 15 + {:ok, access_token} = 16 + Sower.Accounts.AccessToken.create(%{ 17 + "description" => "test", 18 + "user_id" => user.id, 19 + "org_id" => user.org_id, 20 + "permissions" => [%{"role" => "garden:register"}] 21 + }) 19 22 20 - socket = %Phoenix.Socket{ 21 - assigns: %{ 22 - conn_sid: "conn_1", 23 - access_token: %AccessToken{org_id: user.org_id} 24 - } 25 - } 23 + garden = 24 + garden_fixture(%{ 25 + sid: SowerClient.Sid.generate("grdn"), 26 + local_sid: SowerClient.Sid.generate("local") 27 + }) 26 28 27 - assert {:ok, %{conn_sid: "conn_1"}, joined_socket} = 28 - GardenChannel.join( 29 - "garden:#{garden.sid}", 30 - %{"local_sid" => "garden_local_1"}, 31 - socket 32 - ) 29 + encoded_token = Base.encode64(access_token.token) 30 + 31 + {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 32 + 33 + {:ok, _reply, socket} = 34 + subscribe_and_join( 35 + socket, 36 + SowerWeb.GardenChannel, 37 + "agent:#{garden.sid}", 38 + %{"local_sid" => garden.local_sid} 39 + ) 33 40 34 - assert joined_socket.assigns.garden.id == garden.id 35 - assert_received :track_presence 36 - assert_received :replay_unresolved_deployments 41 + assert socket.assigns.garden.id == garden.id 37 42 end 38 43 39 - test "accepts legacy agent: topic prefix for backward compatibility" do 44 + test "rejects join when garden does not exist" do 40 45 user = user_fixture() 41 46 Sower.Repo.put_org_id(user.org_id) 42 47 43 - garden = garden_fixture(%{sid: "garden_join_compat_1", local_sid: "garden_compat_local_1"}) 48 + {:ok, access_token} = 49 + Sower.Accounts.AccessToken.create(%{ 50 + "description" => "test", 51 + "user_id" => user.id, 52 + "org_id" => user.org_id, 53 + "permissions" => [%{"role" => "garden:register"}] 54 + }) 44 55 45 - socket = %Phoenix.Socket{ 46 - assigns: %{ 47 - conn_sid: "conn_2", 48 - access_token: %AccessToken{org_id: user.org_id} 49 - } 50 - } 56 + encoded_token = Base.encode64(access_token.token) 57 + 58 + {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 51 59 52 - assert {:ok, %{conn_sid: "conn_2"}, joined_socket} = 53 - GardenChannel.join( 54 - "agent:#{garden.sid}", 55 - %{"local_sid" => "garden_compat_local_1"}, 56 - socket 60 + assert {:error, %{reason: "unauthorized"}} = 61 + subscribe_and_join( 62 + socket, 63 + SowerWeb.GardenChannel, 64 + "garden:nonexistent_sid", 65 + %{"local_sid" => "some_local_sid"} 57 66 ) 67 + end 58 68 59 - assert joined_socket.assigns.garden.id == garden.id 69 + test "rejects join when local_sid does not match" do 70 + user = user_fixture() 71 + Sower.Repo.put_org_id(user.org_id) 72 + 73 + {:ok, access_token} = 74 + Sower.Accounts.AccessToken.create(%{ 75 + "description" => "test", 76 + "user_id" => user.id, 77 + "org_id" => user.org_id, 78 + "permissions" => [%{"role" => "garden:register"}] 79 + }) 80 + 81 + garden = 82 + garden_fixture(%{ 83 + sid: SowerClient.Sid.generate("grdn"), 84 + local_sid: SowerClient.Sid.generate("local") 85 + }) 86 + 87 + encoded_token = Base.encode64(access_token.token) 88 + 89 + {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 90 + 91 + assert {:error, %{reason: "unauthorized"}} = 92 + subscribe_and_join( 93 + socket, 94 + SowerWeb.GardenChannel, 95 + "garden:#{garden.sid}", 96 + %{"local_sid" => "wrong_local_sid"} 97 + ) 60 98 end 61 99 end 62 100 63 - describe "handle_info/2 replay_unresolved_deployments" do 101 + describe "replay_unresolved_deployments on join" do 64 102 test "replays unresolved deployments and skips terminal ones" do 65 103 user = user_fixture() 66 104 Sower.Repo.put_org_id(user.org_id) 67 105 68 - garden = garden_fixture(%{sid: "garden_replay_1"}) 69 - seed = seed_fixture(%{name: "replay-seed-1", seed_type: "nixos"}) 106 + {:ok, access_token} = 107 + Sower.Accounts.AccessToken.create(%{ 108 + "description" => "test", 109 + "user_id" => user.id, 110 + "org_id" => user.org_id, 111 + "permissions" => [%{"role" => "garden:register"}] 112 + }) 113 + 114 + garden = 115 + garden_fixture(%{ 116 + sid: SowerClient.Sid.generate("grdn"), 117 + local_sid: SowerClient.Sid.generate("local") 118 + }) 119 + 120 + seed = seed_fixture(%{name: "replay-seed", seed_type: "nixos"}) 70 121 71 122 subscription = 72 123 subscription_fixture(%{ ··· 94 145 deployed_at: DateTime.utc_now() |> DateTime.truncate(:second) 95 146 }) 96 147 97 - Phoenix.PubSub.subscribe(Sower.PubSub, "garden:#{garden.sid}") 98 - 99 - socket = %Phoenix.Socket{assigns: %{garden: garden}} 100 - 101 - assert {:noreply, ^socket} = 102 - GardenChannel.handle_info(:replay_unresolved_deployments, socket) 148 + encoded_token = Base.encode64(access_token.token) 149 + {:ok, socket} = connect(SowerWeb.GardenSocket, %{"token" => encoded_token}) 103 150 104 - assert_receive %Broadcast{ 105 - topic: topic, 106 - event: "deployment", 107 - payload: payload 108 - } 151 + {:ok, _reply, _socket} = 152 + subscribe_and_join( 153 + socket, 154 + SowerWeb.GardenChannel, 155 + "garden:#{garden.sid}", 156 + %{"local_sid" => garden.local_sid} 157 + ) 109 158 110 - assert topic == "garden:#{garden.sid}" 159 + assert_push "deployment", payload 111 160 assert payload.sid == unresolved.sid 112 161 assert payload.skipped == false 113 162 assert is_binary(payload.request_id) 114 163 assert is_list(payload.seed_deployments) 115 - 116 - assert Enum.map(Orchestration.list_unresolved_deployments_for_garden(garden), & &1.sid) == [ 117 - unresolved.sid 118 - ] 119 164 end 120 165 end 121 166 end