1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/console/tests/test_consolecallback.py
catborise 82af555d45 fix(console): a serial session follows its user's access; a superuser takes over
- The console worker learns whose session it is and checks every ten
  seconds that the user may still open the console: an owner removed or a
  user deactivated meanwhile loses it, with a line in the terminal. Access
  was checked only when the session started, which could last for days.
- One console per VM: a console left open in a forgotten tab locked
  everyone out, superusers included. A superuser's connection now takes
  the console over, and the other window is told so.
2026-10-26 19:46:41 +03:00

82 lines
3.5 KiB
Python

"""The serial console worker: a paste reaches the VM whole although its
stream takes it in parts or is busy, and the stream is dropped once."""
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_loader
from pathlib import Path
from unittest import TestCase
from unittest.mock import MagicMock, patch
from django.contrib.auth import get_user_model
from django.test import TestCase as DjangoTestCase
from accounts.models import UserInstance
from computes.models import Compute
from instances.models import Instance
CONSOLECALLBACK = Path(__file__).resolve().parent.parent.parent / "conf" / "daemon" / "consolecallback"
def load():
loader = SourceFileLoader("consolecallback_under_test", str(CONSOLECALLBACK))
module = module_from_spec(spec_from_loader(loader.name, loader))
loader.exec_module(module)
return module
class ConsoleWorkerTests(TestCase):
def setUp(self):
self.cb = load()
self.console = MagicMock(running=True)
def test_a_paste_reaches_the_vm_whole(self):
received = []
def send(data):
if len(received) % 2 == 0: # busy every other time
received.append(b"")
return -2
received.append(data[:3]) # takes three bytes at a time
return 3
self.console.stream.send.side_effect = send
with patch.object(self.cb.os, "read", return_value=b"echo hello\r"), patch.object(self.cb.time, "sleep"):
self.cb.from_client(None, 0, None, self.console)
self.assertEqual(b"".join(received), b"echo hello\r")
def test_a_vm_that_takes_nothing_does_not_hold_the_console_for_ever(self):
self.console.stream.send.return_value = -2
clock = iter(range(0, 100))
with patch.object(self.cb.os, "read", return_value=b"x"), patch.object(self.cb.time, "sleep"), \
patch.object(self.cb.time, "monotonic", side_effect=lambda: next(clock)):
self.cb.from_client(None, 0, None, self.console)
self.assertLessEqual(self.console.stream.send.call_count, self.cb.SEND_WAIT + 1)
def test_the_stream_is_dropped_once(self):
console = self.cb.Console.__new__(self.cb.Console)
console.stream = stream = MagicMock()
console.close_stream()
console.close_stream() # the other thread, a moment later
stream.abort.assert_called_once_with()
def test_a_closed_client_ends_the_worker(self):
with patch.object(self.cb.os, "read", return_value=b""):
self.cb.from_client(None, 0, None, self.console)
self.assertFalse(self.console.running)
self.console.stream.send.assert_not_called()
class AccessTests(DjangoTestCase):
"""The worker ends a session whose user may no longer open the console."""
def test_an_owner_removed_meanwhile_loses_the_console(self):
cb = load()
compute = Compute.objects.create(name="c", hostname="192.0.2.1", login="u", password="p", type=1)
vm = Instance.objects.create(compute=compute, name="vm-a", uuid="11111111-0000-0000-0000-000000000009")
owner = get_user_model().objects.create_user("owner", password="pw")
ownership = UserInstance.objects.create(user=owner, instance=vm)
with patch.object(cb.db.connection, "close"): # the test's transaction keeps its connection
self.assertTrue(cb.may_still_open(owner.pk, vm))
ownership.delete()
self.assertFalse(cb.may_still_open(owner.pk, vm))
self.assertFalse(cb.may_still_open(999999, vm))