Skip to content

Commit 3fc2cdd

Browse files
authored
test(integration): cover category-A scenarios from #207 (#99)
* test(integration): cover category-A scenarios from #207 * docs(test): align embedding scenario note with corrected contract
1 parent 1cb6338 commit 3fc2cdd

11 files changed

Lines changed: 558 additions & 0 deletions

‎tests/integration/conftest.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,21 @@
1414
import pytest
1515

1616
from hotdata import ApiClient, Configuration
17+
from hotdata.api.connection_types_api import ConnectionTypesApi
1718
from hotdata.api.connections_api import ConnectionsApi
19+
from hotdata.api.database_context_api import DatabaseContextApi
1820
from hotdata.api.databases_api import DatabasesApi
1921
from hotdata.api.datasets_api import DatasetsApi
22+
from hotdata.api.embedding_providers_api import EmbeddingProvidersApi
2023
from hotdata.api.indexes_api import IndexesApi
24+
from hotdata.api.information_schema_api import InformationSchemaApi
25+
from hotdata.api.jobs_api import JobsApi
26+
from hotdata.api.refresh_api import RefreshApi
2127
from hotdata.api.saved_queries_api import SavedQueriesApi
2228
from hotdata.api.secrets_api import SecretsApi
29+
from hotdata.api.uploads_api import UploadsApi
2330
from hotdata.api.workspaces_api import WorkspacesApi
31+
from hotdata.exceptions import ApiException
2432
from hotdata.models.create_database_request import CreateDatabaseRequest
2533

2634

