Proyecto LCX Dispatcharr multicuenta
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserPreferencesAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="testuser",
|
||||
password="testpass123",
|
||||
user_level=10
|
||||
)
|
||||
self.client = APIClient()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
self.me_url = "/api/accounts/users/me/"
|
||||
|
||||
def test_get_me_returns_user_data(self):
|
||||
"""Test GET /me/ returns current user data"""
|
||||
response = self.client.get(self.me_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["username"], "testuser")
|
||||
|
||||
def test_patch_me_updates_custom_properties(self):
|
||||
"""Test PATCH /me/ updates custom_properties"""
|
||||
nav_order = ["channels", "vods", "sources", "guide", "dvr", "stats"]
|
||||
data = {
|
||||
"custom_properties": {
|
||||
"navOrder": nav_order
|
||||
}
|
||||
}
|
||||
|
||||
response = self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["custom_properties"]["navOrder"], nav_order)
|
||||
|
||||
# Verify database was updated
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.custom_properties["navOrder"], nav_order)
|
||||
|
||||
def test_patch_me_nav_order_persists(self):
|
||||
"""Test navOrder persists and returns correctly"""
|
||||
nav_order = ["settings", "channels", "vods"]
|
||||
data = {
|
||||
"custom_properties": {
|
||||
"navOrder": nav_order
|
||||
}
|
||||
}
|
||||
|
||||
# Update
|
||||
self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
# Fetch again
|
||||
response = self.client.get(self.me_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["custom_properties"]["navOrder"], nav_order)
|
||||
|
||||
def test_patch_me_partial_update_preserves_other_properties(self):
|
||||
"""Test partial update merges into existing custom_properties, preserving other keys"""
|
||||
# Set initial custom_properties
|
||||
self.user.custom_properties = {
|
||||
"theme": "dark",
|
||||
"someOtherSetting": True
|
||||
}
|
||||
self.user.save()
|
||||
|
||||
# Update only navOrder - send delta, not full object
|
||||
nav_order = ["channels", "vods"]
|
||||
data = {
|
||||
"custom_properties": {
|
||||
"navOrder": nav_order
|
||||
}
|
||||
}
|
||||
|
||||
response = self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# Backend merge semantics: existing keys are preserved
|
||||
self.assertEqual(response.data["custom_properties"]["navOrder"], nav_order)
|
||||
self.assertEqual(response.data["custom_properties"]["theme"], "dark")
|
||||
self.assertEqual(response.data["custom_properties"]["someOtherSetting"], True)
|
||||
|
||||
def test_patch_me_with_empty_nav_order(self):
|
||||
"""Test PATCH with empty navOrder array"""
|
||||
data = {
|
||||
"custom_properties": {
|
||||
"navOrder": []
|
||||
}
|
||||
}
|
||||
|
||||
response = self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["custom_properties"]["navOrder"], [])
|
||||
|
||||
def test_patch_me_updates_first_name(self):
|
||||
"""Test PATCH /me/ can update other fields like first_name"""
|
||||
data = {
|
||||
"first_name": "Test"
|
||||
}
|
||||
|
||||
response = self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["first_name"], "Test")
|
||||
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.first_name, "Test")
|
||||
|
||||
def test_patch_me_unauthenticated_fails(self):
|
||||
"""Test PATCH /me/ fails for unauthenticated users"""
|
||||
self.client.logout()
|
||||
unauthenticated_client = APIClient()
|
||||
|
||||
data = {
|
||||
"custom_properties": {
|
||||
"navOrder": ["channels"]
|
||||
}
|
||||
}
|
||||
|
||||
response = unauthenticated_client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_patch_me_cannot_escalate_privileges(self):
|
||||
"""Test PATCH /me/ rejects attempts to change user_level or is_staff"""
|
||||
original_level = self.user.user_level
|
||||
|
||||
data = {"user_level": 99, "is_staff": True, "is_superuser": True}
|
||||
response = self.client.patch(self.me_url, data, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.user_level, original_level)
|
||||
self.assertFalse(self.user.is_staff)
|
||||
self.assertFalse(self.user.is_superuser)
|
||||
@@ -0,0 +1,99 @@
|
||||
import sys
|
||||
import os
|
||||
import importlib
|
||||
from django.test import SimpleTestCase
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import redis as redis_module
|
||||
|
||||
# Ensure the scripts directory is importable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
|
||||
|
||||
|
||||
def _import_wait_for_redis():
|
||||
"""Import (or reimport) the wait_for_redis function from scripts/."""
|
||||
import wait_for_redis as module
|
||||
importlib.reload(module)
|
||||
return module.wait_for_redis
|
||||
|
||||
|
||||
class WaitForRedisTests(SimpleTestCase):
|
||||
"""
|
||||
Tests for scripts/wait_for_redis.py.
|
||||
|
||||
Verifies flush behaviour: full flushdb in AIO mode, selective
|
||||
(non-Celery) key deletion in modular mode.
|
||||
"""
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_aio_mode_calls_flushdb(self, mock_redis_cls):
|
||||
"""In AIO mode (default), flushdb is called after successful ping."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.return_value = True
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
with patch.dict(os.environ, {}, clear=False):
|
||||
os.environ.pop('DISPATCHARR_ENV', None)
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=1, retry_interval=0)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_client.flushdb.assert_called_once()
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_modular_mode_does_not_call_flushdb(self, mock_redis_cls):
|
||||
"""In modular mode, flushdb must NOT be called — selective flush instead."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.return_value = True
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
with patch.dict(os.environ, {'DISPATCHARR_ENV': 'modular'}):
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
# Patch after reload so the mock isn't overwritten by module re-execution
|
||||
with patch('wait_for_redis._flush_non_celery_keys') as mock_selective:
|
||||
result = wait_for_redis(max_retries=1, retry_interval=0)
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_client.flushdb.assert_not_called()
|
||||
mock_selective.assert_called_once_with(mock_client)
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_retries_on_connection_error(self, mock_redis_cls):
|
||||
"""Should retry on ConnectionError and eventually succeed."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.side_effect = [
|
||||
redis_module.exceptions.ConnectionError("refused"),
|
||||
redis_module.exceptions.ConnectionError("refused"),
|
||||
True,
|
||||
]
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=5, retry_interval=0)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(mock_client.ping.call_count, 3)
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_returns_false_after_max_retries(self, mock_redis_cls):
|
||||
"""Should return False when max retries are exhausted."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.side_effect = redis_module.exceptions.ConnectionError("refused")
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=2, retry_interval=0)
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
@patch('wait_for_redis.redis.Redis')
|
||||
def test_unexpected_error_returns_false(self, mock_redis_cls):
|
||||
"""Generic exceptions should return False immediately."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.ping.side_effect = RuntimeError("unexpected")
|
||||
mock_redis_cls.return_value = mock_client
|
||||
|
||||
wait_for_redis = _import_wait_for_redis()
|
||||
result = wait_for_redis(max_retries=5, retry_interval=0)
|
||||
|
||||
self.assertFalse(result)
|
||||
Reference in New Issue
Block a user