diff --git a/.formatter.exs b/.formatter.exs index e945e12b9..05a9ef928 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ - import_deps: [:phoenix], + import_deps: [:phoenix, :ecto], plugins: [Phoenix.LiveView.HTMLFormatter], inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] ] diff --git a/lib/livebook/ecto_types/hex_color.ex b/lib/livebook/ecto_types/hex_color.ex new file mode 100644 index 000000000..398ac4aed --- /dev/null +++ b/lib/livebook/ecto_types/hex_color.ex @@ -0,0 +1,70 @@ +defmodule Livebook.EctoTypes.HexColor do + @moduledoc false + use Ecto.Type + + def type, do: :string + + def load(value), do: {:ok, value} + def dump(value), do: {:ok, value} + + def cast(value) do + if valid?(value) do + {:ok, value} + else + {:error, message: "not a valid color"} + end + end + + @doc """ + Returns a random hex color for a user. + + ## Options + + * `:except` - a list of colors to omit + """ + def random(opts \\ []) do + colors = [ + # red + "#F87171", + # yellow + "#FBBF24", + # green + "#6EE7B7", + # blue + "#60A5FA", + # purple + "#A78BFA", + # pink + "#F472B6", + # salmon + "#FA8072", + # mat green + "#9ED9CC" + ] + + except = opts[:except] || [] + colors = colors -- except + + Enum.random(colors) + end + + @doc """ + Validates if the given hex color is the correct format + + ## Examples + + iex> Livebook.EctoTypes.HexColor.valid?("#111111") + true + + iex> Livebook.EctoTypes.HexColor.valid?("#ABC123") + true + + iex> Livebook.EctoTypes.HexColor.valid?("ABCDEF") + false + + iex> Livebook.EctoTypes.HexColor.valid?("#111") + false + """ + @spec valid?(String.t()) :: boolean() + def valid?(hex_color), do: hex_color =~ ~r/^#[0-9a-fA-F]{6}$/ +end diff --git a/lib/livebook/hubs/fly.ex b/lib/livebook/hubs/fly.ex index 49414b19d..01d8b0f4a 100644 --- a/lib/livebook/hubs/fly.ex +++ b/lib/livebook/hubs/fly.ex @@ -1,31 +1,104 @@ defmodule Livebook.Hubs.Fly do @moduledoc false - defstruct [ - :id, - :access_token, - :hub_name, - :hub_color, - :organization_id, - :organization_type, - :organization_name, - :application_id - ] + use Ecto.Schema + import Ecto.Changeset + + alias Livebook.Hubs @type t :: %__MODULE__{ id: Livebook.Utils.id(), access_token: String.t(), hub_name: String.t(), - hub_color: Livebook.Users.User.hex_color(), + hub_color: String.t(), organization_id: String.t(), organization_type: String.t(), organization_name: String.t(), application_id: String.t() } - def save_fly(fly, params) do - fly = %{fly | hub_name: params["hub_name"], hub_color: params["hub_color"]} + embedded_schema do + field :access_token, :string + field :hub_name, :string + field :hub_color, Livebook.EctoTypes.HexColor + field :organization_id, :string + field :organization_type, :string + field :organization_name, :string + field :application_id, :string + end - Livebook.Hubs.save_hub(fly) + @fields ~w( + access_token + hub_name + hub_color + 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 + 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, add_error(changeset, :application_id, "already exists")} + 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, add_error(changeset, :application_id, "does not exists")} + end + end + + def 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 diff --git a/lib/livebook/users.ex b/lib/livebook/users.ex index 604f3e6d2..a96eee642 100644 --- a/lib/livebook/users.ex +++ b/lib/livebook/users.ex @@ -3,13 +3,37 @@ defmodule Livebook.Users do alias Livebook.Users.User + @doc """ + Returns an `%Ecto.Changeset{}` for tracking user changes. + """ + @spec change_user(User.t(), map()) :: Ecto.Changeset.t() + def change_user(%User{} = user, attrs \\ %{}) do + user + |> User.changeset(attrs) + |> Map.put(:action, :validate) + end + + @doc """ + Updates an User from given changeset. + + With success, notifies interested processes about user data change. + Otherwise, it will return an error tuple with changeset. + """ + @spec update_user(Ecto.Changeset.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} + def update_user(%Ecto.Changeset{} = changeset) do + with {:ok, user} <- Ecto.Changeset.apply_action(changeset, :update) do + broadcast_change(user) + {:ok, user} + end + end + @doc """ Notifies interested processes about user data change. Broadcasts `{:user_change, user}` message under the `"user:{id}"` topic. """ @spec broadcast_change(User.t()) :: :ok - def broadcast_change(user) do + def broadcast_change(%User{} = user) do broadcast_user_message(user.id, {:user_change, user}) :ok end diff --git a/lib/livebook/users/user.ex b/lib/livebook/users/user.ex index 9b1017646..4a317b620 100644 --- a/lib/livebook/users/user.ex +++ b/lib/livebook/users/user.ex @@ -9,7 +9,8 @@ defmodule Livebook.Users.User do # can provide data like name and cursor color # to improve visibility during collaboration. - defstruct [:id, :name, :hex_color] + use Ecto.Schema + import Ecto.Changeset alias Livebook.Utils @@ -22,6 +23,11 @@ defmodule Livebook.Users.User do @type id :: Utils.id() @type hex_color :: String.t() + embedded_schema do + field :name, :string + field :hex_color, Livebook.EctoTypes.HexColor + end + @doc """ Generates a new user. """ @@ -30,79 +36,13 @@ defmodule Livebook.Users.User do %__MODULE__{ id: Utils.random_id(), name: nil, - hex_color: random_hex_color() + hex_color: Livebook.EctoTypes.HexColor.random() } end - @doc """ - Validates `attrs` and returns an updated user. - - In case of validation errors `{:error, errors, user}` tuple - is returned, where `user` is partially updated by using - only the valid attributes. - """ - @spec change(t(), %{binary() => any()}) :: - {:ok, t()} | {:error, list({atom(), String.t()}), t()} - def change(user, attrs \\ %{}) do - {user, []} - |> change_name(attrs) - |> change_hex_color(attrs) - |> case do - {user, []} -> {:ok, user} - {user, errors} -> {:error, errors, user} - end - end - - defp change_name({user, errors}, %{"name" => ""}) do - {%{user | name: nil}, errors} - end - - defp change_name({user, errors}, %{"name" => name}) do - {%{user | name: name}, errors} - end - - defp change_name({user, errors}, _attrs), do: {user, errors} - - defp change_hex_color({user, errors}, %{"hex_color" => hex_color}) do - if Utils.valid_hex_color?(hex_color) do - {%{user | hex_color: hex_color}, errors} - else - {user, [{:hex_color, "not a valid color"} | errors]} - end - end - - defp change_hex_color({user, errors}, _attrs), do: {user, errors} - - @doc """ - Returns a random hex color for a user. - - ## Options - - * `:except` - a list of colors to omit - """ - def random_hex_color(opts \\ []) do - colors = [ - # red - "#F87171", - # yellow - "#FBBF24", - # green - "#6EE7B7", - # blue - "#60A5FA", - # purple - "#A78BFA", - # pink - "#F472B6", - # salmon - "#FA8072", - # mat green - "#9ED9CC" - ] - - except = opts[:except] || [] - colors = colors -- except - - Enum.random(colors) + def changeset(user, attrs \\ %{}) do + user + |> cast(attrs, [:id, :name, :hex_color]) + |> validate_required([:id, :name, :hex_color]) end end diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index 23b51f7dd..f50cd9634 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -176,26 +176,6 @@ defmodule Livebook.Utils do uri.scheme != nil and uri.host not in [nil, ""] end - @doc """ - Validates if the given hex color is the correct format - - ## Examples - - iex> Livebook.Utils.valid_hex_color?("#111111") - true - - iex> Livebook.Utils.valid_hex_color?("#ABC123") - true - - iex> Livebook.Utils.valid_hex_color?("ABCDEF") - false - - iex> Livebook.Utils.valid_hex_color?("#111") - false - """ - @spec valid_hex_color?(String.t()) :: boolean() - def valid_hex_color?(hex_color), do: hex_color =~ ~r/^#[0-9a-fA-F]{6}$/ - @doc ~S""" Validates if the given string forms valid CLI flags. diff --git a/lib/livebook_web.ex b/lib/livebook_web.ex index 752b69fd8..0dc2f8a43 100644 --- a/lib/livebook_web.ex +++ b/lib/livebook_web.ex @@ -62,6 +62,7 @@ defmodule LivebookWeb do # Import basic rendering functionality (render, render_layout, etc) import Phoenix.View + import LivebookWeb.ErrorHelpers alias Phoenix.LiveView.JS alias LivebookWeb.Router.Helpers, as: Routes diff --git a/lib/livebook_web/live/hooks/user_hook.ex b/lib/livebook_web/live/hooks/user_hook.ex index 797333ed3..900d35b7d 100644 --- a/lib/livebook_web/live/hooks/user_hook.ex +++ b/lib/livebook_web/live/hooks/user_hook.ex @@ -33,13 +33,16 @@ defmodule LivebookWeb.UserHook do # `user_data` from session. defp build_current_user(session, socket) do %{"current_user_id" => current_user_id} = session + user = %{User.new() | id: current_user_id} connect_params = get_connect_params(socket) || %{} - user_data = connect_params["user_data"] || session["user_data"] || %{} + attrs = connect_params["user_data"] || session["user_data"] || %{} - case User.change(%{User.new() | id: current_user_id}, user_data) do + changeset = User.changeset(user, attrs) + + case Livebook.Users.update_user(changeset) do {:ok, user} -> user - {:error, _errors, user} -> user + {:error, _changeset} -> user end end end diff --git a/lib/livebook_web/live/hub_live.ex b/lib/livebook_web/live/hub_live.ex index a0551b0c2..846f70119 100644 --- a/lib/livebook_web/live/hub_live.ex +++ b/lib/livebook_web/live/hub_live.ex @@ -12,13 +12,19 @@ defmodule LivebookWeb.HubLive do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, selected_provider: nil, hub: nil, page_title: "Livebook - Hub")} + {:ok, + assign(socket, + selected_provider: nil, + hub: nil, + page_title: "Livebook - Hub" + )} end @impl true def render(assigns) do ~H"""
+ <.live_region role="alert" /> <:logo> - Fly logo + Livebook Enterprise logo <:headline> Control access, manage secrets, and deploy notebooks within your team and company. @@ -137,9 +147,4 @@ defmodule LivebookWeb.HubLive do def handle_event("select_provider", %{"value" => service}, socket) do {:noreply, assign(socket, selected_provider: service)} end - - @impl true - def handle_info({:flash_error, message}, socket) do - {:noreply, put_flash(socket, :error, message)} - end end diff --git a/lib/livebook_web/live/hub_live/fly_component.ex b/lib/livebook_web/live/hub_live/fly_component.ex index 50f3e56b6..3508ce3e5 100644 --- a/lib/livebook_web/live/hub_live/fly_component.ex +++ b/lib/livebook_web/live/hub_live/fly_component.ex @@ -1,9 +1,10 @@ defmodule LivebookWeb.HubLive.FlyComponent do use LivebookWeb, :live_component - alias Livebook.Hubs + import Ecto.Changeset, only: [get_field: 2, add_error: 3] + + alias Livebook.EctoTypes.HexColor alias Livebook.Hubs.{Fly, FlyClient} - alias Livebook.Users.User @impl true def update(assigns, socket) do @@ -21,9 +22,9 @@ defmodule LivebookWeb.HubLive.FlyComponent do id={@id} class="flex flex-col space-y-4" let={f} - for={:fly} - phx-submit="save_hub" - phx-change="update_data" + for={@changeset} + phx-submit="save" + phx-change="validate" phx-target={@myself} phx-debounce="blur" > @@ -35,14 +36,14 @@ defmodule LivebookWeb.HubLive.FlyComponent do phx_change: "fetch_data", phx_debounce: "blur", phx_target: @myself, + value: access_token(@changeset), disabled: @operation == :edit, - value: @data["access_token"], class: "input w-full", autofocus: true, spellcheck: "false", - required: true, autocomplete: "off" ) %> + <%= error_tag(f, :access_token) %>
<%= if length(@apps) > 0 do %> @@ -52,11 +53,9 @@ defmodule LivebookWeb.HubLive.FlyComponent do <%= select(f, :application_id, @select_options, class: "input", - required: true, - phx_target: @myself, - phx_change: "select_app", disabled: @operation == :edit ) %> + <%= error_tag(f, :application_id) %>
@@ -64,7 +63,8 @@ defmodule LivebookWeb.HubLive.FlyComponent do

