402 lines
13 KiB
Elixir
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
|