From 0d62b507b32b8a2ef2463495a760e1fa3639795c Mon Sep 17 00:00:00 2001 From: Alexandre de Souza Date: Fri, 9 May 2025 11:33:00 -0300 Subject: [PATCH] Add tests --- test/livebook_teams/web/admin_live_test.exs | 173 ++++++++++++-- .../web/app_session_live_test.exs | 205 +++++++++++++++++ test/livebook_teams/web/apps_live_test.exs | 212 ++++++++++++++++++ 3 files changed, 572 insertions(+), 18 deletions(-) create mode 100644 test/livebook_teams/web/app_session_live_test.exs create mode 100644 test/livebook_teams/web/apps_live_test.exs diff --git a/test/livebook_teams/web/admin_live_test.exs b/test/livebook_teams/web/admin_live_test.exs index f171dd657..722b9f72c 100644 --- a/test/livebook_teams/web/admin_live_test.exs +++ b/test/livebook_teams/web/admin_live_test.exs @@ -4,28 +4,165 @@ defmodule LivebookWeb.Integration.AdminLiveTest do import Phoenix.LiveViewTest - setup %{teams_auth: teams_auth} do - Application.put_env(:livebook, :teams_auth, teams_auth) - on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end) + describe "topbar" do + setup %{teams_auth: teams_auth} do + Application.put_env(:livebook, :teams_auth, teams_auth) + on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end) - :ok - end - - for page <- ["/", "/settings", "/learn", "/hub", "/apps-dashboard"] do - @tag page: page, teams_auth: :online - test "GET #{page} shows the app server instance topbar warning", %{conn: conn, page: page} do - {:ok, view, _} = live(conn, page) - - assert render(view) =~ - "This Livebook instance has been configured for notebook deployment and is in read-only mode." + :ok end - @tag page: page, teams_auth: :offline - test "GET #{page} shows the offline hub topbar warning", %{conn: conn, page: page} do - {:ok, view, _} = live(conn, page) + for page <- ["/", "/settings", "/learn", "/hub", "/apps-dashboard"] do + @tag page: page, teams_auth: :online + test "GET #{page} shows the app server instance topbar warning", %{conn: conn, page: page} do + {:ok, view, _} = live(conn, page) - assert render(view) =~ - "You are running an offline Workspace for deployment. You cannot modify its settings." + assert render(view) =~ + "This Livebook instance has been configured for notebook deployment and is in read-only mode." + end + + @tag page: page, teams_auth: :offline + test "GET #{page} shows the offline hub topbar warning", %{conn: conn, page: page} do + {:ok, view, _} = live(conn, page) + + assert render(view) =~ + "You are running an offline Workspace for deployment. You cannot modify its settings." + end + end + end + + describe "authorization" do + setup %{conn: conn, node: node} do + Livebook.Teams.Broadcasts.subscribe([:agents, :app_server]) + Livebook.Apps.subscribe() + + {_agent_key, org, deployment_group, team} = create_agent_team_hub(node) + + # we wait until the agent_connected is received by livebook + hub_id = team.id + deployment_group_id = to_string(deployment_group.id) + org_id = to_string(org.id) + + assert_receive {:agent_joined, + %{ + hub_id: ^hub_id, + org_id: ^org_id, + deployment_group_id: ^deployment_group_id + }} + + start_supervised!( + {Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id} + ) + + {conn, code} = authenticate_user_on_teams(conn, node, team) + + {:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team} + end + + test "renders unauthorized admin page if user doesn't have full access", + %{conn: conn, node: node, code: code} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :apps, + prefixes: ["dev-"], + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + assert conn + |> get(~p"/settings") + |> html_response(401) =~ "Not authorized" + end + + test "shows admin page if user have full access", + %{conn: conn, node: node, code: code} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :app_server, + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + {:ok, _view, html} = live(conn, ~p"/settings") + assert html =~ "System settings" + end + + test "renders unauthorized if loses the access in real-time", + %{conn: conn, node: node, code: code} = context do + {:ok, deployment_group} = + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :app_server, + oidc_provider: oidc_provider, + deployment_group: deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + {:ok, view, _html} = live(conn, ~p"/settings") + assert render(view) =~ "System settings" + + erpc_call(node, :update_authorization_group, [ + authorization_group, + %{access_type: :apps, prefixes: ["ops-"]} + ]) + + id = to_string(deployment_group.id) + assert_receive {:server_authorization_updated, %{id: ^id}} + + # If you lose access to the app server, we will redirect to "/" + assert_redirect view, ~p"/" + + # And it will redirect to "/apps" + {:ok, view, _html} = live(conn, ~p"/apps") + assert render(view) =~ "No apps running." end end end diff --git a/test/livebook_teams/web/app_session_live_test.exs b/test/livebook_teams/web/app_session_live_test.exs new file mode 100644 index 000000000..785cf9e04 --- /dev/null +++ b/test/livebook_teams/web/app_session_live_test.exs @@ -0,0 +1,205 @@ +defmodule LivebookWeb.Integration.AppSessionLiveTest do + use Livebook.TeamsIntegrationCase, async: false + + import Phoenix.LiveViewTest + + describe "authorized apps" do + setup %{conn: conn, node: node} do + Livebook.Teams.Broadcasts.subscribe([:agents, :app_deployments, :app_server]) + Livebook.Apps.subscribe() + + {_agent_key, org, deployment_group, team} = create_agent_team_hub(node) + + # we wait until the agent_connected is received by livebook + hub_id = team.id + deployment_group_id = to_string(deployment_group.id) + org_id = to_string(org.id) + + assert_receive {:agent_joined, + %{ + hub_id: ^hub_id, + org_id: ^org_id, + deployment_group_id: ^deployment_group_id + }} + + start_supervised!( + {Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id} + ) + + {conn, code} = authenticate_user_on_teams(conn, node, team) + + {:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team} + end + + @tag :tmp_dir + test "shows app if user doesn't have full access", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :apps, + prefixes: ["dev-"], + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slug = "dev-app" + pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new()) + + {:ok, _view, html} = live(conn, ~p"/apps/#{slug}/sessions/#{session_id}") + assert html =~ "LivebookApp:#{slug}" + end + + @tag :tmp_dir + test "shows app if user have full access", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :app_server, + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slugs = ~w(mkt-app sales-app opt-app) + + for slug <- slugs do + pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new()) + + {:ok, _view, html} = live(conn, ~p"/apps/#{slug}/sessions/#{session_id}") + assert html =~ "LivebookApp:#{slug}" + end + end + + @tag :tmp_dir + test "renders unauthorized if loses the access in real-time", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + {:ok, deployment_group} = + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :apps, + prefixes: ["mkt-"], + oidc_provider: oidc_provider, + deployment_group: deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slug = "mkt-analytics" + pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new()) + path = ~p"/apps/#{slug}/sessions/#{session_id}" + + {:ok, view, _html} = live(conn, path) + assert render(view) =~ "LivebookApp:#{slug}" + + erpc_call(node, :update_authorization_group, [authorization_group, %{prefixes: ["ops-"]}]) + + id = to_string(deployment_group.id) + + assert_receive {:server_authorization_updated, %{id: ^id}} + assert_receive {:app_deployment_updated, %{slug: ^slug}} + assert_redirect view, path + + {:ok, view, _html} = live(conn, path) + assert render(view) =~ "Not authorized" + end + end + + defp deploy_app(slug, team, org, deployment_group, tmp_dir, node) do + source = """ + + + # LivebookApp:#{slug} + + ```elixir + ``` + """ + + {notebook, %{warnings: []}} = Livebook.LiveMarkdown.notebook_from_livemd(source) + + files_dir = Livebook.FileSystem.File.local(tmp_dir) + + {:ok, %Livebook.Teams.AppDeployment{file: zip_content} = app_deployment} = + Livebook.Teams.AppDeployment.new(notebook, files_dir) + + secret_key = Livebook.Teams.derive_key(team.teams_key) + encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key) + + app_deployment_id = + erpc_call(node, :upload_app_deployment, [ + org, + deployment_group, + app_deployment, + encrypted_content, + # broadcast? + true + ]).id + + app_deployment_id = to_string(app_deployment_id) + assert_receive {:app_deployment_started, %{id: ^app_deployment_id}} + + assert_receive {:app_created, %{pid: pid, slug: ^slug}} + + assert_receive {:app_updated, + %{ + slug: ^slug, + sessions: [%{app_status: %{execution: :executed, lifecycle: :active}}] + }} + + on_exit(fn -> + if Process.alive?(pid) do + Livebook.App.close(pid) + end + end) + + pid + end +end diff --git a/test/livebook_teams/web/apps_live_test.exs b/test/livebook_teams/web/apps_live_test.exs new file mode 100644 index 000000000..2830023c9 --- /dev/null +++ b/test/livebook_teams/web/apps_live_test.exs @@ -0,0 +1,212 @@ +defmodule LivebookWeb.Integration.AppsLiveTest do + use Livebook.TeamsIntegrationCase, async: false + + describe "authorized apps" do + setup %{conn: conn, node: node} do + Livebook.Teams.Broadcasts.subscribe([:agents, :app_deployments, :app_server]) + Livebook.Apps.subscribe() + + {_agent_key, org, deployment_group, team} = create_agent_team_hub(node) + + # we wait until the agent_connected is received by livebook + hub_id = team.id + deployment_group_id = to_string(deployment_group.id) + org_id = to_string(org.id) + + assert_receive {:agent_joined, + %{ + hub_id: ^hub_id, + org_id: ^org_id, + deployment_group_id: ^deployment_group_id + }} + + start_supervised!( + {Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id} + ) + + {conn, code} = authenticate_user_on_teams(conn, node, team) + + {:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team} + end + + @tag :tmp_dir + test "shows one app if user doesn't have full access", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :apps, + prefixes: ["dev-"], + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slug = "dev-app" + + deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + + html = + conn + |> get(~p"/apps") + |> html_response(200) + + refute html =~ "No apps running." + assert html =~ slug + end + + @tag :tmp_dir + test "shows all apps if user have full access", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :app_server, + oidc_provider: oidc_provider, + deployment_group: context.deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slugs = ~w(mkt-app sales-app opt-app) + + for slug <- slugs do + deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + end + + html = + conn + |> get(~p"/apps") + |> html_response(200) + + refute html =~ "No apps running." + + for slug <- slugs do + assert html =~ slug + end + end + + @tag :tmp_dir + test "updates the apps list in real-time", + %{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do + {:ok, deployment_group} = + erpc_call(node, :toggle_groups_authorization, [context.deployment_group]) + + oidc_provider = erpc_call(node, :create_oidc_provider, [context.org]) + + authorization_group = + erpc_call(node, :create_authorization_group, [ + %{ + group_name: "marketing", + access_type: :apps, + prefixes: ["mkt-"], + oidc_provider: oidc_provider, + deployment_group: deployment_group + } + ]) + + erpc_call(node, :update_user_info_groups, [ + code, + [ + %{ + "provider_id" => to_string(oidc_provider.id), + "group_name" => authorization_group.group_name + } + ] + ]) + + slug = "marketing-app" + + deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node) + + assert conn + |> get(~p"/apps") + |> html_response(200) =~ "No apps running." + + {:ok, %{groups_auth: false} = deployment_group} = + erpc_call(node, :toggle_groups_authorization, [deployment_group]) + + id = to_string(deployment_group.id) + assert_receive {:server_authorization_updated, %{id: ^id, groups_auth: false}} + + assert conn + |> get(~p"/apps") + |> html_response(200) =~ slug + end + end + + defp deploy_app(slug, team, org, deployment_group, tmp_dir, node) do + source = """ + + + # LivebookApp:#{slug} + + ```elixir + ``` + """ + + {notebook, %{warnings: []}} = Livebook.LiveMarkdown.notebook_from_livemd(source) + + files_dir = Livebook.FileSystem.File.local(tmp_dir) + + {:ok, %Livebook.Teams.AppDeployment{file: zip_content} = app_deployment} = + Livebook.Teams.AppDeployment.new(notebook, files_dir) + + secret_key = Livebook.Teams.derive_key(team.teams_key) + encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key) + + app_deployment_id = + erpc_call(node, :upload_app_deployment, [ + org, + deployment_group, + app_deployment, + encrypted_content, + # broadcast? + true + ]).id + + app_deployment_id = to_string(app_deployment_id) + assert_receive {:app_deployment_started, %{id: ^app_deployment_id}} + + assert_receive {:app_created, %{pid: pid, slug: ^slug}} + + assert_receive {:app_updated, + %{ + slug: ^slug, + sessions: [%{app_status: %{execution: :executed, lifecycle: :active}}] + }} + + on_exit(fn -> + if Process.alive?(pid) do + Livebook.App.close(pid) + end + end) + end +end