Hi, as I said earlier, this is part of channel testing and there is no JS client yet. I think I should add more code to clarify.
The chatting scheme is as follows:
- The user is made to implicitly join a “default” room at start. This room is not shown, but enables the socket connection. This was done to allow the user to send various commands. I’m sorry if it’s a poor approach but I couldn’t think of anything better.
- The user can then send request to join more rooms.
Here’s the relevant code from my channel tests:
setup do
{:ok, _, socket} =
socket("user_id", %{})
#Subscribe to the "default" room so that we can send commands over events
|> subscribe_and_join(RoomChannel, "default")
{:ok, socket: socket}
end
And the unit test:
test "Front-end can get the list of pending user invites", %{socket: socket} do
# create a room and a user and app
ref = push socket, "room", %{"command" => "create", "user_id" => 20, "app_id" => 20, "room_name" => "Room 20"}
assert_reply ref, :ok, %{"room_id" => room_id}
# create another room and user
ref = push socket, "room", %{"command" => "create", "user_id" => 21, "app_id" => 21, "room_name" => "Room 21"}
assert_reply ref, :ok, %{"room_id" => room_id_temp}
# first user joins room
ref = push socket, "room", %{"command" => "join", "room_id" => room_id, "user_id" => 20, "app_id" => 20}
# we must make sure the process has replied to us
assert_reply ref, :ok
# first user invites the second user
ref = push socket, "invite", %{"command" => "send_invite", "room_id" => room_id, "sending_user_id" => 20, "sending_app_id" => 20, "receiving_user_id" => 21, "receiving_app_id" => 21}
assert_reply ref, :ok
ref = push socket, "invite", %{"command" => "get_pending", "user_id" => 21, "app_id" => 21}
assert_reply ref, :ok, %{"data" => [item]}
assert %{"sending_user_id" => 20, "sending_app_id" => 20} = item
end
So basically, here one user sends invite to the other user, and we check the pending invites for the other user.
Here are the relevant functions from the channels code:
@doc"""
This is the first room that the user must join. This is an empty room, created only so that users get a valid connection to the channel system and can start sending commands like 'create room'
"""
def join("default", data, socket) do
{:ok, socket}
end
@doc"""
Handle joining of rooms
"""
def handle_in("room", %{"command" => "join"} = data, socket) do
# send join request to function `join` in same module
%{"room_id" => room_id} = data
# create a pattern so that a different channel is created for the room
response = RoomChannel.join("room_#{room_id}", data, socket)
{:reply, response, socket}
end
@doc"""
Function that handles joining of rooms by users. The only basic check it performs is whether the room is exists or not. If not, it creates one, setting the requesting user as the creator of the room.
Note that nothing special is as such needed to "join" a room. We simply return :ok to the socket, and the Phoenix PubSub system does the rest whenever messages are posted in the room.
"""
def join("room_" <> room_id, data, socket) do
%{"user_id" => user_id, "app_id" => app_id} = data
{room_id, _} = Integer.parse(room_id)
# Create user and app if they don't exist
verify_or_create_user(user_id)
verify_or_create_app(app_id)
# First, check if room exists
room = Repo.get_by(Room, id: room_id)
case room do
nil ->
{:error, %{"reason" => "Room doesn't exist"}}
_ ->
# It's time to create association betwen user and room
# The problem here, however, is that many_to_many
# relations don't work if you also need to update a
# non-key field on the join-through model.
# Therefore, we'll work around this problem by saving the
# RoomUser model directly. See the following link for getting
# convinced that this is indeed the right solution:
# https://elixirforum.com/t/many-to-many-where-joining-model-needs-to-be-updated-also/1785
changeset = RoomUser.changeset(%RoomUser{room_id: room_id, user_id: user_id, app_id: app_id, status: 1})
if changeset.valid? do
Repo.insert!(changeset)
# send a push notifcation to room
broadcast socket, "room", %{"type" => "user_joined", "user_id" => user_id, "app_id" => app_id}
# bind the user id and app id to the socket
socket = assign(socket, :user_id, user_id)
socket = assign(socket, :app_id, app_id)
#IO.puts("=== In the join function ===")
#IO.inspect(socket)
{:ok, socket}
end
end
end
Note that the previous function does assign some data to the socket. Finally, I did an inspect
in the function that gets data related to pending invites, to make sure the socket carried the user id and app id:
def handle_in("invite", %{"command" => "get_pending"} = payload, socket) do
IO.inspect(socket) # show me the socket!
%{"user_id" => user_id, "app_id" => app_id} = payload
invites = Repo.all from i in Invite,
join: r in assoc(i, :room),
where: i.receiving_user_id == ^user_id and i.receiving_app_id == ^app_id and i.status == 1,
preload: [room: r]
data = relevant_data_for_invites(invites)
{:reply, {:ok, %{"data" => data}}, socket}
end
And when I run the test, the socket is:
%Phoenix.Socket{assigns: %{}, channel: Elemental.TxChat.RoomChannel,
channel_pid: #PID<0.441.0>, endpoint: Elemental.TxChat.Endpoint, handler: nil,
id: "user_id", joined: true, pubsub_server: Elemental.TxChat.PubSub,
ref: #Reference<0.0.3.1532>, serializer: Phoenix.ChannelTest.NoopSerializer,
topic: "default", transport: Phoenix.ChannelTest,
transport_name: :channel_test, transport_pid: #PID<0.439.0>}
Which leads to my questions:
- Why didn’t the socket get assigned the values?
- If it’s not possible because of some weird mocking behavior of the channel testing module (or something else), how can I test for events where user connection breaks unexpectedly?