dev vouch dev on at. thats about it
0
fork

Configure Feed

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

add basics of an appview

Luna 8f9e2744 dcdd86a6

+689
+4
appview/.gitignore
··· 1 + _build/ 2 + deps/ 3 + *.db* 4 + .env*
+20
appview/config/config.exs
··· 1 + import Config 2 + 3 + config :atvouch, 4 + auth_url: "http://localhost:8087", 5 + atvouch_endpoint: "https://atvouch.dev" 6 + 7 + config :logger, 8 + level: :info, 9 + format: "$time $metadata[$level] $message\n" 10 + 11 + config :atvouch, Atvouch.Repo, 12 + database: "atvouch_#{config_env()}.db", 13 + pool_size: 1 14 + 15 + config :atvouch, 16 + ecto_repos: [Atvouch.Repo] 17 + 18 + config :tesla, adapter: Tesla.Adapter.Mint 19 + 20 + import_config "#{config_env()}.exs"
+10
appview/config/dev.exs
··· 1 + import Config 2 + 3 + config :atvouch, 4 + port: 4000, 5 + atvouch_did: "did:plc:aa", 6 + atvouch_endpoint: "https://localhost:4000" 7 + 8 + config :logger, 9 + level: :debug, 10 + truncate: :infinity
+10
appview/config/prod.exs
··· 1 + import Config 2 + 3 + config :atvouch, 4 + port: 4000, 5 + atvouch_did: "did:web:api.atvouch.dev", 6 + atvouch_endpoint: "https://api.atvouch.dev" 7 + 8 + config :logger, 9 + level: :info, 10 + truncate: :infinity
+17
appview/config/runtime.exs
··· 1 + import Config 2 + 3 + if tap_uri = System.get_env("TAP_URI") do 4 + config :atvouch, :tap, 5 + uri: tap_uri, 6 + password: System.get_env("TAP_PASSWORD") 7 + end 8 + 9 + if config_env() == :prod do 10 + config :atvouch, 11 + port: System.fetch_env!("PORT"), 12 + atvouch_did: System.fetch_env!("ATVOUCH_DID"), 13 + atvouch_endpoint: System.fetch_env!("ATVOUCH_ENDPOINT"), 14 + metrics_token: System.fetch_env!("ATVOUCH_METRICS_TOKEN") 15 + 16 + config :atvouch, Atvouch.Repo, database: System.fetch_env!("ATVOUCH_DB_PATH") 17 + end
+16
appview/config/test.exs
··· 1 + import Config 2 + 3 + config :atvouch, 4 + port: 4002, 5 + atvouch_did: "did:plc:test", 6 + atvouch_endpoint: "https://localhost:4002" 7 + 8 + config :atvouch, Atvouch.Repo, 9 + pool: Ecto.Adapters.SQL.Sandbox 10 + 11 + config :atvouch, :tap, 12 + uri: "ws://localhost:0/channel", 13 + password: "123" 14 + 15 + config :logger, 16 + level: :warning
+33
appview/lib/atvouch/application.ex
··· 1 + defmodule Atvouch.Application do 2 + use Application 3 + 4 + @impl true 5 + def start(_type, _args) do 6 + port = Application.get_env(:atvouch, :port) 7 + 8 + children = 9 + [ 10 + Atvouch.Repo, 11 + {Bandit, plug: Atvouch.Router, port: port, ip: {127, 0, 0, 1}} 12 + ] ++ tap_children() 13 + 14 + 15 + opts = [strategy: :one_for_one, name: Atvouch.Supervisor] 16 + Supervisor.start_link(children, opts) 17 + end 18 + 19 + defp tap_children do 20 + case Application.get_env(:atvouch, :tap) do 21 + nil -> 22 + [] 23 + 24 + config -> 25 + [ 26 + {Atvouch.Tap.Socket, 27 + uri: Keyword.fetch!(config, :uri), 28 + handler: Atvouch.TapHandler, 29 + password: Keyword.get(config, :password)} 30 + ] 31 + end 32 + end 33 + end
+18
appview/lib/atvouch/auth_client.ex
··· 1 + defmodule Atvouch.AuthClient do 2 + def new(url) do 3 + middleware = [ 4 + {Tesla.Middleware.BaseUrl, url}, 5 + Tesla.Middleware.JSON 6 + ] 7 + 8 + Tesla.client(middleware) 9 + end 10 + 11 + def get_user(client, token) do 12 + Tesla.get(client, "/user", headers: [{"Authorization", "Bearer #{token}"}]) 13 + end 14 + 15 + def invalidate_cache(client, did) do 16 + Tesla.post(client, "/invalidate_cache", %{did: did}) 17 + end 18 + end
+57
appview/lib/atvouch/plugs/did_auth.ex
··· 1 + defmodule Atvouch.Plugs.DIDAuth do 2 + import Plug.Conn 3 + require Logger 4 + 5 + def init(_) do 6 + %{} 7 + end 8 + 9 + def call(conn, config) do 10 + case get_req_header(conn, "authorization") do 11 + [] -> 12 + # No authorization header, continue without authentication 13 + conn 14 + 15 + [auth_header] -> 16 + authenticate_request(conn, auth_header, config) 17 + end 18 + end 19 + 20 + defp auth_fetch(client, token) do 21 + with {:ok, r} <- Atvouch.AuthClient.get_user(client, token), 22 + %Tesla.Env{status: 200, body: body} <- r do 23 + {:ok, {body["did"], body["aud"]}} 24 + else 25 + v -> {:error, v} 26 + end 27 + end 28 + 29 + defp authenticate_request(conn, auth_header, %{}) do 30 + auth_url = Application.fetch_env!(:atvouch, :auth_url) 31 + client = Atvouch.AuthClient.new(auth_url) 32 + 33 + valid_audiences = [ 34 + Application.get_env(:atvouch, :atvouch_did) 35 + ] 36 + 37 + with ["Bearer", token] <- String.split(auth_header, " ", parts: 2), 38 + {:ok, {did, aud}} <- auth_fetch(client, token), 39 + {:did_non_empty, true} <- {:did_non_empty, did != ""}, 40 + {:aud_non_empty, true} <- {:aud_non_empty, aud != ""}, 41 + {:did_not_nil, true} <- {:did_not_nil, did != nil}, 42 + {:aud_not_nil, true} <- {:aud_not_nil, aud != nil}, 43 + {:valid_aud, aud, true} <- {:valid_aud, aud, aud in valid_audiences} do 44 + conn 45 + |> assign(:user_did, did) 46 + |> assign(:user_aud, aud) 47 + else 48 + error -> 49 + Logger.error("Authentication failed: #{inspect(error)}") 50 + 51 + conn 52 + |> put_resp_content_type("application/json") 53 + |> send_resp(:unauthorized, Jason.encode!(%{error: "Invalid authorization"})) 54 + |> halt() 55 + end 56 + end 57 + end
+20
appview/lib/atvouch/repo.ex
··· 1 + defmodule Atvouch.Repo do 2 + use Ecto.Repo, 3 + otp_app: :atvouch, 4 + adapter: Ecto.Adapters.SQLite3 5 + 6 + def init(_type, config) do 7 + config = 8 + config 9 + |> Keyword.put_new(:after_connect, fn conn -> 10 + Exqlite.Sqlite3.execute(conn, "PRAGMA journal_mode = WAL") 11 + Exqlite.Sqlite3.execute(conn, "PRAGMA busy_timeout = 5000") 12 + Exqlite.Sqlite3.execute(conn, "PRAGMA synchronous = NORMAL") 13 + Exqlite.Sqlite3.execute(conn, "PRAGMA cache_size = -6000") 14 + Exqlite.Sqlite3.execute(conn, "PRAGMA foreign_keys = true") 15 + Exqlite.Sqlite3.execute(conn, "PRAGMA temp_store = memory") 16 + end) 17 + 18 + {:ok, config} 19 + end 20 + end
+71
appview/lib/atvouch/router.ex
··· 1 + defmodule Atvouch.Router do 2 + use Plug.Router 3 + use Plug.ErrorHandler 4 + 5 + plug(Plug.Logger, log: :info) 6 + plug(:match) 7 + 8 + plug(Plug.Parsers, 9 + parsers: [:json, :urlencoded], 10 + pass: ["*/*"], 11 + json_decoder: Jason 12 + ) 13 + 14 + plug(:dispatch) 15 + 16 + get "/.well-known/did.json" do 17 + atvouch_did = Application.get_env(:atvouch, :atvouch_did) 18 + atvouch_endpoint = Application.get_env(:atvouch, :atvouch_endpoint) 19 + 20 + did_document = %{ 21 + "@context" => ["https://www.w3.org/ns/did/v1"], 22 + "id" => atvouch_did, 23 + "service" => [ 24 + %{ 25 + "id" => "#atvouch_appview", 26 + "type" => "AtVouchAppview", 27 + "serviceEndpoint" => atvouch_endpoint 28 + } 29 + ] 30 + } 31 + 32 + conn 33 + |> put_resp_content_type("application/json") 34 + |> send_resp(200, Jason.encode!(did_document)) 35 + end 36 + 37 + get "/health" do 38 + conn 39 + |> put_resp_content_type("application/json") 40 + |> send_resp(200, Jason.encode!(%{status: "ok"})) 41 + end 42 + 43 + get "/metrics" do 44 + case get_req_header(conn, "authorization") do 45 + [] -> 46 + conn |> send_resp(401, "Unauthorized") |> halt() 47 + 48 + ["Bearer " <> token] -> 49 + metrics_token = Application.get_env(:atvouch, :metrics_token) 50 + 51 + if metrics_token != nil and token == metrics_token do 52 + metrics = :prometheus_text_format.format() 53 + 54 + conn 55 + |> put_resp_content_type("text/plain; version=0.0.4; charset=utf-8") 56 + |> send_resp(200, metrics) 57 + else 58 + conn |> send_resp(403, "Invalid metrics token") |> halt() 59 + end 60 + 61 + _ -> 62 + conn |> send_resp(403, "Invalid metrics token") |> halt() 63 + end 64 + end 65 + 66 + forward("/xrpc", to: Atvouch.XrpcRouter) 67 + 68 + match _ do 69 + conn |> send_resp(404, "Not found") 70 + end 71 + end
+33
appview/lib/atvouch/tap/client.ex
··· 1 + defmodule Atvouch.Tap.Client do 2 + def new(url, password \\ nil) do 3 + middleware = [ 4 + {Tesla.Middleware.BaseUrl, url}, 5 + Tesla.Middleware.JSON 6 + ] 7 + 8 + middleware = 9 + if password do 10 + [{Tesla.Middleware.BasicAuth, username: "admin", password: password} | middleware] 11 + else 12 + middleware 13 + end 14 + 15 + Tesla.client(middleware) 16 + end 17 + 18 + def add_repos(client, dids) when is_list(dids) do 19 + Tesla.post(client, "/repos/add", %{"dids" => dids}) 20 + end 21 + 22 + def remove_repos(client, dids) when is_list(dids) do 23 + Tesla.post(client, "/repos/remove", %{"dids" => dids}) 24 + end 25 + 26 + def resolve_did(client, did) do 27 + Tesla.get(client, "/resolve/#{did}") 28 + end 29 + 30 + def get_repo_info(client, did) do 31 + Tesla.get(client, "/info/#{did}") 32 + end 33 + end
+67
appview/lib/atvouch/tap/event.ex
··· 1 + defmodule Atvouch.Tap.Event do 2 + defmodule Record do 3 + @type t :: %__MODULE__{ 4 + id: integer(), 5 + action: :create | :update | :delete, 6 + did: String.t(), 7 + rev: String.t(), 8 + collection: String.t(), 9 + rkey: String.t(), 10 + record: map() | nil, 11 + cid: String.t() | nil, 12 + live: boolean() 13 + } 14 + 15 + defstruct [:id, :action, :did, :rev, :collection, :rkey, :record, :cid, live: false] 16 + end 17 + 18 + defmodule Identity do 19 + @type t :: %__MODULE__{ 20 + id: integer(), 21 + did: String.t(), 22 + handle: String.t(), 23 + is_active: boolean(), 24 + status: String.t() 25 + } 26 + 27 + defstruct [:id, :did, :handle, :status, is_active: true] 28 + end 29 + 30 + @action_map %{"create" => :create, "update" => :update, "delete" => :delete} 31 + 32 + @spec parse(map()) :: {:ok, Record.t() | Identity.t()} | {:error, term()} 33 + def parse(%{"type" => "record", "record" => data} = msg) do 34 + action = Map.get(@action_map, data["action"]) 35 + 36 + if is_nil(action) do 37 + {:error, {:unknown_action, msg}} 38 + else 39 + {:ok, 40 + %Record{ 41 + id: msg["id"], 42 + action: action, 43 + did: data["did"], 44 + rev: data["rev"], 45 + collection: data["collection"], 46 + rkey: data["rkey"], 47 + record: data["record"], 48 + cid: data["cid"], 49 + live: data["live"] == true 50 + }} 51 + end 52 + end 53 + 54 + def parse(%{"type" => "identity", "identity" => data} = msg) when is_map(data) do 55 + {:ok, 56 + %Identity{ 57 + id: msg["id"], 58 + did: data["did"], 59 + handle: data["handle"], 60 + is_active: data["is_active"] == true, 61 + status: data["status"] 62 + }} 63 + end 64 + 65 + def parse(%{"type" => type}), do: {:error, {:unknown_type, type}} 66 + def parse(_), do: {:error, :missing_type} 67 + end
+8
appview/lib/atvouch/tap/handler.ex
··· 1 + defmodule Atvouch.Tap.Handler do 2 + @callback handle_record(Atvouch.Tap.Event.Record.t()) :: :ok | {:error, term()} 3 + @callback handle_identity(Atvouch.Tap.Event.Identity.t()) :: :ok | {:error, term()} 4 + @callback handle_connect() :: :ok 5 + @callback handle_disconnect(reason :: term()) :: :ok 6 + 7 + @optional_callbacks handle_connect: 0, handle_disconnect: 1 8 + end
+191
appview/lib/atvouch/tap/socket.ex
··· 1 + defmodule Atvouch.Tap.Socket do 2 + use GenServer 3 + require Logger 4 + 5 + @backoff_steps [1_000, 2_000, 5_000, 10_000, 30_000] 6 + @ping_interval 30_000 7 + 8 + defstruct [ 9 + :uri, 10 + :handler, 11 + :password, 12 + :host, 13 + :port, 14 + :path, 15 + :transport, 16 + :conn_pid, 17 + :stream_ref, 18 + backoff_index: 0 19 + ] 20 + 21 + def start_link(opts) do 22 + GenServer.start_link(__MODULE__, opts, name: opts[:name] || __MODULE__) 23 + end 24 + 25 + @impl true 26 + def init(opts) do 27 + uri = URI.parse(opts[:uri]) 28 + 29 + {transport, default_port} = 30 + case uri.scheme do 31 + "wss" -> {:tls, 443} 32 + _ -> {:tcp, 80} 33 + end 34 + 35 + state = %__MODULE__{ 36 + uri: opts[:uri], 37 + handler: opts[:handler], 38 + password: opts[:password], 39 + host: uri.host, 40 + port: uri.port || default_port, 41 + path: uri.path || "/", 42 + transport: transport 43 + } 44 + 45 + send(self(), :connect) 46 + {:ok, state} 47 + end 48 + 49 + @impl true 50 + def handle_info(:connect, state) do 51 + gun_opts = %{ 52 + protocols: [:http], 53 + transport: state.transport, 54 + tls_opts: [verify: :verify_peer, cacerts: :public_key.cacerts_get()] 55 + } 56 + 57 + case :gun.open(String.to_charlist(state.host), state.port, gun_opts) do 58 + {:ok, conn_pid} -> 59 + Process.monitor(conn_pid) 60 + {:noreply, %{state | conn_pid: conn_pid}} 61 + 62 + {:error, reason} -> 63 + Logger.error("Tap socket: failed to connect: #{inspect(reason)}") 64 + schedule_reconnect(state) 65 + end 66 + end 67 + 68 + def handle_info({:gun_up, conn_pid, :http}, %{conn_pid: conn_pid} = state) do 69 + headers = 70 + if state.password do 71 + encoded = Base.encode64("admin:#{state.password}") 72 + [{<<"authorization">>, <<"Basic #{encoded}">>}] 73 + else 74 + [] 75 + end 76 + 77 + stream_ref = :gun.ws_upgrade(conn_pid, state.path, headers) 78 + {:noreply, %{state | stream_ref: stream_ref}} 79 + end 80 + 81 + def handle_info({:gun_upgrade, conn_pid, stream_ref, [<<"websocket">>], _headers}, %{conn_pid: conn_pid, stream_ref: stream_ref} = state) do 82 + Logger.info("Tap socket: connected to #{state.uri}") 83 + notify_connect(state.handler) 84 + schedule_ping() 85 + {:noreply, %{state | backoff_index: 0}} 86 + end 87 + 88 + def handle_info({:gun_response, conn_pid, _stream_ref, _fin, status, _headers}, %{conn_pid: conn_pid} = state) do 89 + Logger.error("Tap socket: upgrade rejected with status #{status}") 90 + :gun.close(conn_pid) 91 + schedule_reconnect(state) 92 + end 93 + 94 + def handle_info({:gun_ws, conn_pid, _stream_ref, {:text, msg}}, %{conn_pid: conn_pid} = state) do 95 + case Jason.decode(msg) do 96 + {:ok, decoded} -> 97 + handle_message(decoded, state) 98 + 99 + {:error, reason} -> 100 + Logger.warning("Tap socket: failed to decode message: #{inspect(reason)}") 101 + end 102 + 103 + {:noreply, state} 104 + end 105 + 106 + def handle_info({:gun_ws, _conn_pid, _stream_ref, :pong}, state) do 107 + {:noreply, state} 108 + end 109 + 110 + def handle_info({:gun_ws, conn_pid, _stream_ref, {:close, code, reason}}, %{conn_pid: conn_pid} = state) do 111 + Logger.warning("Tap socket: closed code=#{code} reason=#{reason}") 112 + notify_disconnect(state.handler, {:closed, code, reason}) 113 + schedule_reconnect(state) 114 + end 115 + 116 + def handle_info({:gun_down, conn_pid, :http, reason, _}, %{conn_pid: conn_pid} = state) do 117 + Logger.warning("Tap socket: connection down: #{inspect(reason)}") 118 + notify_disconnect(state.handler, reason) 119 + schedule_reconnect(state) 120 + end 121 + 122 + def handle_info({:DOWN, _ref, :process, conn_pid, reason}, %{conn_pid: conn_pid} = state) do 123 + Logger.warning("Tap socket: gun process exited: #{inspect(reason)}") 124 + notify_disconnect(state.handler, reason) 125 + schedule_reconnect(state) 126 + end 127 + 128 + def handle_info(:ping, %{conn_pid: conn_pid, stream_ref: stream_ref} = state) when not is_nil(stream_ref) do 129 + :gun.ws_send(conn_pid, stream_ref, :ping) 130 + schedule_ping() 131 + {:noreply, state} 132 + end 133 + 134 + def handle_info(:ping, state) do 135 + {:noreply, state} 136 + end 137 + 138 + def handle_info(msg, state) do 139 + Logger.debug("Tap socket: unhandled message: #{inspect(msg)}") 140 + {:noreply, state} 141 + end 142 + 143 + defp handle_message(%{"type" => type} = msg, state) when type in ["record", "identity"] do 144 + case Atvouch.Tap.Event.parse(msg) do 145 + {:ok, %Atvouch.Tap.Event.Record{} = event} -> 146 + case state.handler.handle_record(event) do 147 + :ok -> send_ack(state, event.id) 148 + {:error, reason} -> Logger.warning("Tap handler error on record #{event.id}: #{inspect(reason)}") 149 + end 150 + 151 + {:ok, %Atvouch.Tap.Event.Identity{} = event} -> 152 + case state.handler.handle_identity(event) do 153 + :ok -> send_ack(state, event.id) 154 + {:error, reason} -> Logger.warning("Tap handler error on identity #{event.id}: #{inspect(reason)}") 155 + end 156 + 157 + {:error, reason} -> 158 + Logger.warning("Tap socket: failed to parse event: #{inspect(reason)}") 159 + end 160 + end 161 + 162 + defp handle_message(_msg, _state), do: :ok 163 + 164 + defp send_ack(%{conn_pid: conn_pid, stream_ref: stream_ref}, id) do 165 + payload = Jason.encode!(%{"type" => "ack", "id" => id}) 166 + :gun.ws_send(conn_pid, stream_ref, {:text, payload}) 167 + end 168 + 169 + defp schedule_reconnect(%{backoff_index: index} = state) do 170 + delay = Enum.at(@backoff_steps, min(index, length(@backoff_steps) - 1)) 171 + Logger.info("Tap socket: reconnecting in #{delay}ms") 172 + Process.send_after(self(), :connect, delay) 173 + {:noreply, %{state | conn_pid: nil, stream_ref: nil, backoff_index: index + 1}} 174 + end 175 + 176 + defp schedule_ping do 177 + Process.send_after(self(), :ping, @ping_interval) 178 + end 179 + 180 + defp notify_connect(handler) do 181 + if function_exported?(handler, :handle_connect, 0) do 182 + handler.handle_connect() 183 + end 184 + end 185 + 186 + defp notify_disconnect(handler, reason) do 187 + if function_exported?(handler, :handle_disconnect, 1) do 188 + handler.handle_disconnect(reason) 189 + end 190 + end 191 + end
+16
appview/lib/atvouch/tap_handler.ex
··· 1 + defmodule Atvouch.TapHandler do 2 + @behaviour Atvouch.Tap.Handler 3 + require Logger 4 + 5 + @impl true 6 + def handle_record(event) do 7 + Logger.info("tap record: #{event.action} #{event.collection}/#{event.rkey} from #{event.did}") 8 + {:error, :skip} 9 + end 10 + 11 + @impl true 12 + def handle_identity(event) do 13 + Logger.info("tap identity: #{event.did} handle=#{event.handle} active=#{event.is_active}") 14 + {:error, :skip} 15 + end 16 + end
+19
appview/lib/atvouch/xrpc_router.ex
··· 1 + defmodule Atvouch.XrpcRouter do 2 + use Plug.Router 3 + 4 + plug(Atvouch.Plugs.DIDAuth) 5 + plug(:match) 6 + plug(:dispatch) 7 + 8 + get "/dev.atvouch.alive" do 9 + conn 10 + |> put_resp_content_type("application/json") 11 + |> send_resp(200, Jason.encode!(%{alive: true})) 12 + end 13 + 14 + match _ do 15 + conn 16 + |> put_resp_content_type("application/json") 17 + |> send_resp(404, Jason.encode!(%{error: "MethodNotImplemented", message: "XRPC method not found"})) 18 + end 19 + end
+36
appview/mix.exs
··· 1 + defmodule Atvouch.MixProject do 2 + use Mix.Project 3 + 4 + def project do 5 + [ 6 + app: :atvouch, 7 + version: "0.1.0", 8 + elixir: "~> 1.17", 9 + start_permanent: Mix.env() == :prod, 10 + deps: deps() 11 + ] 12 + end 13 + 14 + def application do 15 + [ 16 + extra_applications: [:logger, :runtime_tools], 17 + mod: {Atvouch.Application, []} 18 + ] 19 + end 20 + 21 + defp deps do 22 + [ 23 + {:plug, "~> 1.14"}, 24 + {:bandit, "~> 1.1"}, 25 + {:jason, "~> 1.4"}, 26 + {:ecto_sql, "~> 3.10"}, 27 + {:ecto_sqlite3, "~> 0.21"}, 28 + {:tesla, "~> 1.11"}, 29 + {:mint, "~> 1.0"}, 30 + {:castore, "~> 1.0"}, 31 + {:prometheus_ex, "~> 5.0"}, 32 + {:gun, "~> 2.0"}, 33 + {:cowlib, "~> 2.12", override: true} 34 + ] 35 + end 36 + end
+27
appview/mix.lock
··· 1 + %{ 2 + "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [: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", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, 3 + "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, 4 + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, 5 + "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, 6 + "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, 7 + "ddskerl": {:hex, :ddskerl, "0.4.3", "bb97b90eef1b5906520cebdd38da29b18a770b5567f6297927f53566eae9bebb", [:rebar3], [], "hexpm", "4e0f6047c6a002ce38f6ea155276dd918cd635fd0a5edb9e0b46aeb6f7aff2c2"}, 8 + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 9 + "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"}, 10 + "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, 11 + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"}, 12 + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 13 + "exqlite": {:hex, :exqlite, "0.35.0", "90741471945db42b66cd8ca3149af317f00c22c769cc6b06e8b0a08c5924aae5", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a009e303767a28443e546ac8aab2539429f605e9acdc38bd43f3b13f1568bca9"}, 14 + "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, 15 + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 16 + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 17 + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 18 + "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"}, 19 + "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"}, 20 + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 21 + "prometheus": {:hex, :prometheus, "6.1.2", "e5c3c567cd8b0994425920763405635211183c15bdc47d0cd524807dde20ca1d", [:rebar3], [{:ddskerl, "0.4.3", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "109662001328112b860de112d49b575310be8ff33fe04bf0482eec4b1e5a1278"}, 22 + "prometheus_ex": {:hex, :prometheus_ex, "5.1.0", "a978945b4be5923b87edae3537e3fd61f8d04d755fae865ec4cbb1be7b5ec2e5", [:mix], [{:prometheus, "~> 6.1", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "15d3cd752063a1b51ffcba727fc0276f62d2a4387b523963883d125ea5e0d9e7"}, 23 + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, 24 + "tesla": {:hex, :tesla, "1.16.0", "de77d083aea08ebd1982600693ff5d779d68a4bb835d136a0394b08f69714660", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "eb3bdfc0c6c8a23b4e3d86558e812e3577acff1cb4acb6cfe2da1985a1035b89"}, 25 + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, 26 + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 27 + }
+15
appview/test/atvouch/xrpc_router_test.exs
··· 1 + defmodule Atvouch.XrpcRouterTest do 2 + use ExUnit.Case, async: true 3 + import Plug.Test 4 + 5 + @opts Atvouch.Router.init([]) 6 + 7 + test "GET /xrpc/dev.atvouch.alive returns alive: true" do 8 + conn = 9 + conn(:get, "/xrpc/dev.atvouch.alive") 10 + |> Atvouch.Router.call(@opts) 11 + 12 + assert conn.status == 200 13 + assert Jason.decode!(conn.resp_body) == %{"alive" => true} 14 + end 15 + end
+1
appview/test/test_helper.exs
··· 1 + ExUnit.start()