Compare commits

..

No commits in common. "64e1deca92b62fdfa2b03214020bb9101f299354" and "3afaf346c7a00b9561a9b0284bc525257a1f4b0a" have entirely different histories.

38 changed files with 541 additions and 559 deletions

5
.gitignore vendored
View file

@ -31,13 +31,8 @@ birdy_chat-*.tar
# Ignore digested assets cache.
/priv/static/cache_manifest.json
/priv/static/favicon-*.ico
/priv/static/images/logo-*.svg
/priv/static/robots-*.txt
# Ignore messages folder
/priv/messages/
!priv/messages/README.md
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log

View file

@ -1,60 +1,18 @@
# BirdyChat Tech Challenge
# BirdyChat
This repository implements BirdyChat tech challenge.
To start your Phoenix server:
# Start here
* Run `mix setup` to install and setup dependencies
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
Firstly, check out this repository locally.
Then install required versions of Elixir and Erlang from .tool-versions. asdf-vm will pick them up automatically.
Then you run the test suite with `mix test`.
Then, a scripted demo release is prepared in the prod environment:
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
```
MIX_ENV=prod mix build_release
```
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
Then you can run 3 servers connected to one another:
## Learn more
`_build/prod/rel/birdy_chat/bin/server_1` - runs at localhost:4001
`_build/prod/rel/birdy_chat/bin/server_2` - runs at localhost:4002
`_build/prod/rel/birdy_chat/bin/server_3` - runs at localhost:4003
Out of the box they communicate with one another. Send a json request to one of them and observe the results. Feel free to modify the examples below.
### A local request:
```
curl --request POST \
--url http://localhost:4001/api/messages \
--header 'content-type: application/json' \
--data '{"message":"123","to":"1-user","from":"1-user"}'
```
### A remote request:
```
curl --request POST \
--url http://localhost:4001/api/messages \
--header 'content-type: application/json' \
--data '{"message":"123","to":"2-user","from":"1-user"}'
```
### A request to unknown server:
```
curl --request POST \
--url http://localhost:4001/api/messages \
--header 'content-type: application/json' \
--data '{"message":"123","to":"4-user","from":"1-user"}'
```
Files are saved to `priv/messages`.
## Key rundown of technical details
First and foremost, I tried to keep it as simple as possible - stick to know conventions, leverage existing libraries or frameworks, hence the usage of both Phoenix and Ecto - conventions they provide are understandable to pretty much any Elixir developer.
All important modules are documented and there is a test suite that exacutes all known code paths.
Servers authenticate with one another using Phoenix tokens. Servers communicate via HTTP using JSON. There were other options but I decided to use this one for reasons enumerated in documentation for BirdyChatWeb.Api.Server.Internal.Controller.
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix

View file

@ -21,6 +21,15 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
pubsub_server: BirdyChat.PubSub,
live_view: [signing_salt: "6cr1V8uA"]
# Configure the mailer
#
# By default it uses the "Local" adapter which stores the emails
# locally. You can see the emails in your browser, at "/dev/mailbox".
#
# For production it's recommended to configure a different adapter
# at the `config/runtime.exs`.
config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Local
# Configure esbuild (the version is required)
config :esbuild,
version: "0.25.4",

View file

@ -77,3 +77,6 @@ config :phoenix_live_view,
debug_attributes: true,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false

View file

@ -18,6 +18,12 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
hosts: ["localhost", "127.0.0.1"]
]
# Configure Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Req
# Disable Swoosh Local Memory Storage
config :swoosh, local: false
# Do not print debug messages in production
config :logger, level: :info

View file

@ -21,7 +21,7 @@ if System.get_env("PHX_SERVER") do
end
config :birdy_chat, BirdyChatWeb.Endpoint,
http: [port: String.to_integer(System.get_env("BIRDY_CHAT_PORT", "4000"))]
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
if config_env() == :prod do
# The secret key base is used to sign/encrypt cookies and other secrets.
@ -82,4 +82,22 @@ if config_env() == :prod do
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# ## Configuring the mailer
#
# In production you need to configure the mailer to use a different adapter.
# Here is an example configuration for Mailgun:
#
# config :birdy_chat, BirdyChat.Mailer,
# adapter: Swoosh.Adapters.Mailgun,
# api_key: System.get_env("MAILGUN_API_KEY"),
# domain: System.get_env("MAILGUN_DOMAIN")
#
# Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
# and Finch out-of-the-box. This configuration is typically done at
# compile-time in your config/prod.exs:
#
# config :swoosh, :api_client, Swoosh.ApiClient.Req
#
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
end

View file

@ -7,6 +7,12 @@ config :birdy_chat, BirdyChatWeb.Endpoint,
secret_key_base: "DsOg8g/AU3wogZIm99JWnoDyijeinMJFFfkFdwkSkFcjvHywCXjCxl//NY1cvm7Y",
server: false
# In test we don't send emails
config :birdy_chat, BirdyChat.Mailer, adapter: Swoosh.Adapters.Test
# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false
# Print only warnings and errors during test
config :logger, level: :warning
@ -17,9 +23,6 @@ config :phoenix, :plug_init_mode, :runtime
config :phoenix_live_view,
enable_expensive_runtime_checks: true
# Mock out HTTP requests in test
config :birdy_chat, BirdyChat.Dispatcher, req_opts: [plug: {Req.Test, BirdyChat.Dispatcher}]
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
sort_verified_routes_query_params: true

