Refactor large Rust integration tests
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::common::*;
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, fmt,
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{Json, Router, routing::post};
|
||||
use crank_core::{
|
||||
ExecutionConfig, HttpMethod, MembershipRole, OperationSecurityLevel, Protocol,
|
||||
ResponseCachePolicy, RestTarget, SecretKind, Target, ToolDescription, WorkspaceId,
|
||||
};
|
||||
use crank_core::{IdentityError, IdentityProvider, IdentityProviderKind, LoginOutcome};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::PostgresRegistry;
|
||||
use crank_runtime::SecretCrypto;
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::{Value, json};
|
||||
use serial_test::serial;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use admin_api::{
|
||||
app::build_app,
|
||||
auth::{AuthSettings, BootstrapAdminConfig, hash_password},
|
||||
service::{AdminService, AdminServiceBuilder, OperationPayload},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
const DEFAULT_WORKSPACE_ID: &str = "ws_default";
|
||||
const TEST_AUTH_EMAIL: &str = "owner@crank.local";
|
||||
const TEST_AUTH_PASSWORD: &str = "test-password";
|
||||
const TEST_PASSWORD_PEPPER: &str = "test-password-pepper";
|
||||
const TEST_SESSION_SECRET: &str = "test-session-secret";
|
||||
const TEST_MASTER_KEY: &str = "test-master-key";
|
||||
|
||||
struct TestServer {
|
||||
base_url: String,
|
||||
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
handle: Option<tokio::task::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
let shutdown = self.shutdown.take();
|
||||
let handle = self.handle.take();
|
||||
|
||||
tokio::task::block_in_place(|| {
|
||||
if let Some(shutdown) = shutdown {
|
||||
let _ = shutdown.send(());
|
||||
}
|
||||
if let Some(handle) = handle {
|
||||
let _ = tokio::runtime::Handle::current().block_on(handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TestServer {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.base_url)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for TestServer {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
}
|
||||
|
||||
struct RejectingIdentityProvider;
|
||||
|
||||
#[async_trait]
|
||||
impl IdentityProvider for RejectingIdentityProvider {
|
||||
fn id(&self) -> &str {
|
||||
"rejecting-test-provider"
|
||||
}
|
||||
|
||||
fn kind(&self) -> IdentityProviderKind {
|
||||
IdentityProviderKind::Password
|
||||
}
|
||||
|
||||
async fn login_password(
|
||||
&self,
|
||||
_payload: crank_core::LoginPayload,
|
||||
) -> Result<LoginOutcome, IdentityError> {
|
||||
Err(IdentityError::BadCredentials)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn manages_auth_profiles_and_yaml_upsert() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("yaml");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
let secret = client
|
||||
.post(format!("{base_url}/secrets"))
|
||||
.json(&json!({
|
||||
"name": "crm-api-token",
|
||||
"kind": SecretKind::Token,
|
||||
"value": { "token": "super-secret-token" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let secret = assert_success_json(secret).await;
|
||||
let secret_id = secret["id"].as_str().unwrap();
|
||||
|
||||
let auth_profile = client
|
||||
.post(format!("{base_url}/auth-profiles"))
|
||||
.json(&json!({
|
||||
"name": "crm-header",
|
||||
"kind": "api_key_header",
|
||||
"config": {
|
||||
"api_key_header": {
|
||||
"header_name": "X-Api-Key",
|
||||
"secret_id": secret_id
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_export_target",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap();
|
||||
let yaml = client
|
||||
.get(format!("{base_url}/operations/{operation_id}/export"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
let imported = client
|
||||
.post(format!("{base_url}/operations/import?mode=upsert"))
|
||||
.body(yaml)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(auth_profile["kind"], "api_key_header");
|
||||
assert_eq!(
|
||||
auth_profile["config"]["api_key_header"]["secret_id"],
|
||||
secret_id
|
||||
);
|
||||
assert_eq!(imported["operation_id"], operation_id);
|
||||
assert_eq!(imported["version"], 2);
|
||||
assert_eq!(imported["import_mode"], "upsert");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn manages_workspace_secrets_without_exposing_plaintext() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("secrets");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/secrets"))
|
||||
.json(&json!({
|
||||
"name": "crm-api-token",
|
||||
"kind": SecretKind::Token,
|
||||
"value": { "token": "super-secret-token" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let created = assert_success_json(created).await;
|
||||
let secret_id = created["id"].as_str().unwrap().to_owned();
|
||||
|
||||
let listed = client
|
||||
.get(format!("{base_url}/secrets"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let listed = assert_success_json(listed).await;
|
||||
|
||||
let fetched = client
|
||||
.get(format!("{base_url}/secrets/{secret_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let fetched = assert_success_json(fetched).await;
|
||||
|
||||
let rotated = client
|
||||
.post(format!("{base_url}/secrets/{secret_id}/rotate"))
|
||||
.json(&json!({
|
||||
"value": { "token": "rotated-token" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let rotated = assert_success_json(rotated).await;
|
||||
|
||||
let deleted = client
|
||||
.delete(format!("{base_url}/secrets/{secret_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let deleted = assert_success_json(deleted).await;
|
||||
|
||||
let missing = client
|
||||
.get(format!("{base_url}/secrets/{secret_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let missing_status = missing.status();
|
||||
let missing = missing.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(created["name"], "crm-api-token");
|
||||
assert_eq!(created["kind"], "token");
|
||||
assert_eq!(created["current_version"], 1);
|
||||
assert!(created.get("value").is_none());
|
||||
assert_eq!(listed["items"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(fetched["id"], secret_id);
|
||||
assert!(fetched.get("value").is_none());
|
||||
assert_eq!(rotated["current_version"], 2);
|
||||
assert_eq!(deleted["ok"], true);
|
||||
assert_eq!(missing_status, reqwest::StatusCode::NOT_FOUND);
|
||||
assert_eq!(missing["error"]["code"], "not_found");
|
||||
assert_eq!(
|
||||
missing["error"]["context"],
|
||||
json!({
|
||||
"secret_id": secret_id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn returns_structured_context_for_missing_operation_version() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("missing_operation_version");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_missing_operation_version",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let response = client
|
||||
.get(format!("{base_url}/operations/{operation_id}/versions/99"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::NOT_FOUND);
|
||||
assert_eq!(body["error"]["code"], "not_found");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
format!("operation version 99 for {operation_id} was not found")
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"operation_id": operation_id,
|
||||
"version": 99
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_auth_profile_with_missing_secret() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("missing_secret_auth");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/auth-profiles"))
|
||||
.json(&json!({
|
||||
"name": "crm-header",
|
||||
"kind": "api_key_header",
|
||||
"config": {
|
||||
"api_key_header": {
|
||||
"header_name": "X-Api-Key",
|
||||
"secret_id": "secret_missing"
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::NOT_FOUND);
|
||||
assert_eq!(body["error"]["code"], "not_found");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"secret secret_missing was not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_deleting_secret_referenced_by_auth_profile() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("secret_references");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let secret = client
|
||||
.post(format!("{base_url}/secrets"))
|
||||
.json(&json!({
|
||||
"name": "crm-api-token",
|
||||
"kind": SecretKind::Token,
|
||||
"value": { "token": "super-secret-token" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let secret = assert_success_json(secret).await;
|
||||
let secret_id = secret["id"].as_str().unwrap();
|
||||
|
||||
let auth_profile = client
|
||||
.post(format!("{base_url}/auth-profiles"))
|
||||
.json(&json!({
|
||||
"name": "crm-header",
|
||||
"kind": "api_key_header",
|
||||
"config": {
|
||||
"api_key_header": {
|
||||
"header_name": "X-Api-Key",
|
||||
"secret_id": secret_id
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let auth_profile = assert_success_json(auth_profile).await;
|
||||
let auth_profile_id = auth_profile["id"].as_str().unwrap();
|
||||
|
||||
let response = client
|
||||
.delete(format!("{base_url}/secrets/{secret_id}"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::CONFLICT);
|
||||
assert_eq!(body["error"]["code"], "conflict");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
format!("secret {secret_id} is referenced by auth profile {auth_profile_id}")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn uploads_samples_and_generates_draft() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("draft");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_draft_target",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap();
|
||||
|
||||
client
|
||||
.post(format!(
|
||||
"{base_url}/operations/{operation_id}/samples/input-json"
|
||||
))
|
||||
.json(&json!({ "email": "user@example.com", "name": "Ada" }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
client
|
||||
.post(format!(
|
||||
"{base_url}/operations/{operation_id}/samples/output-json"
|
||||
))
|
||||
.json(&json!({ "id": "lead_123", "status": "created" }))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let generated = client
|
||||
.post(format!(
|
||||
"{base_url}/operations/{operation_id}/drafts/generate"
|
||||
))
|
||||
.json(&json!({
|
||||
"sources": ["input_json_sample", "output_json_sample"]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(generated["generated_draft"]["status"], "available");
|
||||
assert_eq!(
|
||||
generated["input_schema"]["fields"]["email"]["type"],
|
||||
"string"
|
||||
);
|
||||
assert_eq!(
|
||||
generated["output_mapping"]["rules"][0]["target"],
|
||||
"$.output.id"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user