@@ -201,26 +201,17 @@ def test_07_sqlpassword_uid_pwd(self):
201201 ctx ["authentication" ] = "SqlPassword"
202202 _connect_ok (ctx )
203203
204- # #8 — ADPassword + UID + PWD → AAD Password
205- # (will fail against local SQL Server — that's fine, validates pipeline)
204+ # #8 — ADPassword + UID + PWD → blocked (not supported by mssql-py-core)
206205 def test_08_ad_password_uid_pwd (self ):
207206 ctx = _base_ctx ()
208207 ctx ["authentication" ] = "ActiveDirectoryPassword"
209- try :
210- _connect_ok (ctx )
211- except RuntimeError as e :
212- assert "Unsupported Authentication" not in str (e )
213- assert "Both User and Password" not in str (e )
208+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
214209
215- # #9 — ADIntegrated, no creds
210+ # #9 — ADIntegrated, no creds → blocked (not supported by mssql-py-core)
216211 def test_09_ad_integrated_alone (self ):
217212 ctx = _bare_ctx ()
218213 ctx ["authentication" ] = "ActiveDirectoryIntegrated"
219- try :
220- _connect_ok (ctx )
221- except RuntimeError as e :
222- assert "Unsupported Authentication" not in str (e )
223- assert "User or Password" not in str (e )
214+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
224215
225216 # #10 — ADInteractive + UID (as hint)
226217 def test_10_ad_interactive_with_hint (self ):
@@ -354,39 +345,27 @@ def test_20_admsi_system_clears_pwd(self):
354345 except RuntimeError as e :
355346 assert "Both User and Password" not in str (e )
356347
357- # #21 — ADIntegrated + UID → UID silently cleared (ODBC dialog behavior )
348+ # #21 — ADIntegrated + UID → blocked (not supported by mssql-py-core )
358349 def test_21_ad_integrated_clears_uid (self ):
359350 ctx = _bare_ctx ()
360351 ctx ["authentication" ] = "ActiveDirectoryIntegrated"
361352 ctx ["user_name" ] = "user@domain.com"
362- try :
363- _connect_ok (ctx )
364- except RuntimeError as e :
365- assert "ActiveDirectoryIntegrated" not in str (e )
366- assert "User or Password" not in str (e )
353+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
367354
368- # #22 — ADIntegrated + PWD → PWD silently cleared
355+ # #22 — ADIntegrated + PWD → blocked (not supported by mssql-py-core)
369356 def test_22_ad_integrated_clears_pwd (self ):
370357 ctx = _bare_ctx ()
371358 ctx ["authentication" ] = "ActiveDirectoryIntegrated"
372359 ctx ["password" ] = "secret"
373- try :
374- _connect_ok (ctx )
375- except RuntimeError as e :
376- assert "ActiveDirectoryIntegrated" not in str (e )
377- assert "User or Password" not in str (e )
360+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
378361
379- # #23 — ADIntegrated + UID + PWD → both silently cleared
362+ # #23 — ADIntegrated + UID + PWD → blocked (not supported by mssql-py-core)
380363 def test_23_ad_integrated_clears_both (self ):
381364 ctx = _bare_ctx ()
382365 ctx ["authentication" ] = "ActiveDirectoryIntegrated"
383366 ctx ["user_name" ] = "user@domain.com"
384367 ctx ["password" ] = "secret"
385- try :
386- _connect_ok (ctx )
387- except RuntimeError as e :
388- assert "ActiveDirectoryIntegrated" not in str (e )
389- assert "User or Password" not in str (e )
368+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
390369
391370
392371# ═══════════════════════════════════════════════════════════════════════════
@@ -521,7 +500,13 @@ def test_38_adspa_pwd_only(self):
521500# ═══════════════════════════════════════════════════════════════════════════
522501
523502class TestAccessTokenClashes :
524- """ODBC conflict matrix §3.5: rows #39–#43."""
503+ """ODBC conflict matrix §3.5: rows #39–#43.
504+
505+ validate_auth enforces strict ODBC-parity: access_token must be
506+ the sole credential — any auth keyword, UID, or PWD is a conflict.
507+ The Python layer (cursor.py) is responsible for stripping the
508+ authentication keyword after acquiring a token, before calling py-core.
509+ """
525510
526511 # #39 — Access Token + TC=Yes
527512 def test_39_token_tc (self ):
@@ -671,10 +656,7 @@ def test_case_insensitive_sqlpassword(self):
671656 def test_case_insensitive_ad_password (self ):
672657 ctx = _base_ctx ()
673658 ctx ["authentication" ] = "activedirectorypassword"
674- try :
675- _connect_ok (ctx )
676- except RuntimeError as e :
677- assert "Unsupported Authentication" not in str (e )
659+ _expect_validation_error (ctx , "not currently supported by mssql-py-core" )
678660
679661 # Bogus auth keyword
680662 def test_bogus_auth_keyword_rejected (self ):
@@ -754,7 +736,11 @@ def test_ad_managed_identity_tc(self):
754736# ═══════════════════════════════════════════════════════════════════════════
755737
756738class TestAccessTokenClashesExtended :
757- """Access Token conflicts with each AD auth keyword."""
739+ """Access Token conflicts with each AD auth keyword.
740+
741+ validate_auth rejects access_token combined with any other credential.
742+ Python's cursor.py must strip stale fields before calling py-core.
743+ """
758744
759745 def test_token_ad_password (self ):
760746 ctx = _bare_ctx ()
@@ -790,3 +776,49 @@ def test_token_uid_pwd_combined(self):
790776 ctx = _base_ctx ()
791777 ctx ["access_token" ] = "fake-jwt"
792778 _expect_validation_error (ctx , "Access Token" )
779+
780+
781+ # ═══════════════════════════════════════════════════════════════════════════
782+ # Bulkcopy Entra ID: validator enforces clean token-only input
783+ # ═══════════════════════════════════════════════════════════════════════════
784+
785+ class TestBulkcopyTokenValidation :
786+ """Documents the contract between cursor.py and py-core for bulkcopy auth.
787+
788+ When Python acquires a token via azure-identity, it MUST strip the
789+ authentication keyword, user_name, and password before passing the dict
790+ to PyCoreConnection. validate_auth enforces this — access_token must
791+ arrive alone (except server/TLS params).
792+
793+ These tests verify the validator correctly rejects stale fields,
794+ ensuring the Python layer cleans up properly.
795+ """
796+
797+ def test_token_plus_ad_default_rejected (self ):
798+ """cursor.py must strip authentication before calling py-core."""
799+ ctx = _bare_ctx ()
800+ ctx ["authentication" ] = "ActiveDirectoryDefault"
801+ ctx ["access_token" ] = "fake-jwt-default"
802+ _expect_validation_error (ctx , "Access Token" )
803+
804+ def test_token_plus_login_hint_rejected (self ):
805+ """cursor.py must strip user_name (login hint) when token is set."""
806+ ctx = _bare_ctx ()
807+ ctx ["access_token" ] = "fake-jwt-interactive"
808+ ctx ["user_name" ] = "user@domain.com"
809+ _expect_validation_error (ctx , "Access Token" )
810+
811+ def test_token_plus_auth_plus_uid_pwd_rejected (self ):
812+ """All stale fields must be stripped — validator catches any leak."""
813+ ctx = _bare_ctx ()
814+ ctx ["authentication" ] = "ActiveDirectoryPassword"
815+ ctx ["user_name" ] = "user@domain.com"
816+ ctx ["password" ] = "old-password"
817+ ctx ["access_token" ] = "fake-jwt"
818+ _expect_validation_error (ctx , "Access Token" )
819+
820+ def test_token_alone_accepted (self ):
821+ """Clean input: only access_token — passes validation."""
822+ ctx = _bare_ctx ()
823+ ctx ["access_token" ] = "fake-jwt"
824+ _expect_no_validation_error (ctx )
0 commit comments