amplify-excel-migrator 1.1.5__py3-none-any.whl → 1.2.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. amplify_excel_migrator/__init__.py +17 -0
  2. amplify_excel_migrator/auth/__init__.py +6 -0
  3. amplify_excel_migrator/auth/cognito_auth.py +306 -0
  4. amplify_excel_migrator/auth/provider.py +42 -0
  5. amplify_excel_migrator/cli/__init__.py +5 -0
  6. amplify_excel_migrator/cli/commands.py +165 -0
  7. amplify_excel_migrator/client.py +47 -0
  8. amplify_excel_migrator/core/__init__.py +5 -0
  9. amplify_excel_migrator/core/config.py +98 -0
  10. amplify_excel_migrator/data/__init__.py +7 -0
  11. amplify_excel_migrator/data/excel_reader.py +23 -0
  12. amplify_excel_migrator/data/transformer.py +119 -0
  13. amplify_excel_migrator/data/validator.py +48 -0
  14. amplify_excel_migrator/graphql/__init__.py +8 -0
  15. amplify_excel_migrator/graphql/client.py +137 -0
  16. amplify_excel_migrator/graphql/executor.py +405 -0
  17. amplify_excel_migrator/graphql/mutation_builder.py +80 -0
  18. amplify_excel_migrator/graphql/query_builder.py +194 -0
  19. amplify_excel_migrator/migration/__init__.py +8 -0
  20. amplify_excel_migrator/migration/batch_uploader.py +23 -0
  21. amplify_excel_migrator/migration/failure_tracker.py +92 -0
  22. amplify_excel_migrator/migration/orchestrator.py +143 -0
  23. amplify_excel_migrator/migration/progress_reporter.py +57 -0
  24. amplify_excel_migrator/schema/__init__.py +6 -0
  25. model_field_parser.py → amplify_excel_migrator/schema/field_parser.py +100 -22
  26. amplify_excel_migrator/schema/introspector.py +95 -0
  27. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/METADATA +121 -26
  28. amplify_excel_migrator-1.2.15.dist-info/RECORD +40 -0
  29. amplify_excel_migrator-1.2.15.dist-info/entry_points.txt +2 -0
  30. amplify_excel_migrator-1.2.15.dist-info/top_level.txt +2 -0
  31. tests/__init__.py +1 -0
  32. tests/test_cli_commands.py +292 -0
  33. tests/test_client.py +187 -0
  34. tests/test_cognito_auth.py +363 -0
  35. tests/test_config_manager.py +347 -0
  36. tests/test_field_parser.py +615 -0
  37. tests/test_mutation_builder.py +391 -0
  38. tests/test_query_builder.py +384 -0
  39. amplify_client.py +0 -941
  40. amplify_excel_migrator-1.1.5.dist-info/RECORD +0 -9
  41. amplify_excel_migrator-1.1.5.dist-info/entry_points.txt +0 -2
  42. amplify_excel_migrator-1.1.5.dist-info/top_level.txt +0 -3
  43. migrator.py +0 -437
  44. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/WHEEL +0 -0
  45. {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/licenses/LICENSE +0 -0
tests/test_client.py ADDED
@@ -0,0 +1,187 @@
1
+ """Tests for AmplifyClient class"""
2
+
3
+ import pytest
4
+ import pandas as pd
5
+ from unittest.mock import MagicMock
6
+ from amplify_excel_migrator.client import AmplifyClient
7
+
8
+
9
+ class TestBuildForeignKeyLookups:
10
+ """Test build_foreign_key_lookups method for performance optimization"""
11
+
12
+ def test_builds_lookup_cache_for_related_models(self):
13
+ """Test that FK lookup cache is built correctly"""
14
+ client = AmplifyClient(api_endpoint="https://test.com")
15
+
16
+ client._executor.get_primary_field_name = MagicMock(return_value=("name", False, "String"))
17
+ client._executor.get_records = MagicMock(
18
+ return_value=[
19
+ {"id": "reporter-1", "name": "John Doe"},
20
+ {"id": "reporter-2", "name": "Jane Smith"},
21
+ ]
22
+ )
23
+
24
+ df = pd.DataFrame({"photographer": ["John Doe", "Jane Smith"]})
25
+ parsed_model_structure = {
26
+ "fields": [{"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"}]
27
+ }
28
+
29
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
30
+
31
+ # Verify cache was built
32
+ assert "Reporter" in result
33
+ assert result["Reporter"]["lookup"]["John Doe"] == "reporter-1"
34
+ assert result["Reporter"]["lookup"]["Jane Smith"] == "reporter-2"
35
+ assert result["Reporter"]["primary_field"] == "name"
36
+
37
+ # Verify API was called once
38
+ client._executor.get_primary_field_name.assert_called_once_with("Reporter", parsed_model_structure)
39
+ client._executor.get_records.assert_called_once_with("Reporter", "name", False)
40
+
41
+ def test_skips_non_id_fields(self):
42
+ """Test that non-ID fields are skipped"""
43
+ client = AmplifyClient(api_endpoint="https://test.com")
44
+
45
+ client._executor.get_primary_field_name = MagicMock()
46
+ client._executor.get_records = MagicMock()
47
+
48
+ df = pd.DataFrame({"title": ["Story 1"], "content": ["Content 1"]})
49
+ parsed_model_structure = {
50
+ "fields": [
51
+ {"name": "title", "is_id": False, "is_required": True},
52
+ {"name": "content", "is_id": False, "is_required": False},
53
+ ]
54
+ }
55
+
56
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
57
+
58
+ # No lookups should be built
59
+ assert result == {}
60
+ client._executor.get_primary_field_name.assert_not_called()
61
+ client._executor.get_records.assert_not_called()
62
+
63
+ def test_skips_fields_not_in_dataframe(self):
64
+ """Test that fields not in DataFrame columns are skipped"""
65
+ client = AmplifyClient(api_endpoint="https://test.com")
66
+
67
+ client._executor.get_primary_field_name = MagicMock()
68
+
69
+ df = pd.DataFrame({"title": ["Story 1"]})
70
+ parsed_model_structure = {
71
+ "fields": [{"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"}]
72
+ }
73
+
74
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
75
+
76
+ # No lookups should be built
77
+ assert result == {}
78
+ client._executor.get_primary_field_name.assert_not_called()
79
+
80
+ def test_infers_related_model_from_field_name(self):
81
+ """Test that related model is inferred when not explicitly provided"""
82
+ client = AmplifyClient(api_endpoint="https://test.com")
83
+
84
+ client._executor.get_primary_field_name = MagicMock(return_value=("name", False, "String"))
85
+ client._executor.get_records = MagicMock(return_value=[{"id": "author-1", "name": "Author One"}])
86
+
87
+ df = pd.DataFrame({"author": ["Author One"]})
88
+ parsed_model_structure = {
89
+ "fields": [
90
+ {"name": "authorId", "is_id": True, "is_required": True}
91
+ # No related_model - should infer "Author" from "authorId"
92
+ ]
93
+ }
94
+
95
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
96
+
97
+ # Verify cache was built with inferred model name
98
+ assert "Author" in result
99
+ assert result["Author"]["lookup"]["Author One"] == "author-1"
100
+
101
+ # Verify API was called with inferred model name
102
+ client._executor.get_primary_field_name.assert_called_once_with("Author", parsed_model_structure)
103
+
104
+ def test_handles_errors_gracefully(self):
105
+ """Test that errors in fetching don't crash the whole process"""
106
+ client = AmplifyClient(api_endpoint="https://test.com")
107
+
108
+ client._executor.get_primary_field_name = MagicMock(side_effect=Exception("API Error"))
109
+
110
+ df = pd.DataFrame({"photographer": ["John Doe"]})
111
+ parsed_model_structure = {
112
+ "fields": [{"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"}]
113
+ }
114
+
115
+ # Should not raise exception
116
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
117
+
118
+ # Cache should be empty but process continues
119
+ assert result == {}
120
+
121
+ def test_deduplicates_same_related_model(self):
122
+ """Test that the same related model is only fetched once"""
123
+ client = AmplifyClient(api_endpoint="https://test.com")
124
+
125
+ client._executor.get_primary_field_name = MagicMock(return_value=("name", False, "String"))
126
+ client._executor.get_records = MagicMock(return_value=[{"id": "reporter-1", "name": "John Doe"}])
127
+
128
+ df = pd.DataFrame({"photographer": ["John Doe"], "editor": ["Jane Smith"]})
129
+ parsed_model_structure = {
130
+ "fields": [
131
+ {"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"},
132
+ {"name": "editorId", "is_id": True, "is_required": True, "related_model": "Reporter"},
133
+ ]
134
+ }
135
+
136
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
137
+
138
+ # Only one Reporter lookup should exist
139
+ assert len(result) == 1
140
+ assert "Reporter" in result
141
+
142
+ # API should be called only once
143
+ client._executor.get_primary_field_name.assert_called_once()
144
+ client._executor.get_records.assert_called_once()
145
+
146
+ def test_handles_empty_records_response(self):
147
+ """Test that empty records from API don't break the cache"""
148
+ client = AmplifyClient(api_endpoint="https://test.com")
149
+
150
+ client._executor.get_primary_field_name = MagicMock(return_value=("name", False, "String"))
151
+ client._executor.get_records = MagicMock(return_value=None) # API returns None
152
+
153
+ df = pd.DataFrame({"photographer": ["John Doe"]})
154
+ parsed_model_structure = {
155
+ "fields": [{"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"}]
156
+ }
157
+
158
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
159
+
160
+ # Cache should be empty
161
+ assert result == {}
162
+
163
+ def test_filters_out_records_without_primary_field(self):
164
+ """Test that records without the primary field are filtered out"""
165
+ client = AmplifyClient(api_endpoint="https://test.com")
166
+
167
+ client._executor.get_primary_field_name = MagicMock(return_value=("name", False, "String"))
168
+ client._executor.get_records = MagicMock(
169
+ return_value=[
170
+ {"id": "reporter-1", "name": "John Doe"},
171
+ {"id": "reporter-2"}, # Missing name
172
+ {"id": "reporter-3", "name": None}, # None name
173
+ {"id": "reporter-4", "name": "Jane Smith"},
174
+ ]
175
+ )
176
+
177
+ df = pd.DataFrame({"photographer": ["John Doe", "Jane Smith"]})
178
+ parsed_model_structure = {
179
+ "fields": [{"name": "photographerId", "is_id": True, "is_required": True, "related_model": "Reporter"}]
180
+ }
181
+
182
+ result = client.build_foreign_key_lookups(df, parsed_model_structure)
183
+
184
+ # Only valid records should be in cache
185
+ assert len(result["Reporter"]["lookup"]) == 2
186
+ assert "John Doe" in result["Reporter"]["lookup"]
187
+ assert "Jane Smith" in result["Reporter"]["lookup"]
@@ -0,0 +1,363 @@
1
+ """Tests for CognitoAuthProvider class"""
2
+
3
+ import pytest
4
+ from unittest.mock import Mock, patch, MagicMock
5
+ from pycognito.exceptions import ForceChangePasswordException
6
+ from pycognito import MFAChallengeException
7
+ from botocore.exceptions import NoCredentialsError, ProfileNotFound, NoRegionError, ClientError
8
+
9
+ from amplify_excel_migrator.auth import CognitoAuthProvider
10
+
11
+
12
+ @pytest.fixture
13
+ def auth_params():
14
+ """Standard authentication parameters"""
15
+ return {
16
+ "user_pool_id": "us-east-1_testpool",
17
+ "client_id": "test-client-id",
18
+ "region": "us-east-1",
19
+ "admin_group_name": "ADMINS",
20
+ }
21
+
22
+
23
+ @pytest.fixture
24
+ def cognito_provider(auth_params):
25
+ """Create CognitoAuthProvider instance"""
26
+ return CognitoAuthProvider(**auth_params)
27
+
28
+
29
+ @pytest.fixture
30
+ def mock_id_token():
31
+ """Mock ID token with ADMINS group"""
32
+ return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29nbml0bzpncm91cHMiOlsiQURNSU5TIl19.test"
33
+
34
+
35
+ class TestCognitoAuthProviderInitialization:
36
+ """Test CognitoAuthProvider initialization"""
37
+
38
+ def test_initialization(self, cognito_provider, auth_params):
39
+ """Test basic initialization"""
40
+ assert cognito_provider.user_pool_id == auth_params["user_pool_id"]
41
+ assert cognito_provider.client_id == auth_params["client_id"]
42
+ assert cognito_provider.region == auth_params["region"]
43
+ assert cognito_provider.admin_group_name == auth_params["admin_group_name"]
44
+ assert cognito_provider.cognito_client is None
45
+ assert cognito_provider.boto_cognito_admin_client is None
46
+ assert cognito_provider._id_token is None
47
+ assert cognito_provider._mfa_tokens is None
48
+
49
+ def test_custom_admin_group(self, auth_params):
50
+ """Test initialization with custom admin group"""
51
+ auth_params["admin_group_name"] = "CUSTOM_ADMINS"
52
+ provider = CognitoAuthProvider(**auth_params)
53
+ assert provider.admin_group_name == "CUSTOM_ADMINS"
54
+
55
+
56
+ class TestStandardAuthentication:
57
+ """Test standard Cognito authentication"""
58
+
59
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
60
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
61
+ def test_successful_authentication(self, mock_jwt_decode, mock_cognito_class, cognito_provider, mock_id_token):
62
+ """Test successful authentication"""
63
+ mock_cognito = Mock()
64
+ mock_cognito.id_token = mock_id_token
65
+ mock_cognito_class.return_value = mock_cognito
66
+ mock_jwt_decode.return_value = {"cognito:groups": ["ADMINS"]}
67
+
68
+ result = cognito_provider.authenticate("test@example.com", "password123")
69
+
70
+ assert result is True
71
+ assert cognito_provider._id_token == mock_id_token
72
+ mock_cognito.authenticate.assert_called_once_with(password="password123")
73
+
74
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
75
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
76
+ def test_authentication_not_in_admin_group(self, mock_jwt_decode, mock_cognito_class, cognito_provider):
77
+ """Test authentication fails when user not in admin group"""
78
+ mock_cognito = Mock()
79
+ mock_cognito.id_token = "test_token"
80
+ mock_cognito_class.return_value = mock_cognito
81
+ mock_jwt_decode.return_value = {"cognito:groups": ["USERS"]}
82
+
83
+ result = cognito_provider.authenticate("test@example.com", "password123")
84
+ assert result is False
85
+
86
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
87
+ @patch("amplify_excel_migrator.auth.cognito_auth.input")
88
+ def test_authentication_with_mfa_challenge(self, mock_input, mock_cognito_class, cognito_provider, mock_id_token):
89
+ """Test authentication with MFA challenge"""
90
+ mock_cognito = Mock()
91
+ mock_cognito_class.return_value = mock_cognito
92
+
93
+ mfa_tokens = {"ChallengeName": "SMS_MFA", "Session": "test-session"}
94
+ mfa_exception = MFAChallengeException("MFA required", mfa_tokens)
95
+
96
+ mock_cognito.authenticate.side_effect = [mfa_exception, None]
97
+ mock_cognito.id_token = mock_id_token
98
+ mock_input.return_value = "123456"
99
+
100
+ with patch.object(cognito_provider, "_complete_mfa_challenge", return_value=True):
101
+ with patch.object(cognito_provider, "_check_user_in_admins_group"):
102
+ result = cognito_provider.authenticate("test@example.com", "password123")
103
+
104
+ assert result is True
105
+
106
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
107
+ @patch("amplify_excel_migrator.auth.cognito_auth.input")
108
+ def test_authentication_with_password_change(self, mock_input, mock_cognito_class, cognito_provider, mock_id_token):
109
+ """Test authentication with force password change"""
110
+ mock_cognito = Mock()
111
+ mock_cognito_class.return_value = mock_cognito
112
+
113
+ mock_cognito.authenticate.side_effect = [ForceChangePasswordException(), None]
114
+ mock_cognito.id_token = mock_id_token
115
+ mock_input.side_effect = ["newpassword123", "newpassword123"]
116
+
117
+ with patch.object(cognito_provider, "_check_user_in_admins_group"):
118
+ result = cognito_provider.authenticate("test@example.com", "oldpassword")
119
+
120
+ assert result is True
121
+ mock_cognito.new_password_challenge.assert_called_once()
122
+
123
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
124
+ @patch("amplify_excel_migrator.auth.cognito_auth.input")
125
+ def test_authentication_password_mismatch(self, mock_input, mock_cognito_class, cognito_provider):
126
+ """Test authentication fails when new passwords don't match"""
127
+ mock_cognito = Mock()
128
+ mock_cognito_class.return_value = mock_cognito
129
+ mock_cognito.authenticate.side_effect = ForceChangePasswordException()
130
+ mock_input.side_effect = ["newpassword", "differentpassword"]
131
+
132
+ result = cognito_provider.authenticate("test@example.com", "oldpassword")
133
+
134
+ assert result is False
135
+
136
+ @patch("amplify_excel_migrator.auth.cognito_auth.Cognito")
137
+ def test_authentication_general_error(self, mock_cognito_class, cognito_provider):
138
+ """Test authentication handles general errors"""
139
+ mock_cognito = Mock()
140
+ mock_cognito_class.return_value = mock_cognito
141
+ mock_cognito.authenticate.side_effect = Exception("Network error")
142
+
143
+ result = cognito_provider.authenticate("test@example.com", "password123")
144
+
145
+ assert result is False
146
+
147
+
148
+ class TestAdminAuthentication:
149
+ """Test admin authentication"""
150
+
151
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.client")
152
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
153
+ def test_successful_admin_authentication(self, mock_jwt_decode, mock_boto_client, cognito_provider, mock_id_token):
154
+ """Test successful admin authentication"""
155
+ mock_cognito_admin = Mock()
156
+ mock_boto_client.return_value = mock_cognito_admin
157
+ mock_jwt_decode.return_value = {"cognito:groups": ["ADMINS"]}
158
+
159
+ mock_cognito_admin.admin_initiate_auth.return_value = {
160
+ "AuthenticationResult": {"IdToken": mock_id_token, "AccessToken": "access_token"}
161
+ }
162
+
163
+ result = cognito_provider.authenticate_admin("test@example.com", "password123")
164
+
165
+ assert result is True
166
+ assert cognito_provider._id_token == mock_id_token
167
+ mock_cognito_admin.admin_initiate_auth.assert_called_once()
168
+
169
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.Session")
170
+ def test_admin_authentication_with_profile(self, mock_session_class, cognito_provider, mock_id_token):
171
+ """Test admin authentication with AWS profile"""
172
+ mock_session = Mock()
173
+ mock_session_class.return_value = mock_session
174
+ mock_cognito_admin = Mock()
175
+ mock_session.client.return_value = mock_cognito_admin
176
+
177
+ mock_cognito_admin.admin_initiate_auth.return_value = {"AuthenticationResult": {"IdToken": mock_id_token}}
178
+
179
+ with patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode") as mock_jwt:
180
+ mock_jwt.return_value = {"cognito:groups": ["ADMINS"]}
181
+ result = cognito_provider.authenticate_admin("test@example.com", "password", aws_profile="my-profile")
182
+
183
+ assert result is True
184
+ mock_session_class.assert_called_once_with(profile_name="my-profile")
185
+
186
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.client")
187
+ def test_admin_authentication_no_credentials(self, mock_boto_client, cognito_provider):
188
+ """Test admin authentication fails with no AWS credentials"""
189
+ mock_boto_client.side_effect = NoCredentialsError()
190
+
191
+ result = cognito_provider.authenticate_admin("test@example.com", "password123")
192
+ assert result is False
193
+
194
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.Session")
195
+ def test_admin_authentication_profile_not_found(self, mock_session_class, cognito_provider):
196
+ """Test admin authentication fails with invalid profile"""
197
+ mock_session_class.side_effect = ProfileNotFound(profile="invalid")
198
+
199
+ result = cognito_provider.authenticate_admin("test@example.com", "password", aws_profile="invalid")
200
+ assert result is False
201
+
202
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.client")
203
+ def test_admin_authentication_no_region(self, mock_boto_client, cognito_provider):
204
+ """Test admin authentication fails with no region"""
205
+ mock_boto_client.side_effect = NoRegionError()
206
+
207
+ result = cognito_provider.authenticate_admin("test@example.com", "password123")
208
+ assert result is False
209
+
210
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.client")
211
+ def test_admin_authentication_client_error(self, mock_boto_client, cognito_provider):
212
+ """Test admin authentication handles AWS client errors"""
213
+ error_response = {"Error": {"Code": "AccessDenied", "Message": "Access denied"}}
214
+ mock_boto_client.side_effect = ClientError(error_response, "admin_initiate_auth")
215
+
216
+ result = cognito_provider.authenticate_admin("test@example.com", "password123")
217
+ assert result is False
218
+
219
+ @patch("amplify_excel_migrator.auth.cognito_auth.boto3.client")
220
+ def test_admin_authentication_no_result(self, mock_boto_client, cognito_provider):
221
+ """Test admin authentication fails when no AuthenticationResult"""
222
+ mock_cognito_admin = Mock()
223
+ mock_boto_client.return_value = mock_cognito_admin
224
+ mock_cognito_admin.admin_initiate_auth.return_value = {"ChallengeName": "MFA_SETUP"}
225
+
226
+ with patch.object(cognito_provider, "_check_for_mfa_challenges", return_value=False):
227
+ result = cognito_provider.authenticate_admin("test@example.com", "password123")
228
+
229
+ assert result is False
230
+
231
+
232
+ class TestMFAChallenge:
233
+ """Test MFA challenge handling"""
234
+
235
+ def test_complete_sms_mfa_challenge(self, cognito_provider):
236
+ """Test completing SMS MFA challenge"""
237
+ mock_cognito = Mock()
238
+ cognito_provider.cognito_client = mock_cognito
239
+ cognito_provider._mfa_tokens = {"ChallengeName": "SMS_MFA", "Session": "test-session"}
240
+
241
+ result = cognito_provider._complete_mfa_challenge("123456")
242
+
243
+ assert result is True
244
+ mock_cognito.respond_to_sms_mfa_challenge.assert_called_once_with(
245
+ code="123456", mfa_tokens=cognito_provider._mfa_tokens
246
+ )
247
+
248
+ def test_complete_software_token_mfa_challenge(self, cognito_provider):
249
+ """Test completing Software Token MFA challenge"""
250
+ mock_cognito = Mock()
251
+ cognito_provider.cognito_client = mock_cognito
252
+ cognito_provider._mfa_tokens = {"ChallengeName": "SOFTWARE_TOKEN_MFA", "Session": "test-session"}
253
+
254
+ result = cognito_provider._complete_mfa_challenge("123456")
255
+
256
+ assert result is True
257
+ mock_cognito.respond_to_software_token_mfa_challenge.assert_called_once_with(
258
+ code="123456", mfa_tokens=cognito_provider._mfa_tokens
259
+ )
260
+
261
+ def test_complete_mfa_no_tokens(self, cognito_provider):
262
+ """Test MFA challenge fails when no tokens"""
263
+ result = cognito_provider._complete_mfa_challenge("123456")
264
+
265
+ assert result is False
266
+
267
+ def test_complete_mfa_challenge_error(self, cognito_provider):
268
+ """Test MFA challenge handles errors"""
269
+ mock_cognito = Mock()
270
+ cognito_provider.cognito_client = mock_cognito
271
+ cognito_provider._mfa_tokens = {"ChallengeName": "SMS_MFA"}
272
+ mock_cognito.respond_to_sms_mfa_challenge.side_effect = Exception("Invalid code")
273
+
274
+ result = cognito_provider._complete_mfa_challenge("999999")
275
+
276
+ assert result is False
277
+
278
+
279
+ class TestCheckForMFAChallenges:
280
+ """Test _check_for_mfa_challenges method"""
281
+
282
+ @patch("amplify_excel_migrator.auth.cognito_auth.input")
283
+ def test_sms_mfa_challenge(self, mock_input, cognito_provider):
284
+ """Test handling SMS MFA challenge"""
285
+ mock_admin_client = Mock()
286
+ cognito_provider.boto_cognito_admin_client = mock_admin_client
287
+ mock_input.return_value = "123456"
288
+
289
+ response = {"ChallengeName": "SMS_MFA", "Session": "test-session"}
290
+
291
+ cognito_provider._check_for_mfa_challenges(response, "test@example.com")
292
+
293
+ mock_admin_client.admin_respond_to_auth_challenge.assert_called_once()
294
+
295
+ @patch("amplify_excel_migrator.auth.cognito_auth.getpass")
296
+ def test_new_password_required_challenge(self, mock_getpass, cognito_provider):
297
+ """Test handling NEW_PASSWORD_REQUIRED challenge"""
298
+ mock_admin_client = Mock()
299
+ cognito_provider.boto_cognito_admin_client = mock_admin_client
300
+ mock_getpass.return_value = "newpassword123"
301
+
302
+ response = {"ChallengeName": "NEW_PASSWORD_REQUIRED", "Session": "test-session"}
303
+
304
+ cognito_provider._check_for_mfa_challenges(response, "test@example.com")
305
+
306
+ mock_admin_client.admin_respond_to_auth_challenge.assert_called_once()
307
+
308
+ def test_mfa_setup_challenge(self, cognito_provider):
309
+ """Test handling MFA_SETUP challenge"""
310
+ response = {"ChallengeName": "MFA_SETUP"}
311
+
312
+ result = cognito_provider._check_for_mfa_challenges(response, "test@example.com")
313
+
314
+ assert result is False
315
+
316
+
317
+ class TestCheckUserInAdminsGroup:
318
+ """Test _check_user_in_admins_group method"""
319
+
320
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
321
+ def test_user_in_admin_group(self, mock_jwt_decode, cognito_provider):
322
+ """Test user is in admin group"""
323
+ mock_jwt_decode.return_value = {"cognito:groups": ["ADMINS", "USERS"]}
324
+
325
+ cognito_provider._check_user_in_admins_group("test_token")
326
+
327
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
328
+ def test_user_not_in_admin_group(self, mock_jwt_decode, cognito_provider):
329
+ """Test user is not in admin group"""
330
+ mock_jwt_decode.return_value = {"cognito:groups": ["USERS"]}
331
+
332
+ with pytest.raises(PermissionError, match="User is not in ADMINS group"):
333
+ cognito_provider._check_user_in_admins_group("test_token")
334
+
335
+ @patch("amplify_excel_migrator.auth.cognito_auth.jwt.decode")
336
+ def test_user_no_groups(self, mock_jwt_decode, cognito_provider):
337
+ """Test user has no groups"""
338
+ mock_jwt_decode.return_value = {}
339
+
340
+ with pytest.raises(PermissionError, match="User is not in ADMINS group"):
341
+ cognito_provider._check_user_in_admins_group("test_token")
342
+
343
+
344
+ class TestProviderInterface:
345
+ """Test AuthenticationProvider interface implementation"""
346
+
347
+ def test_get_id_token_not_authenticated(self, cognito_provider):
348
+ """Test get_id_token returns None when not authenticated"""
349
+ assert cognito_provider.get_id_token() is None
350
+
351
+ def test_get_id_token_authenticated(self, cognito_provider, mock_id_token):
352
+ """Test get_id_token returns token when authenticated"""
353
+ cognito_provider._id_token = mock_id_token
354
+ assert cognito_provider.get_id_token() == mock_id_token
355
+
356
+ def test_is_authenticated_false(self, cognito_provider):
357
+ """Test is_authenticated returns False when not authenticated"""
358
+ assert cognito_provider.is_authenticated() is False
359
+
360
+ def test_is_authenticated_true(self, cognito_provider, mock_id_token):
361
+ """Test is_authenticated returns True when authenticated"""
362
+ cognito_provider._id_token = mock_id_token
363
+ assert cognito_provider.is_authenticated() is True