Refactor large Rust integration tests
This commit is contained in:
@@ -163,127 +163,3 @@ fn to_reqwest_method(method: HttpMethod) -> reqwest::Method {
|
||||
HttpMethod::Delete => reqwest::Method::DELETE,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query},
|
||||
http::HeaderMap,
|
||||
routing::{get, post},
|
||||
};
|
||||
use crank_core::{HttpMethod, RestTarget};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{RestAdapter, RestAdapterError, RestRequest};
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_rest_request_and_normalizes_json_response() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/users/{user_id}".to_owned(),
|
||||
static_headers: BTreeMap::from([("x-static".to_owned(), "static".to_owned())]),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::from([("user_id".to_owned(), "42".to_owned())]),
|
||||
query_params: BTreeMap::from([("expand".to_owned(), "true".to_owned())]),
|
||||
headers: BTreeMap::from([("x-trace-id".to_owned(), "trace-123".to_owned())]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(
|
||||
response.body,
|
||||
json!({
|
||||
"id": "42",
|
||||
"query": "true",
|
||||
"trace": "trace-123",
|
||||
"static": "static",
|
||||
"payload": { "name": "Ada" }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_unexpected_status_with_normalized_body() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Get,
|
||||
path_template: "/fail".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::new(),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let error = adapter.execute(&target, &request).await.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
RestAdapterError::UnexpectedStatus {
|
||||
status: 502,
|
||||
body: Value::Object(_)
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
async fn spawn_test_server() -> String {
|
||||
let app = Router::new()
|
||||
.route("/users/{user_id}", post(create_user))
|
||||
.route("/fail", get(fail));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
Path(user_id): Path<String>,
|
||||
Query(query): Query<BTreeMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Json<Value> {
|
||||
let trace = headers
|
||||
.get("x-trace-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
let static_header = headers
|
||||
.get("x-static")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(json!({
|
||||
"id": user_id,
|
||||
"query": query.get("expand").cloned().unwrap_or_default(),
|
||||
"trace": trace,
|
||||
"static": static_header,
|
||||
"payload": payload
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fail() -> (axum::http::StatusCode, Json<Value>) {
|
||||
(
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "error": "upstream failed" })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod integration {
|
||||
mod client;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query},
|
||||
http::HeaderMap,
|
||||
routing::{get, post},
|
||||
};
|
||||
use crank_adapter_rest::{RestAdapter, RestAdapterError, RestRequest};
|
||||
use crank_core::{HttpMethod, RestTarget};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_rest_request_and_normalizes_json_response() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/users/{user_id}".to_owned(),
|
||||
static_headers: BTreeMap::from([("x-static".to_owned(), "static".to_owned())]),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::from([("user_id".to_owned(), "42".to_owned())]),
|
||||
query_params: BTreeMap::from([("expand".to_owned(), "true".to_owned())]),
|
||||
headers: BTreeMap::from([("x-trace-id".to_owned(), "trace-123".to_owned())]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status_code, 200);
|
||||
assert_eq!(
|
||||
response.body,
|
||||
json!({
|
||||
"id": "42",
|
||||
"query": "true",
|
||||
"trace": "trace-123",
|
||||
"static": "static",
|
||||
"payload": { "name": "Ada" }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_unexpected_status_with_normalized_body() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = RestAdapter::new();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Get,
|
||||
path_template: "/fail".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::new(),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let error = adapter.execute(&target, &request).await.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
RestAdapterError::UnexpectedStatus {
|
||||
status: 502,
|
||||
body: Value::Object(_)
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
async fn spawn_test_server() -> String {
|
||||
let app = Router::new()
|
||||
.route("/users/{user_id}", post(create_user))
|
||||
.route("/fail", get(fail));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
Path(user_id): Path<String>,
|
||||
Query(query): Query<BTreeMap<String, String>>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<Value>,
|
||||
) -> Json<Value> {
|
||||
let trace = headers
|
||||
.get("x-trace-id")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
let static_header = headers
|
||||
.get("x-static")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Json(json!({
|
||||
"id": user_id,
|
||||
"query": query.get("expand").cloned().unwrap_or_default(),
|
||||
"trace": trace,
|
||||
"static": static_header,
|
||||
"payload": payload
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fail() -> (axum::http::StatusCode, Json<Value>) {
|
||||
(
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "error": "upstream failed" })),
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
mod integration {
|
||||
mod agents_usage;
|
||||
mod common;
|
||||
mod operations_artifacts;
|
||||
mod workspace_access;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::common::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
|
||||
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
|
||||
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::json;
|
||||
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crank_registry::{
|
||||
CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
|
||||
fn test_workspace_id() -> WorkspaceId {
|
||||
WorkspaceId::new("ws_default")
|
||||
}
|
||||
|
||||
fn timestamp(value: &str) -> OffsetDateTime {
|
||||
OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_agent_read_paths() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_agent_01", 1, OperationStatus::Draft);
|
||||
let agent = test_agent("agent_01", AgentStatus::Draft);
|
||||
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
||||
let bindings = vec![AgentOperationBinding {
|
||||
operation_id: operation.id.clone(),
|
||||
operation_version: operation.version,
|
||||
tool_name: "create_lead".to_owned(),
|
||||
tool_title: "Create lead".to_owned(),
|
||||
tool_description_override: Some("Creates CRM lead".to_owned()),
|
||||
enabled: true,
|
||||
}];
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_agent(CreateAgentRequest {
|
||||
agent: &agent,
|
||||
version: &version,
|
||||
bindings: &bindings,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let listed = registry.list_agents(&test_workspace_id()).await.unwrap();
|
||||
let summary = registry
|
||||
.get_agent_summary(&test_workspace_id(), &agent.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let loaded_version = registry
|
||||
.get_agent_version(&test_workspace_id(), &agent.id, version.version)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(listed, vec![summary.clone()]);
|
||||
assert_eq!(summary.id, agent.id);
|
||||
assert_eq!(summary.slug, agent.slug);
|
||||
assert_eq!(summary.display_name, agent.display_name);
|
||||
assert_eq!(summary.status, agent.status);
|
||||
assert_eq!(loaded_version.agent_id, agent.id);
|
||||
assert_eq!(loaded_version.version, version.version);
|
||||
assert_eq!(loaded_version.status, version.status);
|
||||
assert_eq!(loaded_version.snapshot, version);
|
||||
assert_eq!(loaded_version.bindings, bindings);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_published_agent_tool_reads() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_agent_pub_01", 1, OperationStatus::Draft);
|
||||
let operation_v2 = test_operation("op_agent_pub_01", 2, OperationStatus::Draft);
|
||||
let agent = test_agent("agent_pub_01", AgentStatus::Draft);
|
||||
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
||||
let bindings = vec![AgentOperationBinding {
|
||||
operation_id: operation.id.clone(),
|
||||
operation_version: operation_v2.version,
|
||||
tool_name: "create_lead".to_owned(),
|
||||
tool_title: "Create lead".to_owned(),
|
||||
tool_description_override: Some("Creates CRM lead".to_owned()),
|
||||
enabled: true,
|
||||
}];
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &operation_v2,
|
||||
change_note: Some("publishable"),
|
||||
created_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.publish_operation(PublishRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
operation_id: &operation.id,
|
||||
version: operation_v2.version,
|
||||
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_agent(CreateAgentRequest {
|
||||
agent: &agent,
|
||||
version: &version,
|
||||
bindings: &bindings,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.publish_agent(PublishAgentRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
agent_id: &agent.id,
|
||||
version: version.version,
|
||||
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tools = registry
|
||||
.get_published_agent_tools_by_slug("default", &agent.slug)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(tools.len(), 1);
|
||||
assert_eq!(tools[0].workspace_id, test_workspace_id());
|
||||
assert_eq!(tools[0].workspace_slug, "default");
|
||||
assert_eq!(tools[0].agent_id, agent.id);
|
||||
assert_eq!(tools[0].agent_slug, agent.slug);
|
||||
assert_eq!(tools[0].tool_name, bindings[0].tool_name);
|
||||
assert_eq!(tools[0].tool_title, bindings[0].tool_title);
|
||||
assert_eq!(tools[0].tool_description, "Creates CRM lead");
|
||||
assert_eq!(tools[0].operation.id, operation_v2.id);
|
||||
assert_eq!(tools[0].operation.version, operation_v2.version);
|
||||
assert_eq!(tools[0].operation.protocol, operation_v2.protocol);
|
||||
assert!(tools[0].operation.is_published());
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_operation_usage_and_agent_ref_reads() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_usage_01", 1, OperationStatus::Draft);
|
||||
let operation_v2 = test_operation("op_usage_01", 2, OperationStatus::Draft);
|
||||
let agent = test_agent("agent_usage_01", AgentStatus::Draft);
|
||||
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
||||
let bindings = vec![AgentOperationBinding {
|
||||
operation_id: operation.id.clone(),
|
||||
operation_version: operation_v2.version,
|
||||
tool_name: "create_lead".to_owned(),
|
||||
tool_title: "Create lead".to_owned(),
|
||||
tool_description_override: None,
|
||||
enabled: true,
|
||||
}];
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &operation_v2,
|
||||
change_note: Some("publishable"),
|
||||
created_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.publish_operation(PublishRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
operation_id: &operation.id,
|
||||
version: operation_v2.version,
|
||||
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_agent(CreateAgentRequest {
|
||||
agent: &agent,
|
||||
version: &version,
|
||||
bindings: &bindings,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.publish_agent(PublishAgentRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
agent_id: &agent.id,
|
||||
version: version.version,
|
||||
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_invocation_log(CreateInvocationLogRequest {
|
||||
log: &test_invocation_log(
|
||||
"log_usage_ok",
|
||||
&operation.id,
|
||||
Some(agent.id.clone()),
|
||||
crank_core::InvocationStatus::Ok,
|
||||
120,
|
||||
"2026-03-25T12:20:00Z",
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_invocation_log(CreateInvocationLogRequest {
|
||||
log: &test_invocation_log(
|
||||
"log_usage_err",
|
||||
&operation.id,
|
||||
Some(agent.id.clone()),
|
||||
crank_core::InvocationStatus::Error,
|
||||
240,
|
||||
"2026-03-25T12:21:00Z",
|
||||
),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let has_bindings = registry
|
||||
.has_published_agent_bindings_for_operation(&test_workspace_id(), &operation.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let agent_refs = registry
|
||||
.list_operation_agent_refs(&test_workspace_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let usage = registry
|
||||
.list_operation_usage_summaries(&test_workspace_id(), "2026-03-25T12:00:00Z")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(has_bindings);
|
||||
assert_eq!(agent_refs.len(), 1);
|
||||
assert_eq!(agent_refs[0].operation_id, operation.id);
|
||||
assert_eq!(agent_refs[0].agent_id, agent.id);
|
||||
assert_eq!(agent_refs[0].agent_slug, agent.slug);
|
||||
assert_eq!(agent_refs[0].display_name, agent.display_name);
|
||||
assert_eq!(usage.len(), 1);
|
||||
assert_eq!(usage[0].operation_id, operation.id);
|
||||
assert_eq!(usage[0].calls_today, 2);
|
||||
assert_eq!(usage[0].error_rate_pct, 50.0);
|
||||
assert_eq!(usage[0].avg_latency_ms, 180);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
|
||||
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
|
||||
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::json;
|
||||
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crank_registry::{
|
||||
CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
|
||||
fn test_workspace_id() -> WorkspaceId {
|
||||
WorkspaceId::new("ws_default")
|
||||
}
|
||||
|
||||
fn timestamp(value: &str) -> OffsetDateTime {
|
||||
OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn test_operation(id: &str, version: u32, status: OperationStatus) -> RegistryOperation {
|
||||
RegistryOperation {
|
||||
id: OperationId::new(id),
|
||||
name: format!("{id}_tool"),
|
||||
display_name: format!("Display {id}"),
|
||||
category: "general".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
status,
|
||||
version,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::from([("X-Static".to_owned(), "true".to_owned())]),
|
||||
}),
|
||||
input_schema: Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("input".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
"email".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
output_schema: Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("output".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
"id".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: Some(RetryPolicy { max_attempts: 3 }),
|
||||
auth_profile_ref: Some("auth_crank".into()),
|
||||
headers: BTreeMap::from([("X-Request-Id".to_owned(), "static".to_owned())]),
|
||||
..ExecutionConfig::default()
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create lead".to_owned(),
|
||||
description: "Creates CRM lead".to_owned(),
|
||||
tags: vec!["crm".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "email": "a@example.com" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples {
|
||||
input_json_sample_ref: Some("sample_input".into()),
|
||||
output_json_sample_ref: Some("sample_output".into()),
|
||||
}),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["input_json".to_owned(), "output_json".to_owned()],
|
||||
generated_at: Some("2026-03-25T11:59:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: Some(ConfigExport {
|
||||
format_version: "v1".to_owned(),
|
||||
export_mode: ExportMode::Portable,
|
||||
}),
|
||||
wizard_state: Some(WizardState {
|
||||
input_sample: Some(json!({ "email": format!("lead-{version}@example.com") })),
|
||||
output_sample: Some(json!({ "id": format!("lead_{version}") })),
|
||||
test_input: Some(json!({ "email": format!("test-v{version}@example.com") })),
|
||||
}),
|
||||
created_at: timestamp("2026-03-25T11:58:00Z"),
|
||||
updated_at: timestamp(&format!("2026-03-25T12:{version:02}:00Z")),
|
||||
published_at: None,
|
||||
}
|
||||
}
|
||||
pub(super) fn test_agent(id: &str, status: AgentStatus) -> crank_core::Agent {
|
||||
crank_core::Agent {
|
||||
id: AgentId::new(id),
|
||||
workspace_id: test_workspace_id(),
|
||||
slug: format!("{id}_slug"),
|
||||
display_name: format!("Display {id}"),
|
||||
description: format!("Description {id}"),
|
||||
status,
|
||||
current_draft_version: 1,
|
||||
latest_published_version: None,
|
||||
created_at: timestamp("2026-03-25T11:58:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
published_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn test_agent_version(
|
||||
agent_id: &AgentId,
|
||||
version: u32,
|
||||
status: AgentStatus,
|
||||
) -> AgentVersion {
|
||||
AgentVersion {
|
||||
agent_id: agent_id.clone(),
|
||||
version,
|
||||
status,
|
||||
instructions: json!({
|
||||
"system": "triage tickets",
|
||||
"guardrails": ["don't mutate state"]
|
||||
}),
|
||||
tool_selection_policy: json!({
|
||||
"mode": "allow_list",
|
||||
"max_tools": 8
|
||||
}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn test_invocation_log(
|
||||
id: &str,
|
||||
operation_id: &OperationId,
|
||||
agent_id: Option<AgentId>,
|
||||
status: crank_core::InvocationStatus,
|
||||
duration_ms: u64,
|
||||
created_at: &str,
|
||||
) -> InvocationLog {
|
||||
InvocationLog {
|
||||
id: crank_core::InvocationLogId::new(id),
|
||||
workspace_id: test_workspace_id(),
|
||||
agent_id,
|
||||
operation_id: operation_id.clone(),
|
||||
source: crank_core::InvocationSource::AgentToolCall,
|
||||
level: crank_core::InvocationLevel::Info,
|
||||
status,
|
||||
tool_name: "create_lead".to_owned(),
|
||||
message: "invocation".to_owned(),
|
||||
request_id: Some(format!("req_{id}")),
|
||||
status_code: Some(200),
|
||||
duration_ms,
|
||||
error_kind: None,
|
||||
request_preview: json!({"input":"value"}),
|
||||
response_preview: json!({"ok":true}),
|
||||
created_at: timestamp(created_at),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct TestDatabase {
|
||||
admin_pool: PgPool,
|
||||
database_url: String,
|
||||
schema: String,
|
||||
}
|
||||
|
||||
impl TestDatabase {
|
||||
pub(super) async fn new() -> Self {
|
||||
let database_url = crank_test_support::postgres_database_url().await.to_owned();
|
||||
let admin_pool = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let schema = crank_test_support::unique_schema_name("test_registry");
|
||||
|
||||
admin_pool
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
admin_pool,
|
||||
database_url,
|
||||
schema,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn registry(&self) -> PostgresRegistry {
|
||||
PostgresRegistry::connect(&format!(
|
||||
"{}?options=-csearch_path%3D{}",
|
||||
self.database_url, self.schema
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(super) async fn cleanup(&self) {
|
||||
self.admin_pool
|
||||
.execute(sqlx::query(&format!(
|
||||
"drop schema if exists {} cascade",
|
||||
self.schema
|
||||
)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::common::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
|
||||
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
|
||||
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::json;
|
||||
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crank_registry::{
|
||||
CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
|
||||
fn test_workspace_id() -> WorkspaceId {
|
||||
WorkspaceId::new("ws_default")
|
||||
}
|
||||
|
||||
fn timestamp(value: &str) -> OffsetDateTime {
|
||||
OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_versions_and_published_operations() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation_v1, Some("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let operation_v2 = test_operation("op_rest_01", 2, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &operation_v2,
|
||||
change_note: Some("add output mapping"),
|
||||
created_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
registry
|
||||
.publish_operation(PublishRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
operation_id: &operation_v2.id,
|
||||
version: operation_v2.version,
|
||||
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
||||
published_by: Some("alice"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let summary = registry
|
||||
.get_operation_summary(&test_workspace_id(), &operation_v2.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let versions = registry
|
||||
.list_operation_versions(&test_workspace_id(), &operation_v2.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let published = registry
|
||||
.get_published_operation(&operation_v2.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let published_list = registry.list_published_operations().await.unwrap();
|
||||
|
||||
assert_eq!(summary.current_draft_version, 2);
|
||||
assert_eq!(summary.latest_published_version, Some(2));
|
||||
assert_eq!(summary.status, OperationStatus::Published);
|
||||
assert_eq!(versions.len(), 2);
|
||||
assert_eq!(
|
||||
versions[1].change_note.as_deref(),
|
||||
Some("add output mapping")
|
||||
);
|
||||
assert_eq!(
|
||||
versions[1]
|
||||
.snapshot
|
||||
.wizard_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.test_input,
|
||||
Some(json!({ "email": "test-v2@example.com" }))
|
||||
);
|
||||
assert_eq!(published.version, 2);
|
||||
assert_eq!(
|
||||
published.wizard_state.as_ref().unwrap().output_sample,
|
||||
Some(json!({ "id": "lead_2" }))
|
||||
);
|
||||
assert!(published.is_published());
|
||||
assert_eq!(published_list, vec![published.clone()]);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_out_of_order_versions() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_rest_02", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft);
|
||||
let error = registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &invalid,
|
||||
change_note: None,
|
||||
created_by: None,
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
RegistryError::InvalidVersionSequence {
|
||||
expected: 2,
|
||||
actual: 3,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_operation_draft_persists_optional_json_columns_as_sql_null() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let mut operation = test_operation("op_rest_02b", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
operation.generated_draft = None;
|
||||
operation.samples = None;
|
||||
operation.config_export = None;
|
||||
operation.wizard_state = None;
|
||||
operation.updated_at = timestamp("2026-03-25T12:34:00Z");
|
||||
|
||||
registry
|
||||
.update_operation_draft(&test_workspace_id(), &operation)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let stored = registry
|
||||
.get_operation_version(&test_workspace_id(), &operation.id, operation.version)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(stored.snapshot.generated_draft, None);
|
||||
assert_eq!(stored.snapshot.samples, None);
|
||||
assert_eq!(stored.snapshot.config_export, None);
|
||||
assert_eq!(stored.snapshot.wizard_state, None);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_auth_profiles_and_artifact_metadata() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_rest_03", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_profile = AuthProfile {
|
||||
id: "auth_crank".into(),
|
||||
workspace_id: test_workspace_id(),
|
||||
name: "Crank API key".to_owned(),
|
||||
kind: AuthKind::ApiKeyHeader,
|
||||
config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
|
||||
header_name: "X-Api-Key".to_owned(),
|
||||
secret_id: SecretId::new("secret_crank_api_key"),
|
||||
}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
|
||||
registry
|
||||
.save_auth_profile(SaveAuthProfileRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
profile: &auth_profile,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let input_sample = OperationSampleMetadata {
|
||||
id: "sample_input".into(),
|
||||
operation_id: operation.id.clone(),
|
||||
version: 1,
|
||||
sample_kind: SampleKind::InputJson,
|
||||
storage_ref: "file:///tmp/input.json".to_owned(),
|
||||
content_type: "application/json".to_owned(),
|
||||
file_name: Some("input.json".to_owned()),
|
||||
created_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
};
|
||||
let descriptor = DescriptorMetadata {
|
||||
id: "descriptor_01".into(),
|
||||
operation_id: Some(operation.id.clone()),
|
||||
version: Some(1),
|
||||
descriptor_kind: DescriptorKind::DescriptorSet,
|
||||
storage_ref: "file:///tmp/schema.desc".to_owned(),
|
||||
source_name: Some("schema.desc".to_owned()),
|
||||
package_index: Some(json!({ "crm.v1": ["LeadService"] })),
|
||||
created_at: timestamp("2026-03-25T12:02:00Z"),
|
||||
};
|
||||
|
||||
registry
|
||||
.save_sample_metadata(SaveSampleMetadataRequest {
|
||||
sample: &input_sample,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
|
||||
descriptor: &descriptor,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_profiles = registry
|
||||
.list_auth_profiles(&test_workspace_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let samples = registry
|
||||
.list_sample_metadata(&operation.id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let descriptors = registry
|
||||
.list_descriptor_metadata(&operation.id, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(auth_profiles, vec![auth_profile]);
|
||||
assert_eq!(samples, vec![input_sample]);
|
||||
assert_eq!(descriptors, vec![descriptor]);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lists_auth_profiles_referencing_secret() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let primary_secret_id = SecretId::new("secret_primary");
|
||||
let secondary_secret_id = SecretId::new("secret_secondary");
|
||||
let profile = AuthProfile {
|
||||
id: "auth_crank".into(),
|
||||
workspace_id: test_workspace_id(),
|
||||
name: "Crank basic auth".to_owned(),
|
||||
kind: AuthKind::Basic,
|
||||
config: AuthConfig::Basic(crank_core::BasicAuthConfig {
|
||||
username_secret_id: primary_secret_id.clone(),
|
||||
password_secret_id: secondary_secret_id.clone(),
|
||||
}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
|
||||
registry
|
||||
.save_auth_profile(SaveAuthProfileRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
profile: &profile,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let profiles = registry
|
||||
.list_auth_profiles_referencing_secret(&test_workspace_id(), &primary_secret_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profiles, vec![profile]);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::common::*;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
|
||||
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
|
||||
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
|
||||
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::json;
|
||||
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use crank_registry::{
|
||||
CreateAgentRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
||||
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
|
||||
fn test_workspace_id() -> WorkspaceId {
|
||||
WorkspaceId::new("ws_default")
|
||||
}
|
||||
|
||||
fn timestamp(value: &str) -> OffsetDateTime {
|
||||
OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_and_finishes_yaml_import_jobs() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let job_id = YamlImportJobId::new("job_yaml_01");
|
||||
let operation = test_operation("op_rest_04", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
registry
|
||||
.create_yaml_import_job(CreateYamlImportJobRequest {
|
||||
id: &job_id,
|
||||
source_sample_id: None,
|
||||
format_version: "v1",
|
||||
mode: ExportMode::Portable,
|
||||
created_at: ×tamp("2026-03-25T12:00:00Z"),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
registry
|
||||
.finish_yaml_import_job(
|
||||
&job_id,
|
||||
&YamlImportJobCompletion {
|
||||
status: YamlImportJobStatus::Completed,
|
||||
result_operation_id: Some(operation.id.clone()),
|
||||
result_version: Some(2),
|
||||
error_text: None,
|
||||
finished_at: timestamp("2026-03-25T12:05:00Z"),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let job = registry
|
||||
.get_yaml_import_job(&job_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(job.status, YamlImportJobStatus::Completed);
|
||||
assert_eq!(job.result_version, Some(2));
|
||||
assert_eq!(job.mode, ExportMode::Portable);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_workspace_read_paths() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_extra_01"),
|
||||
slug: "extra".to_owned(),
|
||||
display_name: "Extra Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({"region":"eu"}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let mut user = User {
|
||||
id: UserId::new("user_extra_01"),
|
||||
email: "owner@example.com".to_owned(),
|
||||
display_name: "Owner".to_owned(),
|
||||
status: crank_core::UserStatus::Active,
|
||||
created_at: timestamp("2026-03-25T11:00:00Z"),
|
||||
};
|
||||
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user(&user.email, &user.display_name, "hashed-password")
|
||||
.await
|
||||
.unwrap();
|
||||
user.id = user_id;
|
||||
registry
|
||||
.ensure_membership(&workspace.id, &user.id, MembershipRole::Owner)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let all_workspaces = registry.list_workspaces().await.unwrap();
|
||||
let user_workspaces = registry.list_workspaces_for_user(&user.id).await.unwrap();
|
||||
let memberships = registry.list_memberships(&workspace.id).await.unwrap();
|
||||
let loaded = registry
|
||||
.get_workspace(&workspace.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
all_workspaces
|
||||
.iter()
|
||||
.any(|record| record.workspace == workspace)
|
||||
);
|
||||
assert_eq!(user_workspaces.len(), 1);
|
||||
assert_eq!(user_workspaces[0].workspace, workspace);
|
||||
assert_eq!(user_workspaces[0].role, MembershipRole::Owner);
|
||||
assert_eq!(memberships.len(), 1);
|
||||
assert_eq!(memberships[0].workspace_id, workspace.id);
|
||||
assert_eq!(memberships[0].user.id, user.id);
|
||||
assert_eq!(memberships[0].user.email, user.email);
|
||||
assert_eq!(memberships[0].user.display_name, user.display_name);
|
||||
assert_eq!(memberships[0].user.status, user.status);
|
||||
assert_eq!(memberships[0].role, MembershipRole::Owner);
|
||||
assert_eq!(loaded, WorkspaceRecord { workspace });
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_user_profile_and_workspace_access_reads() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_profile_01"),
|
||||
slug: "profile".to_owned(),
|
||||
display_name: "Profile Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let other_workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_profile_02"),
|
||||
slug: "profile-other".to_owned(),
|
||||
display_name: "Other Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
};
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &other_workspace,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = registry
|
||||
.update_user_profile(&user_id, "updated@example.com", "Updated Owner")
|
||||
.await
|
||||
.unwrap();
|
||||
let has_access = registry
|
||||
.user_has_workspace_access(&user_id, &workspace.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let lacks_access = registry
|
||||
.user_has_workspace_access(&user_id, &other_workspace.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.id, user_id);
|
||||
assert_eq!(updated.email, "updated@example.com");
|
||||
assert_eq!(updated.display_name, "Updated Owner");
|
||||
assert!(has_access);
|
||||
assert!(!lacks_access);
|
||||
assert!(updated.created_at.unix_timestamp() > 0);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_and_loads_user_sessions_with_typed_expiration() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_session_01"),
|
||||
slug: "session".to_owned(),
|
||||
display_name: "Session Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user("session@example.com", "Owner", "hashed-password")
|
||||
.await
|
||||
.unwrap();
|
||||
let session_id = UserSessionId::new("sess_01");
|
||||
let expires_at = timestamp("2030-04-06T12:05:00Z");
|
||||
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_user_session(
|
||||
&session_id,
|
||||
&user_id,
|
||||
Some(&workspace.id),
|
||||
"secret-hash-01",
|
||||
&expires_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let session = registry
|
||||
.get_user_session(&session_id, "secret-hash-01")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(session.session_id, session_id);
|
||||
assert_eq!(session.current_workspace_id, Some(workspace.id.clone()));
|
||||
assert_eq!(session.user.id, user_id);
|
||||
assert!(session.user.created_at.unix_timestamp() > 0);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_platform_api_key_read_paths() {
|
||||
let database = TestDatabase::new().await;
|
||||
let registry = database.registry().await;
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_keys_01"),
|
||||
slug: "keys".to_owned(),
|
||||
display_name: "Keys Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let agent = test_agent("agent_keys_01", AgentStatus::Draft);
|
||||
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
||||
let api_key = PlatformApiKey {
|
||||
id: PlatformApiKeyId::new("key_01"),
|
||||
workspace_id: workspace.id.clone(),
|
||||
agent_id: Some(agent.id.clone()),
|
||||
name: "Primary".to_owned(),
|
||||
prefix: "crk_live".to_owned(),
|
||||
scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
status: PlatformApiKeyStatus::Active,
|
||||
created_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
last_used_at: None,
|
||||
};
|
||||
let secret_hash = "secret_hash_01";
|
||||
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_agent(CreateAgentRequest {
|
||||
agent: &agent,
|
||||
version: &version,
|
||||
bindings: &[],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
registry
|
||||
.create_platform_api_key(CreatePlatformApiKeyRequest {
|
||||
api_key: &api_key,
|
||||
secret_hash,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let listed = registry
|
||||
.list_platform_api_keys(&workspace.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let listed_for_agent = registry
|
||||
.list_platform_api_keys_for_agent(&workspace.id, &agent.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
listed,
|
||||
vec![PlatformApiKeyRecord {
|
||||
api_key: api_key.clone()
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
listed_for_agent,
|
||||
vec![PlatformApiKeyRecord {
|
||||
api_key: api_key.clone()
|
||||
}]
|
||||
);
|
||||
|
||||
let resolved = registry
|
||||
.get_platform_api_key_by_secret_for_agent_slug(&workspace.slug, &agent.slug, secret_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(resolved.api_key, api_key);
|
||||
|
||||
registry
|
||||
.touch_platform_api_key(
|
||||
&workspace.id,
|
||||
&PlatformApiKeyId::new("key_01"),
|
||||
×tamp("2026-03-25T12:05:00Z"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let touched = registry
|
||||
.list_platform_api_keys(&workspace.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
touched[0].api_key.last_used_at,
|
||||
Some(timestamp("2026-03-25T12:05:00Z"))
|
||||
);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
Reference in New Issue
Block a user