| 5c6e92fd Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use oxy_core::repo::{IdentityScope, Repo}; |
| 6 |
use oxygenhub::config::Config; |
| 7 |
use oxygenhub::repos::RepoName; |
| 8 |
use oxygenhub::{db, Shared}; |
| 9 |
use std::path::{Path, PathBuf}; |
| 10 |
|
| 11 |
const ROOTFS: &str = "36fc4e3238044e424219a2b9002bf61bbe1805fec14d52cf37e17fae77c9ff16"; |
| 12 |
|
| 13 |
fn hermetic() -> PathBuf { |
| 14 |
static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new(); |
| 15 |
let d = DIR.get_or_init(|| tempfile::tempdir().unwrap()); |
| 16 |
std::env::set_var("OXY_HOME", d.path()); |
| 17 |
d.path().to_path_buf() |
| 18 |
} |
| 19 |
|
| 20 |
struct Hub { |
| 21 |
app: Shared, |
| 22 |
base: String, |
| 23 |
_dir: tempfile::TempDir, |
| 24 |
} |
| 25 |
|
| 26 |
async fn hub() -> Hub { |
| 27 |
hermetic(); |
| 28 |
let dir = tempfile::tempdir().unwrap(); |
| 29 |
let mut config = Config::load(dir.path()).unwrap(); |
| 30 |
config.auth.push_tokens = vec!["site".into()]; |
| 31 |
config.flow.repos = vec!["andy/*".into()]; |
| 32 |
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); |
| 33 |
let port = listener.local_addr().unwrap().port(); |
| 34 |
config.server.base_url = format!("http://127.0.0.1:{port}"); |
| 35 |
let app = oxygenhub::build(config).await.unwrap(); |
| 36 |
let router = oxygenhub::router(app.clone()); |
| 37 |
tokio::spawn(async move { axum_serve(listener, router).await }); |
| 38 |
Hub { app, base: format!("http://127.0.0.1:{port}"), _dir: dir } |
| 39 |
} |
| 40 |
|
| 41 |
async fn axum_serve(listener: tokio::net::TcpListener, router: axum::Router) { |
| 42 |
axum::serve(listener, router).await.unwrap(); |
| 43 |
} |
| 44 |
|
| 45 |
|
| 46 |
async fn oh(dir: &Path, env: &[(&str, &str)], args: &[&str]) -> (i32, String, String) { |
| 47 |
let (dir, env, args): (PathBuf, Vec<(String, String)>, Vec<String>) = ( |
| 48 |
dir.to_path_buf(), |
| 49 |
env.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), |
| 50 |
args.iter().map(|a| a.to_string()).collect(), |
| 51 |
); |
| 52 |
tokio::task::spawn_blocking(move || { |
| 53 |
let out = std::process::Command::new(env!("CARGO_BIN_EXE_oh")) |
| 54 |
.args(&args) |
| 55 |
.current_dir(&dir) |
| 56 |
.env_remove("OH_TOKEN") |
| 57 |
.env_remove("OXY_TOKEN") |
| 58 |
.env_remove("OH_HUB") |
| 59 |
.envs(env) |
| 60 |
.output() |
| 61 |
.unwrap(); |
| 62 |
( |
| 63 |
out.status.code().unwrap_or(-1), |
| 64 |
String::from_utf8_lossy(&out.stdout).into_owned(), |
| 65 |
String::from_utf8_lossy(&out.stderr).into_owned(), |
| 66 |
) |
| 67 |
}) |
| 68 |
.await |
| 69 |
.unwrap() |
| 70 |
} |
| 71 |
|
| 72 |
fn write(root: &Path, rel: &str, content: &str) { |
| 73 |
let p = root.join(rel); |
| 74 |
std::fs::create_dir_all(p.parent().unwrap()).unwrap(); |
| 75 |
std::fs::write(p, content).unwrap(); |
| 76 |
} |
| 77 |
|
| 78 |
#[tokio::test(flavor = "multi_thread")] |
| 79 |
async fn oh_drives_issues_runs_and_releases() { |
| 80 |
let h = hub().await; |
| 81 |
|
| 82 |
db::accounts::create_account(&h.app.db, "andy", "[email protected]", "x", "salt", true).await.unwrap(); |
| 83 |
h.app.repos.create(&RepoName::parse("andy/demo").unwrap(), None).unwrap(); |
| b5229b56 Andy McCall ↶ |
84 |
let demo = db::repos::create(&h.app.db, "andy", "demo", "A demo", hub_api::Visibility::Public).await.unwrap(); |
| 85 |
db::issues::upsert_label(&h.app.db, demo.id, "bug", "#d73a4a", "").await.unwrap(); |
| 5c6e92fd Andy McCall ↶ |
86 |
|
| 87 |
|
| 88 |
let work = tempfile::tempdir().unwrap(); |
| 89 |
let mut c = Repo::init(work.path()).unwrap(); |
| 90 |
c.config.set("remote.origin.url", &format!("{}/andy/demo", h.base)).unwrap(); |
| 91 |
c.config.set("remote.origin.token", "site").unwrap(); |
| 92 |
c.save_config().unwrap(); |
| 93 |
c.create_identity("Andy", "[email protected]", IdentityScope::Repo).unwrap(); |
| 94 |
write( |
| 95 |
work.path(), |
| 96 |
".flow/pipeline.kdl", |
| 97 |
&format!("schema-version 1\nsandbox \"alpine\" {{\n rootfs \"cas://blob/{ROOTFS}\"\n}}\njob \"test\" {{\n sandbox \"alpine\"\n exec \"true\"\n}}\n"), |
| 98 |
); |
| 99 |
c.snapshot("test").unwrap(); |
| 100 |
c.commit("Add a pipeline", &[]).unwrap(); |
| 101 |
let heads = c.bookmark_heads().unwrap(); |
| 102 |
c.tag_create("v1", &heads, "first").unwrap(); |
| 103 |
assert!(c.push("origin", false).unwrap().iter().all(|r| r.ok)); |
| 104 |
let w = work.path(); |
| 105 |
|
| 106 |
let (code, out, err) = oh(w, &[], &["repo", "view", "--plain"]).await; |
| 107 |
assert_eq!(code, 0, "{err}"); |
| 108 |
assert!(out.contains("andy/demo") && out.contains("A demo") && out.contains("oxy clone"), "{out}"); |
| 109 |
|
| 110 |
|
| 111 |
let (code, out, err) = oh(w, &[], &["issue", "create", "--title", "Broken thing", "--body", "It breaks."]).await; |
| 112 |
assert_eq!(code, 0, "{err}"); |
| 113 |
assert!(out.contains("opened #1"), "{out}"); |
| 114 |
let (_, out, _) = oh(w, &[], &["issue", "list", "--json"]).await; |
| 115 |
let list: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 116 |
assert_eq!((list[0]["number"].as_i64(), list[0]["title"].as_str()), (Some(1), Some("Broken thing"))); |
| b5229b56 Andy McCall ↶ |
117 |
let (code, out, err) = |
| 118 |
oh(w, &[], &["issue", "edit", "1", "--title", "Broken thing, badly", "--label", "bug", "--assignee", "andy"]).await; |
| 119 |
assert_eq!(code, 0, "{err}"); |
| 120 |
assert!(out.contains("edited #1"), "{out}"); |
| 121 |
let (_, out, _) = oh(w, &[], &["issue", "view", "1", "--json"]).await; |
| 122 |
let i: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 123 |
assert_eq!(i["title"], "Broken thing, badly"); |
| 124 |
assert_eq!(i["labels"][0]["name"], "bug"); |
| 125 |
assert_eq!(i["assignees"][0], "andy"); |
| 126 |
let (code, _, err) = oh(w, &[], &["issue", "edit", "1"]).await; |
| 127 |
assert_eq!(code, 2); |
| 128 |
assert!(err.contains("say what to change"), "{err}"); |
| 129 |
let (_, out, _) = oh(w, &[], &["issue", "edit", "1", "--no-labels", "--no-assignees", "--json"]).await; |
| 130 |
let i: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 131 |
assert!(i["labels"].as_array().unwrap().is_empty() && i["assignees"].as_array().unwrap().is_empty(), "{out}"); |
| 5c6e92fd Andy McCall ↶ |
132 |
let (_, out, _) = oh(w, &[], &["issue", "comment", "1", "--body", "Seen it too."]).await; |
| 133 |
assert!(out.contains("commented on #1")); |
| 134 |
let (_, out, _) = oh(w, &[], &["issue", "view", "1", "--plain"]).await; |
| 135 |
assert!(out.contains("It breaks.") && out.contains("Seen it too."), "{out}"); |
| 136 |
let (_, out, _) = oh(w, &[], &["issue", "close", "1"]).await; |
| 137 |
assert!(out.contains("closed #1")); |
| 138 |
let (_, out, _) = oh(w, &[], &["issue", "list", "--plain"]).await; |
| 139 |
assert!(out.contains("No open issues."), "{out}"); |
| 140 |
|
| 17a3a84b Andy McCall ↶ |
141 |
|
| 142 |
|
| 143 |
let (code, out, err) = oh(w, &[], &["project", "create", "Roadmap", "-d", "Next up"]).await; |
| 144 |
assert_eq!(code, 0, "{err}"); |
| 145 |
assert!(out.contains("project 1 Roadmap"), "{out}"); |
| 146 |
let (code, out, err) = oh(w, &[], &["issue", "create", "--title", "Plan it", "--project", "1", "--column", "doing"]).await; |
| 147 |
assert_eq!(code, 0, "{err}"); |
| 148 |
assert!(out.contains("opened #2") && out.contains("on project 1"), "{out}"); |
| 149 |
let (code, _, err) = oh(w, &[], &["project", "card", "add", "1", "--note", "Write docs"]).await; |
| 150 |
assert_eq!(code, 0, "{err}"); |
| 151 |
let (code, out, err) = oh(w, &[], &["project", "view", "1", "--plain"]).await; |
| 152 |
assert_eq!(code, 0, "{err}"); |
| 153 |
assert!(out.contains("Doing") && out.contains("andy/demo#2") && out.contains("Plan it") && out.contains("Write docs"), "{out}"); |
| 154 |
let (_, out, _) = oh(w, &[], &["project", "view", "1", "--json"]).await; |
| 155 |
let board: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 156 |
let card = board["columns"][1]["cards"][0]["id"].as_i64().unwrap().to_string(); |
| 157 |
let (code, out, err) = oh(w, &[], &["project", "card", "move", "1", &card, "--column", "Done"]).await; |
| 158 |
assert_eq!(code, 0, "{err}"); |
| 159 |
assert!(out.contains("moved to Done"), "{out}"); |
| 160 |
let (_, out, _) = oh(w, &[], &["project", "view", "1", "--json"]).await; |
| 161 |
let board: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 162 |
assert_eq!(board["columns"][2]["cards"][0]["reference"], "andy/demo#2"); |
| 163 |
let (code, _, err) = oh(w, &[], &["project", "card", "add", "1", "2"]).await; |
| 164 |
assert_ne!(code, 0); |
| 165 |
assert!(err.contains("already on this project"), "{err}"); |
| 166 |
let (code, _, err) = oh(w, &[], &["project", "card", "remove", "1", &card]).await; |
| 167 |
assert_eq!(code, 0, "{err}"); |
| 168 |
let (_, out, _) = oh(w, &[], &["project", "list", "--plain"]).await; |
| 169 |
assert!(out.contains("Roadmap"), "{out}"); |
| 170 |
let (code, out, _) = oh(w, &[], &["project", "close", "1"]).await; |
| 171 |
assert_eq!(code, 0); |
| 172 |
assert!(out.contains("closed project 1"), "{out}"); |
| 173 |
|
| 5c6e92fd Andy McCall ↶ |
174 |
|
| 175 |
let (code, out, err) = oh(w, &[], &["run", "start"]).await; |
| 176 |
assert_eq!(code, 0, "{err}"); |
| 177 |
assert!(out.contains("started run #2"), "{out}"); |
| 178 |
let (_, out, _) = oh(w, &[], &["run", "list", "--json"]).await; |
| 179 |
let runs: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 180 |
assert_eq!(runs.as_array().unwrap().len(), 2); |
| 181 |
let (_, out, _) = oh(w, &[], &["run", "view", "2", "--plain"]).await; |
| 182 |
assert!(out.contains("test") && out.contains("#2"), "{out}"); |
| 183 |
|
| 184 |
|
| 185 |
let (code, out, err) = oh(w, &[], &["release", "create", "v1", "--title", "One", "--notes", "First."]).await; |
| 186 |
assert_eq!(code, 0, "{err}"); |
| 187 |
assert!(out.contains("released v1"), "{out}"); |
| 188 |
let asset = w.join("tool.bin"); |
| 189 |
std::fs::write(&asset, b"bytes").unwrap(); |
| 190 |
let (code, out, err) = oh(w, &[], &["release", "upload", "v1", asset.to_str().unwrap()]).await; |
| 191 |
assert_eq!(code, 0, "{err}"); |
| 192 |
assert!(out.contains("attached tool.bin to v1")); |
| 1f1eb06a Andy McCall ↶ |
193 |
std::fs::remove_file(&asset).unwrap(); |
| 5c6e92fd Andy McCall ↶ |
194 |
let (_, out, _) = oh(w, &[], &["release", "list", "--json"]).await; |
| 195 |
let rel: serde_json::Value = serde_json::from_str(&out).unwrap(); |
| 196 |
assert_eq!(rel[0]["assets"][0]["name"], "tool.bin"); |
| 197 |
let (code, out, _) = oh(w, &[], &["release", "trust", "v1", "--plain"]).await; |
| 198 |
assert_eq!(code, 0, "warnings only: {out}"); |
| 199 |
assert!(out.contains("Tag v1 is signed") && out.contains("tool.bin: no build found"), "{out}"); |
| 1f1eb06a Andy McCall ↶ |
200 |
|
| 201 |
|
| 202 |
c.bookmark_create("topic", None).unwrap(); |
| 203 |
c.bookmark_switch("topic", false).unwrap(); |
| 204 |
write(w, "topic.txt", "more\n"); |
| 205 |
c.snapshot("test").unwrap(); |
| 206 |
c.commit("Add topic", &[]).unwrap(); |
| 207 |
assert!(c.push("origin", false).unwrap().iter().all(|r| r.ok)); |
| 208 |
let (code, out, err) = oh(w, &[], &["proposal", "create", "--title", "Topic", "--from", "topic"]).await; |
| 209 |
assert_eq!(code, 0, "{err}"); |
| 210 |
let n: String = out.split("proposed #").nth(1).unwrap().chars().take_while(|c| c.is_ascii_digit()).collect(); |
| 211 |
let (_, out, _) = oh(w, &[], &["proposal", "view", &n, "--plain"]).await; |
| 212 |
assert!(out.contains("topic → main") && out.contains("Add topic"), "{out}"); |
| 213 |
let (code, out, err) = oh(w, &[], &["proposal", "land", &n]).await; |
| 214 |
assert_eq!(code, 0, "{err}"); |
| 215 |
assert!(out.contains(&format!("landed #{n} on main")), "{out}"); |
| 216 |
let (code, _, err) = oh(w, &[], &["proposal", "land", &n]).await; |
| 217 |
assert_ne!(code, 0); |
| 218 |
assert!(err.contains("HTTP 409") && err.contains("landed"), "{err}"); |
| 5c6e92fd Andy McCall ↶ |
219 |
|
| 220 |
|
| 221 |
let elsewhere = tempfile::tempdir().unwrap(); |
| 222 |
let cfg = elsewhere.path().join("oh.toml"); |
| 223 |
let cfg_s = cfg.to_str().unwrap(); |
| 224 |
let (code, _, err) = oh(elsewhere.path(), &[("OH_CONFIG", cfg_s)], &["issue", "list"]).await; |
| 225 |
assert_ne!(code, 0); |
| 226 |
assert!(err.contains("--repo"), "{err}"); |
| 227 |
let (code, out, err) = |
| 228 |
oh(elsewhere.path(), &[("OH_CONFIG", cfg_s)], &["auth", "login", "--with-hub", &h.base, "--token", "site"]).await; |
| 229 |
assert_eq!(code, 0, "{err}"); |
| 230 |
assert!(out.contains("signed in"), "{out}"); |
| 231 |
#[cfg(unix)] |
| 232 |
{ |
| 233 |
use std::os::unix::fs::PermissionsExt; |
| 234 |
assert_eq!(std::fs::metadata(&cfg).unwrap().permissions().mode() & 0o777, 0o600, "tokens are owner-only"); |
| 235 |
} |
| 236 |
let (code, out, _) = |
| 237 |
oh(elsewhere.path(), &[("OH_CONFIG", cfg_s)], &["issue", "list", "--state", "all", "--repo", "andy/demo", "--plain"]).await; |
| 238 |
assert_eq!(code, 0); |
| 239 |
assert!(out.contains("Broken thing"), "{out}"); |
| 240 |
let (code, out, _) = oh(elsewhere.path(), &[("OH_CONFIG", cfg_s)], &["api", "/repos/andy/demo/labels"]).await; |
| 241 |
assert_eq!(code, 0); |
| 242 |
assert!(out.trim_start().starts_with('['), "{out}"); |
| 243 |
|
| 244 |
let (code, _, err) = oh(w, &[("OXY_TOKEN", "nope")], &["issue", "create", "--title", "x"]).await; |
| 245 |
assert_ne!(code, 0); |
| 246 |
assert!(err.contains("HTTP 401") && err.contains("oh auth login"), "{err}"); |
| 247 |
} |