//! `pic pull` journey: stand up an app with a script, route, cron trigger, //! and a secret, then export it and assert the manifest + script file. //! //! B3: a second case exports a GROUP node (`pic pull --group`) — its scripts, //! a `sealed`+`shared` `[[triggers.kv]]` template, a `[[routes]]` template, a //! shared `collections` entry, and a `[vars]` value — and asserts the exported //! manifest re-applies clean (the round-trip carries the group-only flags). use std::fs; use tempfile::TempDir; use crate::common; use crate::common::cleanup::{GroupGuard, ScriptGuard}; #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn pull_exports_manifest_and_sources() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); // App + script "hello" (deploy derives the name from the file stem). let (script_id, guard) = common::deploy_fixture(&env, "pull", "hello.rhai"); let app = guard.slug().to_string(); // Route → script. common::pic_as(&env) .args([ "routes", "create", "--script", &script_id, "--path", "/hook", "--method", "POST", ]) .assert() .success(); // Cron trigger → script. common::pic_as(&env) .args([ "triggers", "create-cron", "--app", &app, "--script", &script_id, "--schedule", "0 0 * * * *", ]) .assert() .success(); // Secret (name only ends up in the manifest; value stays server-side). common::pic_as(&env) .args(["secrets", "set", "--app", &app, "api_key"]) .write_stdin("xyzzy") .assert() .success(); // Pull into a scratch dir. let out_dir = TempDir::new().expect("pull tempdir"); common::pic_as(&env) .args(["pull", &app, "--dir"]) .arg(out_dir.path()) .assert() .success(); // Manifest exists and captures every resource. let manifest = std::fs::read_to_string(out_dir.path().join("picloud.toml")) .expect("picloud.toml should be written"); assert!( manifest.contains(&format!("slug = \"{app}\"")), "manifest missing app slug:\n{manifest}" ); assert!( manifest.contains("name = \"hello\"") && manifest.contains("scripts/hello.rhai"), "manifest missing script entry:\n{manifest}" ); assert!( manifest.contains("[[routes]]") && manifest.contains("path = \"/hook\""), "manifest missing route:\n{manifest}" ); assert!( manifest.contains("[[triggers.cron]]") && manifest.contains("schedule = \"0 0 * * * *\""), "manifest missing cron trigger:\n{manifest}" ); assert!( manifest.contains("api_key"), "manifest missing secret name:\n{manifest}" ); // Script source was written out faithfully. let src = std::fs::read_to_string(out_dir.path().join("scripts/hello.rhai")) .expect("scripts/hello.rhai should be written"); assert!( src.contains("hello from pic"), "exported source mismatch:\n{src}" ); drop(guard); } fn group_script_id(env: &common::TestEnv, group: &str, name: &str) -> String { let ls = common::pic_as(env) .args(["scripts", "ls", "--group", group]) .output() .expect("scripts ls"); let table = String::from_utf8(ls.stdout).unwrap(); table .lines() .map(common::cells) .find(|c| c.get(2) == Some(&name)) .and_then(|c| c.first().map(|s| (*s).to_string())) .unwrap_or_else(|| panic!("group script `{name}` not found:\n{table}")) } /// B3: `pic pull --group` round-trips a group node, including the group-only /// `sealed`/`shared` template flags and a shared `collections` entry. #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn pull_group_round_trips_templates_and_collections() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let group = common::unique_slug("pullg"); let _g = GroupGuard::new(&env.url, &env.token, &group); common::pic_as(&env) .args(["groups", "create", &group]) .assert() .success(); // A group-owned handler the templates bind. let dir = TempDir::new().expect("group pull tempdir"); fs::create_dir_all(dir.path().join("scripts")).unwrap(); fs::write( dir.path().join("scripts/gh.rhai"), r#"log::info("group handler"); "ok""#, ) .unwrap(); common::pic_as(&env) .args(["scripts", "deploy"]) .arg(dir.path().join("scripts/gh.rhai")) .args(["--group", &group, "--name", "gh"]) .assert() .success(); let _gs = ScriptGuard::new(&env.url, &env.token, &group_script_id(&env, &group, "gh")); // The group declares a shared collection, a sealed+shared kv trigger // template, a sealed route template, and a var. let gmanifest = format!( "[group]\nslug = \"{group}\"\nname = \"PullG\"\n\n\ collections = [\"catalog\"]\n\n\ [vars]\ntier = \"gold\"\n\n\ [[routes]]\nscript = \"gh\"\npath = \"/gpull\"\npath_kind = \"exact\"\n\ host_kind = \"any\"\nsealed = true\n\n\ [[triggers.kv]]\nscript = \"gh\"\ncollection_glob = \"catalog\"\nops = [\"insert\"]\n\ sealed = true\nshared = true\n" ); let gpath = dir.path().join("picloud.toml"); fs::write(&gpath, &gmanifest).unwrap(); common::pic_as(&env) .args(["apply", "--file"]) .arg(&gpath) .assert() .success(); // Pull the group into a FRESH dir. let out_dir = TempDir::new().expect("pull out dir"); common::pic_as(&env) .args(["pull", "--group", &group, "--dir"]) .arg(out_dir.path()) .assert() .success(); let manifest = std::fs::read_to_string(out_dir.path().join("picloud.toml")).expect("picloud.toml written"); assert!( manifest.contains("[group]") && manifest.contains(&format!("slug = \"{group}\"")), "manifest must be a [group] node:\n{manifest}" ); assert!( manifest.contains("catalog"), "shared collection must round-trip:\n{manifest}" ); assert!( manifest.contains("tier = \"gold\"") || manifest.contains("tier = 'gold'"), "group var must round-trip:\n{manifest}" ); assert!( manifest.contains("[[routes]]") && manifest.contains("/gpull"), "route template must round-trip:\n{manifest}" ); assert!( manifest.contains("[[triggers.kv]]") && manifest.contains("collection_glob = \"catalog\""), "kv trigger template must round-trip:\n{manifest}" ); // The group-only flags survive the round-trip (the whole point of B3). assert!( manifest.matches("sealed = true").count() >= 2, "both the route and trigger `sealed = true` must round-trip:\n{manifest}" ); assert!( manifest.contains("shared = true"), "the trigger `shared = true` must round-trip:\n{manifest}" ); // The exported script source landed too. let src = std::fs::read_to_string(out_dir.path().join("scripts/gh.rhai")) .expect("scripts/gh.rhai written"); assert!(src.contains("group handler"), "source mismatch:\n{src}"); // The strong round-trip proof: re-applying the PULLED manifest is accepted // (a faithful export re-plans clean; the server would reject a malformed one). common::pic_as(&env) .args(["apply", "--file"]) .arg(out_dir.path().join("picloud.toml")) .assert() .success(); }