View file

@ -1,54 +0,0 @@
defmodule BirdyChat.Dispatcher do
@moduledoc """
Main dispatcher of messages - decides to either write them to local file system or send them via
HTTP to peers.
It originally started as a websocket connection between servers, but then I decided to rip
it out and replace with simple HTTP request-response for the following reasons:
1. HTTP guarantees immediate feedback (request succeeds or not), making the addition of caching
or retries easy.
2. HTTP requests have well-know semantic so I can i.e use HTTP statuses for error signals
instead of inventing my own error language.
"""
@spec dispatch(Ecto.Changeset.t()) :: :ok | {:error, String.t()}
def dispatch(%Ecto.Changeset{changes: changes} = changeset) do
case changes do
%{routing: :local} -> BirdyChat.MessageWriter.write(changeset.changes)
%{routing: :remote} -> send_to_remote(changeset.changes)
end
end
defp send_to_remote(%{server: server, from: from} = message) do
{name, base_url} =
BirdyChat.Identity.peers()
|> Enum.find(fn {name, _url} -> name == server end)
api_url = base_url <> "/api/internal"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", from)
{_request, result} =
Req.new(url: api_url, retry: false, method: :post)
|> Req.merge(req_opts())
|> Req.Request.put_new_header("authorization", token)
|> Req.merge(json: message)
|> Req.Request.run_request()
# Handle more when you encounter errors
case result do
%Req.Response{status: 201} -> :ok
# This should never happen under normal circumstances so I am commenting this out but maybe
# needs a second look.
# %Req.Response{status: 403} -> {:error, "Unauthorised"}
# Peer is down.
%Req.TransportError{reason: :econnrefused} -> {:error, "peer #{name} is unreachable"}
end
end
def req_opts do
Application.get_env(:birdy_chat, __MODULE__)
|> Access.get(:req_opts, [])
end
end

View file

@ -1,15 +1,4 @@
defmodule BirdyChat.Identity do
@moduledoc """
Server identity, populated on startup from the following environment variables:
- BIRDY_CHAT_IDENTITY: Name of the server, a string that can be formatted into an integer.
- BIRDY_CHAT_PEERS: List of peer servers in format of
`1::http://localhost:4001;2::http://localhost:4002`
When the environment variables are not present the server starts in test mode and can only
communicate with itself.
"""
use Agent
defstruct [:identity, :peers, :mode]
@ -60,18 +49,7 @@ defmodule BirdyChat.Identity do
peers = System.get_env("BIRDY_CHAT_PEERS")
case {identity, peers} do
{nil, nil} ->
%__MODULE__{
identity: "test1",
peers: %{"test2" => "http://localhost:4001"},
mode: :test
}
{identity, peers} ->
peers = parse_peers(peers)
identity = parse_identity(identity)
%__MODULE__{identity: identity, peers: peers, mode: :connected}
{nil, nil} -> %__MODULE__{identity: "test", peers: [], mode: :singleton}
end
end
end

3
lib/birdy_chat/mailer.ex Normal file
View file

@ -0,0 +1,3 @@
defmodule BirdyChat.Mailer do
use Swoosh.Mailer, otp_app: :birdy_chat
end

View file

@ -1,39 +1,10 @@
defmodule BirdyChat.Message do
@moduledoc """
Main module for input validation. I decided to re-use Ecto because of existence of Phoenix.Ecto
that clearly integrates the error messages produced from Ecto into HTTP plumbing of Phoenix.
"""
use Ecto.Schema
embedded_schema do
field :from, :string
field :to, :string
field :message, :string
field :routing, Ecto.Enum, values: [:remote, :local]
field :server, :string
end
@doc """
Validation for inter-server communication. It is essentially the same as validate/1
but without the requirement to communicate with home server only.
This can be also designed as a function that accepts other function or some configuration option
but two separately named functions are easier to understand and less prone to misuse.
"""
def validate_for_inter_server_use(params) do
changeset =
%__MODULE__{}
|> Ecto.Changeset.cast(params, [:from, :to, :message])
|> Ecto.Changeset.validate_required([:from, :to, :message])
|> put_routing()
|> validate_is_local()
if changeset.valid? do
{:ok, changeset}
else
{:error, changeset}
end
end
def validate(params) do
@ -41,8 +12,6 @@ defmodule BirdyChat.Message do
%__MODULE__{}
|> Ecto.Changeset.cast(params, [:from, :to, :message])
|> Ecto.Changeset.validate_required([:from, :to, :message])
|> put_routing()
|> validate_home_server()
if changeset.valid? do
{:ok, changeset}
@ -50,57 +19,4 @@ defmodule BirdyChat.Message do
{:error, changeset}
end
end
defp validate_is_local(%Ecto.Changeset{changes: %{routing: :local}} = changeset) do
changeset
end
defp validate_is_local(%Ecto.Changeset{} = changeset) do
changeset
|> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
end
defp validate_home_server(%Ecto.Changeset{changes: %{from: from}} = changeset) do
identity = BirdyChat.Identity.identity()
if String.starts_with?(from, identity) do
changeset
else
changeset
|> Ecto.Changeset.add_error(:from, "you can only communicate with your home server")
end
end
defp validate_home_server(%Ecto.Changeset{} = changeset) do
changeset
end
defp put_routing(%Ecto.Changeset{changes: %{to: to}} = changeset) do
identity = BirdyChat.Identity.identity()
if String.starts_with?(to, identity) do
changeset
|> Ecto.Changeset.put_change(:routing, :local)
|> Ecto.Changeset.put_change(:server, identity)
else
server =
BirdyChat.Identity.peers()
|> Enum.find(fn {name, _url} -> String.starts_with?(to, name) end)
case server do
{name, _url} ->
changeset
|> Ecto.Changeset.put_change(:routing, :remote)
|> Ecto.Changeset.put_change(:server, name)
nil ->
changeset
|> Ecto.Changeset.add_error(:server, "unknown 'to' server")
end
end
end
defp put_routing(%Ecto.Changeset{} = changeset) do
changeset
end
end