@@ -152,3 +160,60 @@ def saved_queries_api(api_client: ApiClient) -> SavedQueriesApi:
152160
@pytest.fixture
153161
def secrets_api(api_client: ApiClient) -> SecretsApi:
154162
return SecretsApi(api_client)
163+
164+
165+
@pytest.fixture
166+
def database_context_api(api_client: ApiClient) -> DatabaseContextApi:
167+
return DatabaseContextApi(api_client)
168+
169+
170+
@pytest.fixture
171+
def embedding_providers_api(api_client: ApiClient) -> EmbeddingProvidersApi:
172+
return EmbeddingProvidersApi(api_client)
173+
174+
175+
@pytest.fixture
176+
def connection_types_api(api_client: ApiClient) -> ConnectionTypesApi:
177+
return ConnectionTypesApi(api_client)
178+
179+
180+
@pytest.fixture
181+
def jobs_api(api_client: ApiClient) -> JobsApi:
182+
return JobsApi(api_client)
183+
184+
185+
@pytest.fixture
186+
def information_schema_api(api_client: ApiClient) -> InformationSchemaApi:
187+
return InformationSchemaApi(api_client)
188+
189+
190+
@pytest.fixture
191+
def uploads_api(api_client: ApiClient) -> UploadsApi:
192+
return UploadsApi(api_client)
193+
194+
195+
@pytest.fixture
196+
def refresh_api(api_client: ApiClient) -> RefreshApi:
197+
return RefreshApi(api_client)
198+
199+
200+
@pytest.fixture
201+
def scratch_database(databases_api: DatabasesApi, sdkci_name) -> Iterator[str]:
202+
"""Yields the id of a fresh, isolated database, deleting it on teardown.
203+
204+
Unlike the session-scoped `database_id` (the shared `sdkci-shared` db reused
205+
across runs), scenarios that declare schemas/tables/contexts or attach
206+
catalogs need their own throwaway database so they never touch seeded data
207+
or collide with a parallel run. `expires_at` is a safety net: if teardown is
208+
interrupted, the server reclaims the database rather than leaking it.
209+
"""
210+
created = databases_api.create_database(
211+
CreateDatabaseRequest(name=sdkci_name("scratch"), expires_at="2h")
212+
)
213+
try:
214+
yield created.id
215+
finally:
216+
try:
217+
databases_api.delete_database(created.id)
218+
except ApiException:
219+
pass
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Scenario: connection_types_read.
2+
3+
Read-only: list_connection_types returns the available connector catalog, and
4+
get_connection_type fetches one by name with its config schema. Mutates nothing.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from hotdata.api.connection_types_api import ConnectionTypesApi
10+
11+
12+
def test_connection_types_read(connection_types_api: ConnectionTypesApi) -> None:
13+
listing = connection_types_api.list_connection_types()
14+
assert listing.connection_types, "connector catalog is unexpectedly empty"
15+
for ct in listing.connection_types:
16+
assert ct.name
17+
assert ct.label
18+
19+
# Fetch one by name; prefer postgres if present, else the first entry.
20+
names = [ct.name for ct in listing.connection_types]
21+
target = "postgres" if "postgres" in names else names[0]
22+
23+
detail = connection_types_api.get_connection_type(target)
24+
assert detail.name == target
25+
assert detail.label
26+
# Each connector advertises a config schema clients use to build a request.
27+
assert detail.config_schema is not None
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Scenario: database_catalogs_attach.
2+
3+
Attach the seeded connection to a fresh scratch database as a catalog (under an
4+
alias), confirm it's reachable via get_database, then detach it. Reversible and
5+
idempotent — it never mutates the connection itself, only the scratch
6+
database's attachment list (which is torn down with the database).
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from hotdata.api.databases_api import DatabasesApi
12+
from hotdata.models.attach_database_catalog_request import (
13+
AttachDatabaseCatalogRequest,
14+
)
15+
16+
17+
def test_database_catalogs_attach(
18+
databases_api: DatabasesApi, scratch_database: str, connection_id: str
19+
) -> None:
20+
db_id = scratch_database
21+
alias = "sdkci_cat"
22+
23+
databases_api.attach_database_catalog(
24+
db_id,
25+
AttachDatabaseCatalogRequest(connection_id=connection_id, alias=alias),
26+
)
27+
28+
detail = databases_api.get_database(db_id)
29+
attached = [a for a in detail.attachments if a.connection_id == connection_id]
30+
assert attached, (
31+
f"connection {connection_id} not in attachments after attach"
32+
)
33+
assert any(a.alias == alias for a in attached), (
34+
f"alias {alias!r} not reflected in attachment list"
35+
)
36+
37+
databases_api.detach_database_catalog(db_id, connection_id)
38+
39+
detail_after = databases_api.get_database(db_id)
40+
assert all(a.connection_id != connection_id for a in detail_after.attachments), (
41+
f"connection {connection_id} still attached after detach"
42+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Scenario: database_contexts_crud.
2+
3+
Against a fresh scratch database: upsert a named context document, read it back,
4+
confirm it appears in list_database_contexts, upsert the same name again to
5+
verify replace-on-write, delete the context, and confirm it's gone. The
6+
`scratch_database` fixture creates and tears down the owning database.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import pytest
12+
13+
from hotdata.api.database_context_api import DatabaseContextApi
14+
from hotdata.exceptions import ApiException
15+
from hotdata.models.upsert_database_context_request import (
16+
UpsertDatabaseContextRequest,
17+
)
18+
19+
20+
def test_database_contexts_crud(
21+
database_context_api: DatabaseContextApi, scratch_database: str, sdkci_name
22+
) -> None:
23+
db_id = scratch_database
24+
# Context keys follow dataset table-name rules (letter/underscore first).
25+
name = "sdkci_" + sdkci_name("ctx").replace("-", "_")
26+
initial = "First revision of the context document."
27+
replaced = "Second revision — replace-on-write."
28+
29+
upserted = database_context_api.upsert_database_context(
30+
db_id, UpsertDatabaseContextRequest(name=name, content=initial)
31+
)
32+
assert upserted.context.name == name
33+
assert upserted.context.content == initial
34+
35+
got = database_context_api.get_database_context(db_id, name)
36+
assert got.context.name == name
37+
assert got.context.content == initial
38+
39+
listing = database_context_api.list_database_contexts(db_id)
40+
assert any(c.name == name for c in listing.contexts), (
41+
f"context {name} not in list_database_contexts"
42+
)
43+
44+
# Upsert reuses the same name: content is replaced, not appended.
45+
database_context_api.upsert_database_context(
46+
db_id, UpsertDatabaseContextRequest(name=name, content=replaced)
47+
)
48+
got2 = database_context_api.get_database_context(db_id, name)
49+
assert got2.context.content == replaced
50+
51+
database_context_api.delete_database_context(db_id, name)
52+
53+
with pytest.raises(ApiException) as excinfo:
54+
database_context_api.get_database_context(db_id, name)
55+
assert excinfo.value.status == 404
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Scenario: databases_lifecycle.
2+
3+
Create a database (a metadata-only grouping that auto-provisions a default
4+
catalog), read it back, confirm it appears in list_databases, declare a schema
5+
and a table on its default catalog, then delete it and verify it's gone.
6+
7+
Self-cleaning: the database is created with a short `expires_at` and deleted in
8+
a finally block, so a failed assertion never leaks a database into prod.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import pytest
14+
15+
from hotdata.api.databases_api import DatabasesApi
16+
from hotdata.exceptions import ApiException
17+
from hotdata.models.add_managed_schema_request import AddManagedSchemaRequest
18+
from hotdata.models.add_managed_table_request import AddManagedTableRequest
19+
from hotdata.models.create_database_request import CreateDatabaseRequest
20+
21+
22+
def test_databases_lifecycle(databases_api: DatabasesApi, sdkci_name) -> None:
23+
name = sdkci_name("databases-lifecycle")
24+
# Schema/table identifiers must be SQL identifiers (no dashes).
25+
schema_name = "sdkci_schema"
26+
table_name = "sdkci_table"
27+
db_id: str | None = None
28+
29+
try:
30+
created = databases_api.create_database(
31+
CreateDatabaseRequest(name=name, expires_at="2h")
32+
)
33+
db_id = created.id
34+
assert created.id
35+
assert created.name == name
36+
assert created.default_connection_id, (
37+
"create_database must expose the auto-provisioned default catalog connection"
38+
)
39+
40+
detail = databases_api.get_database(db_id)
41+
assert detail.id == db_id
42+
assert detail.name == name
43+
assert detail.default_connection_id == created.default_connection_id
44+
45+
listing = databases_api.list_databases()
46+
assert any(d.id == db_id for d in listing.databases), (
47+
f"created database {db_id} not in list_databases"
48+
)
49+
50+
schema_resp = databases_api.add_database_schema(
51+
db_id, AddManagedSchemaRequest(name=schema_name)
52+
)
53+
assert schema_resp.var_schema == schema_name
54+
55+
table_resp = databases_api.add_database_table(
56+
db_id, schema_name, AddManagedTableRequest(name=table_name)
57+
)
58+
assert table_resp.var_schema == schema_name
59+
assert table_resp.table == table_name
60+
61+
databases_api.delete_database(db_id)
62+
db_id = None
63+
64+
with pytest.raises(ApiException) as excinfo:
65+
databases_api.get_database(created.id)
66+
assert excinfo.value.status == 404
67+
finally:
68+
if db_id is not None:
69+
try:
70+
databases_api.delete_database(db_id)
71+
except ApiException:
72+
pass
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Scenario: embedding_providers_crud.
2+
3+
Register an embedding provider, read it, confirm it appears in
4+
list_embedding_providers, update it, then delete it.
5+
6+
The scenario calls for a `service` provider with **no** api_key/secret_name
7+
(not `local` — the runtime currently rejects `local` as "not yet supported").
8+
A service provider's key is only consulted when embeddings are actually
9+
generated (indexing) — never at create/get/update — so this exercises the full
10+
CRUD surface without any real external credential and without auto-creating a
11+
secret that would need cleanup.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import pytest
17+
18+
from hotdata.api.embedding_providers_api import EmbeddingProvidersApi
19+
from hotdata.exceptions import ApiException
20+
from hotdata.models.create_embedding_provider_request import (
21+
CreateEmbeddingProviderRequest,
22+
)
23+
from hotdata.models.update_embedding_provider_request import (
24+
UpdateEmbeddingProviderRequest,
25+
)
26+
27+
28+
def test_embedding_providers_crud(
29+
embedding_providers_api: EmbeddingProvidersApi, sdkci_name
30+
) -> None:
31+
name = sdkci_name("embprov")
32+
updated_name = sdkci_name("embprov-updated")
33+
provider_id: str | None = None
34+
35+
try:
36+
created = embedding_providers_api.create_embedding_provider(
37+
CreateEmbeddingProviderRequest(
38+
name=name,
39+
provider_type="service",
40+
config={"model": "text-embedding-3-small"},
41+
)
42+
)
43+
provider_id = created.id
44+
assert created.id
45+
assert created.name == name
46+
assert created.provider_type == "service"
47+
48+
got = embedding_providers_api.get_embedding_provider(provider_id)
49+
assert got.id == provider_id
50+
assert got.name == name
51+
52+
listing = embedding_providers_api.list_embedding_providers()
53+
assert any(p.id == provider_id for p in listing.embedding_providers), (
54+
f"created provider {provider_id} not in list_embedding_providers"
55+
)
56+
57+
updated = embedding_providers_api.update_embedding_provider(
58+
provider_id, UpdateEmbeddingProviderRequest(name=updated_name)
59+
)
60+
assert updated.id == provider_id
61+
assert updated.name == updated_name
62+
63+
# Read-after-update reflects the rename.
64+
assert embedding_providers_api.get_embedding_provider(provider_id).name == (
65+
updated_name
66+
)
67+
68+
embedding_providers_api.delete_embedding_provider(provider_id)
69+
provider_id = None
70+
71+
with pytest.raises(ApiException) as excinfo:
72+
embedding_providers_api.get_embedding_provider(created.id)
73+
assert excinfo.value.status == 404
74+
finally:
75+
if provider_id is not None:
76+
try:
77+
embedding_providers_api.delete_embedding_provider(provider_id)
78+
except ApiException:
79+
pass
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Scenario: information_schema_read.
2+
3+
Read-only: information_schema returns the catalog/schema/table metadata visible
4+
to the workspace. Verify the seeded connection's tables surface when filtered by
5+
connection id, including column definitions.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from hotdata.api.information_schema_api import InformationSchemaApi
11+
12+
13+
def test_information_schema_read(
14+
information_schema_api: InformationSchemaApi, connection_id: str
15+
) -> None:
16+
resp = information_schema_api.information_schema(
17+
connection_id=connection_id, include_columns=True
18+
)
19+
# Response is well-formed: count matches the page, has_more drives paging.
20+
assert isinstance(resp.tables, list)
21+
assert resp.count == len(resp.tables)
22+
assert isinstance(resp.has_more, bool)
23+
24+
assert resp.tables, (
25+
f"seeded connection {connection_id} exposed no tables in information_schema"
26+
)
27+
for table in resp.tables:
28+
assert table.table
29+
assert table.var_schema
30+
# include_columns=True must populate column definitions.
31+
assert table.columns is not None

0 commit comments

Comments
 (0)