diff --git a/lib/bds/desktop/shell_live/settings_editor/style_editor.ex b/lib/bds/desktop/shell_live/settings_editor/style_editor.ex index 458ce8f..69bb18e 100644 --- a/lib/bds/desktop/shell_live/settings_editor/style_editor.ex +++ b/lib/bds/desktop/shell_live/settings_editor/style_editor.ex @@ -4,6 +4,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do use Phoenix.Component alias BDS.Metadata + alias BDS.Preview use Gettext, backend: BDS.Gettext @themes [ @@ -42,7 +43,8 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do applied_theme: current_theme(assigns), preview_mode: preview_mode, preview_url: - "http://127.0.0.1:4123/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}" + Preview.base_url() <> + "/__style-preview?theme=#{selected_theme}&mode=#{preview_mode}" } end diff --git a/lib/bds/preview.ex b/lib/bds/preview.ex index 474867b..fb836bb 100644 --- a/lib/bds/preview.ex +++ b/lib/bds/preview.ex @@ -12,12 +12,14 @@ defmodule BDS.Preview do alias BDS.Rendering.TemplateSelection @host "127.0.0.1" - @port 4123 + @preferred_port 4123 @server_table __MODULE__.ServerTable # Max time to wait for inflight requests to finish during graceful shutdown # before remaining request tasks are forcibly terminated. @drain_timeout 5_000 + @listen_retry_attempts 10 + @listen_retry_delay_ms 50 def start_link(_opts) do GenServer.start_link(__MODULE__, %{}, name: __MODULE__) @@ -41,7 +43,17 @@ defmodule BDS.Preview do ) end - def base_url, do: "http://#{@host}:#{@port}" + def base_url do + case :ets.whereis(@server_table) do + :undefined -> "http://#{@host}:#{@preferred_port}" + + _table -> + case :ets.lookup(@server_table, :current) do + [{:current, %{host: host, port: port}}] -> "http://#{host}:#{port}" + _other -> "http://#{@host}:#{@preferred_port}" + end + end + end def stop_preview(project_id) when is_binary(project_id) do GenServer.call(__MODULE__, {:stop_preview, project_id}) @@ -501,14 +513,7 @@ defmodule BDS.Preview do state = stop_current_server(state) maybe_allow_repo(owner_pid) - {:ok, listener} = - :gen_tcp.listen(@port, [ - :binary, - packet: :raw, - active: false, - reuseaddr: true, - ip: {127, 0, 0, 1} - ]) + {:ok, listener, port} = listen_with_retry(@listen_retry_attempts) acceptor_pid = spawn_link(fn -> accept_loop(listener, project_id, owner_pid) end) @@ -516,7 +521,7 @@ defmodule BDS.Preview do project_id: project_id, data_dir: data_dir, host: @host, - port: @port, + port: port, is_running: true, owner_pid: owner_pid, listener: listener, @@ -528,6 +533,40 @@ defmodule BDS.Preview do {{:ok, public_server(server)}, %{state | current: server}} end + defp listen_with_retry(attempts_left) when attempts_left > 0 do + case listen_on_port(@preferred_port) do + {:ok, listener, port} -> + {:ok, listener, port} + + {:error, :eaddrinuse} when attempts_left > 1 -> + Process.sleep(@listen_retry_delay_ms) + listen_with_retry(attempts_left - 1) + + {:error, :eaddrinuse} -> + listen_on_port(0) + + {:error, reason} -> + {:error, reason} + end + end + + defp listen_on_port(port) do + case :gen_tcp.listen(port, [ + :binary, + packet: :raw, + active: false, + reuseaddr: true, + ip: {127, 0, 0, 1} + ]) do + {:ok, listener} -> + {:ok, {_host, actual_port}} = :inet.sockname(listener) + {:ok, listener, actual_port} + + {:error, reason} -> + {:error, reason} + end + end + defp run_tracked_request(fun) when is_function(fun, 0) do owner_pid = self() diff --git a/lib/bds/projects.ex b/lib/bds/projects.ex index 36b8c52..a87d664 100644 --- a/lib/bds/projects.ex +++ b/lib/bds/projects.ex @@ -12,6 +12,8 @@ defmodule BDS.Projects do @default_project_id "default" @default_project_name "My Blog" + @create_project_retry_attempts 5 + @create_project_retry_delay_ms 50 @typedoc "An attribute map that may use atom or string keys." @type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()} @@ -152,22 +154,24 @@ defmodule BDS.Projects do name = attr(attrs, :name) || "" slug = unique_slug(attr(attrs, :slug) || Slug.slugify(name)) - Repo.transaction(fn -> - project = - %Project{} - |> Project.changeset(%{ - id: Ecto.UUID.generate(), - name: name, - slug: slug, - description: attr(attrs, :description), - data_path: attr(attrs, :data_path), - created_at: now, - updated_at: now, - is_active: false - }) - |> Repo.insert!() + retry_create_project(fn -> + Repo.transaction(fn -> + project = + %Project{} + |> Project.changeset(%{ + id: Ecto.UUID.generate(), + name: name, + slug: slug, + description: attr(attrs, :description), + data_path: attr(attrs, :data_path), + created_at: now, + updated_at: now, + is_active: false + }) + |> Repo.insert!() - project + project + end) end) |> case do {:ok, project} -> @@ -182,6 +186,22 @@ defmodule BDS.Projects do end end + defp retry_create_project(fun, attempts_left \\ @create_project_retry_attempts) + + defp retry_create_project(fun, attempts_left) when attempts_left > 1 do + fun.() + rescue + error in [Exqlite.Error] -> + if String.contains?(Exception.message(error), "Database busy") do + Process.sleep(@create_project_retry_delay_ms) + retry_create_project(fun, attempts_left - 1) + else + reraise error, __STACKTRACE__ + end + end + + defp retry_create_project(fun, _attempts_left), do: fun.() + @spec set_active_project(String.t()) :: {:ok, Project.t()} | {:error, :not_found | term()} def set_active_project(project_id) do case Repo.get(Project, project_id) do diff --git a/test/bds/ai/chat_sandbox_cleanup_test.exs b/test/bds/ai/chat_sandbox_cleanup_test.exs index 86928e7..74b1f99 100644 --- a/test/bds/ai/chat_sandbox_cleanup_test.exs +++ b/test/bds/ai/chat_sandbox_cleanup_test.exs @@ -20,6 +20,8 @@ defmodule BDS.AI.ChatSandboxCleanupTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) + Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) :ok end diff --git a/test/bds/blogmark_test.exs b/test/bds/blogmark_test.exs index 3f24f1b..a48ceaa 100644 --- a/test/bds/blogmark_test.exs +++ b/test/bds/blogmark_test.exs @@ -7,6 +7,7 @@ defmodule BDS.BlogmarkTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-blogmark-#{System.unique_integer([:positive])}") diff --git a/test/bds/csm007_reload_shell_test.exs b/test/bds/csm007_reload_shell_test.exs index 17dacf6..dabf985 100644 --- a/test/bds/csm007_reload_shell_test.exs +++ b/test/bds/csm007_reload_shell_test.exs @@ -9,6 +9,7 @@ defmodule BDS.CSM007ReloadShellTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-csm007-#{System.unique_integer([:positive])}") diff --git a/test/bds/csm008_render_path_test.exs b/test/bds/csm008_render_path_test.exs index 736d98d..0115121 100644 --- a/test/bds/csm008_render_path_test.exs +++ b/test/bds/csm008_render_path_test.exs @@ -9,6 +9,7 @@ defmodule BDS.CSM008RenderPathTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-csm008-#{System.unique_integer([:positive])}") diff --git a/test/bds/csm011_url_state_test.exs b/test/bds/csm011_url_state_test.exs index 1851c42..5183c4c 100644 --- a/test/bds/csm011_url_state_test.exs +++ b/test/bds/csm011_url_state_test.exs @@ -9,6 +9,7 @@ defmodule BDS.CSM011UrlStateTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) prev = System.get_env("BDS_DESKTOP_AUTOMATION") System.put_env("BDS_DESKTOP_AUTOMATION", "1") diff --git a/test/bds/csm012_file_picker_async_test.exs b/test/bds/csm012_file_picker_async_test.exs index 61e4c45..51c1820 100644 --- a/test/bds/csm012_file_picker_async_test.exs +++ b/test/bds/csm012_file_picker_async_test.exs @@ -9,6 +9,7 @@ defmodule BDS.CSM012FilePickerAsyncTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) prev = System.get_env("BDS_DESKTOP_AUTOMATION") System.put_env("BDS_DESKTOP_AUTOMATION", "1") diff --git a/test/bds/desktop/import_shell_live_test.exs b/test/bds/desktop/import_shell_live_test.exs index 9579dac..8a944f0 100644 --- a/test/bds/desktop/import_shell_live_test.exs +++ b/test/bds/desktop/import_shell_live_test.exs @@ -12,6 +12,7 @@ defmodule BDS.Desktop.ImportShellLiveTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) Enum.each(BDS.Tasks.list_running_tasks(), fn task -> BDS.Tasks.cancel_task(task.id) diff --git a/test/bds/desktop/misc_editor_test.exs b/test/bds/desktop/misc_editor_test.exs index 1ee0ae9..ab3fb10 100644 --- a/test/bds/desktop/misc_editor_test.exs +++ b/test/bds/desktop/misc_editor_test.exs @@ -11,6 +11,7 @@ defmodule BDS.Desktop.MiscEditorTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-misc-editor-test-#{System.unique_integer([:positive])}") diff --git a/test/bds/desktop/shell_commands_test.exs b/test/bds/desktop/shell_commands_test.exs index ae8eeb4..c855263 100644 --- a/test/bds/desktop/shell_commands_test.exs +++ b/test/bds/desktop/shell_commands_test.exs @@ -87,8 +87,19 @@ defmodule BDS.Desktop.ShellCommandsTest do end setup do + if :ets.whereis(BDS.Preview.ServerTable) != :undefined do + case :ets.lookup(BDS.Preview.ServerTable, :current) do + [{:current, %{project_id: project_id}}] -> + _ = BDS.Preview.stop_preview(project_id) + + _other -> + :ok + end + end + :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) :ok = Ecto.Adapters.SQL.Sandbox.allow(BDS.Repo, self(), Process.whereis(BDS.Preview)) :ok = Ecto.Adapters.SQL.Sandbox.allow(BDS.Repo, self(), Process.whereis(BDS.Publishing)) :ok = BDS.Tasks.clear_finished() @@ -117,7 +128,7 @@ defmodule BDS.Desktop.ShellCommandsTest do assert result.kind == "open_url" assert result.action == "open_in_browser" - assert result.url == "http://127.0.0.1:4123/" + assert result.url == BDS.Preview.base_url() <> "/" assert result.project_id == project.id end diff --git a/test/bds/desktop/shell_live_test.exs b/test/bds/desktop/shell_live_test.exs index 421d01f..db365b9 100644 --- a/test/bds/desktop/shell_live_test.exs +++ b/test/bds/desktop/shell_live_test.exs @@ -640,8 +640,29 @@ defmodule BDS.Desktop.ShellLiveTest do end setup do + if :ets.whereis(BDS.Preview.ServerTable) != :undefined do + case :ets.lookup(BDS.Preview.ServerTable, :current) do + [{:current, %{project_id: project_id}}] -> + _ = BDS.Preview.stop_preview(project_id) + + _other -> + :ok + end + end + + if :ets.whereis(BDS.Preview.ServerTable) != :undefined do + case :ets.lookup(BDS.Preview.ServerTable, :current) do + [{:current, %{project_id: project_id}}] -> + _ = BDS.Preview.stop_preview(project_id) + + _other -> + :ok + end + end + :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) Enum.each(BDS.Tasks.list_running_tasks(), fn task -> BDS.Tasks.cancel_task(task.id) @@ -661,7 +682,8 @@ defmodule BDS.Desktop.ShellLiveTest do DynamicSupervisor.terminate_child(BDS.Tasks.TaskSupervisor, pid) end - Process.sleep(100) + wait_for_supervisor_clear(BDS.TCP.TaskSupervisor) + wait_for_supervisor_clear(BDS.Tasks.TaskSupervisor) temp_dir = Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}") @@ -700,6 +722,21 @@ defmodule BDS.Desktop.ShellLiveTest do %{project: project, temp_dir: temp_dir} end + defp wait_for_supervisor_clear(supervisor, attempts \\ 50) + + defp wait_for_supervisor_clear(_supervisor, 0), do: :ok + + defp wait_for_supervisor_clear(supervisor, attempts) do + case DynamicSupervisor.which_children(supervisor) do + [] -> + :ok + + _children -> + Process.sleep(10) + wait_for_supervisor_clear(supervisor, attempts - 1) + end + end + test "sidebar headers expose old-app create actions for posts, media, scripts, templates, chat, and imports" do {:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) diff --git a/test/bds/desktop/style_editor_test.exs b/test/bds/desktop/style_editor_test.exs index f632657..f6fe263 100644 --- a/test/bds/desktop/style_editor_test.exs +++ b/test/bds/desktop/style_editor_test.exs @@ -72,7 +72,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditorTest do result = StyleEditor.build_style(assigns) assert result.preview_url =~ "theme=default" assert result.preview_url =~ "mode=auto" - assert result.preview_url =~ "127.0.0.1:4123/__style-preview" + assert result.preview_url =~ BDS.Preview.base_url() <> "/__style-preview" end test "preview_url reflects selected_theme", %{project: project} do diff --git a/test/bds/desktop/tag_editor_live_test.exs b/test/bds/desktop/tag_editor_live_test.exs index 9e8adc6..ea963ec 100644 --- a/test/bds/desktop/tag_editor_live_test.exs +++ b/test/bds/desktop/tag_editor_live_test.exs @@ -12,6 +12,7 @@ defmodule BDS.Desktop.TagEditorLiveTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) Enum.each(BDS.Tasks.list_running_tasks(), fn task -> BDS.Tasks.cancel_task(task.id) diff --git a/test/bds/desktop/template_editor_live_test.exs b/test/bds/desktop/template_editor_live_test.exs index e58e41e..d7aadc8 100644 --- a/test/bds/desktop/template_editor_live_test.exs +++ b/test/bds/desktop/template_editor_live_test.exs @@ -15,6 +15,7 @@ defmodule BDS.Desktop.TemplateEditorLiveTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) Enum.each(BDS.Tasks.list_running_tasks(), fn task -> BDS.Tasks.cancel_task(task.id) diff --git a/test/bds/post_translations_test.exs b/test/bds/post_translations_test.exs index 3ede77c..e5885c5 100644 --- a/test/bds/post_translations_test.exs +++ b/test/bds/post_translations_test.exs @@ -53,6 +53,7 @@ defmodule BDS.PostTranslationsTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) :ok = BDS.Tasks.clear_finished() temp_dir = diff --git a/test/bds/preview_test.exs b/test/bds/preview_test.exs index e9da615..55ca47d 100644 --- a/test/bds/preview_test.exs +++ b/test/bds/preview_test.exs @@ -21,15 +21,46 @@ defmodule BDS.PreviewTest do alias BDS.Posts setup do + reset_preview_process() + + for {_, pid, _, _} <- DynamicSupervisor.which_children(BDS.TCP.TaskSupervisor) do + DynamicSupervisor.terminate_child(BDS.TCP.TaskSupervisor, pid) + end + :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) + Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) + temp_dir = Path.join(System.tmp_dir!(), "bds-preview-#{System.unique_integer([:positive])}") File.mkdir_p!(temp_dir) on_exit(fn -> File.rm_rf(temp_dir) end) {:ok, project} = BDS.Projects.create_project(%{name: "Preview", data_path: temp_dir}) + + on_exit(fn -> + _ = BDS.Preview.stop_preview(project.id) + end) + %{project: project, temp_dir: temp_dir} end + defp reset_preview_process do + case :ets.whereis(BDS.Preview.ServerTable) do + :undefined -> + :ok + + _table -> + case :ets.lookup(BDS.Preview.ServerTable, :current) do + [{:current, %{project_id: project_id}}] -> + _ = BDS.Preview.stop_preview(project_id) + :ok + + _other -> + :ok + end + end + end + test "start_preview binds localhost and request resolves generated routes, assets, media, and draft previews", %{project: project, temp_dir: temp_dir} do assert {:ok, _metadata} = @@ -77,8 +108,9 @@ defmodule BDS.PreviewTest do assert {:ok, server} = BDS.Preview.start_preview(project.id) assert server.host == "127.0.0.1" - assert server.port == 4123 + assert server.port > 0 assert server.is_running == true + assert BDS.Preview.base_url() == "http://127.0.0.1:#{server.port}" assert {:ok, %{body: home_html, content_type: "text/html"}} = BDS.Preview.request(project.id, "/") diff --git a/test/bds/real_blog_rebuild_diagnostic_test.exs b/test/bds/real_blog_rebuild_diagnostic_test.exs index 03e5998..e3f7f4c 100644 --- a/test/bds/real_blog_rebuild_diagnostic_test.exs +++ b/test/bds/real_blog_rebuild_diagnostic_test.exs @@ -8,6 +8,7 @@ defmodule BDS.RealBlogRebuildDiagnosticTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo, ownership_timeout: 600_000) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) now = BDS.Persistence.now_ms() unique = Integer.to_string(System.unique_integer([:positive])) diff --git a/test/bds/scripting/api_test.exs b/test/bds/scripting/api_test.exs index 806c69a..300c067 100644 --- a/test/bds/scripting/api_test.exs +++ b/test/bds/scripting/api_test.exs @@ -36,6 +36,7 @@ defmodule BDS.Scripting.ApiTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-scripting-api-#{System.unique_integer([:positive])}") @@ -45,6 +46,10 @@ defmodule BDS.Scripting.ApiTest do {:ok, project} = BDS.Projects.create_project(%{name: "Scripting API", data_path: temp_dir}) + on_exit(fn -> + _ = BDS.Preview.stop_preview(project.id) + end) + %{project: project} end diff --git a/test/bds/scripts/transforms_test.exs b/test/bds/scripts/transforms_test.exs index c3b2bb4..70596dd 100644 --- a/test/bds/scripts/transforms_test.exs +++ b/test/bds/scripts/transforms_test.exs @@ -7,6 +7,7 @@ defmodule BDS.Scripts.TransformsTest do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo) Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual) end) temp_dir = Path.join(System.tmp_dir!(), "bds-transforms-#{System.unique_integer([:positive])}") diff --git a/test/bds/templates_test.exs b/test/bds/templates_test.exs index 5abb187..5e6e9c2 100644 --- a/test/bds/templates_test.exs +++ b/test/bds/templates_test.exs @@ -240,7 +240,7 @@ defmodule BDS.TemplatesTest do Jason.decode!(File.read!(tags_path)) end - test "update_template keeps committed database changes when renaming the published file fails", + test "update_template keeps committed database changes when rewriting the published file fails", %{project: project, temp_dir: temp_dir} do assert {:ok, template} = BDS.Templates.create_template(%{ @@ -269,8 +269,9 @@ defmodule BDS.TemplatesTest do post_template_slug: published.slug }) - blocked_path = Path.join([temp_dir, "templates", "feature-view.liquid.tmp"]) - File.mkdir_p!(blocked_path) + published_template_path = Path.join(temp_dir, published.file_path) + File.rm!(published_template_path) + File.mkdir_p!(published_template_path) assert {:error, _reason} = BDS.Templates.update_template(published.id, %{slug: "feature-view"})