diff --git a/app/api/tests.py b/app/api/tests.py
index 7ce503c..968a851 100644
--- a/app/api/tests.py
+++ b/app/api/tests.py
@@ -1,3 +1,67 @@
from django.test import TestCase
+from django.urls import reverse
+from rest_framework.test import APITestCase, APIClient
+from rest_framework import status
-# Create your tests here.
+from .routers import router
+
+
+class RouterTestCase(TestCase):
+ def test_router_urls(self):
+ """Test that the router generates the expected URL patterns"""
+ url_patterns = router.urls
+
+ # Check that all expected viewsets are registered
+ expected_basenames = ['user', 'torrent', 'file', 'friend-request']
+ registered_basenames = [url.name.split('-')[0] for url in url_patterns if '-list' in url.name]
+
+ for basename in expected_basenames:
+ self.assertIn(basename, registered_basenames)
+
+ # Check that list and detail URLs are generated for each viewset
+ for basename in expected_basenames:
+ list_url_name = f"{basename}-list"
+ detail_url_name = f"{basename}-detail"
+
+ self.assertTrue(any(url.name == list_url_name for url in url_patterns))
+ self.assertTrue(any(url.name == detail_url_name for url in url_patterns))
+
+
+class APIEndpointsTestCase(APITestCase):
+ def setUp(self):
+ from user.models import User
+
+ # Create a test user
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+
+ # Authenticate the client
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ def test_user_endpoint(self):
+ """Test that the users endpoint is accessible"""
+ url = reverse('user-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_torrent_endpoint(self):
+ """Test that the torrents endpoint is accessible"""
+ url = reverse('torrent-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_file_endpoint(self):
+ """Test that the files endpoint is accessible"""
+ url = reverse('file-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_friend_request_endpoint(self):
+ """Test that the friend requests endpoint is accessible"""
+ url = reverse('friendrequest-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
diff --git a/app/frontend/src/components/torrent/App.vue b/app/frontend/src/components/torrent/App.vue
index d69f367..fac9fac 100644
--- a/app/frontend/src/components/torrent/App.vue
+++ b/app/frontend/src/components/torrent/App.vue
@@ -15,7 +15,7 @@
- {{dm_status}}
+
@@ -157,6 +157,7 @@ export default {
this.dm_status = data;
})
this.$qt.on("upload_torrent", data => {
+ console.log(typeof data);
this.uploadTorrentB64(data);
})
this.$qt.on("files_updated", data => {
@@ -220,7 +221,8 @@ export default {
},
async uploadTorrentB64(b64_torrent){
let form = new FormData();
- form.append("torrent", b64_torrent);
+ form.append("file", b64_torrent);
+ form.append("file_mode", "base64");
let response = await fetch("/api/torrents/", {
method: "POST",
headers: {
diff --git a/app/frontend/src/components/torrent/DM.vue b/app/frontend/src/components/torrent/DM.vue
index c10b493..e8d06d2 100644
--- a/app/frontend/src/components/torrent/DM.vue
+++ b/app/frontend/src/components/torrent/DM.vue
@@ -6,12 +6,19 @@
{{ dm_status.pause ? 'Start':'Pause' }}
+
{{fs_speed_format(dm_status.speed)}}/s
@@ -42,7 +49,7 @@ import {fs_format, fs_speed_format} from "@/plugins/utils.js";
export default {
- methods: {fs_format, fs_speed_format},
+
props: {
dm_files: {
required: true,
@@ -60,6 +67,20 @@ export default {
data(){
return {
details_enabled: false,
+ start_available: true
+ }
+ },
+ methods: {
+ fs_format, fs_speed_format,
+ setPause(pause){
+ this.start_available = false
+ this.$qt.callMethod('set_pause', pause);
+ if(pause){
+ setTimeout(() => {this.start_available = true;}, 1000);
+ }else{
+ this.start_available = true;
+ }
+
}
},
computed: {
diff --git a/app/frontend/src/components/torrent/DMDetails.vue b/app/frontend/src/components/torrent/DMDetails.vue
index ed9cf4e..506d114 100644
--- a/app/frontend/src/components/torrent/DMDetails.vue
+++ b/app/frontend/src/components/torrent/DMDetails.vue
@@ -14,8 +14,8 @@
{{ file.rel_path }}
-
+
+ {{file.stats.percent.toFixed(1)}}% ({{fs_speed_format(file.stats.speed)}}/s)
@@ -97,9 +97,10 @@ export default {
for(const [file_id, file] of Object.entries(this.dm_files)){
if(file.downloaded){
finished[file_id] = file;
- }else if(file_id in this.dm_status.downloading){
+ }else if(this.dm_status.downloading.includes(file_id)){
downloading[file_id] = file;
if(file_id in this.dm_status["downloader_stats"]){
+
downloading[file_id]["stats"] = this.dm_status["downloader_stats"][file_id];
}else{
downloading[file_id]["stats"] = {
diff --git a/app/torrent/tests.py b/app/torrent/tests.py
index 7ce503c..516abd4 100644
--- a/app/torrent/tests.py
+++ b/app/torrent/tests.py
@@ -1,3 +1,328 @@
from django.test import TestCase
+from django.urls import reverse
+from rest_framework.test import APITestCase, APIClient
+from rest_framework import status
+from django.conf import settings
+from unittest.mock import patch, MagicMock
-# Create your tests here.
+from .models import Torrent, SharedUser, File
+from user.models import User
+from .views import TorrentViewSet, FileViewSet
+from .utils import Transmission, torrent_proceed, torrent_share
+
+
+class TorrentModelTestCase(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.torrent = Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=1000,
+ transmission_data={}
+ )
+ self.shared_user = User.objects.create_user(
+ username='shareduser',
+ email='shared@example.com',
+ password='sharedpassword'
+ )
+ self.file = File.objects.create(
+ torrent=self.torrent,
+ rel_name='test_file.txt',
+ size=100
+ )
+
+ def test_len_files(self):
+ """Test the len_files property returns the correct count of files"""
+ self.assertEqual(self.torrent.len_files, 1)
+
+ # Add another file and test again
+ File.objects.create(
+ torrent=self.torrent,
+ rel_name='another_file.txt',
+ size=200
+ )
+
+ # Clear cached_property
+ if hasattr(self.torrent, '_len_files'):
+ delattr(self.torrent, '_len_files')
+
+ self.assertEqual(self.torrent.len_files, 2)
+
+ def test_related_users(self):
+ """Test the related_users property returns the correct list of users"""
+ # Initially only the owner
+ self.assertEqual(self.torrent.related_users, [self.user.id])
+
+ # Add a shared user
+ self.torrent.shared_users.add(self.shared_user)
+
+ # Clear cached_property
+ if hasattr(self.torrent, '_related_users'):
+ delattr(self.torrent, '_related_users')
+
+ # Should include both users now
+ self.assertIn(self.user.id, self.torrent.related_users)
+ self.assertIn(self.shared_user.id, self.torrent.related_users)
+
+
+class FileModelTestCase(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.torrent = Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=1000,
+ transmission_data={}
+ )
+ self.file = File.objects.create(
+ torrent=self.torrent,
+ rel_name='test/path/file.mp4',
+ size=100
+ )
+
+ def test_pathname(self):
+ """Test the pathname property returns the correct path"""
+ self.assertEqual(str(self.file.pathname), 'test/path/file.mp4')
+
+ def test_filename(self):
+ """Test the filename property returns the correct filename"""
+ self.assertEqual(self.file.filename, 'file.mp4')
+
+ def test_abs_pathname(self):
+ """Test the abs_pathname property returns the correct absolute path"""
+ expected_path = settings.DOWNLOAD_BASE_DIR / self.file.pathname
+ self.assertEqual(self.file.abs_pathname, expected_path)
+
+ def test_is_video(self):
+ """Test the is_video property correctly identifies video files"""
+ self.assertTrue(self.file.is_video) # mp4 should be identified as video
+
+ # Test non-video file
+ non_video_file = File.objects.create(
+ torrent=self.torrent,
+ rel_name='test/path/document.pdf',
+ size=50
+ )
+ self.assertFalse(non_video_file.is_video)
+
+
+class SharedUserModelTestCase(TestCase):
+ def setUp(self):
+ self.owner = User.objects.create_user(
+ username='owner',
+ email='owner@example.com',
+ password='ownerpassword'
+ )
+ self.shared_user = User.objects.create_user(
+ username='shareduser',
+ email='shared@example.com',
+ password='sharedpassword'
+ )
+ self.torrent = Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.owner,
+ size=1000,
+ transmission_data={}
+ )
+
+ def test_shared_user_creation(self):
+ """Test creating a shared user relationship"""
+ shared = SharedUser.objects.create(
+ user=self.shared_user,
+ torrent=self.torrent
+ )
+ self.assertEqual(shared.user, self.shared_user)
+ self.assertEqual(shared.torrent, self.torrent)
+
+ # Verify the relationship is reflected in the torrent's shared_users
+ self.assertIn(self.shared_user, self.torrent.shared_users.all())
+
+
+class TorrentViewSetTestCase(APITestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ self.torrent = Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=1000,
+ transmission_data={}
+ )
+
+ self.file = File.objects.create(
+ torrent=self.torrent,
+ rel_name='test_file.txt',
+ size=100
+ )
+
+ def test_list_torrents(self):
+ """Test listing torrents"""
+ url = reverse('torrent-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+ self.assertEqual(response.data[0]['id'], self.torrent.id)
+
+ def test_retrieve_torrent(self):
+ """Test retrieving a specific torrent"""
+ url = reverse('torrent-detail', args=[self.torrent.id])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['id'], self.torrent.id)
+ self.assertEqual(response.data['name'], 'Test Torrent')
+
+ @patch('torrent.views.torrent_share')
+ def test_share_torrent(self, mock_torrent_share):
+ """Test sharing a torrent with another user"""
+ mock_torrent_share.return_value = True
+
+ shared_user = User.objects.create_user(
+ username='shareduser',
+ email='shared@example.com',
+ password='sharedpassword'
+ )
+
+ url = reverse('torrent-share', args=[self.torrent.id])
+ response = self.client.post(url, {'user_id': shared_user.id})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertTrue(response.data['success'])
+ mock_torrent_share.assert_called_once()
+
+
+class FileViewSetTestCase(APITestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ self.torrent = Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=1000,
+ transmission_data={}
+ )
+
+ self.file = File.objects.create(
+ torrent=self.torrent,
+ rel_name='test_file.txt',
+ size=100
+ )
+
+ def test_list_files(self):
+ """Test listing files"""
+ url = reverse('file-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_retrieve_file(self):
+ """Test retrieving a specific file"""
+ url = reverse('file-detail', args=[self.file.id])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['id'], str(self.file.id))
+ self.assertEqual(response.data['rel_name'], 'test_file.txt')
+
+
+class TransmissionUtilsTestCase(TestCase):
+ @patch('torrent.utils.Client')
+ def test_transmission_init(self, mock_client):
+ """Test Transmission class initialization"""
+ transmission = Transmission()
+ mock_client.assert_called_once_with(**settings.TRANSMISSION)
+
+ @patch('torrent.utils.Client')
+ def test_add_torrent(self, mock_client):
+ """Test adding a torrent"""
+ mock_instance = mock_client.return_value
+ mock_instance.add_torrent.return_value = MagicMock()
+
+ transmission = Transmission()
+ file_obj = MagicMock()
+ result = transmission.add_torrent(file_obj)
+
+ mock_instance.add_torrent.assert_called_once_with(file_obj)
+ self.assertEqual(result, mock_instance.add_torrent.return_value)
+
+ @patch('torrent.utils.Client')
+ def test_get_data(self, mock_client):
+ """Test getting torrent data"""
+ mock_instance = mock_client.return_value
+ mock_torrent = MagicMock()
+ mock_torrent.progress = 50
+ mock_torrent.fields = {'name': 'Test', 'size': 1000}
+ mock_instance.get_torrent.return_value = mock_torrent
+
+ transmission = Transmission()
+ result = transmission.get_data('hash123')
+
+ mock_instance.get_torrent.assert_called_once_with('hash123', transmission.trpc_args)
+ self.assertEqual(result['progress'], 50)
+ self.assertEqual(result['name'], 'Test')
+ self.assertEqual(result['size'], 1000)
+
+
+class TorrentProceedTestCase(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword',
+ max_size=10000
+ )
+
+ @patch('torrent.utils.transmission_handler')
+ def test_torrent_proceed_size_exceed(self, mock_transmission):
+ """Test torrent_proceed when user size is exceeded"""
+ # Set user's used size to exceed max_size
+ self.user.max_size = 100
+ Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=200, # Exceeds max_size
+ transmission_data={}
+ )
+
+ file_obj = MagicMock()
+ result = torrent_proceed(self.user, file_obj)
+
+ self.assertEqual(result['status'], 'error')
+ self.assertEqual(result['message'], 'Size exceed')
+ mock_transmission.add_torrent.assert_not_called()
+
+ @patch('torrent.utils.transmission_handler')
+ def test_torrent_proceed_transmission_error(self, mock_transmission):
+ """Test torrent_proceed when transmission raises an error"""
+ from transmission_rpc.error import TransmissionError
+
+ mock_transmission.add_torrent.side_effect = TransmissionError('Test error')
+
+ file_obj = MagicMock()
+ result = torrent_proceed(self.user, file_obj)
+
+ self.assertEqual(result['status'], 'error')
+ self.assertEqual(result['message'], 'Transmission Error')
diff --git a/app/torrent/utils.py b/app/torrent/utils.py
index 0fc3af4..8d3c820 100644
--- a/app/torrent/utils.py
+++ b/app/torrent/utils.py
@@ -1,6 +1,9 @@
from django.conf import settings
import traceback
+import base64
+import io
+
from transmission_rpc import Client
from transmission_rpc.error import TransmissionError
@@ -18,8 +21,15 @@ class Transmission:
def __init__(self):
self.client = Client(**settings.TRANSMISSION)
- def add_torrent(self, file):
- return self.client.add_torrent(file)
+ def add_torrent(self, file, file_mode="file_object"):
+ match file_mode:
+ case "file_object":
+ return self.client.add_torrent(file)
+ case "base64":
+ file_content = base64.b64decode(file)
+ file_obj = io.BytesIO(file_content)
+ return self.client.add_torrent(file_obj)
+ return None
def get_data(self, hash_string):
data = self.client.get_torrent(hash_string, self.trpc_args)
@@ -45,7 +55,7 @@ class Transmission:
transmission_handler = Transmission()
-def torrent_proceed(user, file):
+def torrent_proceed(user, file, file_mode="file_object"):
r = {
"torrent": None,
"status": "error",
@@ -58,7 +68,7 @@ def torrent_proceed(user, file):
return r
try:
- torrent_uploaded = transmission_handler.add_torrent(file)
+ torrent_uploaded = transmission_handler.add_torrent(file, file_mode=file_mode)
except TransmissionError:
print(traceback.format_exc())
r["message"] = "Transmission Error"
@@ -74,7 +84,7 @@ def torrent_proceed(user, file):
if torrent.user == user:
r["message"] = "Already exist"
return r
- elif torrent.shared_users.filter(user=user).exists():
+ elif torrent.shared_users.filter(id=user.id).exists():
r["message"] = "Already shared"
return r
else:
diff --git a/app/torrent/views.py b/app/torrent/views.py
index 969b0d8..033ef1e 100644
--- a/app/torrent/views.py
+++ b/app/torrent/views.py
@@ -2,7 +2,7 @@ from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.db.models import Q, Count, OuterRef
+from django.db.models import Q, Count, OuterRef, Sum
from django.db.models.functions import Coalesce
from django.http import HttpResponse, Http404, StreamingHttpResponse
@@ -152,7 +152,9 @@ class TorrentViewSet(mixins.CreateModelMixin,
def create(self, request, *args, **kwargs):
file = request.data["file"]
- r = torrent_proceed(self.request.user, file)
+ file_mode = request.data.get("file_mode", "file_object")
+ r = torrent_proceed(self.request.user, file, file_mode)
+
if r["torrent"]:
r["torrent"] = self.get_serializer_class()(r["torrent"]).data
return Response(r)
diff --git a/app/user/tests.py b/app/user/tests.py
index 7ce503c..3e82271 100644
--- a/app/user/tests.py
+++ b/app/user/tests.py
@@ -1,3 +1,316 @@
from django.test import TestCase
+from django.urls import reverse
+from rest_framework.test import APITestCase, APIClient
+from rest_framework import status
+from unittest.mock import patch, MagicMock
-# Create your tests here.
+from .models import User, FriendRequest, Invitation, UsernameUserManager
+from torrent.models import Torrent
+from .views import UserViewSet, FriendRequestViewSet
+
+
+class UserModelTestCase(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword',
+ max_size=1000000
+ )
+ self.friend = User.objects.create_user(
+ username='frienduser',
+ email='friend@example.com',
+ password='friendpassword'
+ )
+
+ def test_size_used_property(self):
+ """Test the size_used property returns the correct total size of user's torrents"""
+ # Initially no torrents, so size should be 0
+ self.assertEqual(self.user.size_used, 0)
+
+ # Create a torrent for the user
+ Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=5000,
+ transmission_data={}
+ )
+
+ # Create another torrent
+ Torrent.objects.create(
+ id='def456',
+ name='Another Torrent',
+ user=self.user,
+ size=3000,
+ transmission_data={}
+ )
+
+ # Clear cached_property if it exists
+ if hasattr(self.user, 'total_size'):
+ delattr(self.user, 'total_size')
+
+ # Size used should be the sum of torrent sizes
+ self.assertEqual(self.user.size_used, 8000)
+
+ def test_min_infos_property(self):
+ """Test the min_infos property returns the correct user info"""
+ expected_info = {
+ 'username': 'testuser',
+ 'id': self.user.id
+ }
+ self.assertEqual(self.user.min_infos, expected_info)
+
+
+class UsernameUserManagerTestCase(TestCase):
+ def test_create_user(self):
+ """Test creating a regular user"""
+ user = User.objects.create_user(
+ username='newuser',
+ email='new@example.com',
+ password='newpassword'
+ )
+ self.assertFalse(user.is_staff)
+ self.assertFalse(user.is_superuser)
+ self.assertEqual(user.username, 'newuser')
+ self.assertEqual(user.email, 'new@example.com')
+ self.assertTrue(user.check_password('newpassword'))
+
+ def test_create_superuser(self):
+ """Test creating a superuser"""
+ admin = User.objects.create_superuser(
+ username='admin',
+ email='admin@example.com',
+ password='adminpassword'
+ )
+ self.assertTrue(admin.is_staff)
+ self.assertTrue(admin.is_superuser)
+ self.assertEqual(admin.username, 'admin')
+ self.assertEqual(admin.email, 'admin@example.com')
+
+ def test_create_user_without_username(self):
+ """Test creating a user without username raises error"""
+ with self.assertRaises(ValueError):
+ User.objects.create_user(
+ username='',
+ email='test@example.com',
+ password='testpassword'
+ )
+
+ def test_create_user_without_email(self):
+ """Test creating a user without email raises error"""
+ with self.assertRaises(ValueError):
+ User.objects.create_user(
+ username='testuser',
+ email='',
+ password='testpassword'
+ )
+
+
+class FriendRequestModelTestCase(TestCase):
+ def setUp(self):
+ self.sender = User.objects.create_user(
+ username='sender',
+ email='sender@example.com',
+ password='senderpassword'
+ )
+ self.receiver = User.objects.create_user(
+ username='receiver',
+ email='receiver@example.com',
+ password='receiverpassword'
+ )
+
+ def test_friend_request_creation(self):
+ """Test creating a friend request"""
+ friend_request = FriendRequest.objects.create(
+ sender=self.sender,
+ receiver=self.receiver
+ )
+ self.assertEqual(friend_request.sender, self.sender)
+ self.assertEqual(friend_request.receiver, self.receiver)
+
+ def test_unique_together_constraint(self):
+ """Test that the unique_together constraint works"""
+ FriendRequest.objects.create(
+ sender=self.sender,
+ receiver=self.receiver
+ )
+
+ # Creating another request with the same sender and receiver should raise an error
+ with self.assertRaises(Exception):
+ FriendRequest.objects.create(
+ sender=self.sender,
+ receiver=self.receiver
+ )
+
+
+class InvitationModelTestCase(TestCase):
+ def setUp(self):
+ self.creator = User.objects.create_user(
+ username='creator',
+ email='creator@example.com',
+ password='creatorpassword'
+ )
+
+ def test_invitation_creation(self):
+ """Test creating an invitation"""
+ invitation = Invitation.objects.create(
+ created_by=self.creator
+ )
+ self.assertEqual(invitation.created_by, self.creator)
+ self.assertIsNotNone(invitation.token)
+ self.assertIsNone(invitation.user)
+
+ def test_invitation_assignment(self):
+ """Test assigning an invitation to a user"""
+ invitation = Invitation.objects.create(
+ created_by=self.creator
+ )
+
+ new_user = User.objects.create_user(
+ username='newuser',
+ email='new@example.com',
+ password='newpassword'
+ )
+
+ invitation.user = new_user
+ invitation.save()
+
+ # Refresh from database
+ invitation.refresh_from_db()
+ self.assertEqual(invitation.user, new_user)
+
+
+class UserViewSetTestCase(APITestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.friend = User.objects.create_user(
+ username='frienduser',
+ email='friend@example.com',
+ password='friendpassword'
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ def test_list_users(self):
+ """Test listing users"""
+ url = reverse('user-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 2) # Should include both users
+
+ def test_retrieve_user(self):
+ """Test retrieving a specific user"""
+ url = reverse('user-detail', args=[self.friend.id])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['username'], 'frienduser')
+
+ def test_add_friend_request(self):
+ """Test adding a friend request"""
+ url = reverse('user-add-friend-request', args=[self.friend.username])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertTrue(response.data['success'])
+ self.assertEqual(response.data['message'], 'Request sent')
+
+ # Verify the friend request was created
+ self.assertTrue(FriendRequest.objects.filter(
+ sender=self.user,
+ receiver=self.friend
+ ).exists())
+
+ def test_add_friend_request_nonexistent_user(self):
+ """Test adding a friend request to a nonexistent user"""
+ url = reverse('user-add-friend-request', args=['nonexistentuser'])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertFalse(response.data['success'])
+ self.assertEqual(response.data['message'], "User 'nonexistentuser' doesn't exist")
+
+ def test_remove_friend(self):
+ """Test removing a friend"""
+ # First add as friend
+ self.user.friends.add(self.friend)
+
+ url = reverse('user-remove-friend', args=[self.friend.id])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertTrue(response.data['success'])
+
+ # Verify the friend was removed
+ self.assertFalse(self.user.friends.filter(id=self.friend.id).exists())
+
+ @patch('user.views.shutil.disk_usage')
+ def test_user_stats(self, mock_disk_usage):
+ """Test getting user stats"""
+ # Mock disk_usage return value
+ mock_disk_usage.return_value = MagicMock(
+ total=1000000,
+ used=500000,
+ free=500000
+ )
+
+ # Create torrents for the user
+ Torrent.objects.create(
+ id='abc123',
+ name='Test Torrent',
+ user=self.user,
+ size=5000,
+ transmission_data={}
+ )
+
+ url = reverse('user-user-stats')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # Check that the response contains the expected fields
+ self.assertIn('torrents_size', response.data)
+ self.assertIn('torrents_len', response.data)
+ self.assertIn('user_max_size', response.data)
+ self.assertIn('disk_total', response.data)
+ self.assertIn('disk_used', response.data)
+ self.assertIn('disk_free', response.data)
+
+
+class FriendRequestViewSetTestCase(APITestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpassword'
+ )
+ self.sender = User.objects.create_user(
+ username='sender',
+ email='sender@example.com',
+ password='senderpassword'
+ )
+ self.client = APIClient()
+ self.client.force_authenticate(user=self.user)
+
+ # Create a friend request
+ self.friend_request = FriendRequest.objects.create(
+ sender=self.sender,
+ receiver=self.user
+ )
+
+ def test_list_friend_requests(self):
+ """Test listing friend requests"""
+ url = reverse('friendrequest-list')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+ self.assertEqual(response.data[0]['sender']['username'], 'sender')
+
+ def test_delete_friend_request(self):
+ """Test deleting a friend request"""
+ url = reverse('friendrequest-detail', args=[self.friend_request.id])
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ # Verify the friend request was deleted
+ self.assertFalse(FriendRequest.objects.filter(id=self.friend_request.id).exists())
diff --git a/app/user/views.py b/app/user/views.py
index 29dcb58..59938b8 100644
--- a/app/user/views.py
+++ b/app/user/views.py
@@ -98,7 +98,7 @@ class UserViewSet(mixins.RetrieveModelMixin,
stats = User.objects.filter(id=request.user.id).aggregate(
total_size=Sum("torrents__size"),
total_torrent=Count("torrents"),
- total_shared_torrent=Count("torrents_shares")
+ total_shared_torrent=Count("torrents_shares", distinct=True),
)
disk_usage = shutil.disk_usage("/")
diff --git a/docker-compose.yml b/docker-compose.yml
index 838594c..63f153f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,6 @@
services:
redis:
- image: redis:alpine
+ image: valkey/valkey:alpine
restart: unless-stopped
web: