defmodule BDS.ImportExecutionTest do
use ExUnit.Case, async: false
import Ecto.Query
alias BDS.ImportAnalysis
alias BDS.ImportExecution
alias BDS.Posts
alias BDS.Repo
alias BDS.Tags
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir =
Path.join(System.tmp_dir!(), "bds-import-execution-#{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: "Import Execution", data_path: temp_dir})
%{project: project, temp_dir: temp_dir}
end
test "execute_import does not create atoms from malicious report keys", %{
project: project
} do
unique_suffix = :erlang.unique_integer()
unknown_key_1 = "csm001_malicious_#{unique_suffix}"
unknown_key_2 = "csm001_nested_#{unique_suffix}"
malicious_report = %{
"items" => %{
"categories" => [],
"tags" => [],
"posts" => [],
"pages" => [],
"media" => []
},
"details" => %{
"posts" => [],
"pages" => [],
"media" => []
},
unknown_key_1 => "attack",
"extra" => %{unknown_key_2 => "nested_attack"}
}
assert {:ok, _result} =
ImportExecution.execute_import(project.id, malicious_report,
default_author: "Test Author"
)
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_1) end
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_2) end
end
test "execute_import creates tags, posts, pages, and media from the analysis report", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
wxr_path = Path.join(temp_dir, "legacy.xml")
File.write!(wxr_path, basic_wxr_xml())
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, uploads_dir)
assert {:ok, result} =
ImportExecution.execute_import(project.id, report,
uploads_folder_path: uploads_dir,
default_author: "Imported Author"
)
assert result.success
assert result.tags == %{created: 2, skipped: 0}
assert result.posts == %{imported: 1, skipped: 0, errors: 0}
assert result.pages == %{imported: 1, skipped: 0, errors: 0}
assert result.media == %{imported: 1, skipped: 0, errors: 0}
assert result.errors == []
tag_names = project.id |> Tags.list_tags() |> Enum.map(& &1.name) |> Enum.sort()
assert tag_names == ["General", "News"]
posts =
Repo.all(
from post in Posts.Post, where: post.project_id == ^project.id, order_by: [asc: post.slug]
)
assert Enum.map(posts, & &1.slug) == ["about", "hello-world"]
hello_world = Enum.find(posts, &(&1.slug == "hello-world"))
about = Enum.find(posts, &(&1.slug == "about"))
assert hello_world.status == :published
assert hello_world.author == "Importer"
assert hello_world.content == nil
assert hello_world.file_path != ""
assert File.exists?(Path.join(temp_dir, hello_world.file_path))
assert File.read!(Path.join(temp_dir, hello_world.file_path)) =~ "Hello World"
assert about.status == :published
assert about.content == nil
assert "page" in about.categories
imported_media =
Repo.one!(from media in BDS.Media.Media, where: media.project_id == ^project.id)
assert imported_media.original_name == "import-asset.txt"
assert File.exists?(Path.join(temp_dir, imported_media.file_path))
end
test "execute_import skips conflicts by default and can import them with a new slug", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _existing_post} =
Posts.create_post(%{
project_id: project.id,
title: "Conflict Me",
content: "Local body",
checksum: sha256("Local body")
})
wxr_path = Path.join(temp_dir, "conflict.xml")
File.write!(wxr_path, conflict_only_wxr_xml())
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, nil)
assert {:ok, skipped_result} =
ImportExecution.execute_import(project.id, report, default_author: "Imported Author")
assert skipped_result.posts == %{imported: 0, skipped: 1, errors: 0}
assert Repo.aggregate(Posts.Post, :count, :id) == 1
import_report =
put_in(report.items.posts, [%{List.first(report.items.posts) | resolution: "import"}])
assert {:ok, imported_result} =
ImportExecution.execute_import(project.id, import_report,
default_author: "Imported Author"
)
assert imported_result.posts == %{imported: 1, skipped: 0, errors: 0}
slugs =
Repo.all(
from post in Posts.Post,
where: post.project_id == ^project.id,
select: post.slug,
order_by: [asc: post.slug]
)
assert length(slugs) == 2
assert "conflict-me" in slugs
assert Enum.any?(slugs, &(&1 != "conflict-me"))
end
test "execute_import reports phase progress while importing", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
wxr_path = Path.join(temp_dir, "legacy.xml")
File.write!(wxr_path, basic_wxr_xml())
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, uploads_dir)
assert {:ok, _result} =
ImportExecution.execute_import(project.id, report,
uploads_folder_path: uploads_dir,
default_author: "Imported Author",
on_progress: fn phase, current, total, detail ->
send(self(), {:execution_progress, phase, current, total, detail})
end
)
assert_received {:execution_progress, "tags", 0, 2, %{detail: "creating_tags"}}
assert_received {:execution_progress, "posts", 0, 1, %{detail: "importing_posts"}}
assert_received {:execution_progress, "media", 0, 1, %{detail: "importing_media"}}
assert_received {:execution_progress, "pages", 0, 1, %{detail: "importing_pages"}}
assert_received {:execution_progress, "complete", 1, 1, %{detail: "import_complete"}}
end
defp sha256(value) do
:sha256
|> :crypto.hash(value)
|> Base.encode16(case: :lower)
end
defp basic_wxr_xml do
"""