Skip to content

Commit 75e420e

Browse files
authored
Merge pull request #3 from nullchimp/alert-autofix-1
Potential fix for code scanning alert no. 1: Workflow does not contain permissions
2 parents 3ed4478 + 78f81ff commit 75e420e

File tree

5 files changed

+160
-15
lines changed

5 files changed

+160
-15
lines changed

.github/workflows/test-coverage.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# .github/workflows/test-coverage.yml
22
name: Test Coverage
3+
permissions:
4+
contents: read
35

46
on:
57
push:
@@ -21,16 +23,20 @@ jobs:
2123

2224
- name: Set up environment
2325
run: |
26+
python -m venv .venv
27+
source .venv/bin/activate
2428
pip install --upgrade pip
2529
pip install -r requirements.txt
26-
pip install pytest pytest-cov
27-
30+
2831
- name: Set environment variables
2932
run: |
3033
echo "AZURE_OPENAI_API_KEY=dummy-key-for-testing" >> $GITHUB_ENV
31-
echo "PYTHONPATH=$(pwd):$(pwd)/src" >> $GITHUB_ENV
34+
echo "PYTHONPATH=$PYTHONPATH:$(pwd):$(pwd)/src" >> $GITHUB_ENV
35+
3236
- name: Run tests with coverage
33-
run: pytest --cov=src --cov-report=xml --cov-fail-under=80
37+
run: |
38+
source .venv/bin/activate
39+
python -m pytest --cov=src --cov-report=xml --cov-report=term --cov-fail-under=80
3440
3541
- name: Upload coverage report
3642
uses: actions/upload-artifact@v4

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ charset-normalizer==3.4.2
99
click==8.1.8
1010
coverage==7.8.0
1111
dill==0.4.0
12+
fastapi==0.115.12
1213
flake8==7.2.0
1314
google-api-core==2.24.2
1415
google-api-python-client==2.169.0
@@ -48,6 +49,7 @@ Pygments==2.19.1
4849
pylint==3.3.7
4950
pyparsing==3.2.3
5051
pytest==8.3.5
52+
pytest-asyncio==0.26.0
5153
pytest-cov==6.1.1
5254
python-dotenv==1.1.0
5355
python-multipart==0.0.20

tests/conftest.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import sys
3+
import pytest
4+
import asyncio
5+
6+
7+
# Add the src directory to the Python path
8+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
9+
10+
11+
# Set standard environment variables for testing
12+
@pytest.fixture(scope="session", autouse=True)
13+
def setup_test_environment():
14+
"""Set up the test environment with consistent environment variables."""
15+
# Store original environment variables to restore later
16+
original_env = os.environ.copy()
17+
18+
# Set standard test environment variables
19+
test_env = {
20+
"AZURE_OPENAI_API_KEY": "dummy-key-for-testing",
21+
# Add any other environment variables needed for tests
22+
}
23+
24+
# Apply test environment
25+
for key, value in test_env.items():
26+
os.environ[key] = value
27+
28+
# Run the tests
29+
yield
30+
31+
# Restore original environment
32+
for key in test_env.keys():
33+
if key in original_env:
34+
os.environ[key] = original_env[key]
35+
else:
36+
del os.environ[key]
37+
38+
39+
# Configure asyncio for pytest-asyncio
40+
@pytest.fixture(scope="session")
41+
def event_loop_policy():
42+
"""Return the default event loop policy."""
43+
return asyncio.DefaultEventLoopPolicy()
44+
45+
46+
@pytest.fixture
47+
def event_loop(event_loop_policy):
48+
"""Create an instance of the default event loop for each test case."""
49+
loop = event_loop_policy.new_event_loop()
50+
asyncio.set_event_loop(loop)
51+
yield loop
52+
asyncio.set_event_loop(None)
53+
loop.close()
54+
55+
56+
# Add pytest configuration to set the default loop scope
57+
def pytest_configure(config):
58+
"""Configure pytest-asyncio with the default event loop scope."""
59+
config.addinivalue_line(
60+
"markers", "asyncio: mark test to run using an asyncio event loop"
61+
)
62+
63+
# Set the default fixture loop scope
64+
if hasattr(config, 'asyncio_options'):
65+
config.asyncio_options.default_fixture_loop_scope = 'function'

tests/test_main.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,27 @@ async def test_process_one():
1212
"""Test the process_one coroutine."""
1313
import main
1414

15-
# For this test, we need to directly execute the first iteration of the loop
16-
# in process_one to verify that print is called before sleep
15+
# Mock the sleep function to avoid waiting and stop the infinite loop
16+
mock_sleep = AsyncMock()
1717