View file

@ -1,7 +1,5 @@
defmodule BirdyChat.MessageWriter do
@moduledoc """
Simple file writer that stores messages in priv folder of Elixir application/release.
"""
@moduledoc false
@spec write(%{to: String.t(), from: String.t(), message: String.t()}) :: :ok
def write(message) do

View file

@ -88,8 +88,8 @@ defmodule BirdyChatWeb do
import BirdyChatWeb.CoreComponents
# Common modules used in templates
alias BirdyChatWeb.Layouts
alias Phoenix.LiveView.JS
alias BirdyChatWeb.Layouts
# Routes generation with the ~p sigil
unquote(verified_routes())

View file

@ -1,23 +1,14 @@
defmodule BirdyChatWeb.Api.Messages.Controller do
@moduledoc """
The endpoint to be used by users from the "home server".
"""
use BirdyChatWeb, :controller
def create(conn, params) do
case BirdyChat.Message.validate(params) do
{:ok, changeset} ->
case BirdyChat.Dispatcher.dispatch(changeset) do
case BirdyChat.MessageWriter.write(changeset.changes) do
:ok ->
conn
|> put_status(:created)
|> render(:create, message: changeset.changes)
{:error, error} ->
conn
|> put_status(:unprocessable_entity)
|> render(:error, message: error)
end
{:error, changeset} ->

View file

@ -1,14 +1,8 @@
defmodule BirdyChatWeb.Api.Messages.JSON do
@moduledoc false
def render("create.json", %{message: message}) do
message
end
def render("error.json", %{message: message}) do
%{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}}
end
def render("error.json", %{changeset: changeset}) do
errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1)
%{errors: errors}

View file

@ -1,49 +0,0 @@
defmodule BirdyChatWeb.Api.Server.Internal.Controller do
@moduledoc """
A controller for handling inter-server communication. It started off with using Erlang term
format instead of JSON as communication language but then I removed it for the following
reasons:
1. The messages are mostly binaries anyway, there is no big efficiency gain from skipping JSON.
2. Testing JSON is much easier than testing erlang term format.
3. Erlang term format can give an illusion of extra security but unless the transport is HTTPS
then the communication is still inherently unsafe.
4. Erlang term format is difficult to handle for unfamiliar developers, you need to remember
about safe conversion to avoid atom exhaustion attacks or sending an `rm -rf /` function over
the wire.
The endpoint is protected by simple authentication that requires the secret key of all servers
being the same. It is good enough for a demo, but for any real application it would need to be
reconsidered. HTTPS would be a non-negotiable requirement for any user-facing deployment.
"""
use BirdyChatWeb, :controller
def create(conn, params) do
with true <- authorised?(conn.req_headers, params),
{:ok, changeset} <- BirdyChat.Message.validate_for_inter_server_use(params),
:ok <- BirdyChat.MessageWriter.write(changeset.changes) do
conn
|> put_status(:created)
|> render(:create, message: changeset.changes)
else
_any ->
conn
|> put_status(:forbidden)
|> render(:error, message: "Unauthorised")
end
end
defp authorised?(headers, %{"from" => from}) do
case Enum.find(headers, fn {key, _value} -> key == "authorization" end) do
nil ->
false
{"authorization", token} ->
case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 1200) do
{:ok, id} -> id == from
{:error, :invalid} -> false
end
end
end
end

View file

@ -1,20 +0,0 @@
defmodule BirdyChatWeb.Api.Server.Internal.JSON do
@moduledoc false
def render("create.json", %{message: message}) do
message
end
def render("error.json", %{message: message}) do
%{errors: %{"general" => Gettext.dgettext(BirdyChatWeb.Gettext, "errors", message, [])}}
end
def render("error.json", %{changeset: changeset}) do
errors = Ecto.Changeset.traverse_errors(changeset, &get_error/1)
%{errors: errors}
end
def get_error({msg, opts}) do
Gettext.dgettext(BirdyChatWeb.Gettext, "errors", msg, opts)
end
end

View file

