registry: add sqlx checks for workspace and stream reads
This commit is contained in:
@@ -484,52 +484,6 @@ fn assert_immutable_fields(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_workspace_record(row: &PgRow) -> Result<WorkspaceRecord, RegistryError> {
|
||||
Ok(WorkspaceRecord {
|
||||
workspace: Workspace {
|
||||
id: WorkspaceId::new(row.try_get::<String, _>("id")?),
|
||||
slug: row.try_get("slug")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
settings: row.try_get::<Json<Value>, _>("settings_json")?.0,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_workspace_membership_record(
|
||||
row: &PgRow,
|
||||
) -> Result<WorkspaceMembershipRecord, RegistryError> {
|
||||
Ok(WorkspaceMembershipRecord {
|
||||
workspace: Workspace {
|
||||
id: WorkspaceId::new(row.try_get::<String, _>("id")?),
|
||||
slug: row.try_get("slug")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
settings: row.try_get::<Json<Value>, _>("settings_json")?.0,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
},
|
||||
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_membership_record(row: &PgRow) -> Result<MembershipRecord, RegistryError> {
|
||||
Ok(MembershipRecord {
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
user: User {
|
||||
id: UserId::new(row.try_get::<String, _>("user_id")?),
|
||||
email: row.try_get("email")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
created_at: row.try_get("user_created_at")?,
|
||||
},
|
||||
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_user(row: &PgRow) -> Result<User, RegistryError> {
|
||||
Ok(User {
|
||||
id: UserId::new(row.try_get::<String, _>("id")?),
|
||||
@@ -556,36 +510,6 @@ fn map_user_update_error(error: sqlx::Error, user_id: &UserId, email: &str) -> R
|
||||
}
|
||||
}
|
||||
|
||||
fn map_invitation_record(row: &PgRow) -> Result<InvitationRecord, RegistryError> {
|
||||
Ok(InvitationRecord {
|
||||
invitation: InvitationToken {
|
||||
id: InvitationId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
email: row.try_get("email")?,
|
||||
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
token_hash: row.try_get("token_hash")?,
|
||||
expires_at: row.try_get("expires_at")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, RegistryError> {
|
||||
Ok(PlatformApiKeyRecord {
|
||||
api_key: PlatformApiKey {
|
||||
id: PlatformApiKeyId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
name: row.try_get("name")?,
|
||||
prefix: row.try_get("prefix")?,
|
||||
scopes: deserialize_json_value(row.try_get::<Json<Value>, _>("scopes_json")?.0)?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
created_at: row.try_get("created_at")?,
|
||||
last_used_at: row.try_get("last_used_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_stream_session(row: &PgRow) -> Result<StreamSession, RegistryError> {
|
||||
Ok(StreamSession {
|
||||
id: StreamSessionId::new(row.try_get::<String, _>("id")?),
|
||||
@@ -1124,9 +1048,10 @@ mod tests {
|
||||
use crank_core::{
|
||||
ApiKeyHeaderAuthConfig, AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport,
|
||||
ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, JobStatus,
|
||||
OperationId, OperationStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId,
|
||||
StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription, ToolExample,
|
||||
WorkspaceId,
|
||||
MembershipRole, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
|
||||
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples,
|
||||
SecretId, StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription,
|
||||
ToolExample, User, UserId, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
@@ -1136,12 +1061,14 @@ mod tests {
|
||||
use crate::{
|
||||
PostgresRegistry, RegistryError,
|
||||
model::{
|
||||
AsyncJobFilter, CreateAsyncJobRequest, CreateStreamSessionRequest,
|
||||
CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
|
||||
OperationSampleMetadata, PublishRequest, RegistryOperation, SampleKind,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
StreamSessionFilter, UpdateAsyncJobStatusRequest, UpdateStreamSessionStateRequest,
|
||||
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
AsyncJobFilter, CreateAsyncJobRequest, CreatePlatformApiKeyRequest,
|
||||
CreateStreamSessionRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
|
||||
OperationSampleMetadata, PlatformApiKeyRecord, PublishRequest, RegistryOperation,
|
||||
SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, StreamSessionFilter, UpdateAsyncJobStatusRequest,
|
||||
UpdateStreamSessionStateRequest, WorkspaceRecord, YamlImportJobCompletion,
|
||||
YamlImportJobId, YamlImportJobStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1453,6 +1380,150 @@ mod tests {
|
||||
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: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
};
|
||||
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: "2026-03-25T11:00:00Z".to_owned(),
|
||||
};
|
||||
|
||||
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_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: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
};
|
||||
let api_key = PlatformApiKey {
|
||||
id: PlatformApiKeyId::new("key_01"),
|
||||
workspace_id: workspace.id.clone(),
|
||||
name: "Primary".to_owned(),
|
||||
prefix: "crk_live".to_owned(),
|
||||
scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
||||
status: PlatformApiKeyStatus::Active,
|
||||
created_at: "2026-03-25T12:01:00Z".to_owned(),
|
||||
last_used_at: None,
|
||||
};
|
||||
let secret_hash = "secret_hash_01";
|
||||
|
||||
registry
|
||||
.create_workspace(CreateWorkspaceRequest {
|
||||
workspace: &workspace,
|
||||
})
|
||||
.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();
|
||||
assert_eq!(
|
||||
listed,
|
||||
vec![PlatformApiKeyRecord {
|
||||
api_key: api_key.clone()
|
||||
}]
|
||||
);
|
||||
|
||||
let resolved = registry
|
||||
.get_platform_api_key_by_secret_for_workspace_slug(&workspace.slug, secret_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(resolved.api_key, api_key);
|
||||
|
||||
registry
|
||||
.touch_platform_api_key(
|
||||
&workspace.id,
|
||||
&PlatformApiKeyId::new("key_01"),
|
||||
"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.as_deref(),
|
||||
Some("2026-03-25T12:05:00Z")
|
||||
);
|
||||
|
||||
database.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manages_stream_sessions_with_transitions_and_cleanup() {
|
||||
let database = TestDatabase::new().await;
|
||||
|
||||
Reference in New Issue
Block a user