Files
wfe/wfectl/tests/integration.rs
Sienna Meridian Satterwhite 0c239cd484 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`.
2026-04-07 19:09:26 +01:00

221 lines
6.5 KiB
Rust

//! 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());
}