use std::process::Command;
use std::sync::Arc;
use runtime_api::{http, schema, transport::TransportState};
use runtime_auth::{AllowAllAuthProvider, AuthProvider};
use runtime_domain::RuntimeRepository;
use runtime_events::EventBus;
use runtime_git::layout::StorageLayout;
use runtime_storage::MemoryReadModel;
use serde_json::{json, Value};
use tempfile::TempDir;
use tokio::net::TcpListener;
struct ApiHandle {
port: u16,
_server: tokio::task::JoinHandle<()>,
_tmp: TempDir,
}
async fn spawn_api() -> ApiHandle {
let tmp = TempDir::new().expect("tempdir");
let layout = StorageLayout::new(tmp.path());
let event_bus = EventBus::new(16);
let auth: Arc<dyn AuthProvider> = Arc::new(AllowAllAuthProvider);
let repo: Arc<dyn RuntimeRepository> = Arc::new(MemoryReadModel::new(layout.clone()));
let schema = schema::build_schema(
repo.clone(),
event_bus.clone(),
layout.clone(),
auth.clone(),
schema::default_clone_url_base(),
);
let transport_state = TransportState::new(layout, auth, event_bus, repo.clone());
let app = http::router(
schema,
transport_state,
repo.clone(),
runtime_api::hook_bridge::HookBridgeState::disabled(repo.clone()),
runtime_api::runner_bridge::RunnerBridgeState::disabled(repo.clone()),
);
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let port = listener.local_addr().expect("local_addr").port();
let server = tokio::spawn(async move {
axum::serve(listener, app).await.expect("axum serve");
});
ApiHandle {
port,
_server: server,
_tmp: tmp,
}
}
async fn gql(port: u16, body: &Value, token: Option<&str>) -> Value {
let client = reqwest::Client::new();
let mut req = client
.post(format!("http://127.0.0.1:{port}/graphql"))
.json(body);
if let Some(t) = token {
req = req.bearer_auth(t);
}
let resp = req.send().await.expect("send");
resp.json().await.expect("json")
}
#[test]
fn cli_drives_live_api_end_to_end() {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("rt");
let (port, token, config_dir, _api) = rt.block_on(async {
let api = spawn_api().await;
let port = api.port;
let signup = gql(
port,
&json!({
"query": "mutation($i: SignUpInput!) { signUp(input: $i) { token account { id username } } }",
"variables": {
"i": {
"username": "alice",
"displayName": "Alice",
"password": "hunter2hunter2",
}
}
}),
None,
)
.await;
let session = signup["data"]["signUp"]["token"]
.as_str()
.expect("signup token")
.to_string();
let pat = gql(
port,
&json!({
"query": "mutation($i: CreatePatInput!) { createPersonalAccessToken(input: $i) { token } }",
"variables": { "i": { "name": "cli-test", "scopes": ["REPO_READ", "REPO_WRITE", "REPO_ADMIN", "WORKFLOW_DISPATCH"] } }
}),
Some(&session),
)
.await;
let token = pat["data"]["createPersonalAccessToken"]["token"]
.as_str()
.expect("pat token")
.to_string();
gql(
port,
&json!({
"query": "mutation($i: CreateRepositoryInput!) { createRepository(input: $i) { id name } }",
"variables": { "i": { "ownerSlug": "alice", "name": "r1", "visibility": "PUBLIC" } }
}),
Some(&token),
)
.await;
let config_dir = TempDir::new().expect("cfgdir");
let cfg = json!({
"host": format!("http://127.0.0.1:{port}"),
"username": "alice",
"token": token,
});
std::fs::write(
config_dir.path().join("config.json"),
serde_json::to_vec_pretty(&cfg).unwrap(),
)
.expect("write config");
(port, token, config_dir, api)
});
let bin = env!("CARGO_BIN_EXE_runtime");
let host = format!("http://127.0.0.1:{port}");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["auth", "status"])
.output()
.expect("run auth status");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains(&host), "auth status stdout: {stdout}");
assert!(stdout.contains("alice"), "auth status stdout: {stdout}");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["repo", "list"])
.output()
.expect("run repo list");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("alice/r1"), "repo list stdout: {stdout}");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args([
"issue", "create", "alice/r1", "--title", "Hello", "--body", "World",
])
.output()
.expect("run issue create");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("#1"), "issue create stdout: {stdout}");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["issue", "list", "alice/r1", "--json"])
.output()
.expect("run issue list");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
let parsed: Value = serde_json::from_str(&stdout).expect("issue list json");
let arr = parsed.as_array().expect("array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["title"], "Hello");
assert_eq!(arr[0]["status"], "OPEN");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["api", "{ viewer { username } }"])
.output()
.expect("run api");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
let parsed: Value = serde_json::from_str(&stdout).expect("api json");
assert_eq!(parsed["data"]["viewer"]["username"], "alice");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["issue", "close", "alice/r1", "1"])
.output()
.expect("run issue close");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("Closed"), "issue close stdout: {stdout}");
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["issue", "list", "alice/r1", "--state", "closed", "--json"])
.output()
.expect("run issue list closed");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let parsed: Value = serde_json::from_str(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(parsed.as_array().unwrap().len(), 1);
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["auth", "logout"])
.output()
.expect("run auth logout");
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let out = Command::new(bin)
.env("RUNTIME_CONFIG_HOME", config_dir.path())
.args(["auth", "status"])
.output()
.expect("run auth status after logout");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains("Not logged in"),
"post-logout status: {stdout}"
);
drop(token);
}