Skip to content

Commit 86c5534

Browse files
Copilotpamelafox
andcommitted
Fix: Regenerate secrets when Entra apps are recreated
Co-authored-by: pamelafox <[email protected]>
1 parent 7ecf673 commit 86c5534

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

scripts/auth_init.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def create_or_update_application_with_secret(
7171
update_azd_env(app_id_env_var, app_id)
7272
created_app = True
7373

74-
if object_id and os.getenv(app_secret_env_var, "no-secret") == "no-secret":
74+
if object_id and (os.getenv(app_secret_env_var, "no-secret") == "no-secret" or created_app):
7575
print(f"Adding client secret to {app_id}")
7676
client_secret = await add_client_secret(graph_client, object_id)
7777
update_azd_env(app_secret_env_var, client_secret)

tests/test_auth_init.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""Tests for auth_init script functionality."""
2+
3+
import asyncio
4+
import os
5+
import sys
6+
from unittest.mock import AsyncMock, Mock, patch
7+
8+
import pytest
9+
10+
# Add the scripts directory to the path so we can import the modules
11+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
12+
13+
from auth_init import create_or_update_application_with_secret, update_azd_env
14+
from auth_common import get_application
15+
16+
17+
class MockApplication:
18+
"""Mock Application object for testing."""
19+
def __init__(self, display_name="Test App"):
20+
self.display_name = display_name
21+
22+
23+
class MockGraphClient:
24+
"""Mock GraphServiceClient for testing."""
25+
def __init__(self):
26+
self.applications = Mock()
27+
self.applications_with_app_id = Mock()
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_create_or_update_application_with_secret_regenerates_when_app_recreated():
32+
"""Test that secrets are regenerated when applications are recreated."""
33+
34+
# Mock environment variables - simulating the case where app was deleted but env vars remain
35+
with patch.dict(os.environ, {
36+
'AZURE_SERVER_APP_ID': 'old-app-id',
37+
'AZURE_SERVER_APP_SECRET': 'old-secret-from-deleted-app'
38+
}):
39+
40+
# Mock graph client
41+
graph_client = MockGraphClient()
42+
43+
# Mock get_application to return None (app doesn't exist anymore)
44+
with patch('auth_init.get_application', return_value=None):
45+
# Mock create_application to return new app details
46+
with patch('auth_init.create_application', return_value=('new-object-id', 'new-app-id')):
47+
# Mock add_client_secret to return new secret
48+
with patch('auth_init.add_client_secret', return_value='new-secret') as mock_add_secret:
49+
# Mock update_azd_env to track environment updates
50+
with patch('auth_init.update_azd_env') as mock_update_env:
51+
52+
# Call the function
53+
object_id, app_id, created_app = await create_or_update_application_with_secret(
54+
graph_client,
55+
app_id_env_var='AZURE_SERVER_APP_ID',
56+
app_secret_env_var='AZURE_SERVER_APP_SECRET',
57+
request_app=MockApplication()
58+
)
59+
60+
# Verify that a new application was created
61+
assert created_app is True
62+
assert object_id == 'new-object-id'
63+
assert app_id == 'new-app-id'
64+
65+
# Verify that add_client_secret was called (secret was regenerated)
66+
mock_add_secret.assert_called_once_with(graph_client, 'new-object-id')
67+
68+
# Verify that the environment was updated with the new secret
69+
mock_update_env.assert_any_call('AZURE_SERVER_APP_SECRET', 'new-secret')
70+
71+
72+
@pytest.mark.asyncio
73+
async def test_create_or_update_application_with_secret_skips_when_app_exists_and_secret_exists():
74+
"""Test that secrets are NOT regenerated when app exists and secret exists."""
75+
76+
# Mock environment variables - app and secret both exist
77+
with patch.dict(os.environ, {
78+
'AZURE_SERVER_APP_ID': 'existing-app-id',
79+
'AZURE_SERVER_APP_SECRET': 'existing-secret'
80+
}):
81+
82+
# Mock graph client
83+
graph_client = MockGraphClient()
84+
graph_client.applications.by_application_id.return_value.patch = AsyncMock()
85+
86+
# Mock get_application to return existing app
87+
with patch('auth_init.get_application', return_value='existing-object-id'):
88+
# Mock add_client_secret (should not be called)
89+
with patch('auth_init.add_client_secret') as mock_add_secret:
90+
# Mock update_azd_env to track environment updates
91+
with patch('auth_init.update_azd_env') as mock_update_env:
92+
93+
# Call the function
94+
object_id, app_id, created_app = await create_or_update_application_with_secret(
95+
graph_client,
96+
app_id_env_var='AZURE_SERVER_APP_ID',
97+
app_secret_env_var='AZURE_SERVER_APP_SECRET',
98+
request_app=MockApplication()
99+
)
100+
101+
# Verify that no new application was created
102+
assert created_app is False
103+
assert object_id == 'existing-object-id'
104+
assert app_id == 'existing-app-id'
105+
106+
# Verify that add_client_secret was NOT called (secret was not regenerated)
107+
mock_add_secret.assert_not_called()
108+
109+
# Verify that the environment was NOT updated with a new secret
110+
mock_update_env.assert_not_called()
111+
112+
113+
@pytest.mark.asyncio
114+
async def test_create_or_update_application_with_secret_generates_when_app_exists_but_no_secret():
115+
"""Test that secrets are generated when app exists but no secret exists."""
116+
117+
# Mock environment variables - app exists but no secret
118+
with patch.dict(os.environ, {
119+
'AZURE_SERVER_APP_ID': 'existing-app-id'
120+
}, clear=False):
121+
# Ensure the secret env var is not set
122+
if 'AZURE_SERVER_APP_SECRET' in os.environ:
123+
del os.environ['AZURE_SERVER_APP_SECRET']
124+
125+
# Mock graph client
126+
graph_client = MockGraphClient()
127+
graph_client.applications.by_application_id.return_value.patch = AsyncMock()
128+
129+
# Mock get_application to return existing app
130+
with patch('auth_init.get_application', return_value='existing-object-id'):
131+
# Mock add_client_secret to return new secret
132+
with patch('auth_init.add_client_secret', return_value='new-secret') as mock_add_secret:
133+
# Mock update_azd_env to track environment updates
134+
with patch('auth_init.update_azd_env') as mock_update_env:
135+
136+
# Call the function
137+
object_id, app_id, created_app = await create_or_update_application_with_secret(
138+
graph_client,
139+
app_id_env_var='AZURE_SERVER_APP_ID',
140+
app_secret_env_var='AZURE_SERVER_APP_SECRET',
141+
request_app=MockApplication()
142+
)
143+
144+
# Verify that no new application was created
145+
assert created_app is False
146+
assert object_id == 'existing-object-id'
147+
assert app_id == 'existing-app-id'
148+
149+
# Verify that add_client_secret was called (secret was generated)
150+
mock_add_secret.assert_called_once_with(graph_client, 'existing-object-id')
151+
152+
# Verify that the environment was updated with the new secret
153+
mock_update_env.assert_called_once_with('AZURE_SERVER_APP_SECRET', 'new-secret')
154+
155+
156+
async def run_tests():
157+
"""Run all tests."""
158+
print("Running test_create_or_update_application_with_secret_regenerates_when_app_recreated...")
159+
await test_create_or_update_application_with_secret_regenerates_when_app_recreated()
160+
print("✅ PASSED")
161+
162+
print("Running test_create_or_update_application_with_secret_skips_when_app_exists_and_secret_exists...")
163+
await test_create_or_update_application_with_secret_skips_when_app_exists_and_secret_exists()
164+
print("✅ PASSED")
165+
166+
print("Running test_create_or_update_application_with_secret_generates_when_app_exists_but_no_secret...")
167+
await test_create_or_update_application_with_secret_generates_when_app_exists_but_no_secret()
168+
print("✅ PASSED")
169+
170+
print("\n🎉 All tests passed!")
171+
172+
173+
if __name__ == "__main__":
174+
asyncio.run(run_tests())

0 commit comments

Comments
 (0)