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.
- amplify_excel_migrator/__init__.py +17 -0
- amplify_excel_migrator/auth/__init__.py +6 -0
- amplify_excel_migrator/auth/cognito_auth.py +306 -0
- amplify_excel_migrator/auth/provider.py +42 -0
- amplify_excel_migrator/cli/__init__.py +5 -0
- amplify_excel_migrator/cli/commands.py +165 -0
- amplify_excel_migrator/client.py +47 -0
- amplify_excel_migrator/core/__init__.py +5 -0
- amplify_excel_migrator/core/config.py +98 -0
- amplify_excel_migrator/data/__init__.py +7 -0
- amplify_excel_migrator/data/excel_reader.py +23 -0
- amplify_excel_migrator/data/transformer.py +119 -0
- amplify_excel_migrator/data/validator.py +48 -0
- amplify_excel_migrator/graphql/__init__.py +8 -0
- amplify_excel_migrator/graphql/client.py +137 -0
- amplify_excel_migrator/graphql/executor.py +405 -0
- amplify_excel_migrator/graphql/mutation_builder.py +80 -0
- amplify_excel_migrator/graphql/query_builder.py +194 -0
- amplify_excel_migrator/migration/__init__.py +8 -0
- amplify_excel_migrator/migration/batch_uploader.py +23 -0
- amplify_excel_migrator/migration/failure_tracker.py +92 -0
- amplify_excel_migrator/migration/orchestrator.py +143 -0
- amplify_excel_migrator/migration/progress_reporter.py +57 -0
- amplify_excel_migrator/schema/__init__.py +6 -0
- model_field_parser.py → amplify_excel_migrator/schema/field_parser.py +100 -22
- amplify_excel_migrator/schema/introspector.py +95 -0
- {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/METADATA +121 -26
- amplify_excel_migrator-1.2.15.dist-info/RECORD +40 -0
- amplify_excel_migrator-1.2.15.dist-info/entry_points.txt +2 -0
- amplify_excel_migrator-1.2.15.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_cli_commands.py +292 -0
- tests/test_client.py +187 -0
- tests/test_cognito_auth.py +363 -0
- tests/test_config_manager.py +347 -0
- tests/test_field_parser.py +615 -0
- tests/test_mutation_builder.py +391 -0
- tests/test_query_builder.py +384 -0
- amplify_client.py +0 -941
- amplify_excel_migrator-1.1.5.dist-info/RECORD +0 -9
- amplify_excel_migrator-1.1.5.dist-info/entry_points.txt +0 -2
- amplify_excel_migrator-1.1.5.dist-info/top_level.txt +0 -3
- migrator.py +0 -437
- {amplify_excel_migrator-1.1.5.dist-info → amplify_excel_migrator-1.2.15.dist-info}/WHEEL +0 -0
- {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
|