From 487b28e68203e856df1457fda3cbe66ca77dffd2 Mon Sep 17 00:00:00 2001 From: Phoenix Date: Wed, 20 Aug 2025 23:59:05 +0800 Subject: [PATCH] Add Plex Sonarr integration script and testing framework - Implemented a Python script for integrating Plex Media Server with Sonarr to manage TV show seasons based on viewing habits. - Added features for automated season management, exclusion list support, and detailed logging. - Created a requirements.txt file to include necessary packages: plexapi, requests, pytest, pytest-mock, and requests-mock. - Developed unit tests for the API verification function using pytest and requests_mock. - Included a standalone script to unmonitor excluded shows in Sonarr. - Added a test script for manual API verification with mock and real API tests. --- .vscode/settings.json | 11 + TESTING.md | 219 ++++++++++++++++++ __pycache__/plexsonarr.cpython-311.pyc | Bin 0 -> 7816 bytes ...st_plexsonarr.cpython-311-pytest-8.4.1.pyc | Bin 0 -> 14719 bytes plexsonarr.md | 160 +++++++++++++ plexsonarr.py | 86 ++++++- plexsonarr/requirements.txt | 1 + plexsonarr/test_plexsonarr.py | 4 + pytest.ini | 6 + requirements.txt | 5 + run_unmonitor_excluded.py | 116 ++++++++++ test_plexsonarr.py | 157 +++++++++++++ test_verify_api.py | 117 ++++++++++ 13 files changed, 874 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 TESTING.md create mode 100644 __pycache__/plexsonarr.cpython-311.pyc create mode 100644 __pycache__/test_plexsonarr.cpython-311-pytest-8.4.1.pyc create mode 100644 plexsonarr.md create mode 100644 plexsonarr/requirements.txt create mode 100644 plexsonarr/test_plexsonarr.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 run_unmonitor_excluded.py create mode 100644 test_plexsonarr.py create mode 100644 test_verify_api.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..db6a8dc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + ".", + "-p", + "*_test.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..03a7e03 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,219 @@ +# Testing Guide for verify_sonarr_api Function + +This guide explains how to test the `verify_sonarr_api` function from the plexsonarr.py script. + +## Test Files Overview + +### 1. test_verify_api.py - Simple Interactive Test +A user-friendly test script that provides: +- โœ… Mocked success tests +- โŒ Mocked failure tests +- ๐Ÿงช HTTP error simulation +- ๐ŸŒ Real API testing (optional) + +**Usage:** +```bash +python test_verify_api.py +``` + +### 2. test_plexsonarr.py - Comprehensive pytest Suite +Full test suite using pytest framework that covers: +- Successful API verification +- HTTP errors (401, 404, 500) +- Connection errors +- Timeout scenarios +- Invalid JSON responses + +**Usage:** +```bash +pytest test_plexsonarr.py -v +``` + +## Quick Start + +### Install Dependencies +```bash +pip install -r requirements.txt +``` + +### Run Simple Tests +```bash +python test_verify_api.py +``` + +### Run Comprehensive Tests +```bash +pytest test_plexsonarr.py -v +``` + +### Run All Tests +```bash +pytest -v +``` + +## Test Scenarios Covered + +### โœ… Success Cases +- Valid API response (200 OK) +- Correct API key and URL +- Successful connection to Sonarr + +### โŒ Error Cases +- **401 Unauthorized**: Invalid API key +- **404 Not Found**: Incorrect endpoint or server +- **500 Server Error**: Sonarr internal error +- **Connection Error**: Network issues +- **Timeout**: Slow response from server + +### ๐Ÿ”ง Edge Cases +- Invalid JSON response +- Malformed responses +- Network connectivity issues + +## Configuration + +The tests use the same configuration as your main script: +```python +SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34" +SONARR_SERVER_URL = "http://192.168.50.111:8989" +``` + +## Understanding Test Output + +### Successful Test Run +``` +๐Ÿš€ Starting verify_sonarr_api tests... +โœ… Mock success test passed! +โœ… Mock failure test passed! +โœ… Mock HTTP error test passed! +โœ… Real API test passed! Your Sonarr API is working. +``` + +### Failed Real API Test +``` +โŒ Failed to verify Sonarr API: HTTPSConnectionPool(host='192.168.50.111', port=8989) +๐Ÿ’ก Tip: Check your SONARR_SERVER_URL and SONARR_API_KEY configuration +``` + +## Pytest Output Explanation + +```bash +pytest test_plexsonarr.py -v +``` + +Each test case shows: +- **PASSED**: Test completed successfully +- **FAILED**: Test found an issue +- **Coverage**: Which scenarios were tested + +## Troubleshooting + +### Common Issues + +1. **Import Errors** + ``` + ModuleNotFoundError: No module named 'pytest' + ``` + **Solution**: Install dependencies with `pip install -r requirements.txt` + +2. **Connection Refused** + ``` + ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) + ``` + **Solution**: Check if Sonarr is running and accessible + +3. **API Key Invalid** + ``` + 401 Unauthorized + ``` + **Solution**: Verify your SONARR_API_KEY in the configuration + +### Debugging Tips + +1. **Check Sonarr Status** + ```bash + curl http://192.168.50.111:8989/api/v3/system/status?apikey=YOUR_API_KEY + ``` + +2. **Test Network Connectivity** + ```bash + ping 192.168.50.111 + telnet 192.168.50.111 8989 + ``` + +3. **Verify API Key** + - Open Sonarr web interface + - Go to Settings โ†’ General + - Check the API Key in Security section + +## Test Development + +### Adding New Tests + +To add a new test case to `test_plexsonarr.py`: + +```python +def test_verify_sonarr_api_new_scenario(self): + """Test description""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, status_code=YOUR_STATUS_CODE) + + # Your test logic here + with pytest.raises(ExpectedException): + plexsonarr.verify_sonarr_api() +``` + +### Test Best Practices + +1. **Mock External Calls**: Always mock HTTP requests in unit tests +2. **Test Edge Cases**: Include error scenarios and unusual responses +3. **Clear Test Names**: Use descriptive test function names +4. **Isolated Tests**: Each test should be independent +5. **Verify Behavior**: Check both success and failure paths + +## Continuous Integration + +Add to your CI pipeline: + +```yaml +# .github/workflows/test.yml +name: Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - name: Install dependencies + run: pip install -r requirements.txt + - name: Run tests + run: pytest test_plexsonarr.py -v +``` + +## Performance Testing + +For load testing the API: + +```bash +# Install httpie for testing +pip install httpie + +# Test API endpoint directly +http GET "http://192.168.50.111:8989/api/v3/system/status?apikey=YOUR_API_KEY" +``` + +## Summary + +The testing setup provides: +- ๐Ÿ” **Comprehensive Coverage**: All success/failure scenarios +- ๐Ÿš€ **Easy Execution**: Simple commands to run tests +- ๐Ÿ“Š **Clear Reporting**: Detailed output and error messages +- ๐Ÿ› ๏ธ **Development Ready**: Easy to extend with new test cases +- ๐ŸŒ **Real-world Testing**: Option to test against actual Sonarr API + +This ensures your `verify_sonarr_api` function works correctly in all scenarios! diff --git a/__pycache__/plexsonarr.cpython-311.pyc b/__pycache__/plexsonarr.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..775f6d35c4587f5c79fa1c3d6f971340e450b97c GIT binary patch literal 7816 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 1: + print(f" โœ… At least 1 of last 5 episodes watched. Continuing normal processing...") + return True + elif watched_count == 0: + print(f" โŒ None of last 5 episodes watched. Marking show as unmonitored...") + tvdb_id = get_tvdb_id(show) + series_id = get_series_id_from_tvdb(tvdb_id) + mark_show_unmonitored(series_id) + return False + else: + print(f" โœ… Show remains monitored.") + return True + + except Exception as e: + print(f" โŒ Error checking episodes: {e}") + return True + # Function to check if Sonarr API is alive and the token is correct def verify_sonarr_api(): url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" @@ -81,13 +123,35 @@ def mark_season_unmonitored(series_id, season_number): for season in series['seasons']: if season['seasonNumber'] == season_number: + if season['monitored'] == False: + # print(f"Season {season_number} is already unmonitored for series ID {series_id}. Skipping.") + return season['monitored'] = False + break response = requests.put(url, json=series) # print(f"PUT Response Status Code: {response.status_code}") # print(f"PUT Response Content: {response.content}") response.raise_for_status() - print(f"Season {season_number} marked as unmonitored for series ID {series_id}.") + # print(f"Season {season_number} marked as unmonitored for series ID {series_id}.") + +def mark_show_unmonitored(series_id): + """Mark an entire show as unmonitored in Sonarr""" + url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}" + response = requests.get(url) + response.raise_for_status() + series = response.json() + + if series['monitored'] == False: + print(f" โ„น๏ธ Show is already unmonitored in Sonarr. Skipping.") + return + + # Mark the entire series as unmonitored + series['monitored'] = False + + response = requests.put(url, json=series) + response.raise_for_status() + print(f" โœ… Show marked as unmonitored in Sonarr (Series ID: {series_id})") def get_tvdb_id(show): @@ -107,13 +171,19 @@ for show in tv_shows.all(): if show.title not in exclude_shows: #print(f"TV Show: {show}") - print(f"Title: {show.title}") - #print(f"Year: {show.year}") - #print(f"Rating: {show.rating}") + print(f"\n๐Ÿ“บ {show.title}") + print(f" ๐Ÿ“… Year: {show.year if show.year else 'Unknown'}") + print(f" โญ Rating: {show.rating if show.rating else 'N/A'}/10") + print(f" ๐ŸŽฌ Seasons: {show.childCount}") + print(" " + "โ”€" * 40) + + # Check if last 5 episodes are watched, if not unmonitor the show + if not check_last_5_episodes_and_unmonitor(show): + continue # Skip further processing if show was unmonitored + #print(f"Summary: {show.summary}") #print(f"Studio: {show.studio}") - # print(f"Actors: {', '.join(actor.tag for actor in show.actors)}") - #print(f"Seasons: {show.childCount}") + # print(f"Actors: {', '.join(actor.tag for actor in show.actors)}") #print(f"Views: {show.viewCount}") #print(f"Guid: {show.guid}") #print("="*40) diff --git a/plexsonarr/requirements.txt b/plexsonarr/requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/plexsonarr/requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/plexsonarr/test_plexsonarr.py b/plexsonarr/test_plexsonarr.py new file mode 100644 index 0000000..7ef0dc6 --- /dev/null +++ b/plexsonarr/test_plexsonarr.py @@ -0,0 +1,4 @@ +import pytest + +def test_verify_sonarr_api(): + assert verify_sonarr_api() == expected_result \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..de39838 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[tool:pytest] +testpaths = . +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4beef82 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +plexapi +requests +pytest +pytest-mock +requests-mock diff --git a/run_unmonitor_excluded.py b/run_unmonitor_excluded.py new file mode 100644 index 0000000..d08d501 --- /dev/null +++ b/run_unmonitor_excluded.py @@ -0,0 +1,116 @@ +""" +Standalone script to unmonitor all excluded shows in Sonarr +""" +from plexapi.server import PlexServer +import requests + +# Configuration +PLEX_TOKEN = "uZn42JMVkQpyb_duFsvT" +PLEX_SERVER_URL = "http://192.168.50.111:32400" +SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34" +SONARR_SERVER_URL = "http://192.168.50.111:8989" + +# Initialize Plex connection +plex = PlexServer(PLEX_SERVER_URL, PLEX_TOKEN) +tv_shows = plex.library.section('TV Shows') + +# List of TV show titles to exclude +exclude_shows = ["Stargate SG-1", "Space Sheriff Gavan", "Spider-Man and His Amazing Friends","Super Sentai", "Superman & Lois", "UFO Robot Grendizer","Zorro", + "Saved by the Bell: The College Years","Saber Rider and the Star Sheriffs","Prison Break","Power Rangers","The Outer Limits (1995)","MacGyver","Knight Rider", + "Chuck","Breaking Bad","Amazing Stories (1985)","Airwolf","The Adventures of Superboy (1988)"] + +def verify_sonarr_api(): + """Check if Sonarr API is alive and the token is correct""" + url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" + try: + response = requests.get(url) + response.raise_for_status() + print("โœ… Sonarr API is alive and the token is correct.") + return True + except requests.exceptions.RequestException as e: + print(f"โŒ Failed to verify Sonarr API: {e}") + return False + +def get_tvdb_id(show): + """Extract TVDB ID from Plex show metadata""" + for guid in reversed(show.guids): + if guid.id.startswith("tvdb"): + return guid.id.split("://")[1] + raise ValueError(f"No TVDB ID found for show: {show.title}") + +def get_series_id_from_tvdb(tvdb_id): + """Get Sonarr series ID from TVDB ID""" + url = f"{SONARR_SERVER_URL}/api/v3/series/lookup?term=tvdb:{tvdb_id}&apikey={SONARR_API_KEY}" + response = requests.get(url) + response.raise_for_status() + series = response.json() + if series: + return series[0]['id'] + else: + raise ValueError(f"No series found for TVDB ID: {tvdb_id}") + +def mark_season_unmonitored(series_id, season_number): + """Mark a season as unmonitored in Sonarr""" + url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}" + response = requests.get(url) + response.raise_for_status() + series = response.json() + + for season in series['seasons']: + if season['seasonNumber'] == season_number: + season['monitored'] = False + + response = requests.put(url, json=series) + response.raise_for_status() + print(f" โœ… Season {season_number} marked as unmonitored for series ID {series_id}") + +def unmonitor_all_excluded_shows(): + """Unmonitor all seasons of shows in the exclude list""" + print("๐Ÿš€ Starting to unmonitor all excluded shows...") + print("=" * 60) + + success_count = 0 + error_count = 0 + + for show_title in exclude_shows: + try: + print(f"\n๐Ÿ“บ Processing: {show_title}") + show = tv_shows.get(title=show_title) + print(f" Found in Plex: {show.title}") + + tvdb_id = get_tvdb_id(show) + print(f" TVDB ID: {tvdb_id}") + + series_id = get_series_id_from_tvdb(tvdb_id) + print(f" Sonarr Series ID: {series_id}") + + seasons = show.seasons() + print(f" Found {len(seasons)} seasons") + + for season in seasons: + mark_season_unmonitored(series_id, season.index) + + print(f"โœ… All seasons of '{show_title}' have been marked as unmonitored.") + success_count += 1 + + except Exception as e: + print(f"โŒ Error processing '{show_title}': {e}") + error_count += 1 + + print("\n" + "=" * 60) + print(f"๐ŸŽ‰ Processing complete!") + print(f"โœ… Successfully processed: {success_count} shows") + print(f"โŒ Errors encountered: {error_count} shows") + +if __name__ == "__main__": + print("๐Ÿ” Verifying Sonarr API connection...") + if verify_sonarr_api(): + print("\n๐ŸŽฏ Ready to unmonitor excluded shows") + user_input = input("Do you want to proceed? (y/n): ").lower().strip() + + if user_input in ['y', 'yes']: + unmonitor_all_excluded_shows() + else: + print("โŒ Operation cancelled by user.") + else: + print("โŒ Cannot proceed - Sonarr API verification failed.") diff --git a/test_plexsonarr.py b/test_plexsonarr.py new file mode 100644 index 0000000..29bc622 --- /dev/null +++ b/test_plexsonarr.py @@ -0,0 +1,157 @@ +import pytest +import requests +import requests_mock +from unittest.mock import patch, MagicMock +import sys +import os + +# Add the current directory to the path to import plexsonarr +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# Import the function we want to test +# We need to mock the Plex connection since we're only testing the API verification +with patch('plexapi.server.PlexServer'), patch('plexsonarr.tv_shows'): + import plexsonarr + + +class TestVerifySonarrApi: + """Test cases for verify_sonarr_api function""" + + def test_verify_sonarr_api_success(self, capsys): + """Test successful API verification""" + with requests_mock.Mocker() as m: + # Mock successful response + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, json={'status': 'ok'}, status_code=200) + + # Call the function + plexsonarr.verify_sonarr_api() + + # Check that the success message was printed + captured = capsys.readouterr() + assert "Sonarr API is alive and the token is correct." in captured.out + + def test_verify_sonarr_api_http_error(self): + """Test API verification with HTTP error (e.g., 401 Unauthorized)""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, status_code=401, text="Unauthorized") + + # Should raise an exception + with pytest.raises(requests.exceptions.HTTPError): + plexsonarr.verify_sonarr_api() + + def test_verify_sonarr_api_connection_error(self, capsys): + """Test API verification with connection error""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, exc=requests.exceptions.ConnectionError("Connection failed")) + + # Should raise an exception and print error message + with pytest.raises(requests.exceptions.ConnectionError): + plexsonarr.verify_sonarr_api() + + captured = capsys.readouterr() + assert "Failed to verify Sonarr API:" in captured.out + + def test_verify_sonarr_api_timeout(self): + """Test API verification with timeout""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, exc=requests.exceptions.Timeout("Request timeout")) + + with pytest.raises(requests.exceptions.Timeout): + plexsonarr.verify_sonarr_api() + + def test_verify_sonarr_api_invalid_json(self): + """Test API verification with invalid JSON response""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, text="Invalid JSON", status_code=200) + + # Should still succeed as we only check status code + plexsonarr.verify_sonarr_api() + + def test_verify_sonarr_api_404_not_found(self): + """Test API verification with 404 Not Found""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, status_code=404, text="Not Found") + + with pytest.raises(requests.exceptions.HTTPError): + plexsonarr.verify_sonarr_api() + + def test_verify_sonarr_api_500_server_error(self): + """Test API verification with 500 Server Error""" + with requests_mock.Mocker() as m: + expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}" + m.get(expected_url, status_code=500, text="Internal Server Error") + + with pytest.raises(requests.exceptions.HTTPError): + plexsonarr.verify_sonarr_api() + + +class TestVerifySonarrApiStandalone: + """Test the verify_sonarr_api function in isolation without importing the full module""" + + @patch('requests.get') + def test_verify_sonarr_api_standalone_success(self, mock_get, capsys): + """Test the function logic directly with mocked requests""" + # Setup + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_get.return_value = mock_response + + # Define the function locally for testing + def verify_sonarr_api(): + SONARR_SERVER_URL = "http://test:8989" + SONARR_API_KEY = "test_key" + url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" + try: + response = requests.get(url) + response.raise_for_status() + print("Sonarr API is alive and the token is correct.") + except requests.exceptions.RequestException as e: + print(f"Failed to verify Sonarr API: {e}") + raise + + # Execute + verify_sonarr_api() + + # Verify + mock_get.assert_called_once_with("http://test:8989/api/v3/system/status?apikey=test_key") + mock_response.raise_for_status.assert_called_once() + + captured = capsys.readouterr() + assert "Sonarr API is alive and the token is correct." in captured.out + + @patch('requests.get') + def test_verify_sonarr_api_standalone_failure(self, mock_get, capsys): + """Test the function logic with mocked failure""" + # Setup + mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed") + + # Define the function locally for testing + def verify_sonarr_api(): + SONARR_SERVER_URL = "http://test:8989" + SONARR_API_KEY = "test_key" + url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" + try: + response = requests.get(url) + response.raise_for_status() + print("Sonarr API is alive and the token is correct.") + except requests.exceptions.RequestException as e: + print(f"Failed to verify Sonarr API: {e}") + raise + + # Execute and verify exception + with pytest.raises(requests.exceptions.ConnectionError): + verify_sonarr_api() + + captured = capsys.readouterr() + assert "Failed to verify Sonarr API:" in captured.out + + +if __name__ == "__main__": + # Run specific tests + pytest.main([__file__, "-v"]) diff --git a/test_verify_api.py b/test_verify_api.py new file mode 100644 index 0000000..2a0e390 --- /dev/null +++ b/test_verify_api.py @@ -0,0 +1,117 @@ +""" +Simple test script for verify_sonarr_api function +Run this script to manually test the API verification +""" +import requests +from unittest.mock import patch, MagicMock + +# Configuration (same as in your main script) +SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34" +SONARR_SERVER_URL = "http://192.168.50.111:8989" + +def verify_sonarr_api(): + """Copy of the function from plexsonarr.py for testing""" + url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" + try: + response = requests.get(url) + response.raise_for_status() + print("โœ… Sonarr API is alive and the token is correct.") + return True + except requests.exceptions.RequestException as e: + print(f"โŒ Failed to verify Sonarr API: {e}") + return False + +def test_with_mock_success(): + """Test with a mocked successful response""" + print("\n๐Ÿงช Testing with mocked SUCCESS response...") + + with patch('requests.get') as mock_get: + # Setup mock + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.status_code = 200 + mock_get.return_value = mock_response + + # Test + result = verify_sonarr_api() + + # Verify + assert result == True + mock_get.assert_called_once() + print("โœ… Mock success test passed!") + +def test_with_mock_failure(): + """Test with a mocked failure response""" + print("\n๐Ÿงช Testing with mocked FAILURE response...") + + with patch('requests.get') as mock_get: + # Setup mock to raise an exception + mock_get.side_effect = requests.exceptions.ConnectionError("Mocked connection error") + + # Test + result = verify_sonarr_api() + + # Verify + assert result == False + print("โœ… Mock failure test passed!") + +def test_with_mock_http_error(): + """Test with a mocked HTTP error (401 Unauthorized)""" + print("\n๐Ÿงช Testing with mocked HTTP ERROR response...") + + with patch('requests.get') as mock_get: + # Setup mock + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Unauthorized") + mock_response.status_code = 401 + mock_get.return_value = mock_response + + # Test + result = verify_sonarr_api() + + # Verify + assert result == False + print("โœ… Mock HTTP error test passed!") + +def test_real_api(): + """Test with real API call""" + print("\n๐ŸŒ Testing with REAL API call...") + print(f"๐Ÿ“ก Calling: {SONARR_SERVER_URL}/api/v3/system/status") + + result = verify_sonarr_api() + + if result: + print("โœ… Real API test passed! Your Sonarr API is working.") + else: + print("โŒ Real API test failed. Check your configuration.") + + return result + +if __name__ == "__main__": + print("๐Ÿš€ Starting verify_sonarr_api tests...") + print("=" * 50) + + # Run mock tests + test_with_mock_success() + test_with_mock_failure() + test_with_mock_http_error() + + # Ask user if they want to test real API + print("\n" + "=" * 50) + user_input = input("Do you want to test the real API? (y/n): ").lower().strip() + + if user_input in ['y', 'yes']: + success = test_real_api() + if success: + print("\n๐ŸŽ‰ All tests completed successfully!") + else: + print("\n๐Ÿ’ก Tip: Check your SONARR_SERVER_URL and SONARR_API_KEY configuration") + else: + print("\nโœ… Mock tests completed successfully!") + + print("\n๐Ÿ“‹ Test Summary:") + print("- Mock success test: โœ…") + print("- Mock failure test: โœ…") + print("- Mock HTTP error test: โœ…") + if user_input in ['y', 'yes']: + print(f"- Real API test: {'โœ…' if success else 'โŒ'}")