Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ e7fcdb1a8f1e48c564717f1ccd0fa654d182aa73

# Split client/organizations.rs into byoc, invitations, members and rbac (#1173)
9e20dd888a937f5f0b610b22a71aef4ff70b9357

# Split tests/models_test.rs into per-domain tests/models/ modules (#1173)
bb5834b93b5c9d6640c7720991aaef9f750a61ef
4 changes: 3 additions & 1 deletion crates/clickhouse-cloud-api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ from Rust.
HTTP query helper). Used by every integration binary.
- `tests/integration_test.rs`, `tests/integration_postgres_test.rs`, `tests/integration_org_test.rs` —
cloud-service, Postgres-service, and organization lifecycle.
- `tests/client_test.rs`, `tests/models_test.rs`, `tests/model_facade_test.rs`, `tests/run_query_test.rs`,
- `tests/client_test.rs`, `tests/models/`, `tests/model_facade_test.rs`, `tests/run_query_test.rs`,
`tests/service_query_key_cli_test.rs` — offline client, model, facade and query-path coverage.
`tests/models/` is the single `models_test` binary: `main.rs` declares one module per `src/models/` domain
(small domains folded into their parent) plus `cross_domain.rs`; add a model test to its domain's module.
- `tests/clickpipes/` — ClickPipes E2E, including external cloud services. Only Postgres CDC (ClickHouse and
Postgres inside ClickHouse Cloud) runs in CI; its `clickpipe_postgres_cli_cdc_test` target exercises the built
`clickhousectl` supplied through `CLICKHOUSE_CLOUD_TEST_CLICKHOUSECTL_BIN`, which
Expand Down
7 changes: 7 additions & 0 deletions crates/clickhouse-cloud-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ uuid = "1"
wiremock = "0.6"
tempfile = "3.27.0"

# The offline model tests are one binary split into per-domain modules under
# tests/models/; the explicit target keeps the `models_test` name.

[[test]]
name = "models_test"
path = "tests/models/main.rs"

# ClickPipes E2E test binaries live under tests/clickpipes/. Cargo only
# auto-discovers .rs files directly under tests/, so each one is declared here.

Expand Down
99 changes: 99 additions & 0 deletions crates/clickhouse-cloud-api/tests/models/activity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use clickhouse_cloud_api::models::*;

#[test]
fn deserialize_activity() {
let json = r#"{
"actorType": "api",
"actorId": "actor-123",
"createdAt": "2024-06-01T00:00:00Z"
}"#;
let activity: Activity = serde_json::from_str(json).unwrap();
assert_eq!(activity.actor_type, Some(ActivityActortype::Api));
}

#[test]
fn activity_ignores_extra_fields() {
let json = r#"{"actorType":"user","sourceIp":"1.2.3.4"}"#;
let a: Activity = serde_json::from_str(json).unwrap();
assert_eq!(a.actor_type, Some(ActivityActortype::User));
}

#[test]
fn activity_type_new_wire_values_deserialize_to_typed_variants() {
let cases = [
(
"organization_member_update_roles",
ActivityType::Organization_member_update_roles,
),
(
"organization_saml_connection_create",
ActivityType::Organization_saml_connection_create,
),
(
"organization_saml_connection_update",
ActivityType::Organization_saml_connection_update,
),
(
"service_update_snapshot_configuration",
ActivityType::Service_update_snapshot_configuration,
),
("backup_bucket_create", ActivityType::Backup_bucket_create),
("role_update", ActivityType::Role_update),
("service_mcp_enabled", ActivityType::Service_mcp_enabled),
("udf_create", ActivityType::Udf_create),
];
for (wire, expected) in cases {
let parsed: ActivityType = serde_json::from_str(&format!("\"{wire}\"")).unwrap();
assert_eq!(parsed, expected, "{wire} should not be Unknown");
assert_eq!(parsed.to_string(), wire);
assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
}
}

#[test]
fn activity_type_approved_domain_and_org_update_values_round_trip() {
let cases = [
(
"organization_approved_domain_auto_invite",
ActivityType::Organization_approved_domain_auto_invite,
),
(
"organization_approved_domain_auto_join",
ActivityType::Organization_approved_domain_auto_join,
),
(
"organization_approved_domain_delete",
ActivityType::Organization_approved_domain_delete,
),
(
"organization_approved_domain_update",
ActivityType::Organization_approved_domain_update,
),
(
"organization_approved_domain_verify",
ActivityType::Organization_approved_domain_verify,
),
(
"organization_update_public_preview",
ActivityType::Organization_update_public_preview,
),
(
"organization_update_saml_query_ownership_migration",
ActivityType::Organization_update_saml_query_ownership_migration,
),
];
for (wire, expected) in cases {
let parsed: ActivityType = serde_json::from_str(&format!("\"{wire}\"")).unwrap();
assert_eq!(parsed, expected, "{wire} should not be Unknown");
assert_eq!(parsed.to_string(), wire);
assert_eq!(serde_json::to_value(&parsed).unwrap(), wire);
}
}