@ -0,0 +1,35 @@
defmodule BirdyChatWeb.ServerChannel do
use BirdyChatWeb, :channel
@impl true
def join("server:" <> server_id, payload, socket) do
if authorised?(payload, server_id) do
{:ok, socket}
else
{:error, %{reason: "unauthorised"}}
end
end
@impl true
def handle_in("new_message", {:binary, message}, socket) do
result = :erlang.binary_to_term(message, [:safe])
# TODO: Add validation that this is the right server.
case BirdyChat.Message.validate(result) do
{:ok, changeset} ->
case BirdyChat.MessageWriter.write(changeset.changes) do
:ok ->
{:reply, {:ok, {:binary, message}}, socket}
end
end
end
# Simple token-based authentication. Servers should use the same Phoenix secret key so they will
# have the basis for generating tokens.
defp authorised?(%{"token" => token}, server_id) do
case Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 86400) do
{:ok, id} -> id == server_id
{:error, :invalid} -> false
end
end
end

View file

@ -0,0 +1,44 @@
defmodule BirdyChatWeb.ServerSocket do
use Phoenix.Socket
# A Socket handler
#
# It's possible to control the websocket connection and
# assign values that can be accessed by your channel topics.
## Channels
channel "server:*", BirdyChatWeb.ServerChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error` or `{:error, term}`. To control the
# response the client receives in that case, [define an error handler in the
# websocket
# configuration](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket IDs are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# Elixir.BirdyChatWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(socket), do: "user_socket:#{socket.assigns.server_id}"
end

View file

@ -29,7 +29,6 @@ defmodule BirdyChatWeb.CoreComponents do
use Phoenix.Component
use Gettext, backend: BirdyChatWeb.Gettext
alias Phoenix.HTML.Form
alias Phoenix.LiveView.JS
@doc """
@ -201,7 +200,9 @@ defmodule BirdyChatWeb.CoreComponents do
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn -> Form.normalize_value("checkbox", assigns[:value]) end)
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div class="fieldset mb-2">

View file

@ -0,0 +1,7 @@
defmodule BirdyChatWeb.PageController do
use BirdyChatWeb, :controller
def home(conn, _params) do
render(conn, :home)
end
end

View file

@ -0,0 +1,10 @@
defmodule BirdyChatWeb.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.
See the `page_html` directory for all templates available.
"""
use BirdyChatWeb, :html
embed_templates "page_html/*"
end

View file

@ -0,0 +1,202 @@
<Layouts.flash_group flash={@flash} />
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
<svg
viewBox="0 0 1480 957"
fill="none"
aria-hidden="true"
class="absolute inset-0 h-full w-full"
preserveAspectRatio="xMinYMid slice"
>
<path fill="#EE7868" d="M0 0h1480v957H0z" />
<path
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
fill="#FF9F92"
/>
<path
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
fill="#FA8372"
/>
<path
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
fill="#E96856"
fill-opacity=".6"
/>
<path
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
fill="#C42652"
fill-opacity=".2"
/>
<path
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
fill="#A41C42"
fill-opacity=".2"
/>
<path
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
fill="#A41C42"
fill-opacity=".2"
/>
</svg>
</div>
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
<div class="mx-auto max-w-xl lg:mx-0">
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
<div class="mt-10 flex justify-between items-center">
<h1 class="flex items-center text-sm font-semibold leading-6">
Phoenix Framework
<small class="badge badge-warning badge-sm ml-3">
v{Application.spec(:phoenix, :vsn)}
</small>
</h1>
<Layouts.theme_toggle />
</div>
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-balance">
Peace of mind from prototype to production.
</p>
<p class="mt-4 leading-7 text-base-content/70">
Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale.
</p>
<div class="flex">
<div class="w-full sm:w-auto">
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
<path d="m12 4 10-2v18l-10 2V4Z" fill="currentColor" fill-opacity=".15" />
<path
d="M12 4 2 2v18l10 2m0-18v18m0-18 10-2v18l-10 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Guides &amp; Docs
</span>
</a>
<a
href="https://github.com/phoenixframework/phoenix"
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" aria-hidden="true" class="h-6 w-6">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 0C5.37 0 0 5.506 0 12.303c0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291C6 21.67 5.22 20.346 4.98 19.654c-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489A12.536 12.536 0 0 0 24 12.304C24 5.505 18.63 0 12 0Z"
/>
</svg>
Source Code
</span>
</a>
<a
href={"https://github.com/phoenixframework/phoenix/blob/v#{Application.spec(:phoenix, :vsn)}/CHANGELOG.md"}
class="group relative rounded-box px-6 py-4 text-sm font-semibold leading-6 sm:py-6"
>
<span class="absolute inset-0 rounded-box bg-base-200 transition group-hover:bg-base-300 sm:group-hover:scale-105">
</span>
<span class="relative flex items-center gap-4 sm:flex-col">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" class="h-6 w-6">
<path
d="M12 1v6M12 17v6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle
cx="12"
cy="12"
r="4"
fill="currentColor"
fill-opacity=".15"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Changelog
</span>
</a>
</div>
<div class="mt-10 grid grid-cols-1 gap-y-4 text-sm leading-6 text-base-content/80 sm:grid-cols-2">
<div>
<a
href="https://elixirforum.com"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M8 13.833c3.866 0 7-2.873 7-6.416C15 3.873 11.866 1 8 1S1 3.873 1 7.417c0 1.081.292 2.1.808 2.995.606 1.05.806 2.399.086 3.375l-.208.283c-.285.386-.01.905.465.85.852-.098 2.048-.318 3.137-.81a3.717 3.717 0 0 1 1.91-.318c.263.027.53.041.802.041Z" />
</svg>
Discuss on the Elixir Forum
</a>
</div>
<div>
<a
href="https://discord.gg/elixir"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M13.545 2.995c-1.02-.46-2.114-.8-3.257-.994a.05.05 0 0 0-.052.024c-.141.246-.297.567-.406.82a12.377 12.377 0 0 0-3.658 0 8.238 8.238 0 0 0-.412-.82.052.052 0 0 0-.052-.024 13.315 13.315 0 0 0-3.257.994.046.046 0 0 0-.021.018C.356 6.063-.213 9.036.066 11.973c.001.015.01.029.02.038a13.353 13.353 0 0 0 3.996 1.987.052.052 0 0 0 .056-.018c.308-.414.582-.85.818-1.309a.05.05 0 0 0-.028-.069 8.808 8.808 0 0 1-1.248-.585.05.05 0 0 1-.005-.084c.084-.062.168-.126.248-.191a.05.05 0 0 1 .051-.007c2.619 1.176 5.454 1.176 8.041 0a.05.05 0 0 1 .053.006c.08.065.164.13.248.192a.05.05 0 0 1-.004.084c-.399.23-.813.423-1.249.585a.05.05 0 0 0-.027.07c.24.457.514.893.817 1.307a.051.051 0 0 0 .056.019 13.31 13.31 0 0 0 4.001-1.987.05.05 0 0 0 .021-.037c.334-3.396-.559-6.345-2.365-8.96a.04.04 0 0 0-.021-.02Zm-8.198 7.19c-.789 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.637 1.587-1.438 1.587Zm5.316 0c-.788 0-1.438-.712-1.438-1.587 0-.874.637-1.586 1.438-1.586.807 0 1.45.718 1.438 1.586 0 .875-.63 1.587-1.438 1.587Z" />
</svg>
Join our Discord server
</a>
</div>
<div>
<a
href="https://elixir-slack.community/"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M3.361 10.11a1.68 1.68 0 1 1-1.68-1.681h1.68v1.682ZM4.209 10.11a1.68 1.68 0 1 1 3.361 0v4.21a1.68 1.68 0 1 1-3.361 0v-4.21ZM5.89 3.361a1.68 1.68 0 1 1 1.681-1.68v1.68H5.89ZM5.89 4.209a1.68 1.68 0 1 1 0 3.361H1.68a1.68 1.68 0 1 1 0-3.361h4.21ZM12.639 5.89a1.68 1.68 0 1 1 1.68 1.681h-1.68V5.89ZM11.791 5.89a1.68 1.68 0 1 1-3.361 0V1.68a1.68 1.68 0 0 1 3.361 0v4.21ZM10.11 12.639a1.68 1.68 0 1 1-1.681 1.68v-1.68h1.682ZM10.11 11.791a1.68 1.68 0 1 1 0-3.361h4.21a1.68 1.68 0 1 1 0 3.361h-4.21Z" />
</svg>
Join us on Slack
</a>
</div>
<div>
<a
href="https://fly.io/docs/elixir/getting-started/"
class="group -mx-2 -my-0.5 inline-flex items-center gap-3 rounded-lg px-2 py-0.5 hover:bg-base-200 hover:text-base-content"
>
<svg
viewBox="0 0 20 20"
aria-hidden="true"
class="h-4 w-4 fill-base-content/40 group-hover:fill-base-content"
>
<path d="M1 12.5A4.5 4.5 0 005.5 17H15a4 4 0 001.866-7.539 3.504 3.504 0 00-4.504-4.272A4.5 4.5 0 004.06 8.235 4.502 4.502 0 001 12.5z" />
</svg>
Deploy your application
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -15,6 +15,10 @@ defmodule BirdyChatWeb.Endpoint do
websocket: [connect_info: [session: @session_options]],
longpoll: [connect_info: [session: @session_options]]
socket "/socket", BirdyChatWeb.ServerSocket,
websocket: true,
longpoll: false
# Serve at "/" the static files from "priv/static" directory.
#
# When code reloading is disabled (e.g., in production),

View file

@ -14,11 +14,16 @@ defmodule BirdyChatWeb.Router do
plug :accepts, ["json"]
end
scope "/", BirdyChatWeb do
pipe_through :browser
get "/", PageController, :home
end
scope "/api", BirdyChatWeb.Api do
pipe_through [:api]
post "/messages", Messages.Controller, :create
post "/internal", Server.Internal.Controller, :create
end
# Other scopes may use custom stacks.
@ -26,7 +31,7 @@ defmodule BirdyChatWeb.Router do
# pipe_through :api
# end
# Enable LiveDashboard in development
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:birdy_chat, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
@ -39,6 +44,7 @@ defmodule BirdyChatWeb.Router do
pipe_through :browser
live_dashboard "/dashboard", metrics: BirdyChatWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end

14
mix.exs
View file

@ -21,7 +21,7 @@ defmodule BirdyChat.MixProject do
def application do
[
mod: {BirdyChat.Application, []},
extra_applications: [:logger, :runtime_tools]
extra_applications: [:logger, :runtime_tools, :opentelemetry]
]
end
@ -56,6 +56,7 @@ defmodule BirdyChat.MixProject do
app: false,
compile: false,
depth: 1},
{:swoosh, "~> 1.16"},
{:req, "~> 0.5"},
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
@ -63,10 +64,14 @@ defmodule BirdyChat.MixProject do
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"},
{:credo, "~> 1.0", only: [:dev, :test]},
# Static analysis tools
{:credo, "~> 1.0", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}
# Phoenix sockets client
{:slipstream, "~> 1.0"},
# Telemetry
{:opentelemetry, "~> 1.0"},
{:opentelemetry_exporter, "~> 1.0"}
]
end
@ -86,7 +91,6 @@ defmodule BirdyChat.MixProject do
"esbuild birdy_chat --minify",
"phx.digest"
],
build_release: ["setup", "assets.deploy", "release"],
precommit: [
"compile --warnings-as-errors",
"credo --strict",

View file

@ -1,28 +1,37 @@
%{
"acceptor_pool": {:hex, :acceptor_pool, "1.0.1", "d88c2e8a0be9216cf513fbcd3e5a4beb36bee3ff4168e85d6152c6f899359cdb", [:rebar3], [], "hexpm", "f172f3d74513e8edd445c257d596fc84dbdd56d2c6fa287434269648ae5a421e"},
"bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
"credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
"ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mint_web_socket": {:hex, :mint_web_socket, "1.0.5", "60354efeb49b1eccf95dfb75f55b08d692e211970fe735a5eb3188b328be2a90", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "04b35663448fc758f3356cce4d6ac067ca418bbafe6972a3805df984b5f12e61"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"opentelemetry": {:hex, :opentelemetry, "1.7.0", "20d0f12d3d1c398d3670fd44fd1a7c495dd748ab3e5b692a7906662e2fb1a38a", [:rebar3], [{:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "a9173b058c4549bf824cbc2f1d2fa2adc5cdedc22aa3f0f826951187bbd53131"},
"opentelemetry_api": {:hex, :opentelemetry_api, "1.5.0", "1a676f3e3340cab81c763e939a42e11a70c22863f645aa06aafefc689b5550cf", [:mix, :rebar3], [], "hexpm", "f53ec8a1337ae4a487d43ac89da4bd3a3c99ddf576655d071deed8b56a2d5dda"},
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.10.0", "972e142392dbfa679ec959914664adefea38399e4f56ceba5c473e1cabdbad79", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.7.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "33a116ed7304cb91783f779dec02478f887c87988077bfd72840f760b8d4b952"},
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
@ -34,11 +43,16 @@
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"slipstream": {:hex, :slipstream, "1.2.2", "6b07124ac5f62a50327aa38c84edd0284920ac8aba548e04738827838f233ed0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ccb873ddb21aadb37c5c7745014febe6da0aa2cef0c4e73e7d08ce11d18aacd0"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"swoosh": {:hex, :swoosh, "1.22.0", "0d65a95f89aedb5011af13295742294e309b4b4aaca556858d81e3b372b58abc", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c01ced23d8786d1ee1a03e4c16574290b2ccd6267beb8c81d081c4a34574ef6e"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.31.0", "9a910b54d8cb96cc810cabf4c0129f21360f82022b20180849f1442a25ccbb04", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "9d2b41b128d5507bd8ad93e1a998e06d0ab2f9a772af343f4c00bf76c6be1532"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
}

View file

@ -1 +0,0 @@
This folder is used for storing messages local to this server. The contents are otherwise ignored.

View file

@ -1,12 +0,0 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t
export BIRDY_CHAT_PORT=4001
export BIRDY_CHAT_IDENTITY=1
export BIRDY_CHAT_PEERS="2::http://localhost:4002;3::http://localhost:4003"
export PHX_SERVER=true
export RELEASE_NAME=server_1
exec ./birdy_chat start

View file

@ -1,12 +0,0 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t
export BIRDY_CHAT_PORT=4002
export BIRDY_CHAT_IDENTITY=2
export BIRDY_CHAT_PEERS="1::http://localhost:4001g;3::http://localhost:4003"
export PHX_SERVER=true
export RELEASE_NAME=server_2
exec ./birdy_chat start

View file

@ -1,12 +0,0 @@
#!/bin/sh
set -eu
cd -P -- "$(dirname -- "$0")"
export SECRET_KEY_BASE=Yhmq6FzYQt4g5AFHfSdMBKKf4oRo4KRo703FK6b7RwmH5pXlyQNompUOF7/EEC5t
export BIRDY_CHAT_PORT=4003
export BIRDY_CHAT_IDENTITY=3
export BIRDY_CHAT_PEERS="1::http://localhost:4001;2::http://localhost:4002"
export PHX_SERVER=true
export RELEASE_NAME=server_3
exec ./birdy_chat start

View file

@ -1,5 +1,5 @@
defmodule BirdyChat.IdentityTest do
use BirdyChat.DataCase, async: true
use BirdyChat.DataCase
setup do
registry = start_supervised!({Registry, keys: :unique, name: __MODULE__})
@ -11,7 +11,7 @@ defmodule BirdyChat.IdentityTest do
name = {:via, Registry, {__MODULE__, "identity"}}
{:ok, process} = BirdyChat.Identity.start_link(name: name)
mode = BirdyChat.Identity.mode(process)
assert mode == :test
assert mode == :singleton
end
test "can be started by giving values like environment variables" do

View file

@ -1,3 +0,0 @@
defmodule BirdyChat.PeerTest do
use BirdyChat.DataCase, async: true
end

View file

@ -1,82 +0,0 @@
defmodule BirdyChatWeb.Api.Internal.MessagesTest do
use BirdyChatWeb.ConnCase, async: true
setup do
url = ~p"/api/internal"
%{url: url}
end
describe "authorisation" do
setup %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
%{conn: conn}
end
test "returns 403 when authorisation token is missing", %{url: url, conn: conn} do
message = %{"from" => "test2-user", "to" => "test2-someone", "message" => "123"}
body = Jason.encode!(message)
conn = post(conn, url, body)
assert result = json_response(conn, :forbidden)
assert result == %{"errors" => %{"general" => "Unauthorised"}}
end
test "returns 403 when authorisation token is invalid", %{url: url, conn: conn} do
server_id = "test2-user"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "FakeAuth", server_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", token)
message = %{"from" => "test2-user", "to" => "test2-someone", "message" => "123"}
body = Jason.encode!(message)
conn = post(conn, url, body)
assert result = json_response(conn, :forbidden)
assert result == %{"errors" => %{"general" => "Unauthorised"}}
end
end
describe "POST /api/server/messages" do
setup %{conn: conn} do
unique_user_id = System.unique_integer([:positive])
username = "test1-user#{unique_user_id}"
path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{username}.txt"])
on_exit(fn -> File.rm(path) end)
server_id = "test2-user"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", token)
%{conn: conn, username: username, path: path}
end
test "saves message locally", %{conn: conn, username: username, url: url, path: path} do
message = %{"from" => "test2-user", "to" => username, "message" => "123"}
body = Jason.encode!(message)
conn = post(conn, url, body)
assert result = json_response(conn, :created)
expected_result = %{
"from" => "test2-user",
"message" => "123",
"to" => username,
"routing" => "local",
"server" => "test1"
}
assert result == expected_result
contents = File.read!(path)
assert contents == "test2-user: 123\n"
end
end
end

View file

@ -1,81 +1,17 @@
defmodule BirdyChatWeb.Api.MessagesTest do
use BirdyChatWeb.ConnCase, async: true
use BirdyChatWeb.ConnCase
setup %{conn: conn} do
url = ~p"/api/messages"
unique_user_id = System.unique_integer([:positive])
username = "test1-user#{unique_user_id}"
path = Application.app_dir(:birdy_chat, ["priv", "messages", "#{username}.txt"])
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
on_exit(fn -> File.rm(path) end)
conn =
conn
|> put_req_header("content-type", "application/json")
%{conn: conn, url: url, username: username, path: path}
end
describe "POST /api/messages to other server" do
test "returns error when the peer is down", %{conn: conn, url: url} do
message = %{from: "test1-user", to: "test2-user", message: "123"}
Req.Test.expect(BirdyChat.Dispatcher, &Req.Test.transport_error(&1, :econnrefused))
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :unprocessable_entity)
assert result == %{"errors" => %{"general" => "peer test2 is unreachable"}}
end
test "returns error when a peer is unknown", %{conn: conn, url: url} do
message = %{from: "test1-user", to: "fake-user", message: "123"}
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :unprocessable_entity)
expected_result = %{"errors" => %{"server" => ["unknown 'to' server"]}}
assert result == expected_result
end
test "send message to the other server via HTTP", %{conn: conn, url: url} do
message = %{from: "test1-user", to: "test2-user", message: "123"}
string_message = %{
"from" => "test1-user",
"message" => "123",
"to" => "test2-user",
"routing" => "remote",
"server" => "test2"
}
Req.Test.expect(BirdyChat.Dispatcher, fn conn ->
expected_body_params = %{
"from" => "test1-user",
"message" => "123",
"routing" => "remote",
"server" => "test2",
"to" => "test2-user"
}
{"authorization", token} =
Enum.find(conn.req_headers, fn {key, _v} -> key == "authorization" end)
{:ok, "test1-user"} =
Phoenix.Token.verify(BirdyChatWeb.Endpoint, "serverAuth", token, max_age: 1200)
assert conn.body_params == expected_body_params
resp = Jason.encode!(expected_body_params)
Plug.Conn.send_resp(conn, :created, resp)
end)
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :created)
assert result == string_message
end
%{conn: conn, url: url}
end
describe "POST /api/messages" do
@ -95,70 +31,39 @@ defmodule BirdyChatWeb.Api.MessagesTest do
assert result == expected_result
end
test "returns error when you post a message to other server", %{conn: conn, url: url} do
message = %{"from" => "nothomeserver-user", "to" => "test1-user", "message" => "123"}
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :unprocessable_entity)
expected_result = %{
"errors" => %{"from" => ["you can only communicate with your home server"]}
}
assert result == expected_result
end
test "returns message and 201 when successful", %{conn: conn, url: url, username: username} do
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
test "returns message and 201 when successful", %{conn: conn, url: url} do
message = %{"from" => "2-user", "to" => "1-user", "message" => "123"}
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :created)
expected_result = %{
"from" => "test1-user",
"to" => username,
"message" => "123",
"routing" => "local",
"server" => "test1"
}
assert result == expected_result
assert result == message
end
test "writes message to file", %{conn: conn, url: url, username: username, path: path} do
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
test "writes message to file", %{conn: conn, url: url} do
message = %{"from" => "2-user", "to" => "test-user", "message" => "123"}
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert result = json_response(conn, :created)
assert result == message
expected_result = %{
"from" => "test1-user",
"to" => username,
"message" => "123",
"routing" => "local",
"server" => "test1"
}
assert result == expected_result
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
contents = File.read!(path)
assert contents == "test1-user: 123\n"
assert contents == "2-user: 123\n"
end
test "appends message to file", %{conn: conn, url: url, username: username, path: path} do
message = %{"from" => "test1-user", "to" => username, "message" => "123"}
test "appends message to file", %{conn: conn, url: url} do
message = %{"from" => "2-user", "to" => "test-user", "message" => "123"}
payload = Jason.encode!(message)
post(conn, url, payload)
message = %{"from" => "test1-user", "to" => username, "message" => "456"}
message = %{"from" => "2-user", "to" => "test-user", "message" => "456"}
payload = Jason.encode!(message)
conn = post(conn, url, payload)
assert json_response(conn, :created)
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
contents = File.read!(path)
assert contents == "test1-user: 123\ntest1-user: 456\n"
assert contents == "2-user: 123\n2-user: 456\n"
end
end
end

View file

@ -0,0 +1,81 @@
defmodule BirdyChatWeb.ServerChannelTest do
use BirdyChatWeb.ChannelCase
setup do
server_id = "test2"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id)
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
on_exit(fn -> File.rm(path) end)
{:ok, _, socket} =
BirdyChatWeb.ServerSocket
|> socket("server_socket", %{server_id: server_id})
|> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:#{server_id}", %{token: token})
%{socket: socket}
end
describe "authorization" do
test "requires a signed token to join a channel" do
server_id = "test2"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "fakeAuth", server_id)
result =
BirdyChatWeb.ServerSocket
|> socket("server_socket", %{server_id: server_id})
|> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:#{server_id}", %{token: token})
assert result == {:error, %{reason: "unauthorised"}}
end
test "requires server id in token and in channel to match" do
server_id = "test2"
token = Phoenix.Token.sign(BirdyChatWeb.Endpoint, "serverAuth", server_id)
result =
BirdyChatWeb.ServerSocket
|> socket("server_socket", %{server_id: server_id})
|> subscribe_and_join(BirdyChatWeb.ServerChannel, "server:fake", %{token: token})
assert result == {:error, %{reason: "unauthorised"}}
end
end
describe "new_message" do
test "receives message from other server and saves it locally", %{socket: socket} do
message = %{from: "test2-user", to: "test-user", message: "123"}
bin = :erlang.term_to_binary(message)
ref = push(socket, "new_message", {:binary, bin})
assert_reply ref, :ok, {:binary, ^bin}
path = Application.app_dir(:birdy_chat, ["priv", "messages", "test-user.txt"])
contents = File.read!(path)
assert contents == "test2-user: 123\n"
end
test "blows up when sent payload is not safe to decode", %{socket: socket} do
# This unsafe binary is taken from erlang docs directly:
bin = <<131, 119, 8, "tjenixen">>
Process.flag(:trap_exit, true)
push(socket, "new_message", {:binary, bin})
assert_receive {:EXIT, _pid,
{:badarg,
[
{:erlang, :binary_to_term,
[<<131, 119, 8, 116, 106, 101, 110, 105, 120, 101, 110>>, [:safe]],
[error_info: %{module: :erl_erts_errors}]},
{BirdyChatWeb.ServerChannel, :handle_in, 3,
[file: ~c"lib/birdy_chat_web/channels/server_channel.ex", line: 15]},
{Phoenix.Channel.Server, :handle_info, 2,
[file: ~c"lib/phoenix/channel/server.ex", line: 332]},
{:gen_server, :try_handle_info, 3,
[file: ~c"gen_server.erl", line: 2434]},
{:gen_server, :handle_msg, 3, [file: ~c"gen_server.erl", line: 2420]},
{:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 333]}
]}},
100
end
end
end

View file

@ -0,0 +1,8 @@
defmodule BirdyChatWeb.PageControllerTest do
use BirdyChatWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Peace of mind from prototype to production"
end
end

View file

@ -0,0 +1,34 @@
defmodule BirdyChatWeb.ChannelCase do
@moduledoc """
This module defines the test case to be used by
channel tests.
Such tests rely on `Phoenix.ChannelTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use BirdyChatWeb.ChannelCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with channels
import Phoenix.ChannelTest
import BirdyChatWeb.ChannelCase
# The default endpoint for testing
@endpoint BirdyChatWeb.Endpoint
end
end
setup _tags do
:ok
end
end