308 lines
9.7 KiB
Rust
308 lines
9.7 KiB
Rust
#![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;
|
|
}
|