#[test]
fn activity_type_postgres_action_round_trips() {
let parsed: ActivityType = serde_json::from_str("\"postgres_action\"").unwrap();
assert_eq!(parsed, ActivityType::Postgres_action);
assert_eq!(parsed.to_string(), "postgres_action");
assert_eq!(serde_json::to_value(&parsed).unwrap(), "postgres_action");
}
114 changes: 114 additions & 0 deletions crates/clickhouse-cloud-api/tests/models/api_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use clickhouse_cloud_api::models::*;

#[test]
fn deserialize_api_key() {
let json = r#"{
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"name": "My API Key",
"state": "enabled",
"roles": ["admin"],
"createdAt": "2024-01-01T00:00:00Z",
"expireAt": "2025-01-01T00:00:00Z"
}"#;
let key: ApiKey = serde_json::from_str(json).unwrap();
assert_eq!(key.name.as_deref(), Some("My API Key"));
assert_eq!(key.state, Some(ApiKeyState::Enabled));
}

#[test]
fn serialize_api_key_post_request() {
let req = ApiKeyPostRequest {
name: "test-key".to_string(),
..Default::default()
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["name"], "test-key");
}

#[test]
fn api_key_ignores_extra_fields() {
let json = r#"{"name":"key","state":"enabled","rotationPolicy":"weekly"}"#;
let key: ApiKey = serde_json::from_str(json).unwrap();
assert_eq!(key.name.as_deref(), Some("key"));
assert_eq!(key.state, Some(ApiKeyState::Enabled));
}

#[test]
fn api_key_minimal_response() {
let k: ApiKey = serde_json::from_str(r#"{"name":"k"}"#).unwrap();
assert_eq!(k.name.as_deref(), Some("k"));
assert_eq!(k.id, None);
assert_eq!(k.state, None);
}

#[test]
fn api_key_patch_expiry_preserves_omit_set_and_clear() {
let timestamp = chrono::DateTime::parse_from_rfc3339("2030-01-02T03:04:05Z")
.unwrap()
.with_timezone(&chrono::Utc);
for (expire_at, wire) in [
(None, serde_json::json!({})),
(
Some(Some(timestamp)),
serde_json::json!({"expireAt": "2030-01-02T03:04:05Z"}),
),
(Some(None), serde_json::json!({"expireAt": null})),
] {
let request = ApiKeyPatchRequest {
expire_at,
..Default::default()
};
assert_eq!(serde_json::to_value(&request).unwrap(), wire);
assert_eq!(
serde_json::from_value::<ApiKeyPatchRequest>(wire).unwrap(),
request
);
}
}

#[test]
fn api_key_patch_deserialization_preserves_other_fields_and_ignores_unknowns() {
let wire = serde_json::json!({
"expireAt": null,
"assignedRoleIds": ["aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"],
"ipAccessList": [{"source": "10.0.0.0/8", "description": "private"}],
"name": "renamed",
"state": "disabled"
});
let mut extended = wire.clone();
extended["newServerField"] = serde_json::json!({"nested": true});
let request: ApiKeyPatchRequest = serde_json::from_value(extended).unwrap();
assert_eq!(serde_json::to_value(request).unwrap(), wire);
let nulls: ApiKeyPatchRequest = serde_json::from_value(serde_json::json!({
"assignedRoleIds": null, "ipAccessList": null, "name": null, "state": null
}))
.unwrap();
assert_eq!(nulls, ApiKeyPatchRequest::default());
}

#[test]
fn api_key_patch_deserialization_rejects_invalid_recognized_fields() {
for wire in [
serde_json::json!({"expireAt": "tomorrow"}),
serde_json::json!({"expireAt": 42}),
serde_json::json!({"assignedRoleIds": ["invalid"]}),
serde_json::json!({"ipAccessList": [{}]}),
serde_json::json!({"name": false}),
serde_json::json!({"state": {}}),
serde_json::json!(null),
serde_json::json!([]),
] {
assert!(
serde_json::from_value::<ApiKeyPatchRequest>(wire.clone()).is_err(),
"accepted {wire}"
);
}
}

#[cfg(feature = "deprecated-fields")]
#[test]
fn api_key_patch_deserialization_preserves_deprecated_roles() {
let wire = serde_json::json!({"expireAt": null, "roles": ["admin"]});
let request: ApiKeyPatchRequest = serde_json::from_value(wire.clone()).unwrap();
assert_eq!(serde_json::to_value(request).unwrap(), wire);
}
Loading
Loading