18-
# Create a mock for asyncio.sleep that will raise CancelledError after being called
19-
async def mock_sleep(seconds):
20-
raise asyncio.CancelledError()
18+
# Use side_effect to make sleep raise CancelledError after first call
19+
# This ensures we exit the while loop after one iteration
20+
mock_sleep.side_effect = [None, asyncio.CancelledError()]
2121

22-
# Apply the mocks - patching at the exact module where sleep is used
2322
with patch('asyncio.sleep', mock_sleep):
2423
with patch('builtins.print') as mock_print:
2524
try:
26-
# Run process_one - it should print and then hit our mocked sleep
27-
# which raises CancelledError to exit the loop
2825
await main.process_one()
2926
except asyncio.CancelledError:
3027
pass
3128

3229
# Verify print was called with the expected message
33-
mock_print.assert_called_once_with("Processing one...")
30+
mock_print.assert_called_with("Processing one...")
31+
assert mock_print.call_count >= 1 # Should be called at least once
32+
33+
# Verify sleep was called with expected argument
34+
mock_sleep.assert_called_with(1)
35+
assert mock_sleep.call_count >= 1 # Should be called at least once
3436

3537
@pytest.mark.asyncio
3638
async def test_process_two():

tests/utils/test_utils_init.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Ensure src/ is in sys.path for imports
88
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../src')))
99

10-
from utils import chatloop
10+
from src.utils import chatloop
1111

1212
def test_chatloop_decorator_creation():
1313
"""Test that the chatloop decorator returns a proper wrapper function."""
@@ -96,4 +96,74 @@ async def test_func(user_input):
9696
await wrapped()
9797

9898
# Verify function was called twice (once for each input)
99-
assert call_count == 2
99+
assert call_count == 2
100+
101+
@pytest.mark.asyncio
102+
async def test_chatloop_basic_execution():
103+
"""Test the chatloop decorator runs a function once and exits on KeyboardInterrupt."""
104+
# Create a mock function to be decorated
105+
mock_func = MagicMock()
106+
mock_func.return_value = asyncio.Future()
107+
mock_func.return_value.set_result("Test response")
108+
109+
# Apply the decorator
110+
decorated = chatloop("TestChat")(mock_func)
111+
112+
# Mock input/print functions and simulate KeyboardInterrupt after first iteration
113+
with patch('builtins.input', side_effect=["Test input", KeyboardInterrupt()]):
114+
with patch('builtins.print') as mock_print:
115+
await decorated("arg1", kwarg1="value1")
116+
117+
# Verify the function was called with correct parameters
118+
mock_func.assert_called_once_with("Test input", "arg1", kwarg1="value1")
119+
120+
# Verify output was printed
121+
assert any("Test response" in str(call) for call in mock_print.call_args_list)
122+
123+
@pytest.mark.asyncio
124+
async def test_chatloop_exception_handling():
125+
"""Test the chatloop decorator handles exceptions properly."""
126+
# Create a mock function that raises an exception
127+
mock_func = MagicMock()
128+
mock_func.side_effect = [Exception("Test error"), KeyboardInterrupt()]
129+
130+
# Apply the decorator
131+
decorated = chatloop("TestChat")(mock_func)
132+
133+
# Mock input/print and execute
134+
with patch('builtins.input', return_value="Test input"):
135+
with patch('builtins.print') as mock_print:
136+
await decorated()
137+
138+
# Verify error was printed
139+
assert any("Error: Test error" in str(call) for call in mock_print.call_args_list)
140+
141+
@pytest.mark.asyncio
142+
async def test_chatloop_multiple_iterations():
143+
"""Test the chatloop decorator handles multiple chat iterations."""
144+
# Create a sequence of responses
145+
mock_func = MagicMock()
146+
response_future1 = asyncio.Future()
147+
response_future1.set_result("Response 1")
148+
response_future2 = asyncio.Future()
149+
response_future2.set_result("Response 2")
150+
151+
mock_func.side_effect = [response_future1, response_future2]
152+
153+
# Apply the decorator
154+
decorated = chatloop("TestChat")(mock_func)
155+
156+
# Mock inputs and simulate KeyboardInterrupt after second iteration
157+
with patch('builtins.input', side_effect=["Input 1", "Input 2", KeyboardInterrupt()]):
158+
with patch('builtins.print') as mock_print:
159+
await decorated()
160+
161+
# Verify the function was called twice with correct inputs
162+
assert mock_func.call_count == 2
163+
mock_func.assert_any_call("Input 1")
164+
mock_func.assert_any_call("Input 2")
165+
166+
# Verify both responses were printed
167+
printed_strings = [str(call) for call in mock_print.call_args_list]
168+
assert any("Response 1" in s for s in printed_strings)
169+
assert any("Response 2" in s for s in printed_strings)

0 commit comments

Comments
 (0)