Name

- <%= text_input(f, :hub_name, value: @data["hub_name"], class: "input", required: true) %> + <%= text_input(f, :hub_name, class: "input") %> + <%= error_tag(f, :hub_name) %>
@@ -75,17 +75,14 @@ defmodule LivebookWeb.HubLive.FlyComponent do
-
-
+
<%= text_input(f, :hub_color, - value: @data["hub_color"], class: "input", spellcheck: "false", - required: true, maxlength: 7 ) %> + <%= error_tag(f, :hub_color) %>
- <%= submit("Save", class: "button-base button-blue", phx_disable_with: "Saving...") %> + <%= submit("Save", + class: "button-base button-blue", + phx_disable_with: "Saving...", + disabled: not @valid? + ) %> <% end %> @@ -109,75 +111,102 @@ defmodule LivebookWeb.HubLive.FlyComponent do end defp load_data(%{assigns: %{operation: :new}} = socket) do - assign(socket, data: %{}, select_options: [], apps: []) + assign(socket, + changeset: Fly.change_hub(%Fly{}), + selected_app: nil, + select_options: [], + apps: [], + valid?: false + ) end - defp load_data(%{assigns: %{operation: :edit, hub: fly}} = socket) do - data = %{ - "access_token" => fly.access_token, - "application_id" => fly.application_id, - "hub_name" => fly.hub_name, - "hub_color" => fly.hub_color - } + defp load_data(%{assigns: %{operation: :edit, hub: hub}} = socket) do + {:ok, apps} = FlyClient.fetch_apps(hub.access_token) + params = Map.from_struct(hub) - {:ok, apps} = FlyClient.fetch_apps(fly.access_token) - opts = select_options(apps, fly.application_id) - - assign(socket, data: data, selected_app: fly, select_options: opts, apps: apps) + assign(socket, + changeset: Fly.change_hub(hub, params), + selected_app: hub, + select_options: select_options(apps), + apps: apps, + valid?: true + ) end @impl true def handle_event("fetch_data", %{"fly" => %{"access_token" => token}}, socket) do case FlyClient.fetch_apps(token) do {:ok, apps} -> - data = %{"access_token" => token, "hub_color" => User.random_hex_color()} opts = select_options(apps) - {:noreply, assign(socket, data: data, select_options: opts, apps: apps)} + changeset = + socket.assigns.changeset + |> Fly.changeset(%{access_token: token, hub_color: HexColor.random()}) + |> clean_errors() + + {:noreply, + assign(socket, + changeset: changeset, + valid?: changeset.valid?, + select_options: opts, + apps: apps + )} {:error, _} -> - send(self(), {:flash_error, "Invalid Access Token"}) - {:noreply, assign(socket, data: %{}, select_options: [], apps: [])} + changeset = + socket.assigns.changeset + |> Fly.changeset() + |> clean_errors() + |> put_action() + |> add_error(:access_token, "is invalid") + + send(self(), {:flash, :error, "Failed to fetch Applications"}) + + {:noreply, + assign(socket, + changeset: changeset, + valid?: changeset.valid?, + select_options: [], + apps: [] + )} end end def handle_event("randomize_color", _, socket) do - data = Map.replace!(socket.assigns.data, "hub_color", User.random_hex_color()) - {:noreply, assign(socket, data: data)} + changeset = + socket.assigns.changeset + |> clean_errors() + |> Fly.change_hub(%{hub_color: HexColor.random()}) + |> put_action() + + {:noreply, assign(socket, changeset: changeset, valid?: changeset.valid?)} end - def handle_event("save_hub", %{"fly" => params}, socket) do - params = - if socket.assigns.data do - Map.merge(socket.assigns.data, params) + def handle_event("save", %{"fly" => params}, socket) do + {:noreply, save_fly(socket, socket.assigns.operation, params)} + end + + def handle_event("validate", %{"fly" => attrs}, socket) do + params = Map.merge(socket.assigns.changeset.params, attrs) + + application_id = params["application_id"] + selected_app = Enum.find(socket.assigns.apps, &(&1.application_id == application_id)) + opts = select_options(socket.assigns.apps, application_id) + + changeset = + if selected_app do + Fly.change_hub(selected_app, params) else - params + Fly.changeset(socket.assigns.changeset, params) end - case socket.assigns.operation do - :new -> create_fly(socket, params) - :edit -> save_fly(socket, params) - end - end - - def handle_event("select_app", %{"fly" => %{"application_id" => app_id}}, socket) do - selected_app = Enum.find(socket.assigns.apps, &(&1.application_id == app_id)) - opts = select_options(socket.assigns.apps, app_id) - - {:noreply, assign(socket, selected_app: selected_app, select_options: opts)} - end - - def handle_event("update_data", %{"fly" => data}, socket) do - data = - if socket.assigns.data do - Map.merge(socket.assigns.data, data) - else - data - end - - opts = select_options(socket.assigns.apps, data["application_id"]) - - {:noreply, assign(socket, data: data, select_options: opts)} + {:noreply, + assign(socket, + changeset: changeset, + valid?: changeset.valid?, + selected_app: selected_app, + select_options: opts + )} end defp select_options(hubs, app_id \\ nil) do @@ -192,22 +221,50 @@ defmodule LivebookWeb.HubLive.FlyComponent do ] end - Enum.reverse(options ++ [disabled_option]) + [disabled_option] ++ options end - defp create_fly(socket, params) do - if Hubs.hub_exists?(socket.assigns.selected_app.id) do - send(self(), {:flash_error, "Application already exists"}) - {:noreply, assign(socket, data: params)} - else - save_fly(socket, params) + defp save_fly(socket, :new, params) do + case Fly.create_hub(socket.assigns.selected_app, params) do + {:ok, fly} -> + changeset = + fly + |> Fly.change_hub(params) + |> put_action() + + socket + |> assign(changeset: changeset, valid?: changeset.valid?) + |> put_flash(:success, "Hub created successfully") + |> push_redirect(to: Routes.hub_path(socket, :edit, fly.id)) + + {:error, changeset} -> + assign(socket, changeset: put_action(changeset), valid?: changeset.valid?) end end - defp save_fly(socket, params) do - Fly.save_fly(socket.assigns.selected_app, params) - opts = select_options(socket.assigns.apps, params["application_id"]) + defp save_fly(socket, :edit, params) do + id = socket.assigns.selected_app.id - {:noreply, assign(socket, data: params, select_options: opts)} + case Fly.update_hub(socket.assigns.selected_app, params) do + {:ok, fly} -> + changeset = + fly + |> Fly.change_hub(params) + |> put_action() + + socket + |> assign(changeset: changeset, selected_app: fly, valid?: changeset.valid?) + |> put_flash(:success, "Hub updated successfully") + |> push_redirect(to: Routes.hub_path(socket, :edit, id)) + + {:error, changeset} -> + assign(socket, changeset: changeset, valid?: changeset.valid?) + end end + + defp clean_errors(changeset), do: %{changeset | errors: []} + defp put_action(changeset, action \\ :validate), do: %{changeset | action: action} + + defp hub_color(changeset), do: get_field(changeset, :hub_color) + defp access_token(changeset), do: get_field(changeset, :access_token) end diff --git a/lib/livebook_web/live/user_component.ex b/lib/livebook_web/live/user_component.ex index 195793fcc..2b7b7da4e 100644 --- a/lib/livebook_web/live/user_component.ex +++ b/lib/livebook_web/live/user_component.ex @@ -3,18 +3,16 @@ defmodule LivebookWeb.UserComponent do import LivebookWeb.UserHelpers - alias Livebook.Users.User + alias Livebook.EctoTypes.HexColor + alias Livebook.Users @impl true def update(assigns, socket) do socket = assign(socket, assigns) user = socket.assigns.user + changeset = Users.change_user(user) - {:ok, assign(socket, data: user_to_data(user), valid: true, preview_user: user)} - end - - defp user_to_data(user) do - %{"name" => user.name || "", "hex_color" => user.hex_color} + {:ok, assign(socket, changeset: changeset, valid?: changeset.valid?, user: user)} end @impl true @@ -25,11 +23,11 @@ defmodule LivebookWeb.UserComponent do User profile
- <.user_avatar user={@preview_user} class="h-20 w-20" text_class="text-3xl" /> + <.user_avatar user={@user} class="h-20 w-20" text_class="text-3xl" />
<.form let={f} - for={:data} + for={@changeset} phx-submit={@on_save |> JS.push("save")} phx-change="validate" phx-target={@myself} @@ -39,21 +37,21 @@ defmodule LivebookWeb.UserComponent do
Display name
- <%= text_input(f, :name, value: @data["name"], class: "input", spellcheck: "false") %> + <%= text_input(f, :name, class: "input", spellcheck: "false") %> + <%= error_tag(f, :name) %>
Cursor color
-
+
<%= text_input(f, :hex_color, - value: @data["hex_color"], class: "input", spellcheck: "false", maxlength: 7 @@ -66,13 +64,14 @@ defmodule LivebookWeb.UserComponent do > <.remix_icon icon="refresh-line" class="text-xl" /> + <%= error_tag(f, :hex_color) %>
<% end %> + <%= if live_flash(@flash, :success) do %> + + <% end %> + <%= if live_flash(@flash, :warning) do %>
+ String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + end) + end +end diff --git a/mix.exs b/mix.exs index a061cc8be..560eacb82 100644 --- a/mix.exs +++ b/mix.exs @@ -98,6 +98,8 @@ defmodule Livebook.MixProject do {:earmark_parser, "~> 1.4"}, {:castore, "~> 0.1.0"}, {:aws_signature, "~> 0.3.0"}, + {:ecto, "~> 3.8.4"}, + {:phoenix_ecto, "~> 4.4.0"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:floki, ">= 0.27.0", only: :test}, {:bypass, "~> 2.1", only: :test} diff --git a/mix.lock b/mix.lock index fb572489d..07a92815e 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,10 @@ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "earmark_parser": {:hex, :earmark_parser, "1.4.26", "f4291134583f373c7d8755566122908eb9662df4c4b63caa66a0eabe06569b0a", [:mix], [], "hexpm", "48d460899f8a0c52c5470676611c01f64f3337bad0b26ddab43648428d94aabc"}, + "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, + "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, @@ -13,6 +16,7 @@ "libpe": {:hex, :libpe, "1.1.2", "16337b414c690e0ee9c49fe917b059622f001c399303102b98900c05c229cd9a", [:mix], [], "hexpm", "31df0639fafb603b20078c8db9596c8984f35a151c64ec2e483d9136ff9f428c"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "phoenix": {:hex, :phoenix, "1.6.11", "29f3c0fd12fa1fc4d4b05e341578e55bc78d96ea83a022587a7e276884d397e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1664e34f80c25ea4918fbadd957f491225ef601c0e00b4e644b1a772864bfbc2"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, diff --git a/static/images/enterprise.png b/static/images/enterprise.png new file mode 100644 index 000000000..ae2f0f657 Binary files /dev/null and b/static/images/enterprise.png differ diff --git a/test/livebook/ecto_types/hex_color_test.exs b/test/livebook/ecto_types/hex_color_test.exs new file mode 100644 index 000000000..1cce201b6 --- /dev/null +++ b/test/livebook/ecto_types/hex_color_test.exs @@ -0,0 +1,4 @@ +defmodule Livebook.EctoTypes.HexColorTest do + use ExUnit.Case, async: true + doctest Livebook.EctoTypes.HexColor +end diff --git a/test/livebook/hubs/provider_test.exs b/test/livebook/hubs/provider_test.exs index 2a3c1f587..321afd4c5 100644 --- a/test/livebook/hubs/provider_test.exs +++ b/test/livebook/hubs/provider_test.exs @@ -1,13 +1,11 @@ defmodule Livebook.Hubs.ProviderTest do - use ExUnit.Case - - import Livebook.Fixtures + use Livebook.DataCase alias Livebook.Hubs.{Fly, Metadata, Provider} describe "Fly" do test "normalize/1" do - fly = fly_fixture() + fly = build(:fly) assert Provider.normalize(fly) == %Metadata{ id: fly.id, @@ -18,7 +16,7 @@ defmodule Livebook.Hubs.ProviderTest do end test "load/2" do - fly = fly_fixture() + fly = build(:fly) fields = Map.from_struct(fly) assert Provider.load(%Fly{}, fields) == fly diff --git a/test/livebook/hubs_test.exs b/test/livebook/hubs_test.exs index 5790beb89..310c64b75 100644 --- a/test/livebook/hubs_test.exs +++ b/test/livebook/hubs_test.exs @@ -1,7 +1,5 @@ defmodule Livebook.HubsTest do - use ExUnit.Case - - import Livebook.Fixtures + use Livebook.DataCase alias Livebook.Hubs @@ -12,7 +10,7 @@ defmodule Livebook.HubsTest do end test "fetch_hubs/0 returns a list of persisted hubs" do - fly = create_fly("fly-baz") + fly = insert_hub(:fly, id: "fly-baz") assert Hubs.fetch_hubs() == [fly] Hubs.delete_hub("fly-baz") @@ -20,7 +18,7 @@ defmodule Livebook.HubsTest do end test "fetch_metadata/0 returns a list of persisted hubs normalized" do - fly = create_fly("fly-livebook") + fly = insert_hub(:fly, id: "fly-livebook") assert Hubs.fetch_metadatas() == [ %Hubs.Metadata{ @@ -42,26 +40,26 @@ defmodule Livebook.HubsTest do Hubs.fetch_hub!("fly-foo") end - fly = create_fly("fly-foo") + fly = insert_hub(:fly, id: "fly-foo") assert Hubs.fetch_hub!("fly-foo") == fly end test "hub_exists?/1" do refute Hubs.hub_exists?("fly-bar") - create_fly("fly-bar") + insert_hub(:fly, id: "fly-bar") assert Hubs.hub_exists?("fly-bar") end test "save_hub/1 persists hub" do - fly = fly_fixture(id: "fly-foo") + fly = build(:fly, id: "fly-foo") Hubs.save_hub(fly) assert Hubs.fetch_hub!("fly-foo") == fly end test "save_hub/1 updates hub" do - fly = create_fly("fly-foo2") + fly = insert_hub(:fly, id: "fly-foo2") Hubs.save_hub(%{fly | hub_color: "#FFFFFF"}) refute Hubs.fetch_hub!("fly-foo2") == fly diff --git a/test/livebook/users/user_test.exs b/test/livebook/users/user_test.exs index 2eb3358b7..8ed652ec8 100644 --- a/test/livebook/users/user_test.exs +++ b/test/livebook/users/user_test.exs @@ -1,34 +1,35 @@ defmodule Livebook.Users.UserTest do - use ExUnit.Case, async: true + use Livebook.DataCase, async: true alias Livebook.Users.User describe "change/2" do test "given valid attributes returns and updated user" do - user = User.new() + user = build(:user) attrs = %{"name" => "Jake Peralta", "hex_color" => "#000000"} - assert {:ok, %User{name: "Jake Peralta", hex_color: "#000000"}} = User.change(user, attrs) + changeset = User.changeset(user, attrs) + + assert changeset.valid? + assert get_field(changeset, :name) == "Jake Peralta" + assert get_field(changeset, :hex_color) == "#000000" end - test "given empty name sets name to nil" do - user = User.new() + test "given empty name returns an error" do + user = build(:user) attrs = %{"name" => ""} - assert {:ok, %User{name: nil}} = User.change(user, attrs) + changeset = User.changeset(user, attrs) + + refute changeset.valid? + assert "can't be blank" in errors_on(changeset).name end test "given invalid color returns an error" do - user = User.new() + user = build(:user) attrs = %{"hex_color" => "#invalid"} - assert {:error, [{:hex_color, "not a valid color"}], _user} = User.change(user, attrs) - end + changeset = User.changeset(user, attrs) - test "given invalid attribute partially updates the user" do - user = User.new() - current_hex_color = user.hex_color - attrs = %{"hex_color" => "#invalid", "name" => "Jake Peralta"} - - assert {:error, _errors, %User{name: "Jake Peralta", hex_color: ^current_hex_color}} = - User.change(user, attrs) + refute changeset.valid? + assert "not a valid color" in errors_on(changeset).hex_color end end end diff --git a/test/livebook/utils_test.exs b/test/livebook/utils_test.exs index 3268e56fd..14e65e987 100644 --- a/test/livebook/utils_test.exs +++ b/test/livebook/utils_test.exs @@ -1,7 +1,4 @@ defmodule Livebook.UtilsTest do use ExUnit.Case, async: true - - alias Livebook.Utils - - doctest Utils + doctest Livebook.Utils end diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index 4dad262e5..4c4c90beb 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -1,7 +1,6 @@ defmodule LivebookWeb.HomeLiveTest do use LivebookWeb.ConnCase, async: true - import Livebook.Fixtures import Phoenix.LiveViewTest alias Livebook.{Sessions, Session} @@ -248,7 +247,7 @@ defmodule LivebookWeb.HomeLiveTest do end test "render persisted hubs", %{conn: conn} do - fly = create_fly("fly-foo-bar-id") + fly = insert_hub(:fly, id: "fly-foo-bar-id") {:ok, _view, html} = live(conn, "/") assert html =~ "HUBS" @@ -417,11 +416,17 @@ defmodule LivebookWeb.HomeLiveTest do test "handles user profile update", %{conn: conn} do {:ok, view, _} = live(conn, "/") + data = %{user: %{name: "Jake Peralta", hex_color: "#123456"}} view |> element("#user_form") - |> render_submit(%{data: %{hex_color: "#123456"}}) + |> render_change(data) + view + |> element("#user_form") + |> render_submit(data) + + assert render(view) =~ "Jake Peralta" assert render(view) =~ "#123456" end diff --git a/test/livebook_web/live/hub_live_test.exs b/test/livebook_web/live/hub_live_test.exs index a0c55323c..111400b77 100644 --- a/test/livebook_web/live/hub_live_test.exs +++ b/test/livebook_web/live/hub_live_test.exs @@ -1,7 +1,6 @@ defmodule LivebookWeb.HubLiveTest do use LivebookWeb.ConnCase, async: true - import Livebook.Fixtures import Phoenix.LiveViewTest alias Livebook.Hubs @@ -25,38 +24,38 @@ defmodule LivebookWeb.HubLiveTest do {:ok, view, _html} = live(conn, "/hub") - # renders the second step assert view |> element("#fly") |> render_click() =~ "2. Configure your Hub" - # triggers the access_access_token field change - # and shows the fly's third step assert view |> element(~s/input[name="fly[access_token]"]/) |> render_change(%{"fly" => %{"access_token" => "dummy access token"}}) =~ ~s() - # triggers the application_id field change - # and assigns `selected_app` to socket + attrs = %{ + "access_token" => "dummy access token", + "application_id" => "123456789", + "hub_name" => "My Foo Hub", + "hub_color" => "#FF00FF" + } + view - |> element(~s/select[name="fly[application_id]"]/) - |> render_change(%{"fly" => %{"application_id" => "123456789"}}) + |> element("#fly-form") + |> render_change(%{"fly" => attrs}) - # sends the save_hub event to backend - # and checks the new hub on sidebar refute view - |> element("#fly-form") - |> render_submit(%{ - "fly" => %{ - "access_token" => "dummy access token", - "application_id" => "123456789", - "hub_name" => "My Foo Hub", - "hub_color" => "#FF00FF" - } - }) =~ "Application already exists" + |> element("#fly-form .invalid-feedback") + |> has_element?() + + assert {:ok, view, _html} = + view + |> element("#fly-form") + |> render_submit(%{"fly" => attrs}) + |> follow_redirect(conn) + + assert render(view) =~ "Hub created successfully" - # and checks the new hub on sidebar assert view |> element("#hubs") |> render() =~ ~s/style="color: #FF00FF"/ @@ -72,30 +71,38 @@ defmodule LivebookWeb.HubLiveTest do test "updates fly", %{conn: conn} do fly_app_bypass("987654321") - fly = create_fly("fly-987654321", %{application_id: "987654321"}) + fly = insert_hub(:fly, id: "fly-987654321", application_id: "987654321") {:ok, view, _html} = live(conn, "/hub/fly-987654321") - # renders the second step assert render(view) =~ "2. Configure your Hub" assert render(view) =~ ~s() - # sends the save_hub event to backend - # and checks the new hub on sidebar + attrs = %{ + "access_token" => "dummy access token", + "application_id" => "987654321", + "hub_name" => "Personal Hub", + "hub_color" => "#FF00FF" + } + view |> element("#fly-form") - |> render_submit(%{ - "fly" => %{ - "access_token" => "dummy access token", - "application_id" => "987654321", - "hub_name" => "Personal Hub", - "hub_color" => "#FF00FF" - } - }) + |> render_change(%{"fly" => attrs}) + + refute view + |> element("#fly-form .invalid-feedback") + |> has_element?() + + assert {:ok, view, _html} = + view + |> element("#fly-form") + |> render_submit(%{"fly" => attrs}) + |> follow_redirect(conn) + + assert render(view) =~ "Hub updated successfully" - # and checks the new hub on sidebar assert view |> element("#hubs") |> render() =~ ~s/style="color: #FF00FF"/ @@ -112,45 +119,39 @@ defmodule LivebookWeb.HubLiveTest do end test "fails to create existing hub", %{conn: conn} do - fly = create_fly("fly-foo", %{application_id: "foo"}) + fly = insert_hub(:fly, id: "fly-foo", application_id: "foo") fly_app_bypass("foo") {:ok, view, _html} = live(conn, "/hub") - # renders the second step assert view |> element("#fly") |> render_click() =~ "2. Configure your Hub" - # triggers the access_token field change - # and shows the fly's third step assert view |> element(~s/input[name="fly[access_token]"]/) |> render_change(%{"fly" => %{"access_token" => "dummy access token"}}) =~ ~s() - # triggers the application_id field change - # and assigns `selected_app` to socket - view - |> element(~s/select[name="fly[application_id]"]/) - |> render_change(%{"fly" => %{"application_id" => "foo"}}) + attrs = %{ + "access_token" => "dummy access token", + "application_id" => "foo", + "hub_name" => "My Foo Hub", + "hub_color" => "#FF00FF" + } - # sends the save_hub event to backend - # and checks the new hub on sidebar view |> element("#fly-form") - |> render_submit(%{ - "fly" => %{ - "access_token" => "dummy access token", - "application_id" => "foo", - "hub_name" => "My Foo Hub", - "hub_color" => "#FF00FF" - } - }) + |> render_change(%{"fly" => attrs}) - assert render(view) =~ "Application already exists" + refute view + |> element("#fly-form .invalid-feedback") + |> has_element?() + + assert view + |> element("#fly-form") + |> render_submit(%{"fly" => attrs}) =~ "already exists" - # and checks the hub didn't change on sidebar assert view |> element("#hubs") |> render() =~ ~s/style="color: #{fly.hub_color}"/ diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index fcee33173..6c5da251d 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -5,7 +5,6 @@ defmodule LivebookWeb.SessionLiveTest do alias Livebook.{Sessions, Session, Runtime, Users, FileSystem} alias Livebook.Notebook.Cell - alias Livebook.Users.User setup do {:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new()) @@ -577,7 +576,7 @@ defmodule LivebookWeb.SessionLiveTest do describe "connected users" do test "lists connected users", %{conn: conn, session: session} do - user1 = create_user_with_name("Jake Peralta") + user1 = build(:user, name: "Jake Peralta") client_pid = spawn_link(fn -> @@ -601,7 +600,7 @@ defmodule LivebookWeb.SessionLiveTest do Session.subscribe(session.id) - user1 = create_user_with_name("Jake Peralta") + user1 = build(:user, name: "Jake Peralta") client_pid = spawn_link(fn -> @@ -622,7 +621,7 @@ defmodule LivebookWeb.SessionLiveTest do test "updates users list whenever a user changes his data", %{conn: conn, session: session} do - user1 = create_user_with_name("Jake Peralta") + user1 = build(:user, name: "Jake Peralta") client_pid = spawn_link(fn -> @@ -959,11 +958,6 @@ defmodule LivebookWeb.SessionLiveTest do cell_id end - defp create_user_with_name(name) do - {:ok, user} = User.new() |> User.change(%{"name" => name}) - user - end - defp url(port), do: "http://localhost:#{port}" defp close_session_by_id(session_id) do diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 6791440d3..cd7c610fd 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -6,6 +6,7 @@ defmodule LivebookWeb.ConnCase do # Import conveniences for testing with connections import Plug.Conn import Phoenix.ConnTest + import Livebook.Factory import LivebookWeb.ConnCase alias LivebookWeb.Router.Helpers, as: Routes diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 000000000..5e12c3eee --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,44 @@ +defmodule Livebook.DataCase do + @moduledoc """ + This module defines the setup for tests requiring + access to the application's data layer. + + You may define functions here to be used as helpers in + your tests. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use Livebook.DataCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + import Ecto + import Ecto.Changeset + import Ecto.Query + import Livebook.DataCase + import Livebook.Factory + end + end + + @doc """ + A helper that transforms changeset errors into a map of messages. + + assert {:error, changeset} = Accounts.create_user(%{password: "short"}) + assert "password is too short" in errors_on(changeset).password + assert %{password: ["password is too short"]} = errors_on(changeset) + + """ + def errors_on(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> + Regex.replace(~r"%{(\w+)}", message, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 000000000..6157fdcf3 --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,43 @@ +defmodule Livebook.Factory do + @moduledoc false + + def build(:user) do + %Livebook.Users.User{ + id: Livebook.Utils.random_id(), + name: "Jose Valim", + hex_color: Livebook.EctoTypes.HexColor.random() + } + end + + def build(:fly_metadata) do + %Livebook.Hubs.Metadata{ + id: "fly-foo-bar-baz", + name: "My Personal Hub", + color: "#FF00FF", + provider: build(:fly) + } + end + + def build(:fly) do + %Livebook.Hubs.Fly{ + id: "fly-foo-bar-baz", + hub_name: "My Personal Hub", + hub_color: "#FF00FF", + 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(factory_name, attrs \\ %{}) do + factory_name |> build() |> struct!(attrs) + end + + def insert_hub(factory_name, attrs \\ %{}) do + factory_name + |> build(attrs) + |> Livebook.Hubs.save_hub() + end +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex deleted file mode 100644 index 0b8aaf433..000000000 --- a/test/support/fixtures.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Livebook.Fixtures do - @moduledoc false - - def create_fly(id, attrs \\ %{}) do - attrs - |> fly_fixture() - |> Map.replace!(:id, id) - |> Livebook.Hubs.save_hub() - end - - def fly_fixture(attrs \\ %{}) do - fly = %Livebook.Hubs.Fly{ - id: "fly-foo-bar-baz", - hub_name: "My Personal Hub", - hub_color: "#FF00FF", - access_token: Livebook.Utils.random_cookie(), - organization_id: Livebook.Utils.random_id(), - organization_type: "PERSONAL", - organization_name: "Foo", - application_id: "foo-bar-baz" - } - - for {key, value} <- attrs, reduce: fly do - acc -> Map.replace!(acc, key, value) - end - end -end