Skip to content

Commit 069f3df

Browse files
authored
test(connector-factory): stop TestValidateConfig depending on asyncpg (#168)
Two TestValidateConfig cases used "postgres" as the example connector and failed in the default `uv run --extra dev pytest` environment (no asyncpg installed): load_builtin() silently skips registering postgres when its optional SDK is missing, so get_plugin_cls("postgres") returned None and validate_config no-opped instead of validating -- "DID NOT RAISE" was a test-environment gap, not a real bug. Verified by installing `--extra pg`: all 8 cases pass against the real PostgresConfig schema. Registers a fake plugin with its own CONFIG_SCHEMA (mirroring PostgresConfig's dsn/max_read_rows shape) so the class exercises schema enforcement unconditionally, the same way the existing _FakePlugin already does for resolve_target. postgres's own CONFIG_SCHEMA is untouched and still enforced in production. Signed-off-by: Cheney Zhang <chen.zhang@zilliz.com>
1 parent e9b1431 commit 069f3df

1 file changed

Lines changed: 48 additions & 11 deletions

File tree

server/python/tests/test_connector_factory.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
import os
1414
from pathlib import Path
15+
from typing import Optional
1516

1617
import pytest
1718

1819
from mfs_server.config import ServerConfig
19-
from mfs_server.connectors.base import ConnectorPlugin, ObjectConfig
20+
from mfs_server.connectors.base import ConnectorConfigSchema, ConnectorPlugin, ObjectConfig
2021
from mfs_server.connectors.file.plugin import FilePlugin
2122
from mfs_server.connectors.github.plugin import GitHubPlugin
2223
from mfs_server.connectors.registry import load_builtin, register
@@ -72,6 +73,31 @@ def preset_for(self, path: str): # noqa: ARG002
7273

7374
register(_FakePlugin)
7475

76+
77+
# A fake plugin carrying a real CONFIG_SCHEMA, for TestValidateConfig. Mirrors
78+
# PostgresConfig's field shape (dsn / max_read_rows) but registers unconditionally --
79+
# unlike postgres, mysql, etc., whose registration is gated behind an optional SDK
80+
# (load_builtin() silently skips them when e.g. asyncpg isn't installed). Exercising
81+
# schema enforcement against a real optional-SDK connector made these tests pass or
82+
# fail depending on which extras happened to be installed, rather than on the
83+
# behavior under test.
84+
class _ValidateConfigSchema(ConnectorConfigSchema):
85+
dsn: Optional[str] = None
86+
max_read_rows: int = 100000
87+
88+
89+
class _FakeSchemaPlugin:
90+
URI_SCHEME = "mfs-fake-validate-config"
91+
CONFIG_SCHEMA = _ValidateConfigSchema
92+
93+
def __init__(self, config, credential, ctx=None):
94+
self.config = config
95+
self.credential = credential
96+
self.ctx = ctx
97+
98+
99+
register(_FakeSchemaPlugin)
100+
75101
# ConnectorFactory.resolve_target dispatches via registry.get_plugin_cls, so the
76102
# built-in connectors (file/github at minimum) must be registered before the
77103
# resolve_target tests run. load_builtin() skips connectors whose optional SDK
@@ -306,37 +332,48 @@ def test_empty_values_accepted(self):
306332

307333

308334
class TestValidateConfig:
335+
# Uses the fake "mfs-fake-validate-config" scheme registered above, not a real
336+
# connector like postgres -- postgres's own registration is gated behind the
337+
# optional asyncpg SDK (load_builtin() skips it silently when absent), so
338+
# exercising schema enforcement through it made these tests pass or fail
339+
# depending on which extras happened to be installed rather than on the
340+
# behavior under test. The fake schema mirrors PostgresConfig's field shape
341+
# (dsn / max_read_rows) so the cases below still cover the same 3 failure
342+
# shapes; postgres's real CONFIG_SCHEMA is unaffected and still enforced in
343+
# production, just not re-exercised here.
344+
CTYPE = "mfs-fake-validate-config"
345+
309346
def test_valid_config_passes(self, factory: ConnectorFactory):
310-
factory.validate_config("postgres", {"dsn": "env:PG_DSN", "max_read_rows": 500})
347+
factory.validate_config(self.CTYPE, {"dsn": "env:PG_DSN", "max_read_rows": 500})
311348

312349
def test_numeric_field_as_string_is_coerced_not_rejected(self, factory: ConnectorFactory):
313350
# pydantic's default (lax) mode coerces an unambiguous numeric string to
314351
# int at the schema boundary -- the original bug wasn't that "100" is an
315352
# invalid max_read_rows, it's that nothing coerced it before an
316353
# internal `>` comparison crashed on a real string. This must NOT raise.
317-
factory.validate_config("postgres", {"max_read_rows": "100"})
354+
factory.validate_config(self.CTYPE, {"max_read_rows": "100"})
318355

319356
def test_wrong_type_with_no_coercion_path_is_rejected(self, factory: ConnectorFactory):
320357
# A bool has no sensible coercion to the str `dsn` expects -- unlike the
321358
# numeric-string case above, this should be a clean, named rejection
322-
# instead of surfacing 'bool' object has no attribute 'decode' deep in
323-
# asyncpg/urlparse.
359+
# instead of a raw exception surfacing deep in the plugin's own
360+
# connect()/read path.
324361
with pytest.raises(ValueError, match="config_invalid.*dsn"):
325-
factory.validate_config("postgres", {"dsn": True})
362+
factory.validate_config(self.CTYPE, {"dsn": True})
326363

327364
def test_unrecognized_field_is_rejected(self, factory: ConnectorFactory):
328365
with pytest.raises(ValueError, match="config_invalid.*bogus_field"):
329-
factory.validate_config("postgres", {"dsn": "env:PG_DSN", "bogus_field": "nonsense"})
366+
factory.validate_config(self.CTYPE, {"dsn": "env:PG_DSN", "bogus_field": "nonsense"})
330367

331368
def test_credential_ref_and_legacy_alias_and_objects_are_allowed(
332369
self, factory: ConnectorFactory
333370
):
334371
# Cross-cutting fields every connector config may carry regardless of
335372
# type -- declared once on ConnectorConfigSchema, not per-connector.
336-
factory.validate_config("postgres", {"dsn": "env:PG_DSN", "credential_ref": "env:X"})
337-
factory.validate_config("postgres", {"dsn": "env:PG_DSN", "_credential_ref": "env:X"})
373+
factory.validate_config(self.CTYPE, {"dsn": "env:PG_DSN", "credential_ref": "env:X"})
374+
factory.validate_config(self.CTYPE, {"dsn": "env:PG_DSN", "_credential_ref": "env:X"})
338375
factory.validate_config(
339-
"postgres", {"dsn": "env:PG_DSN", "objects": [{"match": "*.csv", "priority": 1}]}
376+
self.CTYPE, {"dsn": "env:PG_DSN", "objects": [{"match": "*.csv", "priority": 1}]}
340377
)
341378

342379
def test_connector_without_config_schema_is_unaffected(self, factory: ConnectorFactory):
@@ -347,7 +384,7 @@ def test_connector_without_config_schema_is_unaffected(self, factory: ConnectorF
347384
factory.validate_config("file", {"root": "/tmp/x", "objects": [{"match": "*"}]})
348385

349386
def test_non_dict_config_is_a_noop(self, factory: ConnectorFactory):
350-
factory.validate_config("postgres", None)
387+
factory.validate_config(self.CTYPE, None)
351388

352389
def test_unknown_ctype_is_a_noop(self, factory: ConnectorFactory):
353390
factory.validate_config("no-such-connector-type", {"anything": 1})

0 commit comments

Comments
 (0)