diff --git a/assets/js/events.js b/assets/js/events.js index 164f45737..7bcee5380 100644 --- a/assets/js/events.js +++ b/assets/js/events.js @@ -49,8 +49,13 @@ export function registerGlobalEventHandlers() { window.addEventListener("lb:clipcopy", (event) => { if ("clipboard" in navigator) { - const text = event.target.textContent; - navigator.clipboard.writeText(text); + const tag = event.target.tagName; + + if (tag === "INPUT") { + navigator.clipboard.writeText(event.target.value); + } else { + navigator.clipboard.writeText(event.target.tagName); + } } else { alert( "Sorry, your browser does not support clipboard copy.\nThis generally requires a secure origin — either HTTPS or localhost." diff --git a/config/dev.exs b/config/dev.exs index 0edd88065..12b0350ad 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -58,7 +58,7 @@ config :livebook, LivebookWeb.Endpoint, live_reload: [ patterns: [ ~r"tmp/static_dev/.*(js|css|png|jpeg|jpg|gif|svg)$", - ~r"lib/livebook_web/(live|views)/.*(ex)$", + ~r"lib/livebook_web/(live|views|components)/.*(ex)$", ~r"lib/livebook_web/templates/.*(eex)$" ] ] diff --git a/lib/livebook/hubs.ex b/lib/livebook/hubs.ex index abeedcb4a..d448709b5 100644 --- a/lib/livebook/hubs.ex +++ b/lib/livebook/hubs.ex @@ -2,7 +2,7 @@ defmodule Livebook.Hubs do @moduledoc false alias Livebook.Storage - alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Metadata, Personal, Provider, Team} + alias Livebook.Hubs.{Broadcasts, Metadata, Personal, Provider, Team} alias Livebook.Secrets.Secret @namespace :hubs @@ -157,14 +157,6 @@ defmodule Livebook.Hubs do Phoenix.PubSub.unsubscribe(Livebook.PubSub, "hubs:#{topic}") end - defp to_struct(%{id: "fly-" <> _} = fields) do - Provider.load(%Fly{}, fields) - end - - defp to_struct(%{id: "enterprise-" <> _} = fields) do - Provider.load(%Enterprise{}, fields) - end - defp to_struct(%{id: "personal-" <> _} = fields) do Provider.load(%Personal{}, fields) end diff --git a/lib/livebook/hubs/enterprise.ex b/lib/livebook/hubs/enterprise.ex deleted file mode 100644 index a96bb8bb3..000000000 --- a/lib/livebook/hubs/enterprise.ex +++ /dev/null @@ -1,203 +0,0 @@ -defmodule Livebook.Hubs.Enterprise do - @moduledoc false - - use Ecto.Schema - import Ecto.Changeset - - alias Livebook.Hubs - - @type t :: %__MODULE__{ - id: String.t() | nil, - org_id: pos_integer() | nil, - user_id: pos_integer() | nil, - org_key_id: pos_integer() | nil, - teams_key: String.t() | nil, - session_token: String.t() | nil, - hub_name: String.t() | nil, - hub_emoji: String.t() | nil - } - - embedded_schema do - field :org_id, :integer - field :user_id, :integer - field :org_key_id, :integer - field :teams_key, :string - field :session_token, :string - field :hub_name, :string - field :hub_emoji, :string - end - - @fields ~w( - org_id - user_id - org_key_id - teams_key - session_token - hub_name - hub_emoji - )a - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking hub changes. - """ - @spec change_hub(t(), map()) :: Ecto.Changeset.t() - def change_hub(%__MODULE__{} = enterprise, attrs \\ %{}) do - changeset(enterprise, attrs) - end - - @doc """ - Returns changeset with applied validations. - """ - @spec validate_hub(t(), map()) :: Ecto.Changeset.t() - def validate_hub(%__MODULE__{} = enterprise, attrs \\ %{}) do - enterprise - |> changeset(attrs) - |> Map.put(:action, :validate) - end - - @doc """ - Creates a Hub. - - With success, notifies interested processes about hub metadatas data change. - Otherwise, it will return an error tuple with changeset. - """ - @spec create_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def create_hub(%__MODULE__{} = enterprise, attrs) do - changeset = changeset(enterprise, attrs) - id = get_field(changeset, :id) - - if Hubs.hub_exists?(id) do - {:error, - changeset - |> add_error(:hub_name, "already exists") - |> Map.replace!(:action, :validate)} - else - with {:ok, struct} <- apply_action(changeset, :insert) do - Hubs.save_hub(struct) - {:ok, struct} - end - end - end - - @doc """ - Updates a Hub. - - With success, notifies interested processes about hub metadatas data change. - Otherwise, it will return an error tuple with changeset. - """ - @spec update_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def update_hub(%__MODULE__{} = enterprise, attrs) do - changeset = changeset(enterprise, attrs) - id = get_field(changeset, :id) - - if Hubs.hub_exists?(id) do - with {:ok, struct} <- apply_action(changeset, :update) do - Hubs.save_hub(struct) - {:ok, struct} - end - else - {:error, - changeset - |> add_error(:hub_name, "does not exists") - |> Map.replace!(:action, :validate)} - end - end - - defp changeset(enterprise, attrs) do - enterprise - |> cast(attrs, @fields) - |> validate_required(@fields) - |> add_id() - end - - defp add_id(changeset) do - case get_field(changeset, :hub_name) do - nil -> changeset - hub_name -> put_change(changeset, :id, "enterprise-#{hub_name}") - end - end -end - -defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do - alias Livebook.Hubs.EnterpriseClient - - def load(enterprise, fields) do - %{ - enterprise - | id: fields.id, - session_token: fields.session_token, - teams_key: fields.teams_key, - org_id: fields.org_id, - user_id: fields.user_id, - org_key_id: fields.org_key_id, - hub_name: fields.hub_name, - hub_emoji: fields.hub_emoji - } - end - - def to_metadata(enterprise) do - %Livebook.Hubs.Metadata{ - id: enterprise.id, - name: enterprise.hub_name, - provider: enterprise, - emoji: enterprise.hub_emoji, - connected?: EnterpriseClient.connected?(enterprise.id) - } - end - - def type(_enterprise), do: "enterprise" - - def connection_spec(enterprise), do: {EnterpriseClient, enterprise} - - def disconnect(enterprise) do - EnterpriseClient.stop(enterprise.id) - end - - def capabilities(_enterprise), do: ~w(connect list_secrets create_secret)a - - def get_secrets(enterprise) do - EnterpriseClient.get_secrets(enterprise.id) - end - - def create_secret(enterprise, secret) do - data = LivebookProto.build_create_secret_request(name: secret.name, value: secret.value) - - case EnterpriseClient.send_request(enterprise.id, data) do - {:create_secret, _} -> - :ok - - {:changeset_error, errors} -> - changeset = - for {field, messages} <- errors, - message <- messages, - reduce: secret do - acc -> - Livebook.Secrets.add_secret_error(acc, field, message) - end - - {:error, changeset} - - {:transport_error, reason} -> - message = "#{enterprise.hub_emoji} #{enterprise.hub_name}: #{reason}" - changeset = Livebook.Secrets.add_secret_error(secret, :hub_id, message) - - {:error, changeset} - end - end - - def update_secret(_enterprise, _secret), do: raise("not implemented") - - def delete_secret(_enterprise, _secret), do: raise("not implemented") - - def connection_error(enterprise) do - reason = EnterpriseClient.get_connection_error(enterprise.id) - "Cannot connect to Hub: #{reason}. Will attempt to reconnect automatically..." - end - - # TODO: implement signing through the enterprise server - def notebook_stamp(_hub, _notebook_source, _metadata) do - :skip - end - - def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented") -end diff --git a/lib/livebook/hubs/enterprise_client.ex b/lib/livebook/hubs/enterprise_client.ex deleted file mode 100644 index c16747a3d..000000000 --- a/lib/livebook/hubs/enterprise_client.ex +++ /dev/null @@ -1,225 +0,0 @@ -defmodule Livebook.Hubs.EnterpriseClient do - @moduledoc false - use GenServer - require Logger - - alias Livebook.Hubs.Broadcasts - alias Livebook.Hubs.Enterprise - alias Livebook.Secrets.Secret - alias Livebook.WebSocket.ClientConnection - - @registry Livebook.HubsRegistry - @supervisor Livebook.HubsSupervisor - - defstruct [:server, :hub, :connection_error, connected?: false, secrets: []] - - @type registry_name :: {:via, Registry, {Livebook.HubsRegistry, String.t()}} - - @doc """ - Connects the Enterprise client with WebSocket server. - """ - @spec start_link(Enterprise.t()) :: GenServer.on_start() - def start_link(%Enterprise{} = enterprise) do - GenServer.start_link(__MODULE__, enterprise, name: registry_name(enterprise.id)) - end - - @doc """ - Stops the WebSocket server. - """ - @spec stop(String.t()) :: :ok - def stop(id) do - if pid = GenServer.whereis(registry_name(id)) do - DynamicSupervisor.terminate_child(@supervisor, pid) - end - - :ok - end - - @doc """ - Sends a request to the WebSocket server. - """ - @spec send_request(String.t() | registry_name() | pid(), WebSocket.proto()) :: {atom(), term()} - def send_request(id, %_struct{} = data) when is_binary(id) do - send_request(registry_name(id), data) - end - - def send_request(pid, %_struct{} = data) do - with {:ok, server} <- GenServer.call(pid, :fetch_server) do - ClientConnection.send_request(server, data) - end - end - - @doc """ - Returns a list of cached secrets. - """ - @spec get_secrets(String.t()) :: list(Secret.t()) - def get_secrets(id) do - GenServer.call(registry_name(id), :get_secrets) - catch - :exit, _ -> [] - end - - @doc """ - Returns the latest error from connection. - """ - @spec get_connection_error(String.t()) :: String.t() | nil - def get_connection_error(id) do - GenServer.call(registry_name(id), :get_connection_error) - catch - :exit, _ -> "connection refused" - end - - @doc """ - Returns if the given enterprise is connected. - """ - @spec connected?(String.t()) :: boolean() - def connected?(id) do - GenServer.call(registry_name(id), :connected?) - catch - :exit, _ -> false - end - - ## GenServer callbacks - - @impl true - def init(%Enterprise{} = enterprise) do - # TODO: Make it work with new struct and `Livebook.Teams` - {:ok, pid} = ClientConnection.start_link(self(), Livebook.Config.teams_url()) - - {:ok, %__MODULE__{hub: enterprise, server: pid}} - end - - @impl true - def handle_continue(:synchronize_user, state) do - data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) - {:handshake, _} = ClientConnection.send_request(state.server, data) - - {:noreply, state} - end - - @impl true - def handle_call(:fetch_server, _caller, state) do - if state.connected? do - {:reply, {:ok, state.server}, state} - else - {:reply, {:transport_error, state.connection_error}, state} - end - end - - def handle_call(:get_secrets, _caller, state) do - {:reply, state.secrets, state} - end - - def handle_call(:get_connection_error, _caller, state) do - {:reply, state.connection_error, state} - end - - def handle_call(:connected?, _caller, state) do - {:reply, state.connected?, state} - end - - @impl true - def handle_info({:connect, :ok, _}, state) do - Broadcasts.hub_connected() - {:noreply, %{state | connected?: true, connection_error: nil}, {:continue, :synchronize_user}} - end - - def handle_info({:connect, :error, reason}, state) do - Broadcasts.hub_connection_failed(reason) - {:noreply, %{state | connected?: false, connection_error: reason}} - end - - def handle_info({:disconnect, :ok, :disconnected}, state) do - Broadcasts.hub_disconnected() - {:stop, :normal, state} - end - - def handle_info({:event, topic, data}, state) do - Logger.debug("Received event #{topic} with data: #{inspect(data)}") - - {:noreply, handle_event(topic, data, state)} - end - - # Private - - defp registry_name(id) do - {:via, Registry, {@registry, id}} - end - - defp put_secret(state, secret) do - state = remove_secret(state, secret) - %{state | secrets: [secret | state.secrets]} - end - - defp remove_secret(state, secret) do - %{state | secrets: Enum.reject(state.secrets, &(&1.name == secret.name))} - end - - defp build_secret(state, %{name: name, value: value}), - do: %Secret{name: name, value: value, hub_id: state.hub.id, readonly: true} - - defp update_hub(state, name) do - case Enterprise.update_hub(state.hub, %{hub_name: name}) do - {:ok, hub} -> %{state | hub: hub} - {:error, _} -> state - end - end - - defp handle_event(:secret_created, secret_created, state) do - secret = build_secret(state, secret_created) - Broadcasts.secret_created(secret) - - put_secret(state, secret) - end - - defp handle_event(:secret_updated, secret_updated, state) do - secret = build_secret(state, secret_updated) - Broadcasts.secret_updated(secret) - - put_secret(state, secret) - end - - defp handle_event(:secret_deleted, secret_deleted, state) do - secret = build_secret(state, secret_deleted) - Broadcasts.secret_deleted(secret) - - remove_secret(state, secret) - end - - defp handle_event(:user_synchronized, user_synchronized, %{secrets: []} = state) do - state = update_hub(state, user_synchronized.name) - secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret) - - %{state | secrets: secrets} - end - - defp handle_event(:user_synchronized, user_synchronized, state) do - state = update_hub(state, user_synchronized.name) - secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret) - - created_secrets = - Enum.reject(secrets, fn secret -> - Enum.find(state.secrets, &(&1.name == secret.name and &1.value == secret.value)) - end) - - deleted_secrets = - Enum.reject(state.secrets, fn secret -> - Enum.find(secrets, &(&1.name == secret.name)) - end) - - updated_secrets = - Enum.filter(secrets, fn secret -> - Enum.find(state.secrets, &(&1.name == secret.name and &1.value != secret.value)) - end) - - events_by_type = [ - secret_deleted: deleted_secrets, - secret_created: created_secrets, - secret_updated: updated_secrets - ] - - for {type, events} <- events_by_type, event <- events, reduce: state do - state -> handle_event(type, event, state) - end - end -end diff --git a/lib/livebook/hubs/fly.ex b/lib/livebook/hubs/fly.ex deleted file mode 100644 index 24757ebac..000000000 --- a/lib/livebook/hubs/fly.ex +++ /dev/null @@ -1,171 +0,0 @@ -defmodule Livebook.Hubs.Fly do - @moduledoc false - - use Ecto.Schema - import Ecto.Changeset - - alias Livebook.Hubs - - @type t :: %__MODULE__{ - id: String.t() | nil, - access_token: String.t() | nil, - hub_name: String.t() | nil, - hub_emoji: String.t() | nil, - organization_id: String.t() | nil, - organization_type: String.t() | nil, - organization_name: String.t() | nil, - application_id: String.t() | nil - } - - embedded_schema do - field :access_token, :string - field :hub_name, :string - field :hub_emoji, :string - field :organization_id, :string - field :organization_type, :string - field :organization_name, :string - field :application_id, :string - end - - @fields ~w( - access_token - hub_name - hub_emoji - organization_id - organization_name - organization_type - application_id - )a - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking hub changes. - """ - @spec change_hub(t(), map()) :: Ecto.Changeset.t() - def change_hub(%__MODULE__{} = fly, attrs \\ %{}) do - changeset(fly, attrs) - end - - @doc """ - Returns changeset with applied validations. - """ - @spec validate_hub(t(), map()) :: Ecto.Changeset.t() - def validate_hub(%__MODULE__{} = fly, attrs \\ %{}) do - fly - |> changeset(attrs) - |> Map.put(:action, :validate) - end - - @doc """ - Creates a Hub. - - With success, notifies interested processes about hub metadatas data change. - Otherwise, it will return an error tuple with changeset. - """ - @spec create_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def create_hub(%__MODULE__{} = fly, attrs) do - changeset = changeset(fly, attrs) - - if Hubs.hub_exists?(fly.id) do - {:error, - changeset - |> add_error(:application_id, "already exists") - |> Map.replace!(:action, :validate)} - else - with {:ok, struct} <- apply_action(changeset, :insert) do - Hubs.save_hub(struct) - {:ok, struct} - end - end - end - - @doc """ - Updates a Hub. - - With success, notifies interested processes about hub metadatas data change. - Otherwise, it will return an error tuple with changeset. - """ - @spec update_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} - def update_hub(%__MODULE__{} = fly, attrs) do - changeset = changeset(fly, attrs) - - if Hubs.hub_exists?(fly.id) do - with {:ok, struct} <- apply_action(changeset, :update) do - Hubs.save_hub(struct) - {:ok, struct} - end - else - {:error, - changeset - |> add_error(:application_id, "does not exists") - |> Map.replace!(:action, :validate)} - end - end - - defp changeset(fly, attrs) do - fly - |> cast(attrs, @fields) - |> validate_required(@fields) - |> add_id() - end - - defp add_id(changeset) do - if application_id = get_field(changeset, :application_id) do - change(changeset, %{id: "fly-#{application_id}"}) - else - changeset - end - end -end - -defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do - def load(fly, fields) do - %{ - fly - | id: fields.id, - access_token: fields.access_token, - hub_name: fields.hub_name, - hub_emoji: fields.hub_emoji, - organization_id: fields.organization_id, - organization_type: fields.organization_type, - organization_name: fields.organization_name, - application_id: fields.application_id - } - end - - def to_metadata(fly) do - %Livebook.Hubs.Metadata{ - id: fly.id, - name: fly.hub_name, - provider: fly, - emoji: fly.hub_emoji, - connected?: false - } - end - - def type(_fly), do: "fly" - - def connection_spec(_fly), do: nil - - def disconnect(_fly), do: raise("not implemented") - - def capabilities(_fly), do: [] - - def get_secrets(_fly), do: [] - - # TODO: Implement the FlyClient.set_secrets/2 - def create_secret(_fly, _secret), do: :ok - - # TODO: Implement the FlyClient.set_secrets/2 - def update_secret(_fly, _secret), do: :ok - - # TODO: Implement the FlyClient.unset_secrets/2 - def delete_secret(_fly, _secret), do: :ok - - def connection_error(_fly), do: raise("not implemented") - - def notebook_stamp(_hub, _notebook_source, _metadata) do - :skip - end - - def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented") -end diff --git a/lib/livebook/hubs/fly_client.ex b/lib/livebook/hubs/fly_client.ex deleted file mode 100644 index b80432b65..000000000 --- a/lib/livebook/hubs/fly_client.ex +++ /dev/null @@ -1,139 +0,0 @@ -defmodule Livebook.Hubs.FlyClient do - @moduledoc false - - alias Livebook.Hubs.Fly - alias Livebook.Utils.HTTP - - def fetch_apps(access_token) do - query = """ - query { - apps { - nodes { - id - organization { - id - name - type - } - } - } - } - """ - - with {:ok, %{"apps" => %{"nodes" => nodes}}} <- graphql(access_token, query) do - apps = - for node <- nodes do - %Fly{ - id: "fly-" <> node["id"], - access_token: access_token, - organization_id: node["organization"]["id"], - organization_type: node["organization"]["type"], - organization_name: node["organization"]["name"], - application_id: node["id"] - } - end - - {:ok, apps} - end - end - - def fetch_app(%Fly{application_id: app_id, access_token: access_token}) do - query = """ - query($appId: String!) { - app(id: $appId) { - id - name - hostname - platformVersion - deployed - status - secrets { - id - name - digest - createdAt - } - } - } - """ - - with {:ok, %{"app" => app}} <- graphql(access_token, query, %{appId: app_id}) do - {:ok, app} - end - end - - def set_secrets(%Fly{access_token: access_token, application_id: application_id}, secrets) do - mutation = """ - mutation($input: SetSecretsInput!) { - setSecrets(input: $input) { - app { - secrets { - id - name - digest - createdAt - } - } - } - } - """ - - input = %{input: %{appId: application_id, secrets: secrets}} - - with {:ok, %{"setSecrets" => %{"app" => app}}} <- graphql(access_token, mutation, input) do - {:ok, app["secrets"]} - end - end - - def unset_secrets(%Fly{access_token: access_token, application_id: application_id}, keys) do - mutation = """ - mutation($input: UnsetSecretsInput!) { - unsetSecrets(input: $input) { - app { - secrets { - id - name - digest - createdAt - } - } - } - } - """ - - input = %{input: %{appId: application_id, keys: keys}} - - with {:ok, %{"unsetSecrets" => %{"app" => app}}} <- graphql(access_token, mutation, input) do - {:ok, app["secrets"]} - end - end - - defp graphql(access_token, query, input \\ %{}) do - headers = [{"Authorization", "Bearer #{access_token}"}] - body = {"application/json", Jason.encode!(%{query: query, variables: input})} - - case HTTP.request(:post, graphql_endpoint(), headers: headers, body: body) do - {:ok, 200, _, body} -> - case Jason.decode!(body) do - %{"errors" => [%{"extensions" => %{"code" => code}}]} -> - {:error, "request failed with code: #{code}"} - - %{"errors" => [%{"message" => message}]} -> - {:error, message} - - %{"data" => data} -> - {:ok, data} - end - - {:ok, _, _, body} -> - {:error, body} - - {:error, _} = error -> - error - end - end - - defp graphql_endpoint do - Application.get_env(:livebook, :fly_graphql_endpoint, "https://api.fly.io/graphql") - end -end diff --git a/lib/livebook_web/live/hub/edit/personal_component.ex b/lib/livebook_web/live/hub/edit/personal_component.ex index 67101cb8c..cf2ba7e75 100644 --- a/lib/livebook_web/live/hub/edit/personal_component.ex +++ b/lib/livebook_web/live/hub/edit/personal_component.ex @@ -1,23 +1,32 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do use LivebookWeb, :live_component + alias Livebook.Hubs alias Livebook.Hubs.Personal alias LivebookWeb.LayoutHelpers @impl true def update(assigns, socket) do + socket = assign(socket, assigns) changeset = Personal.change_hub(assigns.hub) + secrets = Hubs.get_secrets(assigns.hub) + secret_name = assigns.params["secret_name"] secret_value = if assigns.live_action == :edit_secret do - secret = Enum.find(assigns.secrets, &(&1.name == assigns.secret_name)) - secret.value + secrets + |> Enum.find(&(&1.name == secret_name)) + |> Map.get(:value) end {:ok, - socket - |> assign(assigns) - |> assign(changeset: changeset, stamp_changeset: changeset, secret_value: secret_value)} + assign(socket, + secrets: secrets, + changeset: changeset, + stamp_changeset: changeset, + secret_name: secret_name, + secret_value: secret_value + )} end @impl true diff --git a/lib/livebook_web/live/hub/edit/team_component.ex b/lib/livebook_web/live/hub/edit/team_component.ex index 149a0068c..d738e6e8f 100644 --- a/lib/livebook_web/live/hub/edit/team_component.ex +++ b/lib/livebook_web/live/hub/edit/team_component.ex @@ -7,52 +7,141 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do @impl true def update(assigns, socket) do + socket = assign(socket, assigns) changeset = Team.change_hub(assigns.hub) + show_key? = assigns.params["show-key"] == "true" {:ok, socket - |> assign(assigns) + |> assign(show_key: show_key?) |> assign_form(changeset)} end @impl true def render(assigns) do ~H""" -
-
- +
+ <.modal + :if={@show_key} + id="show-key-modal" + width={:medium} + show={true} + patch={~p"/hub/#{@hub.id}"} + > +
+

+ Teams Key +

+
+ This is your Teams Key. If you want to join or invite others + to your organization, you will need to share your Teams Key with them. We + recommend storing it somewhere safe: +
+
+
+ - -
+
+ -
-
-

- General -

- - <.form - :let={f} - id={@id} - class="flex flex-col mt-4 space-y-4" - for={@form} - phx-submit="save" - phx-change="validate" - phx-target={@myself} - > -
- <.emoji_field field={f[:hub_emoji]} label="Emoji" /> + + +
+
+
+ - - +
+
+ + + +
+ +
+
+

+ General +

+ + <.form + :let={f} + id={@id} + class="flex flex-col mt-4 space-y-4" + for={@form} + phx-submit="save" + phx-change="validate" + phx-target={@myself} + > +
+ <.emoji_field field={f[:hub_emoji]} label="Emoji" /> +
+ + + +
diff --git a/lib/livebook_web/live/hub/edit_live.ex b/lib/livebook_web/live/hub/edit_live.ex index 59c470c02..118056a84 100644 --- a/lib/livebook_web/live/hub/edit_live.ex +++ b/lib/livebook_web/live/hub/edit_live.ex @@ -9,32 +9,16 @@ defmodule LivebookWeb.Hub.EditLive do @impl true def mount(_params, _session, socket) do - {:ok, - assign(socket, - hub: nil, - secrets: [], - type: nil, - page_title: "Hub - Livebook", - env_var_id: nil, - secret_name: nil - )} + {:ok, assign(socket, hub: nil, type: nil, page_title: "Hub - Livebook", params: %{})} end @impl true def handle_params(params, _url, socket) do - Livebook.Hubs.subscribe([:secrets]) + Hubs.subscribe([:secrets]) hub = Hubs.fetch_hub!(params["id"]) type = Provider.type(hub) - {:noreply, - assign(socket, - hub: hub, - type: type, - secrets: Hubs.get_secrets(hub), - params: params, - env_var_id: params["env_var_id"], - secret_name: params["secret_name"] - )} + {:noreply, assign(socket, hub: hub, type: type, params: params, counter: 0)} end @impl true @@ -50,8 +34,8 @@ defmodule LivebookWeb.Hub.EditLive do type={@type} hub={@hub} live_action={@live_action} - secrets={@secrets} - secret_name={@secret_name} + params={@params} + counter={@counter} />
@@ -63,16 +47,23 @@ defmodule LivebookWeb.Hub.EditLive do <.live_component module={LivebookWeb.Hub.Edit.PersonalComponent} hub={@hub} - secrets={@secrets} + params={@params} live_action={@live_action} - secret_name={@secret_name} + counter={@counter} id="personal-form" /> """ end defp hub_component(%{type: "team"} = assigns) do - ~H(<.live_component module={LivebookWeb.Hub.Edit.TeamComponent} hub={@hub} id="team-form" />) + ~H""" + <.live_component + module={LivebookWeb.Hub.Edit.TeamComponent} + hub={@hub} + params={@params} + id="team-form" + /> + """ end @impl true @@ -98,21 +89,21 @@ defmodule LivebookWeb.Hub.EditLive do def handle_info({:secret_created, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do {:noreply, socket - |> refresh_secrets() + |> increment_counter() |> put_flash(:success, "Secret created successfully")} end def handle_info({:secret_updated, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do {:noreply, socket - |> refresh_secrets() + |> increment_counter() |> put_flash(:success, "Secret updated successfully")} end def handle_info({:secret_deleted, %{hub_id: id}}, %{assigns: %{hub: %{id: id}}} = socket) do {:noreply, socket - |> refresh_secrets() + |> increment_counter() |> put_flash(:success, "Secret deleted successfully")} end @@ -120,7 +111,5 @@ defmodule LivebookWeb.Hub.EditLive do {:noreply, socket} end - defp refresh_secrets(socket) do - assign(socket, secrets: Livebook.Hubs.get_secrets(socket.assigns.hub)) - end + defp increment_counter(socket), do: assign(socket, counter: socket.assigns.counter + 1) end diff --git a/lib/livebook_web/live/hub/new_live.ex b/lib/livebook_web/live/hub/new_live.ex index ac7a058c9..d478ab9ff 100644 --- a/lib/livebook_web/live/hub/new_live.ex +++ b/lib/livebook_web/live/hub/new_live.ex @@ -132,7 +132,7 @@ defmodule LivebookWeb.Hub.NewLive do
<.password_field - readonly={@selected_option == "new-org"} + :if={@selected_option == "join-org"} field={f[:teams_key]} label="Livebook Teams Key" /> @@ -281,7 +281,7 @@ defmodule LivebookWeb.Hub.NewLive do {:noreply, socket |> put_flash(:success, "Hub added successfully") - |> push_navigate(to: ~p"/hub/#{hub.id}")} + |> push_navigate(to: ~p"/hub/#{hub.id}?show-key=true")} {:error, :expired} -> changeset = Teams.change_org(org, %{user_code: nil}) @@ -293,6 +293,12 @@ defmodule LivebookWeb.Hub.NewLive do |> assign_form(changeset)} {:transport_error, message} -> + Process.send_after( + self(), + {:check_completion_data, device_code}, + @check_completion_data_interval + ) + {:noreply, put_flash(socket, :error, message)} end end diff --git a/test/livebook/hubs/enterprise_client_test.exs b/test/livebook/hubs/enterprise_client_test.exs deleted file mode 100644 index 0975db338..000000000 --- a/test/livebook/hubs/enterprise_client_test.exs +++ /dev/null @@ -1,99 +0,0 @@ -defmodule Livebook.Hubs.EnterpriseClientTest do - use Livebook.EnterpriseIntegrationCase, async: true - @moduletag :capture_log - - alias Livebook.Hubs.EnterpriseClient - alias Livebook.Secrets.Secret - - setup do - Livebook.Hubs.subscribe([:connection, :secrets]) - :ok - end - - describe "start_link/1" do - test "successfully authenticates the web socket connection", %{url: url, token: token} do - enterprise = build(:enterprise, url: url, token: token) - - EnterpriseClient.start_link(enterprise) - assert_receive :hub_connected - end - - test "rejects the websocket with invalid address", %{token: token} do - enterprise = build(:enterprise, url: "http://localhost:9999", token: token) - - EnterpriseClient.start_link(enterprise) - assert_receive {:hub_connection_failed, "connection refused"} - end - - test "rejects the web socket connection with invalid credentials", %{url: url} do - enterprise = build(:enterprise, url: url, token: "foo") - - EnterpriseClient.start_link(enterprise) - assert_receive {:hub_connection_failed, reason} - assert reason =~ "the given token is invalid" - end - end - - describe "handle events" do - setup %{test: test, url: url, token: token} do - node = EnterpriseServer.get_node() - hub_id = "enterprise-#{test}" - - insert_hub(:enterprise, - id: hub_id, - external_id: to_string(test), - url: url, - token: token - ) - - assert_receive :hub_connected - - {:ok, node: node, hub_id: hub_id} - end - - test "receives a secret_created event", %{node: node, hub_id: id} do - name = "API_TOKEN_ID" - value = Livebook.Utils.random_id() - :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - secret = %Secret{name: name, value: value, hub_id: id, readonly: true} - - assert_receive {:secret_created, ^secret} - assert secret in EnterpriseClient.get_secrets(id) - end - - test "receives a secret_updated event", %{node: node, hub_id: id} do - name = "SUPER_SUDO_USER" - value = "JakePeralta" - new_value = "ChonkyCat" - enterprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - secret = %Secret{name: name, value: value, hub_id: id, readonly: true} - updated_secret = %Secret{name: name, value: new_value, hub_id: id, readonly: true} - - assert_receive {:secret_created, ^secret} - assert secret in EnterpriseClient.get_secrets(id) - refute updated_secret in EnterpriseClient.get_secrets(id) - - :erpc.call(node, Enterprise.Integration, :update_secret, [enterprise_secret, new_value]) - - assert_receive {:secret_updated, ^updated_secret} - - assert updated_secret in EnterpriseClient.get_secrets(id) - refute secret in EnterpriseClient.get_secrets(id) - end - - test "receives a secret_deleted event", %{node: node, hub_id: id} do - name = "SUPER_DELETE" - value = "JakePeralta" - enteprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - secret = %Secret{name: name, value: value, hub_id: id, readonly: true} - - assert_receive {:secret_created, ^secret} - assert secret in EnterpriseClient.get_secrets(id) - - :erpc.call(node, Enterprise.Integration, :delete_secret, [enteprise_secret]) - - assert_receive {:secret_deleted, ^secret} - refute secret in EnterpriseClient.get_secrets(id) - end - end -end diff --git a/test/livebook/hubs/fly_client_test.exs b/test/livebook/hubs/fly_client_test.exs deleted file mode 100644 index 64e68135b..000000000 --- a/test/livebook/hubs/fly_client_test.exs +++ /dev/null @@ -1,225 +0,0 @@ -defmodule Livebook.Hubs.FlyClientTest do - use Livebook.DataCase - - alias Livebook.Hubs.{Fly, FlyClient} - - setup do - bypass = Bypass.open() - - Application.put_env( - :livebook, - :fly_graphql_endpoint, - "http://localhost:#{bypass.port}" - ) - - on_exit(fn -> - Application.delete_env(:livebook, :fly_graphql_endpoint) - end) - - {:ok, bypass: bypass, url: "http://localhost:#{bypass.port}"} - end - - describe "fetch_apps/1" do - test "fetches an empty list of apps", %{bypass: bypass} do - response = %{"data" => %{"apps" => %{"nodes" => []}}} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - assert {:ok, []} = FlyClient.fetch_apps("some valid token") - end - - test "fetches a list of apps", %{bypass: bypass} do - app = %{ - "id" => "foo-app", - "organization" => %{ - "id" => "l3soyvjmvtmwtl6l2drnbfuvltipprge", - "name" => "Foo Bar", - "type" => "PERSONAL" - } - } - - app_id = app["id"] - - response = %{"data" => %{"apps" => %{"nodes" => [app]}}} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - assert {:ok, [%Fly{application_id: ^app_id}]} = FlyClient.fetch_apps("some valid token") - end - - test "returns unauthorized when token is invalid", %{bypass: bypass} do - error = %{"extensions" => %{"code" => "UNAUTHORIZED"}} - response = %{"data" => nil, "errors" => [error]} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_apps("foo") - end - end - - describe "fetch_app/1" do - test "fetches an application", %{bypass: bypass} do - app = %{ - "id" => "foo-app", - "name" => "foo-app", - "hostname" => "foo-app.fly.dev", - "platformVersion" => "nomad", - "deployed" => true, - "status" => "running", - "secrets" => [ - %{ - "createdAt" => to_string(DateTime.utc_now()), - "digest" => to_string(Livebook.Utils.random_cookie()), - "id" => Livebook.Utils.random_short_id(), - "name" => "FOO" - } - ] - } - - response = %{"data" => %{"app" => app}} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - assert {:ok, ^app} = FlyClient.fetch_app(hub) - end - - test "returns unauthorized when token is invalid", %{bypass: bypass} do - error = %{"extensions" => %{"code" => "UNAUTHORIZED"}} - response = %{"data" => nil, "errors" => [error]} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - assert {:error, "request failed with code: UNAUTHORIZED"} = FlyClient.fetch_app(hub) - end - end - - describe "set_secrets/2" do - test "puts a list of secrets inside application", %{bypass: bypass} do - secrets = [ - %{ - "createdAt" => to_string(DateTime.utc_now()), - "digest" => to_string(Livebook.Utils.random_cookie()), - "id" => Livebook.Utils.random_short_id(), - "name" => "FOO" - } - ] - - response = %{"data" => %{"setSecrets" => %{"app" => %{"secrets" => secrets}}}} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - assert {:ok, ^secrets} = FlyClient.set_secrets(hub, [%{key: "FOO", value: "BAR"}]) - end - - test "returns error when input is invalid", %{bypass: bypass} do - message = - "Variable $input of type SetSecretsInput! was provided invalid value for secrets.0.Value (Field is not defined on SecretInput), secrets.0.value (Expected value to not be null)" - - error = %{ - "extensions" => %{ - "problems" => [ - %{ - "explanation" => "Field is not defined on SecretInput", - "path" => ["secrets", 0, "Value"] - }, - %{ - "explanation" => "Expected value to not be null", - "path" => ["secrets", 0, "value"] - } - ], - "value" => %{ - "appId" => "myfoo-test-livebook", - "secrets" => [%{"Value" => "BAR", "key" => "FOO"}] - } - }, - "locations" => [%{"column" => 10, "line" => 1}], - "message" => message - } - - response = %{"data" => nil, "errors" => [error]} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - assert {:error, ^message} = FlyClient.set_secrets(hub, [%{key: "FOO", Value: "BAR"}]) - end - - test "returns unauthorized when token is invalid", %{bypass: bypass} do - error = %{"extensions" => %{"code" => "UNAUTHORIZED"}} - response = %{"data" => nil, "errors" => [error]} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - - assert {:error, "request failed with code: UNAUTHORIZED"} = - FlyClient.set_secrets(hub, [%{key: "FOO", value: "BAR"}]) - end - end - - describe "unset_secrets/2" do - test "deletes a list of secrets inside application", %{bypass: bypass} do - response = %{"data" => %{"unsetSecrets" => %{"app" => %{"secrets" => []}}}} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - assert {:ok, []} = FlyClient.unset_secrets(hub, ["FOO"]) - end - - test "returns unauthorized when token is invalid", %{bypass: bypass} do - error = %{"extensions" => %{"code" => "UNAUTHORIZED"}} - response = %{"data" => nil, "errors" => [error]} - - Bypass.expect_once(bypass, "POST", "/", fn conn -> - conn - |> Plug.Conn.put_resp_content_type("application/json") - |> Plug.Conn.resp(200, Jason.encode!(response)) - end) - - hub = build(:fly) - - assert {:error, "request failed with code: UNAUTHORIZED"} = - FlyClient.unset_secrets(hub, ["FOO"]) - end - end -end diff --git a/test/livebook/hubs_test.exs b/test/livebook/hubs_test.exs index 495f06d63..0e69bb531 100644 --- a/test/livebook/hubs_test.exs +++ b/test/livebook/hubs_test.exs @@ -4,53 +4,53 @@ defmodule Livebook.HubsTest do alias Livebook.Hubs test "get_hubs/0 returns a list of persisted hubs" do - fly = insert_hub(:fly, id: "fly-baz") - assert fly in Hubs.get_hubs() + team = insert_hub(:team, id: "team-baz") + assert team in Hubs.get_hubs() - Hubs.delete_hub("fly-baz") - refute fly in Hubs.get_hubs() + Hubs.delete_hub("team-baz") + refute team in Hubs.get_hubs() end test "get_metadata/0 returns a list of persisted hubs normalized" do - fly = insert_hub(:fly, id: "fly-livebook") - metadata = Hubs.Provider.to_metadata(fly) + team = insert_hub(:team, id: "team-livebook") + metadata = Hubs.Provider.to_metadata(team) assert metadata in Hubs.get_metadatas() - Hubs.delete_hub("fly-livebook") + Hubs.delete_hub("team-livebook") refute metadata in Hubs.get_metadatas() end - test "fetch_hub!/1 returns one persisted fly" do + test "fetch_hub!/1 returns one persisted team" do assert_raise Livebook.Storage.NotFoundError, - ~s/could not find entry in \"hubs\" with ID "fly-exception-foo"/, + ~s/could not find entry in \"hubs\" with ID "team-exception-foo"/, fn -> - Hubs.fetch_hub!("fly-exception-foo") + Hubs.fetch_hub!("team-exception-foo") end - fly = insert_hub(:fly, id: "fly-exception-foo") + team = insert_hub(:team, id: "team-exception-foo") - assert Hubs.fetch_hub!("fly-exception-foo") == fly + assert Hubs.fetch_hub!("team-exception-foo") == team end test "hub_exists?/1" do - refute Hubs.hub_exists?("fly-bar") - insert_hub(:fly, id: "fly-bar") - assert Hubs.hub_exists?("fly-bar") + refute Hubs.hub_exists?("team-bar") + insert_hub(:team, id: "team-bar") + assert Hubs.hub_exists?("team-bar") end test "save_hub/1 persists hub" do - fly = build(:fly, id: "fly-foo") - Hubs.save_hub(fly) + team = build(:team, id: "team-foo") + Hubs.save_hub(team) - assert Hubs.fetch_hub!("fly-foo") == fly + assert Hubs.fetch_hub!("team-foo") == team end test "save_hub/1 updates hub" do - fly = insert_hub(:fly, id: "fly-foo2") - Hubs.save_hub(%{fly | hub_emoji: "🐈"}) + team = insert_hub(:team, id: "team-foo2") + Hubs.save_hub(%{team | hub_emoji: "🐈"}) - refute Hubs.fetch_hub!("fly-foo2") == fly - assert Hubs.fetch_hub!("fly-foo2").hub_emoji == "🐈" + refute Hubs.fetch_hub!("team-foo2") == team + assert Hubs.fetch_hub!("team-foo2").hub_emoji == "🐈" end end diff --git a/test/livebook/hubs/docs_test.exs b/test/livebook/intellisense/docs_test.exs similarity index 100% rename from test/livebook/hubs/docs_test.exs rename to test/livebook/intellisense/docs_test.exs diff --git a/test/livebook/live_markdown/export_test.exs b/test/livebook/live_markdown/export_test.exs index 589f8605b..5ea1ec4bb 100644 --- a/test/livebook/live_markdown/export_test.exs +++ b/test/livebook/live_markdown/export_test.exs @@ -1122,16 +1122,16 @@ defmodule Livebook.LiveMarkdown.ExportTest do end test "persists hub id when not default" do - Livebook.Factory.insert_hub(:fly, id: "fly-persisted-id") + Livebook.Factory.insert_hub(:team, id: "team-persisted-id") notebook = %{ Notebook.new() | name: "My Notebook", - hub_id: "fly-persisted-id" + hub_id: "team-persisted-id" } expected_document = """ - + # My Notebook """ diff --git a/test/livebook/live_markdown/import_test.exs b/test/livebook/live_markdown/import_test.exs index 769c1ab93..e08493e46 100644 --- a/test/livebook/live_markdown/import_test.exs +++ b/test/livebook/live_markdown/import_test.exs @@ -721,17 +721,17 @@ defmodule Livebook.LiveMarkdown.ImportTest do end test "imports notebook hub id when exists" do - Livebook.Factory.insert_hub(:fly, id: "fly-persisted-id") + Livebook.Factory.insert_hub(:team, id: "team-persisted-id") markdown = """ - + # My Notebook """ {notebook, []} = Import.notebook_from_livemd(markdown) - assert %Notebook{name: "My Notebook", hub_id: "fly-persisted-id"} = notebook + assert %Notebook{name: "My Notebook", hub_id: "team-persisted-id"} = notebook end test "imports ignores hub id when does not exist" do diff --git a/test/livebook/teams_test.exs b/test/livebook/teams_test.exs index 410dd78cb..ad04045c1 100644 --- a/test/livebook/teams_test.exs +++ b/test/livebook/teams_test.exs @@ -78,12 +78,14 @@ defmodule Livebook.TeamsTest do ) %{ - org: %{id: id, name: name, keys: [%{id: org_key_id}]}, - user: %{id: user_id}, - sessions: [%{token: token}] - } = org_request.user_org + token: token, + user_org: %{ + org: %{id: id, name: name, keys: [%{id: org_key_id}]}, + user: %{id: user_id} + } + } = org_request.user_org_session - assert Teams.get_org_request_completion_data(org) == + assert Teams.get_org_request_completion_data(org, org_request.device_code) == {:ok, %{ "id" => id, @@ -108,12 +110,13 @@ defmodule Livebook.TeamsTest do user_code: org_request.user_code ) - assert Teams.get_org_request_completion_data(org) == {:ok, :awaiting_confirmation} + assert Teams.get_org_request_completion_data(org, org_request.device_code) == + {:ok, :awaiting_confirmation} end test "returns error when org request doesn't exist" do org = build(:org, id: 0) - assert {:transport_error, _embarrassing} = Teams.get_org_request_completion_data(org) + assert {:transport_error, _embarrassing} = Teams.get_org_request_completion_data(org, "") end test "returns error when org request expired", %{node: node} do @@ -135,7 +138,8 @@ defmodule Livebook.TeamsTest do user_code: org_request.user_code ) - assert Teams.get_org_request_completion_data(org) == {:error, :expired} + assert Teams.get_org_request_completion_data(org, org_request.device_code) == + {:error, :expired} end end end diff --git a/test/livebook/web_socket/client_connection_test.exs b/test/livebook/web_socket/client_connection_test.exs deleted file mode 100644 index 2d69cdac1..000000000 --- a/test/livebook/web_socket/client_connection_test.exs +++ /dev/null @@ -1,168 +0,0 @@ -defmodule Livebook.WebSocket.ClientConnectionTest do - use Livebook.EnterpriseIntegrationCase, async: true - - @moduletag :capture_log - - alias Livebook.WebSocket.ClientConnection - - describe "connect" do - test "successfully authenticates the websocket connection", %{url: url, token: token} do - headers = [{"X-Auth-Token", token}] - - assert {:ok, _conn} = ClientConnection.start_link(self(), url, headers) - assert_receive {:connect, :ok, :connected} - end - - test "rejects the websocket with invalid address", %{token: token} do - headers = [{"X-Auth-Token", token}] - - assert {:ok, _conn} = ClientConnection.start_link(self(), "http://localhost:9999", headers) - assert_receive {:connect, :error, "connection refused"} - end - - test "rejects the websocket connection with invalid credentials", %{url: url} do - headers = [{"X-Auth-Token", "foo"}] - - assert {:ok, _conn} = ClientConnection.start_link(self(), url, headers) - - assert_receive {:connect, :error, reason} - assert reason =~ "the given token is invalid" - - assert {:ok, _conn} = ClientConnection.start_link(self(), url) - - assert_receive {:connect, :error, reason} - assert reason =~ "could not get the token from the connection" - end - end - - describe "send_request/2" do - setup %{url: url, token: token} do - headers = [{"X-Auth-Token", token}] - - {:ok, conn} = ClientConnection.start_link(self(), url, headers) - assert_receive {:connect, :ok, :connected} - - {:ok, conn: conn} - end - - test "successfully sends a session request", %{conn: conn, user: %{id: id, email: email}} do - data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) - - assert {:handshake, handshake_response} = ClientConnection.send_request(conn, data) - assert %{id: _, user: %{id: ^id, email: ^email}} = handshake_response - end - - test "successfully sends a create secret message", %{conn: conn} do - data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "Jake Peralta") - assert {:create_secret, _} = ClientConnection.send_request(conn, data) - end - - test "sends a create secret message, but receive a changeset error", %{conn: conn} do - data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "") - assert {:changeset_error, errors} = ClientConnection.send_request(conn, data) - assert "can't be blank" in errors.value - end - end - - describe "reconnect event" do - setup %{test: name} do - start_new_instance(name) - - url = EnterpriseServer.url(name) - token = EnterpriseServer.token(name) - headers = [{"X-Auth-Token", token}] - - assert {:ok, conn} = ClientConnection.start_link(self(), url, headers) - assert_receive {:connect, :ok, :connected} - - on_exit(fn -> stop_new_instance(name) end) - - {:ok, conn: conn} - end - - test "receives the disconnect message from websocket server", %{conn: conn, test: name} do - EnterpriseServer.disconnect(name) - - assert_receive {:connect, :error, "socket closed"} - assert_receive {:connect, :error, "connection refused"} - - assert Process.alive?(conn) - end - - test "reconnects after websocket server is up", %{test: name} do - EnterpriseServer.disconnect(name) - - assert_receive {:connect, :error, "socket closed"} - assert_receive {:connect, :error, "connection refused"} - - Process.sleep(1000) - - # Wait until the server is up again - assert EnterpriseServer.reconnect(name) == :ok - - assert_receive {:connect, :ok, :connected}, 5000 - end - end - - describe "handle events from server" do - setup %{url: url, token: token} do - headers = [{"X-Auth-Token", token}] - - {:ok, conn} = ClientConnection.start_link(self(), url, headers) - assert_receive {:connect, :ok, :connected} - - {:ok, conn: conn} - end - - test "receives a secret_created event", %{node: node} do - name = "MY_SECRET_ID" - value = Livebook.Utils.random_id() - :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - - assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} - end - - test "receives a secret_updated event", %{node: node} do - name = "API_USERNAME" - value = "JakePeralta" - secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - - assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} - - new_value = "ChonkyCat" - :erpc.call(node, Enterprise.Integration, :update_secret, [secret, new_value]) - - assert_receive {:event, :secret_updated, %{name: ^name, value: ^new_value}} - end - - test "receives a secret_deleted event", %{node: node} do - name = "DELETE_ME" - value = "JakePeralta" - secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) - - assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} - - :erpc.call(node, Enterprise.Integration, :delete_secret, [secret]) - - assert_receive {:event, :secret_deleted, %{name: ^name, value: ^value}} - end - - test "receives a user_synchronized event", %{conn: conn, node: node} do - data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) - assert {:handshake, _} = ClientConnection.send_request(conn, data) - - id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"]) - name = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_NAME"]) - - assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: []}} - - secret = :erpc.call(node, Enterprise.Integration, :create_secret, ["SESSION", "123"]) - assert_receive {:event, :secret_created, %{name: "SESSION", value: "123"}} - - assert {:handshake, _} = ClientConnection.send_request(conn, data) - - assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: secrets}} - assert LivebookProto.Secret.new!(name: secret.name, value: secret.value) in secrets - end - end -end diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index 4126d97cd..8782d1b6f 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -206,13 +206,13 @@ defmodule LivebookWeb.HomeLiveTest do end test "render persisted hubs", %{conn: conn} do - fly = insert_hub(:fly, id: "fly-foo-bar-id") + team = insert_hub(:team, id: "team-foo-bar-id") {:ok, _view, html} = live(conn, ~p"/") assert html =~ "HUBS" - assert html =~ fly.hub_name + assert html =~ team.hub_name - Livebook.Hubs.delete_hub("fly-foo-bar-id") + Livebook.Hubs.delete_hub("team-foo-bar-id") end end diff --git a/test/livebook_web/live/hub/edit_live_test.exs b/test/livebook_web/live/hub/edit_live_test.exs index b3ec61044..d97dbacba 100644 --- a/test/livebook_web/live/hub/edit_live_test.exs +++ b/test/livebook_web/live/hub/edit_live_test.exs @@ -71,15 +71,17 @@ defmodule LivebookWeb.Hub.EditLiveTest do |> render_submit(attrs) assert_receive {:secret_created, ^secret} + assert_patch(view, "/hub/#{hub.id}") assert render(view) =~ "Secret created successfully" - assert render(view) =~ secret.name + assert render(element(view, "#hub-secrets-list")) =~ secret.name assert secret in Livebook.Hubs.get_secrets(hub) end test "updates secret", %{conn: conn, hub: hub} do - {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") secret = insert_secret(name: "PERSONAL_EDIT_SECRET", value: "GetTheBonk") + {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") + attrs = %{ secret: %{ name: secret.name, @@ -112,15 +114,17 @@ defmodule LivebookWeb.Hub.EditLiveTest do updated_secret = %{secret | value: new_value} assert_receive {:secret_updated, ^updated_secret} + assert_patch(view, "/hub/#{hub.id}") assert render(view) =~ "Secret updated successfully" - assert render(view) =~ secret.name + assert render(element(view, "#hub-secrets-list")) =~ secret.name assert updated_secret in Livebook.Hubs.get_secrets(hub) end test "deletes secret", %{conn: conn, hub: hub} do - {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") secret = insert_secret(name: "PERSONAL_DELETE_SECRET", value: "GetTheBonk") + {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") + refute view |> element("#secrets-form button[disabled]") |> has_element?() diff --git a/test/livebook_web/live/hub/new/enterprise_component_test.exs b/test/livebook_web/live/hub/new/enterprise_component_test.exs deleted file mode 100644 index 6d185a5b3..000000000 --- a/test/livebook_web/live/hub/new/enterprise_component_test.exs +++ /dev/null @@ -1,168 +0,0 @@ -defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do - use Livebook.EnterpriseIntegrationCase, async: true - @moduletag :capture_log - - import Phoenix.LiveViewTest - - alias Livebook.Hubs - - describe "enterprise" do - test "persists new hub", %{conn: conn, url: url, token: token} do - node = EnterpriseServer.get_node() - id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"]) - Livebook.Hubs.delete_hub("enterprise-#{id}") - - {:ok, view, _html} = live(conn, ~p"/hub") - - assert view - |> element("#enterprise") - |> render_click() =~ "2. Configure your Hub" - - view - |> element("#enterprise-form") - |> render_change(%{ - "enterprise" => %{ - "url" => url, - "token" => token - } - }) - - view - |> element("#connect") - |> render_click() - - assert render(view) =~ to_string(id) - - attrs = %{ - "url" => url, - "token" => token, - "hub_name" => "Enterprise", - "hub_emoji" => "🐈" - } - - view - |> element("#enterprise-form") - |> render_change(%{"enterprise" => attrs}) - - refute view - |> element("#enterprise-form .invalid-feedback") - |> has_element?() - - result = - view - |> element("#enterprise-form") - |> render_submit(%{"enterprise" => attrs}) - - assert {:ok, view, _html} = follow_redirect(result, conn) - - assert render(view) =~ "Hub added successfully" - - hubs_html = view |> element("#hubs") |> render() - assert hubs_html =~ "🐈" - assert hubs_html =~ "/hub/enterprise-#{id}" - assert hubs_html =~ "Enterprise" - end - - test "fails with invalid token", %{test: name, conn: conn} do - start_new_instance(name) - - url = EnterpriseServer.url(name) - - {:ok, view, _html} = live(conn, ~p"/hub") - token = "foo bar baz" - - assert view - |> element("#enterprise") - |> render_click() =~ "2. Configure your Hub" - - view - |> element("#enterprise-form") - |> render_change(%{ - "enterprise" => %{ - "url" => url, - "token" => token - } - }) - - view - |> element("#connect") - |> render_click() - - assert render(view) =~ "the given token is invalid" - refute render(view) =~ "enterprise[hub_name]" - after - stop_new_instance(name) - end - - test "fails to create existing hub", %{test: name, conn: conn} do - start_new_instance(name) - - node = EnterpriseServer.get_node(name) - url = EnterpriseServer.url(name) - token = EnterpriseServer.token(name) - - id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"]) - user = :erpc.call(node, Enterprise.Integration, :create_user, []) - - another_token = - :erpc.call(node, Enterprise.Integration, :generate_user_session_token!, [user]) - - hub = - insert_hub(:enterprise, - id: "enterprise-#{id}", - external_id: id, - url: url, - token: another_token - ) - - {:ok, view, _html} = live(conn, ~p"/hub") - - assert view - |> element("#enterprise") - |> render_click() =~ "2. Configure your Hub" - - view - |> element("#enterprise-form") - |> render_change(%{ - "enterprise" => %{ - "url" => url, - "token" => token - } - }) - - view - |> element("#connect") - |> render_click() - - assert render(view) =~ to_string(id) - - attrs = %{ - "url" => url, - "token" => token, - "hub_name" => "Enterprise", - "hub_emoji" => "🐈" - } - - view - |> element("#enterprise-form") - |> render_change(%{"enterprise" => attrs}) - - refute view - |> element("#enterprise-form .invalid-feedback") - |> has_element?() - - assert view - |> element("#enterprise-form") - |> render_submit(%{"enterprise" => attrs}) =~ "already exists" - - hubs_html = view |> element("#hubs") |> render() - assert hubs_html =~ hub.hub_emoji - assert hubs_html =~ ~p"/hub/#{hub.id}" - assert hubs_html =~ hub.hub_name - - assert Hubs.fetch_hub!(hub.id) == hub - after - stop_new_instance(name) - end - end -end diff --git a/test/livebook_web/live/hub/new_live_test.exs b/test/livebook_web/live/hub/new_live_test.exs index a3df892df..9656008b6 100644 --- a/test/livebook_web/live/hub/new_live_test.exs +++ b/test/livebook_web/live/hub/new_live_test.exs @@ -4,7 +4,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do alias Livebook.Teams.Org import Phoenix.LiveViewTest - @check_completion_data_interval 5000 test "render hub selection cards", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/hub") @@ -17,9 +16,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do describe "new-org" do test "persist a new hub", %{conn: conn, node: node, user: user} do name = "new-org-test" - teams_key = Livebook.Teams.Org.teams_key() - key_hash = Org.key_hash(build(:org, teams_key: teams_key)) - path = ~p"/hub/team-#{name}" {:ok, view, _html} = live(conn, ~p"/hub") @@ -29,7 +25,7 @@ defmodule LivebookWeb.Hub.NewLiveTest do |> render_click() # builds the form data - attrs = %{"org" => %{"name" => name, "teams_key" => teams_key, "emoji" => "🐈"}} + attrs = %{"org" => %{"name" => name, "emoji" => "🐈"}} # finds the form and change data form = element(view, "#new-org-form") @@ -38,11 +34,8 @@ defmodule LivebookWeb.Hub.NewLiveTest do # submits the form render_submit(form, attrs) - # gets the org request by name and key hash - org_request = - :erpc.call(node, Hub.Integration, :get_org_request_by!, [ - [name: name, key_hash: key_hash] - ]) + # gets the org request by name + org_request = :erpc.call(node, Hub.Integration, :get_org_request_by!, [[name: name]]) # check if the form has the url to confirm link_element = element(view, "#new-org-form a") @@ -55,14 +48,18 @@ defmodule LivebookWeb.Hub.NewLiveTest do # check if the page redirected to edit hub page # and check the flash message %{"success" => "Hub added successfully"} = - assert_redirect(view, path, @check_completion_data_interval) + assert_redirect(view, "/hub/team-#{name}?show-key=true", check_completion_data_interval()) + + # access the page and shows the teams key modal + {:ok, view, _html} = live(conn, "/hub/team-#{name}?show-key=true") + assert has_element?(view, "#show-key-modal") + + # access the page when closes the modal + assert {:ok, view, _html} = live(conn, "/hub/team-#{name}") + refute has_element?(view, "#show-key-modal") # checks if the hub is in the sidebar - {:ok, view, _html} = live(conn, path) - hubs_html = view |> element("#hubs") |> render() - assert hubs_html =~ "🐈" - assert hubs_html =~ path - assert hubs_html =~ name + assert_hub(view, "/hub/team-#{name}", name) end end @@ -71,7 +68,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do name = "join-org-test" teams_key = Livebook.Teams.Org.teams_key() key_hash = Org.key_hash(build(:org, teams_key: teams_key)) - path = ~p"/hub/team-#{name}" {:ok, view, _html} = live(conn, ~p"/hub") @@ -112,14 +108,28 @@ defmodule LivebookWeb.Hub.NewLiveTest do # check if the page redirected to edit hub page # and check the flash message %{"success" => "Hub added successfully"} = - assert_redirect(view, path, @check_completion_data_interval) + assert_redirect(view, "/hub/team-#{name}?show-key=true", check_completion_data_interval()) + + # access the page and shows the teams key modal + {:ok, view, _html} = live(conn, "/hub/team-#{name}?show-key=true") + assert has_element?(view, "#show-key-modal") + + # access the page when closes the modal + assert {:ok, view, _html} = live(conn, "/hub/team-#{name}") + refute has_element?(view, "#show-key-modal") # checks if the hub is in the sidebar - {:ok, view, _html} = live(conn, path) - hubs_html = view |> element("#hubs") |> render() - assert hubs_html =~ "🐈" - assert hubs_html =~ path - assert hubs_html =~ name + assert_hub(view, "/hub/team-#{name}", name) end end + + defp check_completion_data_interval(), do: 2000 + + defp assert_hub(view, path, name, emoji \\ "🐈") do + hubs_html = view |> element("#hubs") |> render() + + assert hubs_html =~ emoji + assert hubs_html =~ path + assert hubs_html =~ name + end end diff --git a/test/livebook_web/live/session_live/secrets_component_test.exs b/test/livebook_web/live/session_live/secrets_component_test.exs deleted file mode 100644 index a385e0a84..000000000 --- a/test/livebook_web/live/session_live/secrets_component_test.exs +++ /dev/null @@ -1,200 +0,0 @@ -defmodule LivebookWeb.SessionLive.SecretsComponentTest do - use Livebook.EnterpriseIntegrationCase, async: true - - import Livebook.SessionHelpers - import Phoenix.LiveViewTest - - alias Livebook.Session - alias Livebook.Sessions - - describe "enterprise" do - setup %{test: name} do - start_new_instance(name) - - node = EnterpriseServer.get_node(name) - url = EnterpriseServer.url(name) - token = EnterpriseServer.token(name) - - id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"]) - hub_id = "enterprise-#{id}" - - Livebook.Hubs.subscribe([:connection, :secrets]) - Livebook.Hubs.delete_hub(hub_id) - - enterprise = - insert_hub(:enterprise, - id: hub_id, - external_id: id, - url: url, - token: token - ) - - {:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new()) - Session.set_notebook_hub(session.pid, hub_id) - - on_exit(fn -> - Livebook.Hubs.delete_hub(hub_id) - Session.close(session.pid) - stop_new_instance(name) - end) - - {:ok, enterprise: enterprise, session: session, node: node} - end - - test "creates a secret on Enterprise hub", - %{conn: conn, session: session, enterprise: enterprise} do - id = enterprise.id - - secret = - build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", hub_id: id, readonly: true) - - {:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/secrets") - - attrs = %{ - secret: %{ - name: secret.name, - value: secret.value, - hub_id: enterprise.id - } - } - - form = element(view, ~s{form[phx-submit="save"]}) - render_change(form, attrs) - render_submit(form, attrs) - - assert_receive {:secret_created, ^secret} - assert has_element?(view, "#hub-#{enterprise.id}-secret-#{secret.name}") - end - - test "toggle a secret from Enterprise hub", - %{conn: conn, session: session, enterprise: enterprise, node: node} do - secret = - build(:secret, - name: "POSTGRES_PASSWORD", - value: "postgres", - hub_id: enterprise.id, - readonly: true - ) - - {:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}") - - :erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value]) - assert_receive {:secret_created, ^secret} - - Session.set_secret(session.pid, secret) - assert_session_secret(view, session.pid, secret) - end - - test "adding a missing secret using 'Add secret' button", - %{conn: conn, session: session, enterprise: enterprise} do - secret = - build(:secret, - name: "PGPASS", - value: "postgres", - hub_id: enterprise.id, - readonly: true - ) - - # Subscribe and executes the code to trigger - # the `System.EnvError` exception and outputs the 'Add secret' button - Session.subscribe(session.id) - section_id = insert_section(session.pid) - code = ~s{System.fetch_env!("LB_#{secret.name}")} - cell_id = insert_text_cell(session.pid, section_id, :code, code) - - Session.queue_cell_evaluation(session.pid, cell_id) - assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}} - - # Enters the session to check if the button exists - {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") - expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}" - add_secret_button = element(view, "a[href='#{expected_url}']") - assert has_element?(add_secret_button) - - # Clicks the button and fills the form to create a new secret - # that prefilled the name with the received from exception. - render_click(add_secret_button) - secrets_component = with_target(view, "#secrets-modal") - form_element = element(secrets_component, "form[phx-submit='save']") - assert has_element?(form_element) - attrs = %{value: secret.value, hub_id: enterprise.id} - render_submit(form_element, %{secret: attrs}) - - # Checks we received the secret created event from Enterprise - assert_receive {:secret_created, ^secret} - - # Checks if the secret is persisted - assert secret in Livebook.Hubs.get_secrets() - - # Checks if the secret exists and is inside the session, - # then executes the code cell again and checks if the - # secret value is what we expected. - assert_session_secret(view, session.pid, secret) - Session.queue_cell_evaluation(session.pid, cell_id) - - assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} - - assert output == "\e[32m\"#{secret.value}\"\e[0m" - end - - test "granting access for missing secret using 'Add secret' button", - %{conn: conn, session: session, enterprise: enterprise, node: node} do - secret = - build(:secret, - name: "MYSQL_PASS", - value: "admin", - hub_id: enterprise.id, - readonly: true - ) - - # Subscribe and executes the code to trigger - # the `System.EnvError` exception and outputs the 'Add secret' button - Session.subscribe(session.id) - section_id = insert_section(session.pid) - code = ~s{System.fetch_env!("LB_#{secret.name}")} - cell_id = insert_text_cell(session.pid, section_id, :code, code) - - Session.queue_cell_evaluation(session.pid, cell_id) - assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}} - - # Enters the session to check if the button exists - {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") - expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}" - add_secret_button = element(view, "a[href='#{expected_url}']") - assert has_element?(add_secret_button) - - # Persist the secret from the Enterprise - :erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value]) - - # Grant we receive the event, even with eventually delay - assert_receive {:secret_created, ^secret}, 10_000 - - # Checks if the secret is persisted - assert secret in Livebook.Hubs.get_secrets() - - # Clicks the button and checks if the 'Grant access' banner - # is being shown, so clicks it's button to set the app secret - # to the session, allowing the user to fetches the secret. - render_click(add_secret_button) - secrets_component = with_target(view, "#secrets-modal") - - assert render(secrets_component) =~ - "in #{hub_label(enterprise)}. Allow this session to access it?" - - grant_access_button = element(secrets_component, "button", "Grant access") - render_click(grant_access_button) - - # Checks if the secret exists and is inside the session, - # then executes the code cell again and checks if the - # secret value is what we expected. - assert_session_secret(view, session.pid, secret) - Session.queue_cell_evaluation(session.pid, cell_id) - - assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} - - assert output == "\e[32m\"#{secret.value}\"\e[0m" - end - end -end diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 945a99cc6..8640a9ae5 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -1479,7 +1479,7 @@ defmodule LivebookWeb.SessionLiveTest do describe "hubs" do test "selects the notebook hub", %{conn: conn, session: session} do - hub = insert_hub(:fly) + hub = insert_hub(:team) id = hub.id personal_id = Livebook.Hubs.Personal.id() diff --git a/test/support/enterprise_integration_case.ex b/test/support/enterprise_integration_case.ex deleted file mode 100644 index 2d80e6fcb..000000000 --- a/test/support/enterprise_integration_case.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Livebook.EnterpriseIntegrationCase do - use ExUnit.CaseTemplate - - alias Livebook.EnterpriseServer - - using do - quote do - use LivebookWeb.ConnCase - - @moduletag :enterprise_integration - - import Livebook.EnterpriseIntegrationCase, - only: [start_new_instance: 1, stop_new_instance: 1] - - alias Livebook.EnterpriseServer - end - end - - setup_all do - case EnterpriseServer.start() do - {:ok, _} -> :ok - {:error, {:already_started, _}} -> :ok - end - - {:ok, - url: EnterpriseServer.url(), - token: EnterpriseServer.token(), - user: EnterpriseServer.user(), - node: EnterpriseServer.get_node()} - end - - def start_new_instance(name) do - suffix = Ecto.UUID.generate() |> :erlang.phash2() |> to_string() - app_port = Enum.random(1000..9000) |> to_string() - - {:ok, _} = - EnterpriseServer.start(name, - env: %{ - "DATABASE_URL" => - "postgres://postgres:postgres@localhost:5432/enterprise_integration_#{suffix}" - }, - app_port: app_port - ) - end - - def stop_new_instance(name) do - EnterpriseServer.disconnect(name) - EnterpriseServer.drop_database(name) - end -end diff --git a/test/support/factory.ex b/test/support/factory.ex index 8e743f6c8..91169150b 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -9,38 +9,21 @@ defmodule Livebook.Factory do } end - def build(:fly_metadata) do - :fly |> build() |> Livebook.Hubs.Provider.to_metadata() + def build(:team_metadata) do + :team |> build() |> Livebook.Hubs.Provider.to_metadata() end - def build(:fly) do - %Livebook.Hubs.Fly{ - id: "fly-foo-bar-baz", - hub_name: "My Personal Hub", - hub_emoji: "🚀", - access_token: Livebook.Utils.random_cookie(), - organization_id: Livebook.Utils.random_id(), - organization_type: "PERSONAL", - organization_name: "Foo", - application_id: "foo-bar-baz" - } - end + def build(:team) do + org = build(:org) - def build(:enterprise_metadata) do - :enterprise |> build() |> Livebook.Hubs.Provider.to_metadata() - end - - def build(:enterprise) do - name = "Enteprise #{Livebook.Utils.random_short_id()}" - - %Livebook.Hubs.Enterprise{ - id: "enterprise-#{name}", - hub_name: name, + %Livebook.Hubs.Team{ + id: "team-#{org.name}", + hub_name: org.name, hub_emoji: "🏭", org_id: 1, user_id: 1, org_key_id: 1, - teams_key: Livebook.Utils.random_id(), + teams_key: org.teams_key, session_token: Livebook.Utils.random_cookie() } end diff --git a/test/support/integration/enterprise_server.ex b/test/support/integration/enterprise_server.ex deleted file mode 100644 index b0a1d72e7..000000000 --- a/test/support/integration/enterprise_server.ex +++ /dev/null @@ -1,307 +0,0 @@ -defmodule Livebook.EnterpriseServer do - @moduledoc false - use GenServer - - defstruct [:token, :user, :node, :port, :app_port, :url, :env] - - @name __MODULE__ - @timeout 10_000 - @default_enterprise_dir "../enterprise" - - def available?() do - System.get_env("ENTERPRISE_PATH") != nil or File.exists?(@default_enterprise_dir) - end - - def start(name \\ @name, opts \\ []) do - GenServer.start(__MODULE__, opts, name: name) - end - - def url(name \\ @name) do - GenServer.call(name, :fetch_url, @timeout) - end - - def token(name \\ @name) do - GenServer.call(name, :fetch_token, @timeout) - end - - def user(name \\ @name) do - GenServer.call(name, :fetch_user, @timeout) - end - - def get_node(name \\ @name) do - GenServer.call(name, :fetch_node, @timeout) - end - - def drop_database(name \\ @name) do - app_port = GenServer.call(name, :fetch_port) - state_env = GenServer.call(name, :fetch_env) - - app_port |> env(state_env) |> mix(["ecto.drop", "--quiet"]) - end - - def reconnect(name \\ @name) do - GenServer.cast(name, :reconnect) - end - - def disconnect(name \\ @name) do - GenServer.cast(name, :disconnect) - end - - # GenServer Callbacks - - @impl true - def init(opts) do - state = struct!(__MODULE__, opts) - - {:ok, %{state | node: enterprise_node()}, {:continue, :start_enterprise}} - end - - @impl true - def handle_continue(:start_enterprise, state) do - ensure_app_dir!() - prepare_database(state) - - {:noreply, %{state | port: start_enterprise(state)}} - end - - @impl true - def handle_call(:fetch_token, _from, state) do - state = if state.token, do: state, else: create_enterprise_token(state) - - {:reply, state.token, state} - end - - @impl true - def handle_call(:fetch_user, _from, state) do - state = if state.user, do: state, else: create_enterprise_user(state) - - {:reply, state.user, state} - end - - @impl true - def handle_call(:fetch_url, _from, state) do - state = if state.app_port, do: state, else: %{state | app_port: app_port()} - url = state.url || fetch_url(state) - - {:reply, url, %{state | url: url}} - end - - def handle_call(:fetch_node, _from, state) do - {:reply, state.node, state} - end - - def handle_call(:fetch_port, _from, state) do - port = state.app_port || app_port() - {:reply, port, state} - end - - def handle_call(:fetch_env, _from, state) do - {:reply, state.env, state} - end - - @impl true - def handle_cast(:reconnect, state) do - if state.port do - {:noreply, state} - else - {:noreply, %{state | port: start_enterprise(state)}} - end - end - - def handle_cast(:disconnect, state) do - if state.port do - Port.close(state.port) - end - - {:noreply, %{state | port: nil}} - end - - # Port Callbacks - - @impl true - def handle_info({_port, {:data, message}}, state) do - info(message) - {:noreply, state} - end - - def handle_info({_port, {:exit_status, status}}, _state) do - error("enterprise quit with status #{status}") - System.halt(status) - end - - # Private - - defp create_enterprise_token(state) do - if user = state.user do - token = call_erpc_function(state.node, :generate_user_session_token!, [user]) - %{state | token: token} - else - user = call_erpc_function(state.node, :create_user) - token = call_erpc_function(state.node, :generate_user_session_token!, [user]) - - %{state | user: user, token: token} - end - end - - defp create_enterprise_user(state) do - %{state | user: call_erpc_function(state.node, :create_user)} - end - - defp call_erpc_function(node, function, args \\ []) do - :erpc.call(node, Enterprise.Integration, function, args) - end - - defp start_enterprise(state) do - env = - for {key, value} <- env(state), into: [] do - {String.to_charlist(key), String.to_charlist(value)} - end - - args = [ - "-e", - "spawn(fn -> IO.gets([]) && System.halt(0) end)", - "--sname", - to_string(state.node), - "--cookie", - to_string(Node.get_cookie()), - "-S", - "mix", - "phx.server" - ] - - port = - Port.open({:spawn_executable, elixir_executable()}, [ - :exit_status, - :use_stdio, - :stderr_to_stdout, - :binary, - :hide, - env: env, - cd: app_dir(), - args: args - ]) - - wait_on_start(state, port) - end - - defp fetch_url(state) do - port = state.app_port || app_port() - "http://localhost:#{port}" - end - - defp prepare_database(state) do - :ok = mix(state, ["ecto.drop", "--quiet"]) - :ok = mix(state, ["ecto.create", "--quiet"]) - :ok = mix(state, ["ecto.migrate", "--quiet"]) - end - - defp ensure_app_dir! do - dir = app_dir() - - unless File.exists?(dir) do - IO.puts( - "Unable to find #{dir}, make sure to clone the enterprise repository " <> - "into it to run integration tests or set ENTERPRISE_PATH to its location" - ) - - System.halt(1) - end - end - - defp app_dir do - System.get_env("ENTERPRISE_PATH", @default_enterprise_dir) - end - - defp app_port do - System.get_env("ENTERPRISE_PORT", "4043") - end - - defp debug do - System.get_env("ENTERPRISE_DEBUG", "false") - end - - defp proto do - System.get_env("ENTERPRISE_LIVEBOOK_PROTO_PATH") - end - - defp wait_on_start(state, port) do - url = state.url || fetch_url(state) - - case :httpc.request(:get, {~c"#{url}/public/health", []}, [], []) do - {:ok, _} -> - port - - {:error, _} -> - Process.sleep(10) - wait_on_start(state, port) - end - end - - defp mix(state, args) when is_struct(state) do - state |> env() |> mix(args) - end - - defp mix(env, args) do - cmd_opts = [ - stderr_to_stdout: true, - env: env, - cd: app_dir(), - into: IO.stream(:stdio, :line) - ] - - args = ["--erl", "-elixir ansi_enabled true", "-S", "mix" | args] - - case System.cmd(elixir_executable(), args, cmd_opts) do - {_, 0} -> :ok - _ -> :error - end - end - - defp env(state) do - app_port = state.app_port || app_port() - env(app_port, state.env) - end - - defp env(app_port, state_env) do - env = %{ - "MIX_ENV" => "livebook", - "PORT" => to_string(app_port), - "DEBUG" => debug() - } - - env = if proto(), do: Map.merge(env, %{"LIVEBOOK_PROTO_PATH" => proto()}), else: env - - if state_env do - Map.merge(env, state_env) - else - env - end - end - - defp elixir_executable do - System.find_executable("elixir") - end - - defp enterprise_node do - :"enterprise_#{Livebook.Utils.random_short_id()}@#{hostname()}" - end - - defp hostname do - [nodename, hostname] = - node() - |> Atom.to_charlist() - |> :string.split(~c"@") - - with {:ok, nodenames} <- :erl_epmd.names(hostname), - true <- List.keymember?(nodenames, nodename, 0) do - hostname - else - _ -> - raise "Error" - end - end - - defp info(message), do: log([:blue, message <> "\n"]) - defp error(message), do: log([:red, message <> "\n"]) - defp log(data), do: data |> IO.ANSI.format() |> IO.write() -end diff --git a/test/test_helper.exs b/test/test_helper.exs index bfeff7e8e..da3f691b9 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -55,7 +55,6 @@ ExUnit.start( assert_receive_timeout: if(windows?, do: 2_500, else: 1_500), exclude: [ erl_docs: erl_docs_available?, - enterprise_integration: not Livebook.EnterpriseServer.available?(), teams_integration: not Livebook.TeamsServer.available?() ] )