@@ -1436,6 +1447,10 @@ defmodule LivebookWeb.SessionLive do
|> put_flash(:info, "An existing secret has been updated on your Livebook Enterprise")}
end
+ def handle_info(:hubs_changed, socket) do
+ {:noreply, assign(socket, saved_secrets: get_saved_secrets())}
+ end
+
def handle_info({:error, error}, socket) do
message = error |> to_string() |> upcase_first()
@@ -2273,11 +2288,21 @@ defmodule LivebookWeb.SessionLive do
:ok
end
- defp is_secret_on_session?(secret, secrets) do
- Map.has_key?(secrets, secret.name)
+ defp secret_toggled?(secret, secrets) do
+ Map.has_key?(secrets, secret.name) and secrets[secret.name] == secret.value
end
defp get_saved_secrets do
Enum.sort(Hubs.get_secrets() ++ Secrets.get_secrets())
end
+
+ defp secret_label(%{origin: {:hub, id}}, hubs), do: fetch_hub!(id, hubs).emoji
+ defp secret_label(_, _), do: nil
+
+ defp secret_tooltip(%{origin: {:hub, id}}, hubs), do: fetch_hub!(id, hubs).name
+ defp secret_tooltip(_, _), do: nil
+
+ defp fetch_hub!(id, hubs) do
+ Enum.find(hubs, &(&1.id == id)) || raise "unknown hub id: #{id}"
+ end
end
diff --git a/lib/livebook_web/live/session_live/secrets_component.ex b/lib/livebook_web/live/session_live/secrets_component.ex
index 228e7e4ec..9b91371e4 100644
--- a/lib/livebook_web/live/session_live/secrets_component.ex
+++ b/lib/livebook_web/live/session_live/secrets_component.ex
@@ -10,22 +10,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|> assign(assigns)
|> assign(hubs: Livebook.Hubs.get_hubs([:secrets]))
- prefill_form = prefill_secret_name(socket)
-
- socket =
- if socket.assigns[:data] do
- socket
- else
- assign(socket,
- data: %{"name" => prefill_form, "value" => "", "store" => "session"},
- errors: [{"value", {"can't be blank", []}}],
- title: title(socket),
- grant_access: must_grant_access(socket),
- has_prefill: prefill_form != ""
- )
- end
-
- {:ok, socket}
+ {:ok, assign(socket, prefill_assigns(socket))}
end
@impl true
@@ -35,8 +20,12 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<%= @title %>
- <%= if @grant_access do %>
- <.grant_access_message grant_access={@grant_access} target={@myself} />
+ <%= if @grant_access_name do %>
+ <.grant_access_message
+ secret_name={@grant_access_name}
+ secret_origin={@grant_access_origin}
+ target={@myself}
+ />
<% end %>
<%= if @select_secret_ref do %>
@@ -49,7 +38,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<%= for {secret_name, _} <- Enum.sort(@secrets) do %>
<.secret_with_badge
secret_name={secret_name}
- origin="session"
+ secret_origin="session"
stored="Session"
action="select_secret"
active={secret_name == @prefill_secret_name}
@@ -59,7 +48,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<%= for secret <- @saved_secrets do %>
<.secret_with_badge
secret_name={secret.name}
- origin={origin(secret)}
+ secret_store={store(secret)}
+ secret_origin={origin(secret)}
stored={stored(secret)}
action="select_secret"
active={false}
@@ -150,6 +140,44 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
"""
end
+ defp secret_with_badge(%{secret_store: "hub"} = assigns) do
+ ~H"""
+
+ <%= @secret_name %>
+
+ <%= if @active do %>
+
+ <% end %>
+ <%= @stored %>
+
+
+ """
+ end
+
defp secret_with_badge(assigns) do
~H"""
@@ -198,20 +226,42 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
icon="error-warning-fill"
class="align-middle text-2xl flex text-gray-100 rounded-lg py-2"
/>
-
- There is a secret named
- <%= @grant_access %>
- in your Livebook app. Allow this session to access it?
-
+ <%= if @secret_origin in ["app", "startup"] do %>
+
+ There is a secret named
+ <%= @secret_name %>
+ in your Livebook app. Allow this session to access it?
+
+ <% else %>
+
+ There is a secret named
+ <%= @secret_name %>
+ in your Livebook Hub. Allow this session to access it?
+
+ <% end %>
-
+ <%= if @secret_origin in ["app", "startup"] do %>
+
+ <% else %>
+
+ <% 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