99from azure .core .credentials import TokenCredential
1010
1111from PowerPlatform .Dataverse .client import DataverseClient
12- from PowerPlatform .Dataverse .core .config import DataverseConfig
12+ from PowerPlatform .Dataverse .core .config import DataverseConfig , OperationContext
1313from PowerPlatform .Dataverse .data ._odata import _ODataClient , _USER_AGENT
1414
1515
16+ class TestOperationContextValidation (unittest .TestCase ):
17+ """Tests for OperationContext format validation and PII rejection."""
18+
19+ def test_valid_single_pair (self ):
20+ ctx = OperationContext (operation_context = "app=test/1.0" )
21+ self .assertEqual (ctx .operation_context , "app=test/1.0" )
22+
23+ def test_valid_multiple_pairs (self ):
24+ ctx = OperationContext (operation_context = "app=test/1.0;skill=dv-data;agent=claude-code" )
25+ self .assertEqual (ctx .operation_context , "app=test/1.0;skill=dv-data;agent=claude-code" )
26+
27+ def test_valid_with_dots_slashes_hyphens (self ):
28+ ctx = OperationContext (operation_context = "app=dataverse-skills/1.2.1" )
29+ self .assertEqual (ctx .operation_context , "app=dataverse-skills/1.2.1" )
30+
31+ def test_reject_empty (self ):
32+ with self .assertRaises (ValueError ):
33+ OperationContext (operation_context = "" )
34+
35+ def test_reject_email (self ):
36+ with self .assertRaises (ValueError ):
37+ OperationContext (operation_context = "myname@email.com" )
38+
39+ def test_reject_freeform_text (self ):
40+ with self .assertRaises (ValueError ):
41+ OperationContext (operation_context = "my bank password is 1234" )
42+
43+ def test_reject_control_chars (self ):
44+ for bad in ["has\r newline" , "has\n newline" , "has\x00 null" ]:
45+ with self .assertRaises (ValueError ):
46+ OperationContext (operation_context = bad )
47+
48+ def test_reject_spaces (self ):
49+ with self .assertRaises (ValueError ):
50+ OperationContext (operation_context = "app=my app" )
51+
52+ def test_reject_no_equals (self ):
53+ with self .assertRaises (ValueError ):
54+ OperationContext (operation_context = "justaplainstring" )
55+
56+
1657class TestOperationContextConfig (unittest .TestCase ):
1758 """Tests for operation_context on DataverseConfig."""
1859
@@ -21,47 +62,57 @@ def test_default_is_none(self):
2162 self .assertIsNone (config .operation_context )
2263
2364 def test_explicit_value (self ):
24- config = DataverseConfig (operation_context = "app=test/1.0;agent=claude-code" )
25- self .assertEqual (config .operation_context , "app=test/1.0;agent=claude-code" )
65+ ctx = OperationContext (operation_context = "app=test/1.0;agent=claude-code" )
66+ config = DataverseConfig (operation_context = ctx )
67+ self .assertEqual (config .operation_context .operation_context , "app=test/1.0;agent=claude-code" )
2668
2769 def test_default_constructor_is_none (self ):
2870 config = DataverseConfig ()
2971 self .assertIsNone (config .operation_context )
3072
3173
3274class TestOperationContextClient (unittest .TestCase ):
33- """Tests for operation_context kwarg on DataverseClient."""
75+ """Tests for context kwarg on DataverseClient."""
3476
3577 def setUp (self ):
3678 self .mock_credential = MagicMock (spec = TokenCredential )
3779 self .base_url = "https://example.crm.dynamics.com"
3880
3981 def test_kwarg_sets_config (self ):
82+ ctx = OperationContext (operation_context = "app=test/1.0;skill=dv-data;agent=claude-code" )
4083 client = DataverseClient (
4184 self .base_url ,
4285 self .mock_credential ,
43- operation_context = "app=test/1.0;skill=dv-data;agent=claude-code" ,
86+ context = ctx ,
87+ )
88+ self .assertEqual (
89+ client ._config .operation_context .operation_context ,
90+ "app=test/1.0;skill=dv-data;agent=claude-code" ,
4491 )
45- self .assertEqual (client ._config .operation_context , "app=test/1.0;skill=dv-data;agent=claude-code" )
4692
4793 def test_no_kwarg_leaves_config_default (self ):
4894 client = DataverseClient (self .base_url , self .mock_credential )
4995 self .assertIsNone (client ._config .operation_context )
5096
51- def test_config_and_kwarg_raises (self ):
52- config = DataverseConfig (operation_context = "app=test/1.0" )
97+ def test_config_and_context_raises (self ):
98+ ctx = OperationContext (operation_context = "app=test/1.0" )
99+ config = DataverseConfig (operation_context = ctx )
53100 with self .assertRaises (ValueError ):
54101 DataverseClient (
55102 self .base_url ,
56103 self .mock_credential ,
57104 config = config ,
58- operation_context = "app=other/2.0" ,
105+ context = OperationContext ( operation_context = "app=other/2.0" ) ,
59106 )
60107
61108 def test_config_alone_works (self ):
62- config = DataverseConfig (operation_context = "app=test/1.0;agent=copilot" )
109+ ctx = OperationContext (operation_context = "app=test/1.0;agent=copilot" )
110+ config = DataverseConfig (operation_context = ctx )
63111 client = DataverseClient (self .base_url , self .mock_credential , config = config )
64- self .assertEqual (client ._config .operation_context , "app=test/1.0;agent=copilot" )
112+ self .assertEqual (
113+ client ._config .operation_context .operation_context ,
114+ "app=test/1.0;agent=copilot" ,
115+ )
65116
66117
67118class TestOperationContextUserAgent (unittest .TestCase ):
@@ -80,26 +131,19 @@ def test_default_user_agent_unchanged(self):
80131 self .assertEqual (headers ["User-Agent" ], _USER_AGENT )
81132
82133 def test_operation_context_appended (self ):
83- ctx = "app=dataverse-skills/1.2.1;skill=dv-data;agent=claude-code"
134+ ctx_str = "app=dataverse-skills/1.2.1;skill=dv-data;agent=claude-code"
135+ ctx = OperationContext (operation_context = ctx_str )
84136 config = DataverseConfig (operation_context = ctx )
85137 odata = _ODataClient (self .dummy_auth , self .base_url , config = config )
86138 headers = odata ._headers ()
87- self .assertEqual (headers ["User-Agent" ], f"{ _USER_AGENT } ({ ctx } )" )
139+ self .assertEqual (headers ["User-Agent" ], f"{ _USER_AGENT } ({ ctx_str } )" )
88140
89141 def test_none_context_no_parentheses (self ):
90142 config = DataverseConfig (operation_context = None )
91143 odata = _ODataClient (self .dummy_auth , self .base_url , config = config )
92144 headers = odata ._headers ()
93145 self .assertNotIn ("(" , headers ["User-Agent" ])
94146
95- def test_empty_string_context_no_parentheses (self ):
96- config = DataverseConfig (operation_context = "" )
97- odata = _ODataClient (self .dummy_auth , self .base_url , config = config )
98- headers = odata ._headers ()
99- self .assertNotIn ("(" , headers ["User-Agent" ])
100-
101- def test_control_chars_rejected (self ):
102- for bad in ["has\r newline" , "has\n newline" , "has\x00 null" ]:
103- config = DataverseConfig (operation_context = bad )
104- with self .assertRaises (ValueError ):
105- _ODataClient (self .dummy_auth , self .base_url , config = config )
147+ def test_empty_string_rejected_at_creation (self ):
148+ with self .assertRaises (ValueError ):
149+ OperationContext (operation_context = "" )
0 commit comments