Skip to content

Commit e9a85fc

Browse files
committed
test: expand integration scenario coverage
1 parent fa09fd4 commit e9a85fc

9 files changed

Lines changed: 506 additions & 0 deletions

tests/common/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,19 @@ pub async fn shared_database_id(client: &Client) -> String {
173173
.expect("create_database should succeed");
174174
created.id
175175
}
176+
177+
/// Create a fresh, uniquely-named `sdkci-*` database and return its id.
178+
///
179+
/// Unlike [`shared_database_id`], this is for scenarios that exercise the
180+
/// database lifecycle itself (contexts, catalog attach/detach) and tear the
181+
/// database back down at the end. The `sdkci-` prefix keeps any leak
182+
/// identifiable to the nightly sweep if the test panics before cleanup.
183+
pub async fn create_scratch_database(client: &Client, scenario: &str) -> String {
184+
use hotdata::apis::databases_api;
185+
let mut request = hotdata::models::CreateDatabaseRequest::new();
186+
request.name = Some(Some(sdkci_name(scenario)));
187+
let created = databases_api::create_database(client.configuration(), request)
188+
.await
189+
.expect("create_database should succeed");
190+
created.id
191+
}

tests/connection_types_read.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//! Scenario: connection_types_read.
2+
//!
3+
//! Defined in www.hotdata.dev/api/test-scenarios.yaml — read-only. list_connection_types
4+
//! returns the connector catalog, and get_connection_type fetches one by name
5+
//! with its config schema. No fixtures created.
6+
7+
mod common;
8+
9+
use hotdata::apis::connection_types_api;
10+
11+
#[tokio::test]
12+
async fn connection_types_read() {
13+
let client = skip_if_no_creds!();
14+
let config = client.configuration();
15+
16+
let listing = connection_types_api::list_connection_types(config)
17+
.await
18+
.expect("list_connection_types should succeed");
19+
assert!(
20+
!listing.connection_types.is_empty(),
21+
"expected a non-empty connector catalog"
22+
);
23+
for ct in &listing.connection_types {
24+
assert!(!ct.name.is_empty(), "connection type missing a name");
25+
assert!(
26+
!ct.label.is_empty(),
27+
"connection type {} missing a label",
28+
ct.name
29+
);
30+
}
31+
32+
// Fetch one by name and confirm the detail echoes the catalog entry.
33+
let first = &listing.connection_types[0];
34+
let detail = connection_types_api::get_connection_type(config, &first.name)
35+
.await
36+
.expect("get_connection_type should succeed");
37+
assert_eq!(detail.name, first.name);
38+
assert_eq!(detail.label, first.label);
39+
40+
// A fabricated connector name must not resolve to a real type.
41+
let bogus = connection_types_api::get_connection_type(config, "sdkci-not-a-connector").await;
42+
match bogus {
43+
Err(err) => assert_eq!(
44+
common::status_of(&err),
45+
Some(404),
46+
"expected 404 for an unknown connection type, got {err:?}"
47+
),
48+
Ok(_) => panic!("get_connection_type should 404 for an unknown connector name"),
49+
}
50+
}

tests/database_catalogs_attach.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! Scenario: database_catalogs_attach.
2+
//!
3+
//! Defined in www.hotdata.dev/api/test-scenarios.yaml — create a scratch
4+
//! database, attach the seeded connection as a catalog (with an alias), confirm
5+
//! it's reachable via the database's attachments, then detach it and delete the
6+
//! database. Reversible and idempotent — never mutates the connection itself.
7+
8+
mod common;
9+
10+
use hotdata::apis::databases_api;
11+
use hotdata::models;
12+
13+
#[tokio::test]
14+
async fn database_catalogs_attach() {
15+
let (client, connection_id) = skip_if_no_connection!();
16+
let config = client.configuration();
17+
18+
let database_id = common::create_scratch_database(&client, "dbcat-attach").await;
19+
let alias = "sdkci_catalog";
20+
21+
let mut attach = models::AttachDatabaseCatalogRequest::new(connection_id.clone());
22+
attach.alias = Some(Some(alias.to_string()));
23+
databases_api::attach_database_catalog(config, &database_id, attach)
24+
.await
25+
.expect("attach_database_catalog should succeed");
26+
27+
let attached = databases_api::get_database(config, &database_id)
28+
.await
29+
.expect("get_database should succeed after attach");
30+
let found = attached
31+
.attachments
32+
.iter()
33+
.find(|a| a.connection_id == connection_id);
34+
let found = found.unwrap_or_else(|| {
35+
panic!("attached connection {connection_id} not present in database attachments")
36+
});
37+
assert_eq!(
38+
found.alias.as_ref().and_then(|a| a.as_deref()),
39+
Some(alias),
40+
"attachment alias should round-trip"
41+
);
42+
43+
databases_api::detach_database_catalog(config, &database_id, &connection_id)
44+
.await
45+
.expect("detach_database_catalog should succeed");
46+
47+
let detached = databases_api::get_database(config, &database_id)
48+
.await
49+
.expect("get_database should succeed after detach");
50+
assert!(
51+
!detached
52+
.attachments
53+
.iter()
54+
.any(|a| a.connection_id == connection_id),
55+
"connection {connection_id} should be gone from attachments after detach"
56+
);
57+
58+
// Tear down the scratch database.
59+
databases_api::delete_database(config, &database_id)
60+
.await
61+
.expect("delete_database should succeed");
62+
}

