Files
bDS2/test/bds/cli_test.exs

402 lines
13 KiB
Elixir

defmodule BDS.CLITest do
use ExUnit.Case, async: false
import Ecto.Query
import ExUnit.CaptureIO
alias BDS.CLI
alias BDS.CLI.Commands
alias BDS.CliSync.Notification
alias BDS.Repo
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Repo.delete_all(Notification)
temp_dir = Path.join(System.tmp_dir!(), "bds-cli-test-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
{:ok, temp_dir: temp_dir}
end
defp create_active_project(temp_dir, name \\ "CLI Test") do
{:ok, project} = BDS.Projects.create_project(%{name: name, data_path: temp_dir})
{:ok, project} = BDS.Projects.set_active_project(project.id)
project
end
defp notifications do
Repo.all(Notification)
end
describe "argv decoding" do
test "splits on the unit separator and drops empties" do
assert CLI.env_argv("post" <> <<0x1F>> <> "--title" <> <<0x1F>> <> "Hello" <> <<0x1F>>) ==
["post", "--title", "Hello"]
assert CLI.env_argv(nil) == []
assert CLI.env_argv("") == []
end
end
describe "parser" do
test "parses every subcommand" do
optimus = CLI.optimus()
assert {:ok, [:rebuild], %{flags: %{incremental: true}}} =
Optimus.parse(optimus, ["rebuild", "--incremental"])
assert {:ok, [:repair], %{args: %{part: "post-links"}}} =
Optimus.parse(optimus, ["repair", "post-links"])
assert {:error, [:repair], _errors} = Optimus.parse(optimus, ["repair", "nonsense"])
assert {:ok, [:render], %{flags: %{force: true, incremental: false}}} =
Optimus.parse(optimus, ["render", "--force"])
assert {:ok, [:upload], _result} = Optimus.parse(optimus, ["upload"])
assert {:ok, [:push], _result} = Optimus.parse(optimus, ["push"])
assert {:ok, [:pull], _result} = Optimus.parse(optimus, ["pull"])
assert {:ok, [:post], %{options: %{title: "Hi", tags: "a,b"}}} =
Optimus.parse(optimus, ["post", "--title", "Hi", "--tags", "a,b"])
assert {:ok, [:media], %{args: %{file: "img.jpg"}, options: %{language: "de"}}} =
Optimus.parse(optimus, ["media", "img.jpg", "--language", "de"])
assert {:ok, [:gallery], %{options: %{title: "Trip"}, unknown: ["a.jpg", "b.jpg"]}} =
Optimus.parse(optimus, ["gallery", "--title", "Trip", "a.jpg", "b.jpg"])
assert {:ok, [:config, :get], %{args: %{key: "ui.theme"}}} =
Optimus.parse(optimus, ["config", "get", "ui.theme"])
assert {:ok, [:config, :set], %{args: %{key: "k", value: "v"}}} =
Optimus.parse(optimus, ["config", "set", "k", "v"])
assert {:ok, [:config, :list], _result} = Optimus.parse(optimus, ["config", "list"])
assert {:ok, [:project, :list], _result} = Optimus.parse(optimus, ["project", "list"])
assert {:ok, [:project, :add], %{args: %{path: "/tmp/x"}, options: %{name: "X"}}} =
Optimus.parse(optimus, ["project", "add", "/tmp/x", "--name", "X"])
assert {:ok, [:project, :switch], %{args: %{project: "blog"}}} =
Optimus.parse(optimus, ["project", "switch", "blog"])
assert {:ok, [:tui], _result} = Optimus.parse(optimus, ["tui"])
assert {:ok, [:lua], %{args: %{script: "cleanup"}, unknown: ["arg1"]}} =
Optimus.parse(optimus, ["lua", "cleanup", "arg1"])
end
test "help exits 0 and unknown commands exit 1" do
{help_code, help_output} = with_io(fn -> CLI.run(["--help"]) end)
assert help_code == 0
assert help_output =~ "rebuild"
assert help_output =~ "gallery"
{code, _output} = with_io(:stderr, fn -> CLI.run(["frobnicate"]) end)
assert code == 1
end
end
describe "config commands" do
test "set, get, and list operate on the global settings store", %{temp_dir: _temp_dir} do
assert {:ok, "cli.test.key = hello"} =
Commands.dispatch([:config, :set], %{args: %{key: "cli.test.key", value: "hello"}})
assert {:ok, "hello"} =
Commands.dispatch([:config, :get], %{args: %{key: "cli.test.key"}})
{:ok, output} = with_io(fn -> Commands.dispatch([:config, :list], %{}) end)
assert output =~ "cli.test.key=hello"
assert {:error, message} =
Commands.dispatch([:config, :get], %{args: %{key: "cli.test.missing"}})
assert message =~ "not set"
assert Enum.any?(
notifications(),
&(&1.entity_type == "setting" and &1.entity_id == "cli.test.key" and
&1.from_cli == true)
)
end
end
describe "project commands" do
test "add, switch, and list manage the project registry", %{temp_dir: temp_dir} do
assert {:ok, added_message} =
Commands.dispatch([:project, :add], %{
args: %{path: temp_dir},
options: %{name: "CLI Added"}
})
assert added_message =~ "Added project"
project =
Enum.find(BDS.Projects.list_projects(), &(&1.name == "CLI Added"))
assert project
assert {:ok, "Active project: CLI Added"} =
Commands.dispatch([:project, :switch], %{args: %{project: project.slug}})
{:ok, output} = with_io(fn -> Commands.dispatch([:project, :list], %{}) end)
assert output =~ "CLI Added"
assert output =~ "* " <> project.id
assert {:error, message} =
Commands.dispatch([:project, :switch], %{args: %{project: "no-such-project"}})
assert message =~ "No project matches"
entity_types = Enum.map(notifications(), & &1.entity_type)
assert "project" in entity_types
end
test "add rejects a missing directory" do
assert {:error, message} =
Commands.dispatch([:project, :add], %{
args: %{path: "/nonexistent/bds-cli"},
options: %{}
})
assert message =~ "not a directory"
end
end
describe "post command" do
test "creates a post from options and writes a cli notification", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
{{:ok, message}, output} =
with_io(fn ->
Commands.dispatch([:post], %{
flags: %{no_translate: true},
options: %{
title: "From the CLI",
content: "Hello from the command line",
language: "en",
tags: "cli,test",
categories: "notes"
}
})
end)
assert message =~ "Created post"
assert output == "" or output =~ "translation"
post =
Repo.one!(
from post in BDS.Posts.Post, where: post.title == "From the CLI"
)
assert post.tags == ["cli", "test"]
assert post.categories == ["notes"]
assert post.language == "en"
assert post.status == :draft
assert Enum.any?(
notifications(),
&(&1.entity_type == "post" and &1.entity_id == post.id and &1.action == :created)
)
end
test "creates a post from JSON on stdin", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
json =
Jason.encode!(%{
"title" => "Stdin Post",
"content" => "Body from stdin",
"language" => "en",
"tags" => ["json", "cli"]
})
{{:ok, message}, _output} =
with_io([input: json], fn ->
Commands.dispatch([:post], %{flags: %{stdin: true, no_translate: true}, options: %{}})
end)
assert message =~ "Created post"
post = Repo.one!(from post in BDS.Posts.Post, where: post.title == "Stdin Post")
assert post.tags == ["json", "cli"]
end
test "requires a title", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
assert {:error, message} = Commands.dispatch([:post], %{flags: %{}, options: %{}})
assert message =~ "--title is required"
end
end
describe "media command" do
test "imports a file and reports enrichment best-effort", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
source = Path.join(temp_dir, "cli-image.png")
File.write!(source, "fake png bytes")
{result, _output} =
with_io(fn ->
Commands.dispatch([:media], %{args: %{file: source}, options: %{}})
end)
assert {:ok, message} = result
assert message =~ "Imported media"
assert Enum.any?(notifications(), &(&1.entity_type == "media" and &1.action == :created))
end
test "rejects a missing file", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
assert {:error, message} =
Commands.dispatch([:media], %{
args: %{file: Path.join(temp_dir, "missing.png")},
options: %{}
})
assert message =~ "No such file"
end
end
describe "rebuild command" do
test "incremental rebuild diffs and applies from the filesystem", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
{result, _output} =
with_io(fn ->
Commands.dispatch([:rebuild], %{flags: %{incremental: true}})
end)
assert {:ok, message} = result
assert message =~ "differences"
end
end
describe "gallery and tui guards" do
test "gallery without images errors", %{temp_dir: temp_dir} do
create_active_project(temp_dir)
assert {:error, message} =
Commands.dispatch([:gallery], %{
flags: %{},
options: %{title: "Trip"},
unknown: []
})
assert message =~ "image files"
end
test "tui is delegated to the launcher script" do
assert {:error, message} = Commands.dispatch([:tui], %{})
assert message =~ "launcher"
end
end
describe "lua command" do
test "rejects unknown and non-utility scripts", %{temp_dir: temp_dir} do
project = create_active_project(temp_dir)
assert {:error, message} =
Commands.dispatch([:lua], %{args: %{script: "missing"}, unknown: []})
assert message =~ "No script"
{:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Macro Script",
kind: :macro,
content: "function render() return \"x\" end",
entrypoint: "render"
})
assert {:error, macro_message} =
Commands.dispatch([:lua], %{args: %{script: script.slug}, unknown: []})
assert macro_message =~ "macro"
end
test "runs a utility script synchronously", %{temp_dir: temp_dir} do
project = create_active_project(temp_dir)
{:ok, script} =
BDS.Scripts.create_script(%{
project_id: project.id,
title: "Task Script",
kind: :utility,
content: "function main() return \"done-42\" end",
entrypoint: "main"
})
assert {:ok, message} =
Commands.dispatch([:lua], %{args: %{script: script.slug}, unknown: []})
assert message =~ "done-42"
end
end
describe "full rebuild step sharing" do
test "the CLI and GUI share the canonical full-rebuild sequence" do
names = Enum.map(BDS.Maintenance.full_rebuild_steps("project-id"), & &1.name)
assert names == [
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
]
end
end
describe "installer" do
test "installs a shim pointing at the release launcher", %{temp_dir: temp_dir} do
release_root = Path.join(temp_dir, "release")
launcher = Path.join([release_root, "cli", "bin", "bds-cli"])
File.mkdir_p!(Path.dirname(launcher))
File.write!(launcher, "#!/bin/sh\n")
bin_dir = Path.join(temp_dir, "local-bin")
assert {:ok, installed} =
BDS.CLI.Install.install(bin_dir: bin_dir, release_root: release_root)
assert installed == Path.join(bin_dir, "bds-cli")
content = File.read!(installed)
assert content =~ launcher
assert content =~ ~s(exec ")
assert Bitwise.band(File.stat!(installed).mode, 0o111) != 0
end
test "reports no_release outside a packaged release", %{temp_dir: temp_dir} do
assert {:error, :no_release} =
BDS.CLI.Install.install(
bin_dir: Path.join(temp_dir, "bin"),
release_root: Path.join(temp_dir, "not-a-release")
)
end
end
describe "settings form action" do
test "the data section exposes the install action and run_action handles it" do
fields = BDS.UI.SettingsForm.load("data", "nonexistent-project").fields
action = Enum.find(fields, &(&1.type == :action))
assert action.key == "install_cli"
# Outside a packaged release the action reports the error message.
assert {:error, message} = BDS.UI.SettingsForm.run_action("install_cli")
assert message =~ "packaged application"
assert {:error, _unknown} = BDS.UI.SettingsForm.run_action("nope")
end
end
end