From 5f6e7176fc0af21192e10b278f421308c25f171b Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Fri, 3 Feb 2023 13:15:46 -0300 Subject: [PATCH] Allow hub secret toggling (#1675) --- lib/livebook/hubs.ex | 17 +- lib/livebook/hubs/broadcasts.ex | 6 +- lib/livebook_web/live/hooks/sidebar_hook.ex | 6 +- lib/livebook_web/live/live_helpers.ex | 4 +- lib/livebook_web/live/session_live.ex | 69 ++++-- .../live/session_live/secrets_component.ex | 228 ++++++++++-------- .../session_live/secrets_component_test.exs | 177 +++++++++++--- test/livebook_web/live/session_live_test.exs | 184 +++++++------- test/support/enterprise_integration_case.ex | 5 +- test/support/session_helpers.ex | 71 ++++++ 10 files changed, 515 insertions(+), 252 deletions(-) create mode 100644 test/support/session_helpers.ex diff --git a/lib/livebook/hubs.ex b/lib/livebook/hubs.ex index 616e2ce61..6c64734c0 100644 --- a/lib/livebook/hubs.ex +++ b/lib/livebook/hubs.ex @@ -82,7 +82,7 @@ defmodule Livebook.Hubs do attributes = struct |> Map.from_struct() |> Map.to_list() :ok = Storage.insert(@namespace, struct.id, attributes) :ok = connect_hub(struct) - :ok = Broadcasts.hubs_metadata_changed() + :ok = Broadcasts.hub_changed() struct end @@ -90,14 +90,23 @@ defmodule Livebook.Hubs do @doc false def delete_hub(id) do with {:ok, hub} <- get_hub(id) do - :ok = Provider.disconnect(hub) + :ok = Broadcasts.hub_changed() :ok = Storage.delete(@namespace, id) - :ok = Broadcasts.hubs_metadata_changed() + :ok = disconnect_hub(hub) end :ok end + defp disconnect_hub(hub) do + Task.Supervisor.start_child(Livebook.TaskSupervisor, fn -> + Process.sleep(30_000) + :ok = Provider.disconnect(hub) + end) + + :ok + end + @doc false def clean_hubs do for hub <- get_hubs(), do: delete_hub(hub.id) @@ -112,7 +121,7 @@ defmodule Livebook.Hubs do Topic `hubs:crud`: - * `:hubs_metadata_changed` + * `:hub_changed` Topic `hubs:connection`: diff --git a/lib/livebook/hubs/broadcasts.ex b/lib/livebook/hubs/broadcasts.ex index 3c734cf54..ad46d3eb4 100644 --- a/lib/livebook/hubs/broadcasts.ex +++ b/lib/livebook/hubs/broadcasts.ex @@ -12,9 +12,9 @@ defmodule Livebook.Hubs.Broadcasts do @doc """ Broadcasts when hubs changed under `hubs:crud` topic """ - @spec hubs_metadata_changed() :: broadcast() - def hubs_metadata_changed do - broadcast(@crud_topic, :hubs_metadata_changed) + @spec hub_changed() :: broadcast() + def hub_changed do + broadcast(@crud_topic, :hub_changed) end @doc """ diff --git a/lib/livebook_web/live/hooks/sidebar_hook.ex b/lib/livebook_web/live/hooks/sidebar_hook.ex index 10552c115..ab5a672d3 100644 --- a/lib/livebook_web/live/hooks/sidebar_hook.ex +++ b/lib/livebook_web/live/hooks/sidebar_hook.ex @@ -24,16 +24,16 @@ defmodule LivebookWeb.SidebarHook do {:halt, put_flash(socket, :info, "Livebook is shutting down. You can close this page.")} end - @connection_events ~w(hub_connected hub_disconnected hubs_metadata_changed)a + @connection_events ~w(hub_connected hub_disconnected hubs_changed)a defp handle_info(event, socket) when event in @connection_events do - {:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())} + {:cont, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())} end @error_events ~w(hub_connection_failed hub_disconnection_failed)a defp handle_info({event, _reason}, socket) when event in @error_events do - {:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())} + {:cont, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())} end defp handle_info(_event, socket), do: {:cont, socket} diff --git a/lib/livebook_web/live/live_helpers.ex b/lib/livebook_web/live/live_helpers.ex index 954272599..ea51a3b85 100644 --- a/lib/livebook_web/live/live_helpers.ex +++ b/lib/livebook_web/live/live_helpers.ex @@ -277,12 +277,14 @@ defmodule LivebookWeb.LiveHelpers do <.switch_checkbox name="likes_cats" label="I very much like cats" + tooltip="Cats" checked={@likes_cats} /> """ def switch_checkbox(assigns) do assigns = assigns |> assign_new(:label, fn -> nil end) + |> assign_new(:tooltip, fn -> nil end) |> assign_new(:disabled, fn -> false end) |> assign_new(:class, fn -> "" end) |> assign( @@ -293,7 +295,7 @@ defmodule LivebookWeb.LiveHelpers do ~H"""
<%= if @label do %> - <%= @label %> + <%= @label %> <% end %>
@@ -219,6 +269,33 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do """ end + defp prefill_assigns(socket) do + secret_name = socket.assigns[:prefill_secret_name] + + assigns = %{ + data: %{"name" => secret_name, "value" => "", "store" => "session"}, + errors: [{"value", {"can't be blank", []}}], + title: title(socket), + grant_access_name: nil, + grant_access_origin: "app", + has_prefill: !is_nil(secret_name) + } + + case Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) do + %Secret{name: name, origin: {:hub, id}} -> + %{assigns | grant_access_name: name, grant_access_origin: id} + + %Secret{name: name, origin: origin} -> + %{assigns | grant_access_name: name, grant_access_origin: to_string(origin)} + + nil -> + assigns + end + end + + defp store(%{origin: {:hub, _id}}), do: "hub" + defp store(%{origin: origin}), do: to_string(origin) + defp origin(%{origin: {:hub, id}}), do: id defp origin(%{origin: origin}), do: to_string(origin) @@ -237,34 +314,11 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do else {:error, %{errors: errors}} -> {:noreply, assign(socket, errors: errors)} - - {:error, socket} -> - {:noreply, socket} end end - def handle_event( - "select_secret", - %{"secret_name" => secret_name, "origin" => "session"}, - socket - ) do - {:noreply, - socket - |> push_patch(to: socket.assigns.return_to) - |> push_secret_selected(secret_name)} - end - - def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => "app"}, socket) do - grant_access(socket.assigns.saved_secrets, secret_name, :app, socket) - - {:noreply, - socket - |> push_patch(to: socket.assigns.return_to) - |> push_secret_selected(secret_name)} - end - - def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => hub_id}, socket) do - grant_access(socket.assigns.saved_secrets, secret_name, {:hub, hub_id}, socket) + def handle_event("select_secret", %{"name" => secret_name} = attrs, socket) do + grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), socket) {:noreply, socket @@ -281,8 +335,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do end end - def handle_event("grant_access", %{"secret_name" => secret_name}, socket) do - grant_access(socket.assigns.saved_secrets, secret_name, :app, socket) + def handle_event("grant_access", %{"name" => secret_name} = attrs, socket) do + grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), socket) {:noreply, socket @@ -296,28 +350,12 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do push_event(socket, "secret_selected", %{select_secret_ref: ref, secret_name: secret_name}) end - defp prefill_secret_name(socket) do - if unavailable_secret?(socket, socket.assigns.prefill_secret_name), - do: socket.assigns.prefill_secret_name, - else: "" - end - - defp unavailable_secret?(_socket, nil), do: false - defp unavailable_secret?(_socket, ""), do: false - - defp unavailable_secret?(socket, preselect_name) do - not session?(socket, preselect_name) and - not app?(socket, preselect_name) and - not hub?(socket, preselect_name) - end - defp title(%{assigns: %{select_secret_ref: nil}}), do: "Add secret" defp title(%{assigns: %{select_secret_options: %{"title" => title}}}), do: title defp title(_), do: "Select secret" - defp build_origin(%{"store" => "session"}), do: :session - defp build_origin(%{"store" => "app"}), do: :app defp build_origin(%{"store" => "hub", "hub_id" => id}), do: {:hub, id} + defp build_origin(%{"store" => store}), do: String.to_existing_atom(store) defp build_attrs(%{"name" => name, "value" => value} = attrs) do %{name: name, value: value, origin: build_origin(attrs)} @@ -332,38 +370,16 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do Livebook.Session.set_secret(socket.assigns.session.pid, secret) end - defp set_secret(_socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do - Livebook.Hubs.create_secret(secret) + defp set_secret(socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do + with :ok <- Livebook.Hubs.create_secret(secret) do + Livebook.Session.set_secret(socket.assigns.session.pid, secret) + end end defp grant_access(secrets, secret_name, origin, socket) do secret = Enum.find(secrets, &(&1.name == secret_name and &1.origin == origin)) - if secret, - do: set_secret(socket, secret), - else: :ok - end - - defp must_grant_access(%{assigns: %{prefill_secret_name: secret_name}} = socket) do - if not session?(socket, secret_name) and - (app?(socket, secret_name) or hub?(socket, secret_name)) do - secret_name - end - end - - defp session?(socket, secret_name) do - Enum.any?(socket.assigns.secrets, &(elem(&1, 0) == secret_name)) - end - - defp app?(socket, secret_name) do - Enum.any?( - socket.assigns.saved_secrets, - &(&1.name == secret_name and &1.origin in [:app, :startup]) - ) - end - - defp hub?(socket, secret_name) do - Enum.any?(socket.assigns.saved_secrets, &(&1.name == secret_name)) + if secret, do: Livebook.Session.set_secret(socket.assigns.session.pid, secret) end defp hubs_options(hubs, hub_id) do diff --git a/test/livebook_web/live/session_live/secrets_component_test.exs b/test/livebook_web/live/session_live/secrets_component_test.exs index 8877c65a0..cdcd1f359 100644 --- a/test/livebook_web/live/session_live/secrets_component_test.exs +++ b/test/livebook_web/live/session_live/secrets_component_test.exs @@ -1,14 +1,21 @@ 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 %{url: url, token: token} do - id = Livebook.Utils.random_short_id() + 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]) @@ -25,38 +32,18 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do {:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new()) on_exit(fn -> + Livebook.Hubs.delete_hub(hub_id) Session.close(session.pid) + stop_new_instance(name) end) - {:ok, enterprise: enterprise, session: session} + {:ok, enterprise: enterprise, session: session, node: node} end - test "shows the connected hubs dropdown", %{ - conn: conn, - session: session, - enterprise: enterprise - } do - secret = build(:secret, name: "LESS_IMPORTANT_SECRET", value: "123", origin: enterprise.id) - {:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id)) - - assert view - |> element(~s{form[phx-submit="save"]}) - |> render_change(%{ - data: %{ - name: secret.name, - value: secret.value, - store: "hub" - } - }) =~ ~s() - end - - test "creates a secret on Enterprise hub", %{ - conn: conn, - session: session, - enterprise: enterprise - } do + 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", origin: id) + secret = build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", origin: {:hub, id}) {:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id)) attrs = %{ @@ -72,8 +59,142 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do render_change(form, attrs) render_submit(form, attrs) + assert_receive {:secret_created, ^secret} assert render(view) =~ "A new secret has been created on your Livebook Enterprise" assert has_element?(view, "#hub-#{enterprise.id}-secret-#{attrs.data.name}-title") + + assert has_element?( + view, + "#hub-#{enterprise.id}-secret-#{secret.name}-title span", + enterprise.hub_emoji + ) + 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", + origin: {:hub, enterprise.id} + ) + + {:ok, view, _html} = live(conn, Routes.session_path(conn, :page, 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", + origin: {:hub, enterprise.id} + ) + + # 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, "/sessions/#{session.id}") + expected_url = Routes.session_path(conn, :secrets, session.id, 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) + data = %{value: secret.value, store: "hub", hub_id: enterprise.id} + render_submit(form_element, %{data: data}) + + # 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", + origin: {:hub, enterprise.id} + ) + + # 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, "/sessions/#{session.id}") + expected_url = Routes.session_path(conn, :secrets, session.id, 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 your Livebook Hub. 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 7eaae9ef2..4eddfb6f9 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -1,6 +1,7 @@ defmodule LivebookWeb.SessionLiveTest do use LivebookWeb.ConnCase, async: true + import Livebook.SessionHelpers import Phoenix.LiveViewTest alias Livebook.{Sessions, Session, Settings, Runtime, Users, FileSystem} @@ -921,7 +922,7 @@ defmodule LivebookWeb.SessionLiveTest do |> Plug.Conn.resp(200, "# My notebook") end) - index_url = url(bypass.port) <> "/index.livemd" + index_url = bypass_url(bypass.port) <> "/index.livemd" {:ok, session} = Sessions.create_session(origin: {:url, index_url}) assert {:error, {:live_redirect, %{to: "/sessions/" <> session_id}}} = @@ -949,7 +950,7 @@ defmodule LivebookWeb.SessionLiveTest do |> Plug.Conn.resp(200, "# My notebook") end) - index_url = url(bypass.port) <> "/index.livemd" + index_url = bypass_url(bypass.port) <> "/index.livemd" {:ok, session} = Sessions.create_session(origin: {:url, index_url}) assert {:error, {:live_redirect, %{to: "/sessions/" <> session_id}}} = @@ -969,7 +970,7 @@ defmodule LivebookWeb.SessionLiveTest do Plug.Conn.resp(conn, 500, "Error") end) - index_url = url(bypass.port) <> "/index.livemd" + index_url = bypass_url(bypass.port) <> "/index.livemd" {:ok, session} = Sessions.create_session(origin: {:url, index_url}) @@ -1108,7 +1109,7 @@ defmodule LivebookWeb.SessionLiveTest do assert app_secret in Livebook.Secrets.get_secrets() end - test "shows the 'Add secret' button for unavailable secrets", %{conn: conn, session: session} do + test "shows the 'Add secret' button for missing secrets", %{conn: conn, session: session} do secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", origin: :session) Session.subscribe(session.id) section_id = insert_section(session.pid) @@ -1125,9 +1126,12 @@ defmodule LivebookWeb.SessionLiveTest do |> has_element?() end - test "adding an unavailable secret using 'Add secret' button", + test "adding a missing secret using 'Add secret' button", %{conn: conn, session: session} do secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", origin: :session) + + # 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}")} @@ -1136,24 +1140,27 @@ defmodule LivebookWeb.SessionLiveTest do 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, "/sessions/#{session.id}") - expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name) - add_secret_button = element(view, "a[href='#{expected_url}']") - assert has_element?(add_secret_button) - render_click(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) render_submit(form_element, %{data: %{value: secret.value, store: "session"}}) - assert_session_secret(view, session.pid, secret) + # Checks if the secret isn't an app secret refute secret in Livebook.Secrets.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, @@ -1165,6 +1172,9 @@ defmodule LivebookWeb.SessionLiveTest do test "granting access for unavailable secret using 'Add secret' button", %{conn: conn, session: session} do secret = insert_secret(name: "UNAVAILABLESECRET", value: "123456") + + # 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}")} @@ -1173,22 +1183,30 @@ defmodule LivebookWeb.SessionLiveTest do 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, "/sessions/#{session.id}") - expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name) - add_secret_button = element(view, "a[href='#{expected_url}']") - assert has_element?(add_secret_button) + + # Checks if the secret is persisted assert secret in Livebook.Secrets.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 your Livebook app. 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, @@ -1203,8 +1221,73 @@ defmodule LivebookWeb.SessionLiveTest do {:ok, view, _} = live(conn, "/sessions/#{session.id}") - assert render(view) =~ secret.name - assert render(view) =~ secret.value + # 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) + + # Sets the secret directly + Session.set_secret(session.pid, secret) + + # 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 unavailable startup secret using 'Add secret' button", + %{conn: conn, session: session} do + secret = build(:secret, name: "MYSTARTUPSECRET", value: "ChonkyCat", origin: :startup) + Livebook.Secrets.set_temporary_secrets([secret]) + + # 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, "/sessions/#{session.id}") + expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name) + add_secret_button = element(view, "a[href='#{expected_url}']") + assert has_element?(add_secret_button) + + # Checks if the secret is persisted + assert secret in Livebook.Secrets.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 your Livebook app. 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 @@ -1303,71 +1386,4 @@ defmodule LivebookWeb.SessionLiveTest do assert output == "\e[32m\"#{String.replace(initial_os_path, "\\", "\\\\")}\"\e[0m" end end - - # Helpers - - defp wait_for_session_update(session_pid) do - # This call is synchronous, so it gives the session time - # for handling the previously sent change messages. - Session.get_data(session_pid) - :ok - end - - # Utils for sending session requests, waiting for the change to be applied - # and retrieving new ids if applicable. - - defp insert_section(session_pid) do - Session.insert_section(session_pid, 0) - %{notebook: %{sections: [section]}} = Session.get_data(session_pid) - section.id - end - - defp insert_text_cell(session_pid, section_id, type, content \\ "") do - Session.insert_cell(session_pid, section_id, 0, type, %{source: content}) - %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid) - cell.id - end - - defp evaluate_setup(session_pid) do - Session.queue_cell_evaluation(session_pid, "setup") - assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}} - end - - defp insert_cell_with_output(session_pid, section_id, output) do - code = - quote do - send( - Process.group_leader(), - {:io_request, self(), make_ref(), {:livebook_put_output, unquote(Macro.escape(output))}} - ) - end - |> Macro.to_string() - - 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, _, _}} - cell_id - end - - defp url(port), do: "http://localhost:#{port}" - - defp close_session_by_id(session_id) do - {:ok, session} = Sessions.fetch_session(session_id) - Session.close(session.pid) - end - - defp assert_session_secret(view, session_pid, secret) do - selector = - case secret do - %{name: name, origin: :session} -> "#session-secret-#{name}-title" - %{name: name, origin: :app} -> "#app-secret-#{name}-title" - %{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-title" - end - - assert has_element?(view, selector) - secrets = Session.get_data(session_pid).secrets - - assert Map.has_key?(secrets, secret.name) - assert secrets[secret.name] == secret.value - end end diff --git a/test/support/enterprise_integration_case.ex b/test/support/enterprise_integration_case.ex index 4c733d9fb..2d80e6fcb 100644 --- a/test/support/enterprise_integration_case.ex +++ b/test/support/enterprise_integration_case.ex @@ -23,7 +23,10 @@ defmodule Livebook.EnterpriseIntegrationCase do end {:ok, - url: EnterpriseServer.url(), token: EnterpriseServer.token(), user: EnterpriseServer.user()} + url: EnterpriseServer.url(), + token: EnterpriseServer.token(), + user: EnterpriseServer.user(), + node: EnterpriseServer.get_node()} end def start_new_instance(name) do diff --git a/test/support/session_helpers.ex b/test/support/session_helpers.ex new file mode 100644 index 000000000..2efb9eb1d --- /dev/null +++ b/test/support/session_helpers.ex @@ -0,0 +1,71 @@ +defmodule Livebook.SessionHelpers do + @moduledoc false + + alias Livebook.{Session, Sessions} + + import ExUnit.Assertions + import Phoenix.LiveViewTest + + def wait_for_session_update(session_pid) do + # This call is synchronous, so it gives the session time + # for handling the previously sent change messages. + Session.get_data(session_pid) + :ok + end + + def evaluate_setup(session_pid) do + Session.queue_cell_evaluation(session_pid, "setup") + assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}} + end + + def insert_cell_with_output(session_pid, section_id, output) do + code = + quote do + send( + Process.group_leader(), + {:io_request, self(), make_ref(), {:livebook_put_output, unquote(Macro.escape(output))}} + ) + end + |> Macro.to_string() + + 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, _, _}} + cell_id + end + + def bypass_url(port), do: "http://localhost:#{port}" + + def close_session_by_id(session_id) do + {:ok, session} = Sessions.fetch_session(session_id) + Session.close(session.pid) + end + + def insert_section(session_pid) do + Session.insert_section(session_pid, 0) + %{notebook: %{sections: [section]}} = Session.get_data(session_pid) + section.id + end + + def insert_text_cell(session_pid, section_id, type, content \\ " ") do + Session.insert_cell(session_pid, section_id, 0, type, %{source: content}) + %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid) + cell.id + end + + def assert_session_secret(view, session_pid, secret) do + selector = + case secret do + %{name: name, origin: :session} -> "#session-secret-#{name}-title" + %{name: name, origin: :app} -> "#app-secret-#{name}-title" + %{name: name, origin: :startup} -> "#startup-secret-#{name}-title" + %{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-title" + end + + assert has_element?(view, selector) + secrets = Session.get_data(session_pid).secrets + + assert Map.has_key?(secrets, secret.name) + assert secrets[secret.name] == secret.value + end +end