tests/database_contexts_crud.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Scenario: database_contexts_crud.
2+
//!
3+
//! Defined in www.hotdata.dev/api/test-scenarios.yaml — create a scratch
4+
//! database, upsert a named context document, read it back, confirm it appears
5+
//! in list_database_contexts, delete the context, then delete the database.
6+
//! Upserting the same name twice verifies replace-on-write.
7+
8+
mod common;
9+
10+
use hotdata::apis::{database_context_api, databases_api};
11+
use hotdata::models;
12+
13+
#[tokio::test]
14+
async fn database_contexts_crud() {
15+
let client = skip_if_no_creds!();
16+
let config = client.configuration();
17+
18+
let database_id = common::create_scratch_database(&client, "dbctx-crud").await;
19+
let context_name = "sdkci_context";
20+
21+
// Initial upsert.
22+
let initial = database_context_api::upsert_database_context(
23+
config,
24+
&database_id,
25+
models::UpsertDatabaseContextRequest::new(
26+
"first revision".to_string(),
27+
context_name.to_string(),
28+
),
29+
)
30+
.await
31+
.expect("upsert_database_context (create) should succeed");
32+
assert_eq!(initial.context.name, context_name);
33+
assert_eq!(initial.context.content, "first revision");
34+
35+
// Upsert the same name again — replace-on-write, not a duplicate.
36+
database_context_api::upsert_database_context(
37+
config,
38+
&database_id,
39+
models::UpsertDatabaseContextRequest::new(
40+
"second revision".to_string(),
41+
context_name.to_string(),
42+
),
43+
)
44+
.await
45+
.expect("upsert_database_context (replace) should succeed");
46+
47+
let fetched = database_context_api::get_database_context(config, &database_id, context_name)
48+
.await
49+
.expect("get_database_context should succeed");
50+
assert_eq!(fetched.context.name, context_name);
51+
assert_eq!(
52+
fetched.context.content, "second revision",
53+
"upsert with an existing name should replace the content"
54+
);
55+
56+
let listing = database_context_api::list_database_contexts(config, &database_id)
57+
.await
58+
.expect("list_database_contexts should succeed");
59+
let matches: Vec<_> = listing
60+
.contexts
61+
.iter()
62+
.filter(|c| c.name == context_name)
63+
.collect();
64+
assert_eq!(
65+
matches.len(),
66+
1,
67+
"expected exactly one context named {context_name} (replace, not append), got {}",
68+
matches.len()
69+
);
70+
71+
database_context_api::delete_database_context(config, &database_id, context_name)
72+
.await
73+
.expect("delete_database_context should succeed");
74+
75+
let after_delete =
76+
database_context_api::get_database_context(config, &database_id, context_name).await;
77+
match after_delete {
78+
Err(err) => assert_eq!(
79+
common::status_of(&err),
80+
Some(404),
81+
"expected 404 after context delete, got {err:?}"
82+
),
83+
Ok(_) => panic!("get_database_context should fail with 404 after delete"),
84+
}
85+
86+
// Tear down the scratch database.
87+
databases_api::delete_database(config, &database_id)
88+
.await
89+
.expect("delete_database should succeed");
90+
}

