feat(wfectl): new CLI client + wfe-ci builder image
wfectl is a command-line client for wfe-server with 17 subcommands
covering the full workflow lifecycle:
* Auth: login (OAuth2 PKCE via Ory Hydra), logout, whoami
* Definitions: register (YAML → gRPC), validate (local compile),
definitions list
* Instances: run, get, list, cancel, suspend, resume
* Events: publish
* Streaming: watch (lifecycle), logs, search-logs (full-text)
Key design points:
* `validate` compiles YAML locally via `wfe-yaml::load_workflow_from_str`
with the full executor feature set enabled — instant feedback, no
server round-trip, no auth required. Uses the same compile path as
the server's `register` RPC so what passes validation is guaranteed
to register.
* Lookup commands accept either UUID or human name; the server
resolves the identifier for us. Display tables show both columns.
* `run --name <N>` lets users override the auto-generated
`{def_id}-{N}` instance name when they want a sticky reference.
* Table and JSON output formats, shared bearer-token or cached-login
auth path, direct token injection via `WFECTL_TOKEN`.
* 5 new unit tests for the validate command cover happy path, unknown
step type rejection, and missing file handling.
Dockerfile.ci ships the prebuilt image used as the `image:` for
kubernetes CI steps: rust stable, cargo-nextest, cargo-llvm-cov,
sccache (configured via WFE_SCCACHE_* env), buildctl for in-cluster
buildkitd, kubectl, tea for Gitea releases, and git. Published to
`src.sunbeam.pt/studio/wfe-ci:latest`.
This commit is contained in:
194
wfectl/tests/auth_commands.rs
Normal file
194
wfectl/tests/auth_commands.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Tests for `whoami` and `logout` commands using a temp HOME with a fake token.
|
||||
|
||||
use chrono::Utc;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use wfectl::auth::{StoredToken, save_token};
|
||||
use wfectl::commands::{logout, whoami};
|
||||
use wfectl::config::Config;
|
||||
use wfectl::output::OutputFormat;
|
||||
|
||||
/// Serialize HOME-mutating tests so they don't race.
|
||||
static HOME_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn make_test_token(domain: &str) -> StoredToken {
|
||||
StoredToken {
|
||||
access_token: "ory_at_test".into(),
|
||||
refresh_token: Some("ory_rt_test".into()),
|
||||
// {"email":"alice@test.com","name":"Alice","groups":["admin","employee"]}
|
||||
id_token: Some("h.eyJlbWFpbCI6ImFsaWNlQHRlc3QuY29tIiwibmFtZSI6IkFsaWNlIiwiZ3JvdXBzIjpbImFkbWluIiwiZW1wbG95ZWUiXX0.s".into()),
|
||||
expires_at: Utc::now() + chrono::Duration::seconds(3600),
|
||||
issuer: "https://auth.test.com/".into(),
|
||||
domain: domain.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_temp_home<F: FnOnce(PathBuf)>(f: F) {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
f(tmp.path().to_path_buf());
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whoami_table_format_with_full_claims() {
|
||||
with_temp_home(|_| {});
|
||||
// Re-acquire the lock for the async section.
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = make_test_token("test.com");
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let cfg = Config {
|
||||
server: "http://localhost".into(),
|
||||
issuer: "https://auth.test.com/".into(),
|
||||
default_format: Default::default(),
|
||||
};
|
||||
let args = whoami::WhoamiArgs { issuer: None };
|
||||
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whoami_json_format() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = make_test_token("test.com");
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let cfg = Config {
|
||||
server: "http://localhost".into(),
|
||||
issuer: "https://auth.test.com/".into(),
|
||||
default_format: Default::default(),
|
||||
};
|
||||
let args = whoami::WhoamiArgs { issuer: None };
|
||||
whoami::run(args, &cfg, OutputFormat::Json).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whoami_when_not_logged_in() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let cfg = Config {
|
||||
server: "http://localhost".into(),
|
||||
issuer: "https://auth.notlogged.in/".into(),
|
||||
default_format: Default::default(),
|
||||
};
|
||||
let args = whoami::WhoamiArgs { issuer: None };
|
||||
// Should not panic, just print a message.
|
||||
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whoami_with_explicit_issuer_arg() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = make_test_token("override.com");
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let cfg = Config::default();
|
||||
let args = whoami::WhoamiArgs {
|
||||
issuer: Some("https://auth.override.com/".into()),
|
||||
};
|
||||
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_removes_token() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = make_test_token("test.com");
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let cfg = Config {
|
||||
server: "http://localhost".into(),
|
||||
issuer: "https://auth.test.com/".into(),
|
||||
default_format: Default::default(),
|
||||
};
|
||||
let args = logout::LogoutArgs { issuer: None };
|
||||
logout::run(args, &cfg).await.unwrap();
|
||||
|
||||
// Verify the token file is gone.
|
||||
let path = wfectl::auth::token_path("test.com");
|
||||
assert!(!path.exists());
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_when_not_logged_in_is_noop() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let cfg = Config {
|
||||
server: "http://localhost".into(),
|
||||
issuer: "https://auth.nothing.com/".into(),
|
||||
default_format: Default::default(),
|
||||
};
|
||||
let args = logout::LogoutArgs { issuer: None };
|
||||
logout::run(args, &cfg).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_with_explicit_issuer() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved_home = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = make_test_token("explicit.com");
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let cfg = Config::default();
|
||||
let args = logout::LogoutArgs {
|
||||
issuer: Some("https://auth.explicit.com/".into()),
|
||||
};
|
||||
logout::run(args, &cfg).await.unwrap();
|
||||
|
||||
if let Some(h) = saved_home {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
261
wfectl/tests/auth_oidc.rs
Normal file
261
wfectl/tests/auth_oidc.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
//! Tests for OAuth2/OIDC code paths in auth.rs that talk to a real (mock) HTTP server.
|
||||
|
||||
use chrono::Utc;
|
||||
use std::sync::Mutex;
|
||||
use wiremock::matchers::{body_string_contains, method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
use wfectl::auth::{StoredToken, discover, ensure_valid, exchange_code, refresh, save_token};
|
||||
|
||||
static HOME_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn discovery_body(server: &MockServer) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"issuer": server.uri(),
|
||||
"authorization_endpoint": format!("{}/oauth2/auth", server.uri()),
|
||||
"token_endpoint": format!("{}/oauth2/token", server.uri()),
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_fetches_endpoints() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(discovery_body(&server)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let doc = discover(&server.uri()).await.unwrap();
|
||||
assert!(doc.authorization_endpoint.ends_with("/oauth2/auth"));
|
||||
assert!(doc.token_endpoint.ends_with("/oauth2/token"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_handles_404() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let result = discover(&server.uri()).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_handles_invalid_json() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string("not json"))
|
||||
.mount(&server)
|
||||
.await;
|
||||
let result = discover(&server.uri()).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exchange_code_success() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth2/token"))
|
||||
.and(body_string_contains("grant_type=authorization_code"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"access_token": "ory_at_new",
|
||||
"refresh_token": "ory_rt_new",
|
||||
"id_token": "header.eyJzdWIiOiJ1c2VyIn0.sig",
|
||||
"expires_in": 3600,
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let discovery = wfectl::auth::DiscoveryDoc {
|
||||
authorization_endpoint: format!("{}/oauth2/auth", server.uri()),
|
||||
token_endpoint: format!("{}/oauth2/token", server.uri()),
|
||||
};
|
||||
let token = exchange_code(
|
||||
&discovery,
|
||||
"auth-code",
|
||||
"verifier",
|
||||
"http://127.0.0.1:9876/callback",
|
||||
&server.uri(),
|
||||
"test.com",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(token.access_token, "ory_at_new");
|
||||
assert_eq!(token.refresh_token, Some("ory_rt_new".into()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exchange_code_handles_error_response() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth2/token"))
|
||||
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
|
||||
"error": "invalid_grant",
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let discovery = wfectl::auth::DiscoveryDoc {
|
||||
authorization_endpoint: format!("{}/oauth2/auth", server.uri()),
|
||||
token_endpoint: format!("{}/oauth2/token", server.uri()),
|
||||
};
|
||||
let result = exchange_code(&discovery, "bad", "v", "uri", &server.uri(), "test.com").await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_success() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(discovery_body(&server)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth2/token"))
|
||||
.and(body_string_contains("grant_type=refresh_token"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"access_token": "ory_at_refreshed",
|
||||
"expires_in": 3600,
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let token = StoredToken {
|
||||
access_token: "ory_at_old".into(),
|
||||
refresh_token: Some("ory_rt_old".into()),
|
||||
id_token: None,
|
||||
expires_at: Utc::now() - chrono::Duration::seconds(10),
|
||||
issuer: server.uri(),
|
||||
domain: "test.com".into(),
|
||||
};
|
||||
let refreshed = refresh(&token).await.unwrap();
|
||||
assert_eq!(refreshed.access_token, "ory_at_refreshed");
|
||||
// Old refresh token preserved when new one isn't returned.
|
||||
assert_eq!(refreshed.refresh_token, Some("ory_rt_old".into()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_without_refresh_token_fails() {
|
||||
let token = StoredToken {
|
||||
access_token: "ory_at".into(),
|
||||
refresh_token: None,
|
||||
id_token: None,
|
||||
expires_at: Utc::now(),
|
||||
issuer: "https://auth.test.com/".into(),
|
||||
domain: "test.com".into(),
|
||||
};
|
||||
let result = refresh(&token).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_handles_token_endpoint_error() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(discovery_body(&server)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth2/token"))
|
||||
.respond_with(ResponseTemplate::new(401))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let token = StoredToken {
|
||||
access_token: "ory_at_old".into(),
|
||||
refresh_token: Some("ory_rt_old".into()),
|
||||
id_token: None,
|
||||
expires_at: Utc::now() - chrono::Duration::seconds(10),
|
||||
issuer: server.uri(),
|
||||
domain: "test.com".into(),
|
||||
};
|
||||
let result = refresh(&token).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_valid_returns_existing_when_fresh() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let token = StoredToken {
|
||||
access_token: "ory_at_fresh".into(),
|
||||
refresh_token: Some("ory_rt_x".into()),
|
||||
id_token: None,
|
||||
expires_at: Utc::now() + chrono::Duration::seconds(3600),
|
||||
issuer: "https://auth.fresh.com/".into(),
|
||||
domain: "fresh.com".into(),
|
||||
};
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let result = ensure_valid("fresh.com").await.unwrap();
|
||||
assert_eq!(result.access_token, "ory_at_fresh");
|
||||
|
||||
if let Some(h) = saved {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_valid_refreshes_when_stale() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/openid-configuration"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(discovery_body(&server)))
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth2/token"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"access_token": "ory_at_refreshed_via_ensure",
|
||||
"expires_in": 3600,
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let domain = "stale.com";
|
||||
let token = StoredToken {
|
||||
access_token: "ory_at_old".into(),
|
||||
refresh_token: Some("ory_rt_old".into()),
|
||||
id_token: None,
|
||||
expires_at: Utc::now() - chrono::Duration::seconds(10),
|
||||
issuer: server.uri(),
|
||||
domain: domain.into(),
|
||||
};
|
||||
save_token(&token).unwrap();
|
||||
|
||||
let result = ensure_valid(domain).await.unwrap();
|
||||
assert_eq!(result.access_token, "ory_at_refreshed_via_ensure");
|
||||
|
||||
if let Some(h) = saved {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_valid_errors_when_no_token() {
|
||||
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let saved = std::env::var("HOME").ok();
|
||||
unsafe { std::env::set_var("HOME", tmp.path()) };
|
||||
|
||||
let result = ensure_valid("nonexistent.com").await;
|
||||
assert!(result.is_err());
|
||||
|
||||
if let Some(h) = saved {
|
||||
unsafe { std::env::set_var("HOME", h) };
|
||||
}
|
||||
}
|
||||
401
wfectl/tests/commands.rs
Normal file
401
wfectl/tests/commands.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
//! Drive the wfectl command handlers against a stub gRPC server. This tests
|
||||
//! the full command -> client -> server -> response -> formatter pipeline.
|
||||
|
||||
mod stub;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use wfectl::client::build as build_client;
|
||||
use wfectl::commands;
|
||||
use wfectl::output::OutputFormat;
|
||||
|
||||
use stub::spawn_stub;
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_command_table_output() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
|
||||
// Write a temp YAML file
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), "workflow:\n id: x\n version: 1\n steps: []").unwrap();
|
||||
|
||||
let args = commands::register::RegisterArgs {
|
||||
file: tmp.path().to_path_buf(),
|
||||
config: vec![("key".into(), "val".into())],
|
||||
};
|
||||
commands::register::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_command_json_output() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), "workflow:\n id: x\n version: 1\n steps: []").unwrap();
|
||||
let args = commands::register::RegisterArgs {
|
||||
file: tmp.path().to_path_buf(),
|
||||
config: vec![],
|
||||
};
|
||||
commands::register::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_command_missing_file_errors() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::register::RegisterArgs {
|
||||
file: PathBuf::from("/nonexistent/file.yaml"),
|
||||
config: vec![],
|
||||
};
|
||||
let result = commands::register::run(args, client, OutputFormat::Table).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn definitions_list_table() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::definitions::DefinitionsArgs {
|
||||
cmd: commands::definitions::DefinitionsCmd::List,
|
||||
};
|
||||
commands::definitions::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn definitions_list_json() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::definitions::DefinitionsArgs {
|
||||
cmd: commands::definitions::DefinitionsCmd::List,
|
||||
};
|
||||
commands::definitions::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_command_with_inline_data() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::run::RunArgs {
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
data: None,
|
||||
data_json: Some(r#"{"key":"value"}"#.into()),
|
||||
name: None,
|
||||
};
|
||||
commands::run::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_command_with_data_file() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), r#"{"deploy":true}"#).unwrap();
|
||||
let args = commands::run::RunArgs {
|
||||
definition_id: "ci".into(),
|
||||
version: 2,
|
||||
data: Some(tmp.path().to_path_buf()),
|
||||
data_json: None,
|
||||
name: None,
|
||||
};
|
||||
commands::run::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_command_no_data() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::run::RunArgs {
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
data: None,
|
||||
data_json: None,
|
||||
name: None,
|
||||
};
|
||||
commands::run::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_command_invalid_json_errors() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::run::RunArgs {
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
data: None,
|
||||
data_json: Some("not json".into()),
|
||||
name: None,
|
||||
};
|
||||
let result = commands::run::run(args, client, OutputFormat::Table).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_command_table() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::get::GetArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
};
|
||||
commands::get::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_command_json() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::get::GetArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
};
|
||||
commands::get::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_command_no_filters() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::list::ListArgs {
|
||||
query: None,
|
||||
status: None,
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
commands::list::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_command_all_filters() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::list::ListArgs {
|
||||
query: Some("foo".into()),
|
||||
status: Some(commands::list::StatusFilter::Complete),
|
||||
limit: 50,
|
||||
skip: 10,
|
||||
};
|
||||
commands::list::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_command_each_status_variant() {
|
||||
use commands::list::StatusFilter;
|
||||
for status in [
|
||||
StatusFilter::Runnable,
|
||||
StatusFilter::Suspended,
|
||||
StatusFilter::Complete,
|
||||
StatusFilter::Terminated,
|
||||
] {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::list::ListArgs {
|
||||
query: None,
|
||||
status: Some(status),
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
commands::list::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_suspend_resume_commands() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client1 = build_client(&server, "test-token").await.unwrap();
|
||||
let client2 = build_client(&server, "test-token").await.unwrap();
|
||||
let client3 = build_client(&server, "test-token").await.unwrap();
|
||||
|
||||
commands::cancel::run(
|
||||
commands::cancel::CancelArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
},
|
||||
client1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
commands::suspend::run(
|
||||
commands::suspend::SuspendArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
},
|
||||
client2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
commands::resume::run(
|
||||
commands::resume::ResumeArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
},
|
||||
client3,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_command_with_inline_data() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::publish::PublishArgs {
|
||||
event_name: "order.paid".into(),
|
||||
event_key: "order-42".into(),
|
||||
data: None,
|
||||
data_json: Some(r#"{"amount":100}"#.into()),
|
||||
};
|
||||
commands::publish::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_command_with_file() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
std::fs::write(tmp.path(), r#"{"amount":100}"#).unwrap();
|
||||
let args = commands::publish::PublishArgs {
|
||||
event_name: "order.paid".into(),
|
||||
event_key: "order-42".into(),
|
||||
data: Some(tmp.path().to_path_buf()),
|
||||
data_json: None,
|
||||
};
|
||||
commands::publish::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_command_no_data() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::publish::PublishArgs {
|
||||
event_name: "test".into(),
|
||||
event_key: "x".into(),
|
||||
data: None,
|
||||
data_json: None,
|
||||
};
|
||||
commands::publish::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watch_command_streams_events() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::watch::WatchArgs {
|
||||
workflow_id: Some("wf-1".into()),
|
||||
};
|
||||
commands::watch::run(args, client).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watch_command_no_filter() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::watch::WatchArgs { workflow_id: None };
|
||||
commands::watch::run(args, client).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logs_command_basic() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::logs::LogsArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
step: None,
|
||||
follow: false,
|
||||
};
|
||||
commands::logs::run(args, client).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logs_command_with_step_filter() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::logs::LogsArgs {
|
||||
workflow_id: "wf-1".into(),
|
||||
step: Some("build".into()),
|
||||
follow: true,
|
||||
};
|
||||
commands::logs::run(args, client).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_logs_command_table() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::search_logs::SearchLogsArgs {
|
||||
query: "needle".into(),
|
||||
workflow: None,
|
||||
step: None,
|
||||
stream: None,
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
commands::search_logs::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_logs_command_with_filters_json() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::search_logs::SearchLogsArgs {
|
||||
query: "needle".into(),
|
||||
workflow: Some("wf-1".into()),
|
||||
step: Some("build".into()),
|
||||
stream: Some(commands::search_logs::StreamFilter::Stdout),
|
||||
limit: 100,
|
||||
skip: 5,
|
||||
};
|
||||
commands::search_logs::run(args, client, OutputFormat::Json)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_logs_each_stream_variant() {
|
||||
use commands::search_logs::StreamFilter;
|
||||
for stream in [StreamFilter::Stdout, StreamFilter::Stderr] {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let client = build_client(&server, "test-token").await.unwrap();
|
||||
let args = commands::search_logs::SearchLogsArgs {
|
||||
query: "x".into(),
|
||||
workflow: None,
|
||||
step: None,
|
||||
stream: Some(stream),
|
||||
limit: 10,
|
||||
skip: 0,
|
||||
};
|
||||
commands::search_logs::run(args, client, OutputFormat::Table)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
220
wfectl/tests/integration.rs
Normal file
220
wfectl/tests/integration.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! Raw gRPC client tests against the in-process stub server.
|
||||
//! Verifies the wire protocol and bearer auth interceptor.
|
||||
|
||||
mod stub;
|
||||
|
||||
use wfe_server_protos::wfe::v1::{
|
||||
CancelWorkflowRequest, GetWorkflowRequest, ListDefinitionsRequest, PublishEventRequest,
|
||||
RegisterWorkflowRequest, ResumeWorkflowRequest, SearchLogsRequest, SearchWorkflowsRequest,
|
||||
StartWorkflowRequest, SuspendWorkflowRequest, WatchLifecycleRequest, WorkflowStatus,
|
||||
};
|
||||
|
||||
use wfectl::client::build as build_client;
|
||||
|
||||
use stub::spawn_stub;
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_register_workflow() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.register_workflow(RegisterWorkflowRequest {
|
||||
yaml: "workflow:\n id: x\n version: 1\n steps: []".into(),
|
||||
config: Default::default(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.definitions.len(), 1);
|
||||
assert_eq!(resp.definitions[0].step_count, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_list_definitions() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.list_definitions(ListDefinitionsRequest {})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.definitions.len(), 1);
|
||||
assert_eq!(resp.definitions[0].id, "ci");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_start_workflow_with_data() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.start_workflow(StartWorkflowRequest {
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
data: Some(wfectl::struct_util::json_object_to_struct(
|
||||
&serde_json::json!({"key": "value"}),
|
||||
)),
|
||||
name: String::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.workflow_id, "wf-ci-1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_get_workflow_returns_instance() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.get_workflow(GetWorkflowRequest {
|
||||
workflow_id: "wf-ci-1".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
let instance = resp.instance.unwrap();
|
||||
assert_eq!(instance.id, "wf-ci-1");
|
||||
assert_eq!(instance.definition_id, "ci");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_cancel_suspend_resume() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
client
|
||||
.cancel_workflow(CancelWorkflowRequest {
|
||||
workflow_id: "wf-1".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.suspend_workflow(SuspendWorkflowRequest {
|
||||
workflow_id: "wf-1".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.resume_workflow(ResumeWorkflowRequest {
|
||||
workflow_id: "wf-1".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_search_workflows() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.search_workflows(SearchWorkflowsRequest {
|
||||
query: "ci".into(),
|
||||
status_filter: WorkflowStatus::Complete as i32,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.total, 1);
|
||||
assert_eq!(resp.results[0].id, "wf-1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_publish_event() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.publish_event(PublishEventRequest {
|
||||
event_name: "order.paid".into(),
|
||||
event_key: "order-42".into(),
|
||||
data: None,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.event_id, "evt-order.paid-order-42");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_watch_lifecycle_stream() {
|
||||
use futures::StreamExt;
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let mut stream = client
|
||||
.watch_lifecycle(WatchLifecycleRequest {
|
||||
workflow_id: String::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
let mut count = 0;
|
||||
while let Some(event) = stream.next().await {
|
||||
let event = event.unwrap();
|
||||
assert_eq!(event.workflow_id, "wf-1");
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_stream_logs() {
|
||||
use futures::StreamExt;
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let mut stream = client
|
||||
.stream_logs(wfe_server_protos::wfe::v1::StreamLogsRequest {
|
||||
workflow_id: "wf-1".into(),
|
||||
step_name: String::new(),
|
||||
follow: false,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
let entry = stream.next().await.unwrap().unwrap();
|
||||
assert_eq!(entry.step_name, "build");
|
||||
assert_eq!(entry.data, b"hello\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn client_search_logs() {
|
||||
let (server, _seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "test-token").await.unwrap();
|
||||
let resp = client
|
||||
.search_logs(SearchLogsRequest {
|
||||
query: "needle".into(),
|
||||
workflow_id: String::new(),
|
||||
step_name: String::new(),
|
||||
stream_filter: 0,
|
||||
skip: 0,
|
||||
take: 10,
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_inner();
|
||||
assert_eq!(resp.total, 1);
|
||||
assert!(resp.results[0].line.contains("needle"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_interceptor_sends_bearer_header() {
|
||||
let (server, seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "ory_at_test_token").await.unwrap();
|
||||
client
|
||||
.list_definitions(ListDefinitionsRequest {})
|
||||
.await
|
||||
.unwrap();
|
||||
let captured = seen.lock().await.clone();
|
||||
assert_eq!(captured, Some("Bearer ory_at_test_token".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_token_sends_no_header() {
|
||||
let (server, seen) = spawn_stub().await;
|
||||
let mut client = build_client(&server, "").await.unwrap();
|
||||
client
|
||||
.list_definitions(ListDefinitionsRequest {})
|
||||
.await
|
||||
.unwrap();
|
||||
let captured = seen.lock().await.clone();
|
||||
assert!(captured.is_none());
|
||||
}
|
||||
312
wfectl/tests/stub/mod.rs
Normal file
312
wfectl/tests/stub/mod.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
//! Shared in-process gRPC stub server for command and integration tests.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::transport::Server;
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
use wfe_server_protos::wfe::v1::{
|
||||
CancelWorkflowRequest, CancelWorkflowResponse, DefinitionSummary, GetWorkflowRequest,
|
||||
GetWorkflowResponse, ListDefinitionsRequest, ListDefinitionsResponse, PublishEventRequest,
|
||||
PublishEventResponse, RegisterWorkflowRequest, RegisterWorkflowResponse, RegisteredDefinition,
|
||||
ResumeWorkflowRequest, ResumeWorkflowResponse, SearchLogsRequest, SearchLogsResponse,
|
||||
SearchWorkflowsRequest, SearchWorkflowsResponse, StartWorkflowRequest, StartWorkflowResponse,
|
||||
SuspendWorkflowRequest, SuspendWorkflowResponse, WatchLifecycleRequest, WorkflowInstance,
|
||||
WorkflowSearchResult, WorkflowStatus,
|
||||
wfe_server::{Wfe, WfeServer},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StubWfe {
|
||||
pub seen_authorization: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl StubWfe {
|
||||
async fn capture_auth<T>(&self, req: &Request<T>) {
|
||||
if let Some(val) = req.metadata().get("authorization") {
|
||||
if let Ok(s) = val.to_str() {
|
||||
let mut guard = self.seen_authorization.lock().await;
|
||||
*guard = Some(s.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl Wfe for StubWfe {
|
||||
async fn register_workflow(
|
||||
&self,
|
||||
req: Request<RegisterWorkflowRequest>,
|
||||
) -> Result<Response<RegisterWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let inner = req.into_inner();
|
||||
Ok(Response::new(RegisterWorkflowResponse {
|
||||
definitions: vec![RegisteredDefinition {
|
||||
definition_id: format!("test-{}", inner.yaml.len()),
|
||||
version: 1,
|
||||
step_count: 3,
|
||||
name: "Test Workflow".into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_definitions(
|
||||
&self,
|
||||
req: Request<ListDefinitionsRequest>,
|
||||
) -> Result<Response<ListDefinitionsResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
Ok(Response::new(ListDefinitionsResponse {
|
||||
definitions: vec![DefinitionSummary {
|
||||
id: "ci".into(),
|
||||
version: 1,
|
||||
description: "CI pipeline".into(),
|
||||
step_count: 5,
|
||||
name: "Continuous Integration".into(),
|
||||
}],
|
||||
}))
|
||||
}
|
||||
|
||||
async fn start_workflow(
|
||||
&self,
|
||||
req: Request<StartWorkflowRequest>,
|
||||
) -> Result<Response<StartWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let inner = req.into_inner();
|
||||
let workflow_id = format!("wf-{}-{}", inner.definition_id, inner.version);
|
||||
let name = if inner.name.is_empty() {
|
||||
format!("{}-1", inner.definition_id)
|
||||
} else {
|
||||
inner.name
|
||||
};
|
||||
Ok(Response::new(StartWorkflowResponse { workflow_id, name }))
|
||||
}
|
||||
|
||||
async fn get_workflow(
|
||||
&self,
|
||||
req: Request<GetWorkflowRequest>,
|
||||
) -> Result<Response<GetWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let id = req.into_inner().workflow_id;
|
||||
Ok(Response::new(GetWorkflowResponse {
|
||||
instance: Some(WorkflowInstance {
|
||||
id: id.clone(),
|
||||
name: "ci-1".into(),
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
description: "test instance".into(),
|
||||
reference: "ref-1".into(),
|
||||
status: WorkflowStatus::Runnable as i32,
|
||||
data: None,
|
||||
create_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
complete_time: None,
|
||||
execution_pointers: vec![wfe_server_protos::wfe::v1::ExecutionPointer {
|
||||
id: "ptr-1".into(),
|
||||
step_id: 0,
|
||||
step_name: "build".into(),
|
||||
status: wfe_server_protos::wfe::v1::PointerStatus::Complete as i32,
|
||||
start_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
end_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_100,
|
||||
nanos: 0,
|
||||
}),
|
||||
retry_count: 0,
|
||||
active: false,
|
||||
}],
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn cancel_workflow(
|
||||
&self,
|
||||
req: Request<CancelWorkflowRequest>,
|
||||
) -> Result<Response<CancelWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
Ok(Response::new(CancelWorkflowResponse {}))
|
||||
}
|
||||
|
||||
async fn suspend_workflow(
|
||||
&self,
|
||||
req: Request<SuspendWorkflowRequest>,
|
||||
) -> Result<Response<SuspendWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
Ok(Response::new(SuspendWorkflowResponse {}))
|
||||
}
|
||||
|
||||
async fn resume_workflow(
|
||||
&self,
|
||||
req: Request<ResumeWorkflowRequest>,
|
||||
) -> Result<Response<ResumeWorkflowResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
Ok(Response::new(ResumeWorkflowResponse {}))
|
||||
}
|
||||
|
||||
async fn search_workflows(
|
||||
&self,
|
||||
req: Request<SearchWorkflowsRequest>,
|
||||
) -> Result<Response<SearchWorkflowsResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
Ok(Response::new(SearchWorkflowsResponse {
|
||||
results: vec![WorkflowSearchResult {
|
||||
id: "wf-1".into(),
|
||||
name: "ci-1".into(),
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
status: WorkflowStatus::Complete as i32,
|
||||
reference: "ref-1".into(),
|
||||
description: "test".into(),
|
||||
create_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
}],
|
||||
total: 1,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn publish_event(
|
||||
&self,
|
||||
req: Request<PublishEventRequest>,
|
||||
) -> Result<Response<PublishEventResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let event = req.into_inner();
|
||||
Ok(Response::new(PublishEventResponse {
|
||||
event_id: format!("evt-{}-{}", event.event_name, event.event_key),
|
||||
}))
|
||||
}
|
||||
|
||||
type WatchLifecycleStream = tokio_stream::wrappers::ReceiverStream<
|
||||
Result<wfe_server_protos::wfe::v1::LifecycleEvent, Status>,
|
||||
>;
|
||||
async fn watch_lifecycle(
|
||||
&self,
|
||||
req: Request<WatchLifecycleRequest>,
|
||||
) -> Result<Response<Self::WatchLifecycleStream>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
let _ = tx
|
||||
.send(Ok(wfe_server_protos::wfe::v1::LifecycleEvent {
|
||||
event_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
workflow_id: "wf-1".into(),
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
event_type: wfe_server_protos::wfe::v1::LifecycleEventType::Started as i32,
|
||||
step_id: 0,
|
||||
step_name: String::new(),
|
||||
error_message: String::new(),
|
||||
}))
|
||||
.await;
|
||||
let _ = tx
|
||||
.send(Ok(wfe_server_protos::wfe::v1::LifecycleEvent {
|
||||
event_time: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_001,
|
||||
nanos: 0,
|
||||
}),
|
||||
workflow_id: "wf-1".into(),
|
||||
definition_id: "ci".into(),
|
||||
version: 1,
|
||||
event_type: wfe_server_protos::wfe::v1::LifecycleEventType::StepCompleted as i32,
|
||||
step_id: 1,
|
||||
step_name: "build".into(),
|
||||
error_message: String::new(),
|
||||
}))
|
||||
.await;
|
||||
drop(tx);
|
||||
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(
|
||||
rx,
|
||||
)))
|
||||
}
|
||||
|
||||
type StreamLogsStream = tokio_stream::wrappers::ReceiverStream<
|
||||
Result<wfe_server_protos::wfe::v1::LogEntry, Status>,
|
||||
>;
|
||||
async fn stream_logs(
|
||||
&self,
|
||||
req: Request<wfe_server_protos::wfe::v1::StreamLogsRequest>,
|
||||
) -> Result<Response<Self::StreamLogsStream>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
let _ = tx
|
||||
.send(Ok(wfe_server_protos::wfe::v1::LogEntry {
|
||||
workflow_id: "wf-1".into(),
|
||||
step_name: "build".into(),
|
||||
step_id: 0,
|
||||
stream: wfe_server_protos::wfe::v1::LogStream::Stdout as i32,
|
||||
data: b"hello\n".to_vec(),
|
||||
timestamp: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
}))
|
||||
.await;
|
||||
let _ = tx
|
||||
.send(Ok(wfe_server_protos::wfe::v1::LogEntry {
|
||||
workflow_id: "wf-1".into(),
|
||||
step_name: "build".into(),
|
||||
step_id: 0,
|
||||
stream: wfe_server_protos::wfe::v1::LogStream::Stderr as i32,
|
||||
data: b"warning\n".to_vec(),
|
||||
timestamp: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_001,
|
||||
nanos: 0,
|
||||
}),
|
||||
}))
|
||||
.await;
|
||||
drop(tx);
|
||||
Ok(Response::new(tokio_stream::wrappers::ReceiverStream::new(
|
||||
rx,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn search_logs(
|
||||
&self,
|
||||
req: Request<SearchLogsRequest>,
|
||||
) -> Result<Response<SearchLogsResponse>, Status> {
|
||||
self.capture_auth(&req).await;
|
||||
let inner = req.into_inner();
|
||||
Ok(Response::new(SearchLogsResponse {
|
||||
results: vec![wfe_server_protos::wfe::v1::LogSearchResult {
|
||||
workflow_id: "wf-1".into(),
|
||||
definition_id: "ci".into(),
|
||||
step_name: "build".into(),
|
||||
line: format!("matched {}", inner.query),
|
||||
stream: wfe_server_protos::wfe::v1::LogStream::Stdout as i32,
|
||||
timestamp: Some(prost_types::Timestamp {
|
||||
seconds: 1_700_000_000,
|
||||
nanos: 0,
|
||||
}),
|
||||
}],
|
||||
total: 1,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the stub server on an ephemeral port and return its URL + the
|
||||
/// shared `seen_authorization` slot so tests can assert on it.
|
||||
pub async fn spawn_stub() -> (String, Arc<Mutex<Option<String>>>) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr: SocketAddr = listener.local_addr().unwrap();
|
||||
let stub = StubWfe::default();
|
||||
let seen = stub.seen_authorization.clone();
|
||||
|
||||
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
|
||||
tokio::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(WfeServer::new(stub))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
(format!("http://{addr}"), seen)
|
||||
}
|
||||
Reference in New Issue
Block a user