//! Generates an app and scaffolds with the real `glacier` binary, then
//! builds the result with plain cargo, runs its generated tests, and runs
//! the app, so the generators can't drift from the framework without a
//! failing test.
//!
//! Controllers default to the .glacier dialect, compiled by the app's
//! build.rs; one scaffold uses `--dialect rust` to cover plain Rust too.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
/// One generated app builds at a time: they share a target directory, and
/// building them side by side has failed to find a generated file.
static BUILDING: Mutex<()> = Mutex::new(());
fn glacier(dir: &Path, args: &[&str]) -> String {
let out = Command::new(env!("CARGO_BIN_EXE_glacier"))
.args(args)
.current_dir(dir)
.output()
.expect("run glacier");
assert!(
out.status.success(),
"glacier {args:?} failed:\n{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
/// The workspace's target dir, so the app reuses already-built crates.
fn target_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/generated-apps")
}
/// The same folder every run, emptied first: an app built at the same path
/// reuses its last build in the shared target (`target/generated-apps`),
/// where a new path each run would add a whole new build to it.
fn fresh_root(name: &str) -> std::path::PathBuf {
let root = std::env::temp_dir().join(format!("glacier-{name}-app"));
let _ = std::fs::remove_dir_all(&root);
root
}
fn cargo(app: &Path, args: &[&str]) -> String {
let out = Command::new("cargo")
.args(args)
.args(["--offline", "--quiet"])
.current_dir(app)
.env("CARGO_TARGET_DIR", target_dir())
// Incremental files would pile up in the shared target; a test
// build doesn't need them.
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run cargo");
let output = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(out.status.success(), "cargo {args:?} failed:\n{output}");
assert!(
!output.contains("warning"),
"cargo {args:?} has warnings:\n{output}"
);
output
}
#[test]
fn new_app_with_scaffolds_builds_tests_and_runs() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("gen");
std::fs::create_dir_all(&root).unwrap();
glacier(&root, &["new", "shop"]);
let app = root.join("shop");
let controllers = app.join("src/controllers");
// For agents: conventions, and the MCP server for clients to start.
let agents = std::fs::read_to_string(app.join("AGENTS.md")).unwrap();
assert!(agents.starts_with("# Working on shop"), "{agents}");
let mcp: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(app.join(".mcp.json")).unwrap()).unwrap();
assert_eq!(mcp["mcpServers"]["glacier"]["args"][0], "mcp");
// The starter controller is .glacier by default.
assert!(controllers.join("pages_controller.glacier").exists());
assert!(!controllers.join("pages_controller.rs").exists());
// So is the route table.
assert!(app.join("src/routes.glacier").exists());
assert!(!app.join("src/routes.rs").exists());
// Webhooks, in `.glacier` with an inline handler.
glacier(&app, &["generate", "webhook", "stripe"]);
let webhooks = std::fs::read_to_string(app.join("src/webhooks.glacier")).unwrap();
assert!(
webhooks.contains("provider :stripe, secret: secret("),
"{webhooks}"
);
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
assert!(
routes.contains("post \"/webhooks/:provider\", glacier.webhooks.receive"),
"{routes}"
);
// One scaffold in plain Rust, two in .glacier (the default).
glacier(
&app,
&[
"generate",
"scaffold",
"Product",
"name:string",
"description:text:optional",
"price:float",
"stock:integer",
"featured:boolean",
"--dialect",
"rust",
],
);
assert!(controllers.join("products_controller.rs").exists());
// Scopes on a model, chained from .glacier below.
let product = app.join("src/models/product.rs");
let model = std::fs::read_to_string(&product).unwrap();
std::fs::write(
&product,
format!(
"{model}\n#[glacier::scopes]\nimpl Product {{\n fn cheap(products: glacier::db::Query<Self>) -> glacier::db::Query<Self> {{\n products.filter(|p| p.price.lt(10.0))\n }}\n\n fn named(products: glacier::db::Query<Self>, name: &str) -> glacier::db::Query<Self> {{\n let name = name.to_owned();\n products.filter(|p| p.name.eq(name))\n }}\n}}\n"
),
)
.unwrap();
glacier(&app, &["g", "scaffold", "Comment", "body:text"]);
// Migrations live beside the schema, with no list to keep.
let migrations: Vec<String> = std::fs::read_dir(app.join("db/migrations"))
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.collect();
assert!(
migrations
.iter()
.any(|m| m.ends_with("_create_comments.glacier")),
"{migrations:?}"
);
assert!(!app.join("src/migrations").exists());
// A foreign key: its tests compile but wait for parent records.
glacier(
&app,
&["g", "scaffold", "Review", "product:references", "body:text"],
);
// Generators for a model or a controller alone write their tests too.
glacier(
&app,
&[
"g",
"model",
"Tag",
"name:string",
"slug:string",
"kind:string",
],
);
// A .glacier model using every kind of member, replacing the generated one.
std::fs::write(
app.join("src/models/tag.glacier"),
r##"model Tag do
field name: String, required, length(min: 2, max: 30)
field slug: String
field kind: enum(topic, language), default: :topic
scope named(name: &str), where(name: name)
scope languages, where(kind: :language)
def label(self) -> String do
format!("#{}", self.name)
end
def rename(self, name: &str) do
self.update(name: name)!
end
def language_count() -> i64 do
Tag.languages().count()!
end
before_save do
self.slug = self.name.to_lowercase()
end
end
"##,
)
.unwrap();
std::fs::write(
app.join("tests/factories/tag.glacier"),
"factory Tag do\n name: \"Example name\"\n slug: \"\"\n kind: \"topic\"\nend\n",
)
.unwrap();
let tag_tests = std::fs::read_to_string(app.join("tests/models/tag.test.glacier")).unwrap();
std::fs::write(
app.join("tests/models/tag.test.glacier"),
format!(
"{tag_tests}\ntest \"model features\" do\n tag = Tag.new(name: \"Rust\", kind: :language)\n assert tag.save()!\n assert tag.slug == \"rust\"\n assert tag.label() == \"#Rust\"\n assert tag.kind.is_language()\n assert Tag.languages().count()! == 1\n assert Tag.language_count()! == 1\n assert Tag.named(\"Rust\").exists()!\n tag.rename(\"Elixir\")!\n assert Tag.find(tag.id)!.slug == \"elixir\"\n short = Tag.new(name: \"x\")\n refute short.save()!\n assert short.errors_on(\"name\") == [\"is too short (minimum is 2 characters)\"]\nend\n"
),
)
.unwrap();
glacier(&app, &["g", "controller", "Reports", "summary"]);
glacier(
&app,
&["g", "model", "Note", "body:text", "--dialect", "rust"],
);
glacier(
&app,
&["g", "controller", "Legal", "terms", "--dialect", "rust"],
);
for file in [
"tests/models/tag.test.glacier",
"tests/factories/tag.glacier",
"tests/controllers/reports_controller.test.glacier",
"tests/models/comment.test.glacier",
"tests/note_model.rs",
"tests/legal_controller.rs",
"tests/product_model.rs",
] {
assert!(app.join(file).exists(), "{file}");
}
assert!(controllers.join("comments_controller.glacier").exists());
assert!(
!controllers.join("comments_controller.rs").exists(),
"a .glacier scaffold writes only its source"
);
// `glacier::app!()` finds every module: no lists to keep.
assert!(!controllers.join("mod.rs").exists());
assert!(!app.join("src/models/mod.rs").exists());
let lib = std::fs::read_to_string(app.join("src/lib.rs")).unwrap();
assert!(lib.contains("glacier::app!();"), "{lib}");
let main = std::fs::read_to_string(app.join("src/main.rs")).unwrap();
assert!(main.contains("glacier::serve!();"), "{main}");
assert!(app.join("src/app.glacier").exists());
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
let home = routes.find("get \"/\"").unwrap();
let products = routes
.find("resources \"/products\", ProductsController\n")
.unwrap();
assert!(
products > home,
"new resources go at the end of the scope:\n{routes}"
);
// A hand-written .glacier controller using multi-line arms and closures,
// cond, with, and blocks assigned to names.
std::fs::write(
controllers.join("stats_controller.glacier"),
r#"controller Stats do
def index do
case Product.count()! do
0 ->
Ok(Response.text("none"))
n ->
cheap = Product.filter(fn p ->
p.price.lt(10.0)
end)!
size = cond do
n > 100 -> "many"
n > 1 -> "some"
true -> "one"
end
first = with Some(p) <- cheap.first(),
true <- p.stock > 0 do
p.name.clone()
else
"none in stock".to_owned()
end
Ok(Response.text(format!("{n} products ({size}), {} cheap: {first}", cheap.len())))
end
end
end
"#,
)
.unwrap();
// Generated and hand-written .glacier files are already in canonical form.
glacier(&app, &["fmt", "--check"]);
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
std::fs::write(
app.join("src/routes.glacier"),
routes.replace(
"get \"/\", PagesController.home, as: :home",
"get \"/\", PagesController.home, as: :home\n get \"/stats\", StatsController.index, as: :stats",
),
)
.unwrap();
// A hand-written .glacier test file, and a factory for the Rust
// scaffold's model.
std::fs::write(
app.join("tests/controllers/stats_controller.test.glacier"),
r#"# Calls to helpers are awaited for you.
def stats -> String do
get("/stats").body
end
describe "/stats" do
test "counts products" do
assert stats() == "none"
insert(Product, price: 5.0)
insert(Product, name: "Pricey", price: 50.0)
assert Product.count()! == 2
assert stats() == "2 products (some), 1 cheap: Lamp"
refute stats() =~ "none"
end
test "posts keyword lists" do
get "/products/new"
res = post "/products", [name: "Lamp", price: 3.5, stock: 2]
assert res.status == :see_other
assert get(res.location).body =~ "Lamp"
end
test "waits for later", skip: "shows how to skip" do
assert false
end
end
test "active record keywords" do
insert(Product, name: "Lamp", price: 5.0)
insert(Product, name: "Desk", price: 50.0, description: "Oak")
assert Product.cheap().count()! == 1
assert Product.named("Desk").cheap().count()! == 0
assert Product.where(price: 5.0).cheap().count()! == 1
assert Product.where(name: "Lamp").count()! == 1
assert Product.where_not(description: nil).count()! == 1
assert Product.exists(name: "Desk")!
assert Product.all().order(name: :asc).pluck(:name)! == ["Desk", "Lamp"]
lamp = Product.find_by(name: "Lamp")!.unwrap()
lamp.price = 7.5
lamp.description = "Brass"
assert lamp.changed("price")
assert lamp.save()!
lamp.update(stock: 3)!
assert Product.find(lamp.id)!.stock == 3
assert Product.where(stock: 3).update_all(stock: 0)! == 1
chair = Product.new(name: "Chair", price: 20.0)
assert chair.save()!
assert Product.count()! == 3
end
"#,
)
.unwrap();
std::fs::write(
app.join("tests/factories/product.glacier"),
"factory Product do\n name: \"Lamp\"\n price: 9.5\n stock: 1\nend\n",
)
.unwrap();
// A mailer in .glacier, and a test that sends its mail.
glacier(&app, &["generate", "mailer", "ReportMailer", "ready"]);
assert!(app.join("src/mailers/report_mailer.glacier").exists());
std::fs::create_dir_all(app.join("tests/mailers")).unwrap();
std::fs::write(
app.join("tests/mailers/report_mailer.test.glacier"),
"test \"sends the report mail\" do\n ReportMailer.ready(\"ada@example.com\", \"Ada\").deliver_now()!\n mail = delivered_mail()\n assert mail.len() == 1\n assert mail[0].subject == \"Ready\"\nend\n",
)
.unwrap();
// `glacier::tests!()` finds every test file too.
let suite = std::fs::read_to_string(app.join("tests/glacier.rs")).unwrap();
assert!(suite.contains("glacier::tests!();"), "{suite}");
glacier(&app, &["fmt", "--check"]);
// Plain cargo: build.rs compiles the .glacier files first.
cargo(&app, &["build"]);
assert!(
app.join(".glacier/controllers/comments_controller.rs")
.exists(),
"build.rs compiled it into the hidden build directory"
);
let source_files: Vec<String> = std::fs::read_dir(&controllers)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert!(
source_files.iter().all(|f| f.ends_with(".glacier")
|| ["products_controller.rs", "legal_controller.rs", "mod.rs"].contains(&f.as_str())),
"src/ holds only what you write: {source_files:?}"
);
let ignore = std::fs::read_to_string(app.join(".gitignore")).unwrap();
assert!(
ignore.contains("/.glacier/"),
"compiled output is ignored by git"
);
// Every scaffold's generated tests pass, whatever its dialect.
let tests = cargo(&app, &["test"]);
assert!(
tests.contains("5 passed; 0 failed; 0 ignored"),
"products' Rust tests pass:\n{tests}"
);
// Controllers: pages 1, comments 5, reviews 1 (4 wait for a Product
// factory that didn't exist yet), reports 1, stats 2 (1 skipped).
// Models: comment 2, review 1 (1 waits), tag 2. Mailers: report 1.
// Plus the Active Record keywords test. All in one binary.
assert!(
tests.contains("18 passed; 0 failed; 6 ignored"),
".glacier tests pass:\n{tests}"
);
// A failing assertion explains itself and points at its .glacier line.
std::fs::write(
app.join("tests/failing.test.glacier"),
"test \"fails\" do\n res = get \"/\"\n assert res.status == :not_found\nend\n",
)
.unwrap();
std::fs::write(
app.join("tests/glacier.rs"),
suite.replace(
"models::tag);",
"models::tag, controllers::stats_controller, factories::product, failing);",
),
)
.unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_glacier"))
.args(["test", "--offline", "--", "fails"])
.current_dir(&app)
.env("CARGO_TARGET_DIR", target_dir())
// Incremental files would pile up in the shared target; a test
// build doesn't need them.
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run glacier test");
let printed = String::from_utf8_lossy(&out.stdout);
assert!(!out.status.success(), "{printed}");
for expected in [
"tests/failing.test.glacier:3:",
"assertion failed: res.status == :not_found",
"left: :ok (200)",
] {
assert!(
printed.contains(expected),
"missing {expected:?}:\n{printed}"
);
}
std::fs::remove_file(app.join("tests/failing.test.glacier")).unwrap();
let run = |task: &str| {
let out = Command::new(target_dir().join("debug/shop"))
.current_dir(&app)
.env("GLACIER_ENV", "test")
.env("GLACIER_TASK", task)
.output()
.expect("run app");
assert!(
out.status.success(),
"{task}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
};
let migrated = run("db:migrate");
for table in ["products", "comments", "reviews"] {
assert!(migrated.contains(&format!("_create_{table}")), "{migrated}");
}
let table = run("routes");
for row in [
"Pages#home",
"Products#edit",
"Comments#index",
"Comments#update",
"Reviews#destroy",
] {
assert!(table.contains(row), "missing {row} in:\n{table}");
}
std::fs::remove_dir_all(&root).unwrap();
}
/// Adds a scope to the end of the app's route table.
fn add_scope(app: &Path, scope: &str) {
let path = app.join("src/routes.glacier");
let routes = std::fs::read_to_string(&path).unwrap();
let close = routes.rfind("\nend").unwrap() + 1;
let updated = format!("{}{scope}{}", &routes[..close], &routes[close..]);
std::fs::write(path, updated).unwrap();
}
#[test]
fn authentication_for_pages_and_apis() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("auth");
std::fs::create_dir_all(&root).unwrap();
// Pages: sign up, in and out, and a page only for signed-in users.
glacier(&root, &["new", "accounts"]);
let app = root.join("accounts");
let out = glacier(&app, &["generate", "authentication"]);
assert!(out.contains("glacier db migrate"), "{out}");
// Its middleware joins the app's settings.
let settings = std::fs::read_to_string(app.join("src/app.glacier")).unwrap();
assert!(settings.contains(" middleware Logger\n # Who's signed in, for every request.\n middleware Authentication\n"), "{settings}");
std::fs::write(
app.join("src/controllers/accounts_controller.glacier"),
"alias Auth\n\ncontroller Accounts do\n def show(ctx) do\n email_address = Auth.current_user(&ctx).map(fn u -> u.email_address.clone() end).unwrap_or_default()\n render email_address: email_address, title: \"Account\"\n end\nend\n",
)
.unwrap();
std::fs::create_dir_all(app.join("src/views/accounts")).unwrap();
std::fs::write(
app.join("src/views/accounts/show.html"),
"{% props email_address: String %}\n<h1>Signed in as {{ email_address }}</h1>\n",
)
.unwrap();
add_scope(
&app,
"\n scope \"/\", through: [:browser, :authenticated] do\n get \"/account\", AccountsController.show, as: :account\n end\n",
);
std::fs::write(
app.join("tests/controllers/accounts_controller.test.glacier"),
"test \"sends visitors to sign in, then back\" do\n insert(User, email_address: \"user@example.com\")\n res = get \"/account\"\n assert res.location == \"/session/new\"\n get \"/session/new\"\n res = post \"/session\", [email_address: \"user@example.com\", password: \"password\"]\n assert res.location == \"/account\"\n assert get(\"/account\").body =~ \"Signed in as user@example.com\"\nend\n",
)
.unwrap();
// Notes that are each user's own.
glacier(
&app,
&[
"generate",
"scaffold",
"Note",
"title:string",
"--access",
"user",
],
);
let notes =
std::fs::read_to_string(app.join("src/controllers/notes_controller.glacier")).unwrap();
assert!(
notes.contains("Note.for(access).find(params[:id])!"),
"{notes}"
);
let note = std::fs::read_to_string(app.join("src/models/note.glacier")).unwrap();
assert!(note.contains(" owned_by :user\n"), "{note}");
// Orgs whose members share rows.
let out = glacier(&app, &["generate", "access", "org"]);
assert!(out.contains("in_org"), "{out}");
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
assert!(
// The current org is kept in the session.
routes.contains("scope \"/\", through: [:browser, :authenticated, :in_org] do\n get \"/org\", OrgsController.show, as: :org"),
"{routes}"
);
// Projects that are the current org's.
glacier(
&app,
&[
"generate",
"scaffold",
"Project",
"name:string",
"--access",
"org",
],
);
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
assert!(
routes.contains(
":authenticated, :in_org] do\n resources \"/projects\", ProjectsController\n"
),
"{routes}"
);
// With no `--access`, an app with orgs scaffolds the org's records; a
// unique field is unique within the org, and indexed so.
let out = glacier(
&app,
&["generate", "scaffold", "Label", "name:string:unique"],
);
assert!(out.contains("Owned by the request's org"), "{out}");
let label = std::fs::read_to_string(app.join("src/models/label.glacier")).unwrap();
assert!(label.contains(" owned_by :org\n"), "{label}");
assert!(
label.contains("field name: String, required, unique"),
"{label}"
);
let migration = std::fs::read_dir(app.join("db/migrations"))
.unwrap()
.filter_map(|entry| entry.ok())
.find(|entry| {
entry
.file_name()
.to_string_lossy()
.ends_with("create_labels.glacier")
})
.map(|entry| std::fs::read_to_string(entry.path()).unwrap())
.unwrap();
assert!(
migration.contains(" add_index :labels, [:org_id, :name], unique: true\n"),
"{migration}"
);
// `--no-access`: records everyone shares.
glacier(
&app,
&[
"generate",
"scaffold",
"Country",
"name:string",
"--no-access",
],
);
let country = std::fs::read_to_string(app.join("src/models/country.glacier")).unwrap();
assert!(!country.contains("owned_by"), "{country}");
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// pages 1, sessions 3, registrations 2, user 6, accounts 1, orgs 4,
// notes 6, note 2, projects 6, project 2, labels 6, label 2,
// countries 5, country 2, invitations 1 more (another address).
assert!(
tests.contains("75 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
// APIs: tokens instead of cookies, and 401 without one.
glacier(&root, &["new", "tokens"]);
let app = root.join("tokens");
glacier(&app, &["generate", "authentication", "--api"]);
std::fs::write(
app.join("src/controllers/me_controller.glacier"),
"alias Auth\n\ncontroller Me do\n def show(ctx) do\n email_address = Auth.current_user(&ctx).map(fn u -> u.email_address.clone() end)\n json [email_address: email_address]\n end\nend\n",
)
.unwrap();
add_scope(
&app,
"\n scope \"/\", through: [:api, :authenticated] do\n get \"/me\", MeController.show, as: :me\n end\n",
);
std::fs::write(
app.join("tests/controllers/me_controller.test.glacier"),
"test \"needs a token\" do\n res = get \"/me\"\n assert res.status == :unauthorized\nend\n\ntest \"knows who the token belongs to\" do\n user = insert(User, email_address: \"user@example.com\")\n token = Session.start(user.id, None)!\n glacier::testing::bearer(&token)\n res = get \"/me\"\n assert res.status == :ok\n assert res.body == r#\"{\"email_address\":\"user@example.com\"}\"#\nend\n",
)
.unwrap();
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// pages 1, sessions 3, registrations 2, user 6, me 2.
assert!(
tests.contains("16 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn orgs_at_the_front_of_the_path() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("prefix");
std::fs::create_dir_all(&root).unwrap();
glacier(&root, &["new", "teams"]);
let app = root.join("teams");
glacier(&app, &["generate", "authentication"]);
// `/42/projects/7`, as Basecamp's URLs.
glacier(&app, &["generate", "access", "org", "--urls", "prefix"]);
let settings = std::fs::read_to_string(app.join("src/app.glacier")).unwrap();
assert!(
settings.contains("middleware glacier::middleware::IdPrefix"),
"{settings}"
);
glacier(
&app,
&[
"generate",
"scaffold",
"Project",
"name:string",
"--access",
"org",
],
);
let tests =
std::fs::read_to_string(app.join("tests/controllers/projects_controller.test.glacier"))
.unwrap();
assert!(
tests.contains("glacier::testing::path_prefix(\"/#{org.id}\")"),
"{tests}"
);
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// pages 1, sessions 3, registrations 2, user 6, orgs 4, projects 6,
// project 2, invitations 4.
assert!(
tests.contains("51 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn orgs_in_nested_paths() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("nested");
std::fs::create_dir_all(&root).unwrap();
glacier(&root, &["new", "studio"]);
let app = root.join("studio");
glacier(&app, &["generate", "authentication"]);
// `/orgs/42/projects/7`.
glacier(&app, &["generate", "access", "org", "--urls", "nested"]);
glacier(
&app,
&[
"generate",
"scaffold",
"Project",
"name:string",
"--access",
"org",
],
);
let index = std::fs::read_to_string(app.join("src/views/projects/index.html")).unwrap();
assert!(index.contains("Project::collection_path()"), "{index}");
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// pages 1, sessions 3, registrations 2, user 6, orgs 3, projects 6,
// project 2, invitations 4.
assert!(
tests.contains("50 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn authentication_with_impersonation_and_api_tokens() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("extras");
std::fs::create_dir_all(&root).unwrap();
glacier(&root, &["new", "staffed"]);
let app = root.join("staffed");
let out = glacier(
&app,
&[
"generate",
"authentication",
"--impersonation",
"--api-tokens",
],
);
assert!(out.contains("/impersonation"), "{out}");
assert!(out.contains("/api_tokens"), "{out}");
let layout = std::fs::read_to_string(app.join("src/views/layouts/app.html")).unwrap();
assert!(layout.contains("impersonation()"), "{layout}");
let middleware =
std::fs::read_to_string(app.join("src/middleware/authentication.glacier")).unwrap();
assert!(
middleware.contains("ApiToken.find_by_token(&token)!")
&& middleware.contains("Auth.impersonated(&ctx, &signed_in_user)!"),
"{middleware}"
);
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// The authentication generator's 35, impersonations 2, API tokens 4.
assert!(
tests.contains("41 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
// An API app has no pages for either.
glacier(&root, &["new", "service", "--api"]);
let out = Command::new(env!("CARGO_BIN_EXE_glacier"))
.args(["generate", "authentication", "--api-tokens"])
.current_dir(root.join("service"))
.output()
.unwrap();
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("for an app with pages"));
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn api_app_generates_json_controllers() {
let _one_at_a_time = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_root("api");
std::fs::create_dir_all(&root).unwrap();
glacier(&root, &["new", "service", "--api"]);
let app = root.join("service");
assert!(!app.join("src/views").exists(), "no views");
assert!(!app.join("public").exists(), "no assets");
// In an API app the generators write JSON controllers, with no views.
glacier(
&app,
&[
"generate",
"scaffold",
"Post",
"title:string",
"body:text:optional",
],
);
glacier(
&app,
&[
"generate",
"scaffold",
"Note",
"text:string",
"--dialect",
"rust",
],
);
glacier(&app, &["generate", "controller", "Reports", "summary"]);
glacier(&app, &["generate", "authentication"]);
glacier(&app, &["generate", "job", "SendReport", "report_id:bigint"]);
// A job is one by what it implements, not its folder: moved beside the
// reports, it's still in `jobs::all()` and seen by the tests.
std::fs::create_dir_all(app.join("src/reports")).unwrap();
std::fs::rename(
app.join("src/jobs/send_report.glacier"),
app.join("src/reports/send_report.glacier"),
)
.unwrap();
glacier(&app, &["generate", "access", "org"]);
glacier(
&app,
&[
"generate",
"scaffold",
"Task",
"title:string",
"--access",
"user",
],
);
// An org's invoices, under /orgs/:org_id (a JSON API's default).
glacier(
&app,
&[
"generate",
"scaffold",
"Invoice",
"number:string",
"--access",
"org",
],
);
glacier(&app, &["generate", "policy", "Post"]);
glacier(
&app,
&[
"generate",
"client",
"ConvertKit",
"https://api.convertkit.com/v3",
],
);
assert!(app.join("src/clients/convert_kit.glacier").exists());
glacier(&app, &["generate", "middleware", "RequireBeta"]);
assert!(app.join("src/middleware/require_beta.glacier").exists());
std::fs::create_dir_all(app.join("tests/policies")).unwrap();
std::fs::write(
app.join("tests/policies/post_policy.test.glacier"),
"alias Policies.PostPolicy\n\ntest \"denies until decided\" do\n user = insert(User)\n post = insert(Post)\n refute PostPolicy.update(&user, &post)\n refute PostPolicy.index(&user)\nend\n",
)
.unwrap();
std::fs::create_dir_all(app.join("tests/jobs")).unwrap();
std::fs::write(
app.join("tests/jobs/send_report.test.glacier"),
"test \"runs a queued report\" do\n job = SendReport { report_id: 7 }\n job.enqueue()!\n assert glacier::testing::perform_jobs()! == 1\nend\n",
)
.unwrap();
assert!(!app.join("src/views").exists(), "no views");
let routes = std::fs::read_to_string(app.join("src/routes.glacier")).unwrap();
assert!(
routes.contains(
"resources \"/posts\", PostsController, only: [:index, :show, :create, :update, :destroy]"
),
"{routes}"
);
assert!(routes.contains("post \"/session\""), "{routes}");
glacier(&app, &["fmt", "--check"]);
let tests = cargo(&app, &["test"]);
// home 2, posts 5, post 2, reports 1, sessions 3, registrations 2, user 7,
// send_report 1, post_policy 1, convert_kit 1, orgs 3, tasks 4, task 2,
// invoices 4, invoice 2.
assert!(
tests.contains("41 passed; 0 failed; 0 ignored"),
"the generated tests pass:\n{tests}"
);
// The Rust scaffold's own: note 2, notes 4.
assert!(tests.contains("2 passed; 0 failed"), "{tests}");
assert!(tests.contains("4 passed; 0 failed"), "{tests}");
std::fs::remove_dir_all(&root).ok();
}