tests/embedding_providers_crud.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//! Scenario: embedding_providers_crud.
2+
//!
3+
//! Defined in www.hotdata.dev/api/test-scenarios.yaml — register a `local`
4+
//! embedding provider (no external credentials), read it, confirm it appears in
5+
//! list_embedding_providers, update it, then delete it. `provider_type=local`
6+
//! keeps the test from needing a real OpenAI key.
7+
8+
mod common;
9+
10+
use hotdata::apis::embedding_providers_api;
11+
use hotdata::models;
12+
13+
#[tokio::test]
14+
async fn embedding_providers_crud() {
15+
let client = skip_if_no_creds!();
16+
let config = client.configuration();
17+
18+
// Provider names follow the same underscore-normalized convention as secrets.
19+
let name = common::sdkci_name("embprov-crud").replace('-', "_");
20+
let renamed = format!("{name}_renamed");
21+
22+
let created = embedding_providers_api::create_embedding_provider(
23+
config,
24+
models::CreateEmbeddingProviderRequest::new(name.clone(), "local".to_string()),
25+
)
26+
.await
27+
.expect("create_embedding_provider should succeed");
28+
assert_eq!(created.name, name);
29+
assert_eq!(created.provider_type, "local");
30+
assert!(!created.id.is_empty(), "created provider must have an id");
31+
32+
let fetched = embedding_providers_api::get_embedding_provider(config, &created.id)
33+
.await
34+
.expect("get_embedding_provider should succeed");
35+
assert_eq!(fetched.id, created.id);
36+
assert_eq!(fetched.name, name);
37+
assert_eq!(fetched.provider_type, "local");
38+
39+
let listing = embedding_providers_api::list_embedding_providers(config)
40+
.await
41+
.expect("list_embedding_providers should succeed");
42+
assert!(
43+
listing
44+
.embedding_providers
45+
.iter()
46+
.any(|p| p.id == created.id),
47+
"created provider {} not present in list_embedding_providers",
48+
created.id
49+
);
50+
51+
let mut update = models::UpdateEmbeddingProviderRequest::new();
52+
update.name = Some(Some(renamed.clone()));
53+
let updated = embedding_providers_api::update_embedding_provider(config, &created.id, update)
54+
.await
55+
.expect("update_embedding_provider should succeed");
56+
assert_eq!(updated.id, created.id);
57+
assert_eq!(updated.name, renamed);
58+
59+
embedding_providers_api::delete_embedding_provider(config, &created.id)
60+
.await
61+
.expect("delete_embedding_provider should succeed");
62+
63+
let after_delete = embedding_providers_api::get_embedding_provider(config, &created.id).await;
64+
match after_delete {
65+
Err(err) => assert_eq!(
66+
common::status_of(&err),
67+
Some(404),
68+
"expected 404 after delete, got {err:?}"
69+
),
70+
Ok(_) => panic!("get_embedding_provider should fail with 404 after delete"),
71+
}
72+
}

tests/information_schema_read.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Scenario: information_schema_read.
2+
//!
3+
//! Defined in www.hotdata.dev/api/test-scenarios.yaml — read-only. The
4+
//! information_schema endpoint returns catalog/schema/table metadata visible to
5+
//! the workspace; scoped to the seeded connection, verify its tables appear.
6+
7+
mod common;
8+
9+
use hotdata::apis::information_schema_api;
10+
11+
#[tokio::test]
12+
async fn information_schema_read() {
13+
let (client, connection_id) = skip_if_no_connection!();
14+
let config = client.configuration();
15+
16+
let resp = information_schema_api::information_schema(
17+
config,
18+
Some(&connection_id), // scope to the seeded connection
19+
None, // schema
20+
None, // table
21+
Some(true), // include_columns
22+
Some(50), // limit
23+
None, // cursor
24+
)
25+
.await
26+
.expect("information_schema should succeed");
27+
28+
// `count` reflects the number of tables in this page.
29+
assert_eq!(
30+
resp.count as usize,
31+
resp.tables.len(),
32+
"count ({}) should match the returned tables ({})",
33+
resp.count,
34+
resp.tables.len()
35+
);
36+
37+
if resp.tables.is_empty() {
38+
eprintln!(
39+
"information_schema_read: seeded connection has no synced tables yet \
40+
(empty information_schema tolerated)"
41+
);
42+
return;
43+
}
44+
45+
for t in &resp.tables {
46+
assert!(!t.schema.is_empty(), "table missing a schema name");
47+
assert!(!t.table.is_empty(), "table missing a table name");
48+
assert!(
49+
!t.connection.is_empty(),
50+
"table {}.{} missing its owning connection",
51+
t.schema,
52+
t.table
53+
);
54+
// We asked for columns; synced tables should expose them.
55+
if t.synced {
56+
let columns = t.columns.as_ref().and_then(|c| c.as_ref());
57+
assert!(
58+
columns.is_some_and(|c| !c.is_empty()),
59+
"synced table {}.{} should report columns when include_columns=true",
60+
t.schema,
61+
t.table
62+
);
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)