243 lines
5.7 KiB
Elixir
243 lines
5.7 KiB
Elixir
defmodule BDS.Scripts.TransformsTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
alias BDS.Scripts
|
|
alias BDS.Scripts.Transforms
|
|
|
|
setup do
|
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
|
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
|
|
|
|
temp_dir =
|
|
Path.join(System.tmp_dir!(), "bds-transforms-#{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: "Transforms", data_path: temp_dir})
|
|
|
|
%{project: project}
|
|
end
|
|
|
|
defp transform(project_id, title, content, opts \\ []) do
|
|
{:ok, script} =
|
|
Scripts.create_script(%{
|
|
project_id: project_id,
|
|
title: title,
|
|
kind: :transform,
|
|
content: content,
|
|
entrypoint: Keyword.get(opts, :entrypoint, "main")
|
|
})
|
|
|
|
script =
|
|
case Keyword.get(opts, :enabled, true) do
|
|
true ->
|
|
script
|
|
|
|
false ->
|
|
{:ok, s} = Scripts.update_script(script.id, %{enabled: false})
|
|
s
|
|
end
|
|
|
|
script
|
|
end
|
|
|
|
test "runs enabled transforms in deterministic order (updated_at, slug, id)", %{
|
|
project: project
|
|
} do
|
|
# Each transform appends its marker to content so we can read execution order.
|
|
transform(project.id, "Bravo", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "B"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
# Ensure distinct updated_at ordering by spacing out creation.
|
|
Process.sleep(5)
|
|
|
|
transform(project.id, "Alpha", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "A"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://x"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
# Bravo created first (earlier updated_at) so runs before Alpha.
|
|
assert result.data["content"] == "BA"
|
|
assert result.errors == []
|
|
end
|
|
|
|
test "disabled transforms and transforms from other projects are skipped", %{project: project} do
|
|
{:ok, other} =
|
|
BDS.Projects.create_project(%{
|
|
name: "Other",
|
|
data_path:
|
|
Path.join(
|
|
System.tmp_dir!(),
|
|
"bds-transforms-other-#{System.unique_integer([:positive])}"
|
|
)
|
|
})
|
|
|
|
transform(
|
|
project.id,
|
|
"Disabled",
|
|
"""
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "D"
|
|
return data
|
|
end
|
|
""",
|
|
enabled: false
|
|
)
|
|
|
|
transform(other.id, "Foreign", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "F"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
transform(project.id, "Enabled", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "E"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://x"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
assert result.data["content"] == "E"
|
|
end
|
|
|
|
test "pipeline continues after a failing transform, keeping last valid state", %{
|
|
project: project
|
|
} do
|
|
transform(project.id, "First", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "1"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
Process.sleep(5)
|
|
|
|
transform(project.id, "Boom", """
|
|
function main(_data, _ctx)
|
|
error("boom")
|
|
end
|
|
""")
|
|
|
|
Process.sleep(5)
|
|
|
|
transform(project.id, "Third", """
|
|
function main(data, _ctx)
|
|
data.content = data.content .. "3"
|
|
return data
|
|
end
|
|
""")
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://x"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
# Boom's failure does not roll back "1" and does not stop "3".
|
|
assert result.data["content"] == "13"
|
|
assert [%{reason: _}] = result.errors
|
|
end
|
|
|
|
test "receives blogmark context with source and originating url", %{project: project} do
|
|
transform(project.id, "Ctx", """
|
|
function main(data, ctx)
|
|
data.content = ctx.source .. "|" .. ctx.url
|
|
return data
|
|
end
|
|
""")
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://example.com/a"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
assert result.data["content"] == "blogmark|http://example.com/a"
|
|
end
|
|
|
|
test "per-script toast budget caps and truncates toasts", %{project: project} do
|
|
long = String.duplicate("x", 500)
|
|
|
|
transform(project.id, "Noisy", """
|
|
function main(data, _ctx)
|
|
local toasts = {}
|
|
for i = 1, 10 do toasts[i] = "#{long}" end
|
|
return { data = data, toasts = toasts }
|
|
end
|
|
""")
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://x"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
# max 5 per script
|
|
assert length(result.toasts) == 5
|
|
# truncated to 300 chars
|
|
assert Enum.all?(result.toasts, &(String.length(&1) == 300))
|
|
end
|
|
|
|
test "total toast budget caps across the whole pipeline", %{project: project} do
|
|
body = """
|
|
function main(data, _ctx)
|
|
local toasts = {}
|
|
for i = 1, 5 do toasts[i] = "msg" end
|
|
return { data = data, toasts = toasts }
|
|
end
|
|
"""
|
|
|
|
# 5 transforms x 5 toasts each = 25 emitted, total budget is 20.
|
|
for i <- 1..5 do
|
|
transform(project.id, "T#{i}", body)
|
|
Process.sleep(3)
|
|
end
|
|
|
|
data = %{
|
|
"title" => "t",
|
|
"content" => "",
|
|
"tags" => [],
|
|
"categories" => [],
|
|
"url" => "http://x"
|
|
}
|
|
|
|
assert {:ok, result} = Transforms.run(project.id, data)
|
|
assert length(result.toasts) == 20
|
|
end
|
|
end
|