""" DELETE /api/m3u/accounts/{id}/ behavior. The endpoint always cascade-deletes auto-created channels owned by the account. Manual channels survive even if some of their streams were owned by the deleted account; only the streams from that account go away with the account, and the channel keeps any streams sourced from other accounts. The legacy ``cleanup_channels`` query parameter is accepted but ignored. """ from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from apps.channels.models import ( Channel, ChannelGroup, ChannelStream, Stream, ) from apps.m3u.models import M3UAccount User = get_user_model() class M3UAccountDestroyTests(TestCase): def setUp(self): self.user = User.objects.create_user( username="destroyer", password="testpass123" ) self.user.user_level = 10 self.user.save() self.client = APIClient() self.client.force_authenticate(user=self.user) # Patching the proxy stop here keeps the Redis-backed call out # of the test path. The endpoint loops over every auto-created # channel before the DB transaction; the real implementation is # exercised by integration tests, not unit tests. self._stop_patch = patch( "apps.proxy.live_proxy.services.channel_service." "ChannelService.stop_channel" ) self._stop_patch.start() self.addCleanup(self._stop_patch.stop) self.account = M3UAccount.objects.create( name="ProviderA", server_url="http://example.com/a.m3u", ) self.other_account = M3UAccount.objects.create( name="ProviderB", server_url="http://example.com/b.m3u", ) self.group = ChannelGroup.objects.create(name="News") def _make_stream(self, account, name="ESPN"): return Stream.objects.create( name=name, url=f"http://example.com/{name.lower()}.m3u8", m3u_account=account, channel_group=self.group, tvg_id=name.lower(), last_seen=timezone.now(), ) def test_cascade_deletes_auto_created_channels(self): # Two auto-created channels under the account being deleted. for n in (101.0, 102.0): ch = Channel.objects.create( channel_number=n, name=f"Auto {n}", channel_group=self.group, auto_created=True, auto_created_by=self.account, ) ChannelStream.objects.create( channel=ch, stream=self._make_stream(self.account, name=f"Auto{int(n)}"), order=0, ) response = self.client.delete( f"/api/m3u/accounts/{self.account.id}/" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["deleted_channels"], 2) self.assertFalse(M3UAccount.objects.filter(id=self.account.id).exists()) self.assertFalse( Channel.objects.filter(auto_created_by_id=self.account.id).exists() ) def test_manual_channel_survives_with_other_provider_streams(self): # Manual channel with one stream from each account. The provider # account's stream goes away with the account; the other # account's stream stays, and the channel survives. manual = Channel.objects.create( channel_number=200.0, name="Manual", channel_group=self.group, auto_created=False, ) provider_stream = self._make_stream(self.account, name="ProviderA") other_stream = self._make_stream(self.other_account, name="ProviderB") ChannelStream.objects.create(channel=manual, stream=provider_stream, order=0) ChannelStream.objects.create(channel=manual, stream=other_stream, order=1) response = self.client.delete( f"/api/m3u/accounts/{self.account.id}/" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["deleted_channels"], 0) manual.refresh_from_db() remaining = list(manual.channelstream_set.values_list("stream__id", flat=True)) self.assertEqual(remaining, [other_stream.id]) def test_legacy_cleanup_channels_param_is_ignored(self): # Behavior must be identical with or without the deprecated # ``cleanup_channels`` query parameter. ch = Channel.objects.create( channel_number=300.0, name="Auto", channel_group=self.group, auto_created=True, auto_created_by=self.account, ) response = self.client.delete( f"/api/m3u/accounts/{self.account.id}/?cleanup_channels=false" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["deleted_channels"], 1) self.assertFalse(Channel.objects.filter(id=ch.id).exists()) def test_no_op_when_account_has_no_auto_created_channels(self): response = self.client.delete( f"/api/m3u/accounts/{self.account.id}/" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["deleted_channels"], 0)