1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 04:36:48 +00:00

test: each app keeps its tests in a tests package

131 test modules sat next to the code they test; instances alone had
51 of them beside views.py and utils.py. They move into a
tests/ package per app, and the JavaScript checks into
webvirtcloud/tests/js. Each non-empty tests.py becomes tests/test_views.py;
the empty generated stubs are removed.

Imports of the app's own modules become absolute, paths built from
__file__ go one level up, the live helper is instances.tests.livetest,
and coverage omits */tests/*. The suite discovers the same 960 tests as
before.
This commit is contained in:
catborise 2026-10-12 10:37:29 +03:00
parent 0906838193
commit c55f52f025
154 changed files with 27 additions and 39 deletions

View file

View file

@ -0,0 +1,50 @@
from django.test import RequestFactory, TestCase
from django.urls import reverse
from appsettings.models import AppSettings
from appsettings.settings import app_settings, get_settings
class AppSettingsTestCase(TestCase):
def setUp(self):
self.client.login(request=RequestFactory().get("/"), username="admin", password="admin")
def test_get_settings_populates_singleton(self):
AppSettings.objects.update_or_create(
key="CUSTOM_TEST_SETTING",
defaults={"value": "test_value_123", "name": "Custom Test Setting"}
)
get_settings()
self.assertEqual(getattr(app_settings, "CUSTOM_TEST_SETTING", None), "test_value_123")
def test_appsettings_view_get(self):
response = self.client.get(reverse("appsettings"))
self.assertEqual(response.status_code, 200)
def test_appsettings_update_setting(self):
setting = AppSettings.objects.get(key="SHOW_ACCESS_ROOT_PASSWORD")
original_value = setting.value
new_val = "False" if original_value == "True" else "True"
response = self.client.post(
reverse("appsettings"),
{
"SHOW_ACCESS_ROOT_PASSWORD": new_val
}
)
self.assertRedirects(response, reverse("appsettings"))
setting.refresh_from_db()
self.assertEqual(setting.value, new_val)
def test_appsettings_non_superuser_forbidden(self):
from django.contrib.auth.models import User
regular_user = User.objects.create_user(username="regular", password="pwd")
self.client.force_login(regular_user)
response = self.client.get(reverse("appsettings"))
self.assertEqual(response.status_code, 403)
def test_clones_are_not_migrated_automatically(self):
# a clone stays on the host of the VM it was made from; the setting
# that sent it to a random host is removed by migration 0015
self.assertFalse(AppSettings.objects.filter(key="CLONE_INSTANCE_AUTO_MIGRATE").exists())

View file

@ -0,0 +1,36 @@
"""get_settings() runs on every request (middleware). A database error must not
leave the loop reading an unbound variable (the old bare except did), and must
not crash the request chain over a transient read."""
from unittest.mock import patch
from django.db import DatabaseError
from django.test import TestCase
from appsettings.models import AppSettings
from appsettings.settings import app_settings, get_settings
class GetSettingsTestCase(TestCase):
def test_a_database_error_does_not_raise(self):
app_settings.MARKER = "kept"
with patch("appsettings.settings.AppSettings.objects") as objects:
objects.all.side_effect = DatabaseError("db down")
get_settings() # must not raise UnboundLocalError or DatabaseError
self.assertEqual(app_settings.MARKER, "kept")
def test_settings_are_loaded_when_the_database_works(self):
AppSettings.objects.update_or_create(key="TEST_MARKER_KEY", defaults={"name": "m", "value": "from-db"})
try:
get_settings()
self.assertEqual(app_settings.TEST_MARKER_KEY, "from-db")
finally:
if hasattr(app_settings, "TEST_MARKER_KEY"):
delattr(app_settings, "TEST_MARKER_KEY")
def test_a_query_that_fails_during_iteration_does_not_raise(self):
app_settings.MARKER2 = "kept"
with patch("appsettings.settings.AppSettings.objects") as objects:
objects.all.return_value.__iter__.side_effect = DatabaseError("cursor died")
get_settings()
self.assertEqual(app_settings.MARKER2, "kept")

View file

@ -0,0 +1,42 @@
"""The default settings rows of the first migrations carry fixed ids, which a
PostgreSQL id sequence does not see. A later migration that inserts without
an id must not collide with them."""
import importlib
import unittest
from types import SimpleNamespace
from django.apps import apps
from django.db import connection
from django.test import TestCase, TransactionTestCase
from appsettings.models import AppSettings
login_lockout = importlib.import_module("appsettings.migrations.0014_login_lockout_settings")
@unittest.skipUnless(connection.vendor == "postgresql", "id sequences are a PostgreSQL concern")
class SequenceAfterFixedIdsTestCase(TransactionTestCase):
serialized_rollback = True
def test_login_lockout_settings_insert_after_fixed_ids(self):
with connection.schema_editor() as schema_editor:
login_lockout.del_settings(apps, schema_editor)
# the state after the fixed-id inserts: rows exist, the sequence starts at 1
with connection.cursor() as cursor:
cursor.execute("SELECT setval(pg_get_serial_sequence('appsettings_appsettings', 'id'), 1, false)")
login_lockout.add_settings(apps, schema_editor)
self.assertEqual(AppSettings.objects.filter(key__startswith="LOGIN_").count(), 2)
nic_type = importlib.import_module("appsettings.migrations.0016_nic_type_rtl8139")
class NicTypeTestCase(TestCase):
def test_the_misspelt_nic_type_becomes_rtl8139(self):
setting = AppSettings.objects.get(key="INSTANCE_NIC_DEFAULT_TYPE")
self.assertEqual(setting.choices, "default,e1000,e1000e,rtl8139,virtio")
AppSettings.objects.filter(pk=setting.pk).update(value="rt18139", choices="default,e1000,e1000e,rt18139,virtio")
nic_type.fix_rtl8139(apps, SimpleNamespace(connection=connection))
setting.refresh_from_db()
self.assertEqual((setting.value, setting.choices), ("rtl8139", "default,e1000,e1000e,rtl8139,virtio"))

View file

@ -0,0 +1,99 @@
"""The theme name posted on the settings page must be one of the themes in
SASS_DIR/wvc-themes: it is written into @import paths and the compiled CSS."""
import os
import shutil
import tempfile
from pathlib import Path
from appsettings.models import AppSettings
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.messages import get_messages
from django.test import TestCase, override_settings
from django.urls import reverse
class ThemeNameTests(TestCase):
def setUp(self):
self.client.force_login(User.objects.create_superuser("th-admin", "th@example.com", "pw"))
self.sass = tempfile.TemporaryDirectory()
self.addCleanup(self.sass.cleanup)
os.makedirs(os.path.join(self.sass.name, "wvc-themes", "flatly"))
AppSettings.objects.filter(key="SASS_DIR").update(value=self.sass.name)
self.before = AppSettings.objects.get(key="BOOTSTRAP_THEME").value
def test_a_name_that_is_not_a_theme_is_refused(self):
for theme in ("../../etc", "flatly';@import '/etc/passwd", "nope", ""):
with self.subTest(theme=theme):
self.client.post(reverse("appsettings"), {"BOOTSTRAP_THEME": theme})
self.assertFalse(os.path.exists(os.path.join(self.sass.name, "wvc-main.scss")))
self.assertEqual(AppSettings.objects.get(key="BOOTSTRAP_THEME").value, self.before)
class SassDirTests(TestCase):
"""SASS_DIR must be a directory inside the project: the page lists it and
writes wvc-main.scss into it."""
def setUp(self):
self.client.force_login(User.objects.create_superuser("sd-admin", "sd@example.com", "pw"))
self.before = AppSettings.objects.get(key="SASS_DIR").value
def test_a_directory_outside_the_project_is_refused(self):
outside = tempfile.TemporaryDirectory()
self.addCleanup(outside.cleanup)
for value in (outside.name, "/etc", "../", "dev/scss/../../..", "no/such/dir"):
with self.subTest(value=value):
self.client.post(reverse("appsettings"), {"SASS_DIR": value})
self.assertEqual(AppSettings.objects.get(key="SASS_DIR").value, self.before)
def test_the_project_scss_directory_is_accepted(self):
self.client.post(reverse("appsettings"), {"SASS_DIR": "dev/scss"})
self.assertEqual(AppSettings.objects.get(key="SASS_DIR").value, "dev/scss")
def test_a_stored_directory_outside_the_project_is_not_used(self):
outside = tempfile.TemporaryDirectory()
self.addCleanup(outside.cleanup)
os.makedirs(os.path.join(outside.name, "wvc-themes", "flatly"))
AppSettings.objects.filter(key="SASS_DIR").update(value=outside.name)
self.client.post(reverse("appsettings"), {"BOOTSTRAP_THEME": "flatly"})
self.assertFalse(os.path.exists(os.path.join(outside.name, "wvc-main.scss")))
class ThemeCssTests(TestCase):
"""A theme change also rewrites the collected wvc-main.min.css: nginx
serves STATIC_ROOT, not static/."""
def setUp(self):
self.client.force_login(User.objects.create_superuser("tc-admin", "tc@example.com", "pw"))
project = tempfile.TemporaryDirectory()
self.addCleanup(project.cleanup)
self.project = Path(project.name)
shutil.copytree(Path(settings.BASE_DIR, "dev", "scss"), self.project / "dev" / "scss")
(self.project / "static" / "css").mkdir(parents=True)
cwd = os.getcwd()
os.chdir(self.project)
self.addCleanup(os.chdir, cwd)
AppSettings.objects.filter(key="SASS_DIR").update(value="dev/scss")
def change_theme(self, static_root):
with override_settings(BASE_DIR=self.project, STATIC_ROOT=static_root):
response = self.client.post(reverse("appsettings"), {"BOOTSTRAP_THEME": "flatly"})
self.assertIn("Theme is changed. Now: flatly", [str(m) for m in get_messages(response.wsgi_request)])
self.assertEqual(AppSettings.objects.get(key="BOOTSTRAP_THEME").value, "flatly")
source = (self.project / "static" / "css" / "wvc-main.min.css").read_text()
self.assertIn(".btn", source)
return source
def test_the_collected_css_is_the_new_theme(self):
served = self.project / "staticfiles" / "css" / "wvc-main.min.css"
served.parent.mkdir(parents=True)
served.write_text("old theme")
self.assertEqual(served.read_text(), "old theme")
self.assertEqual(self.change_theme(self.project / "staticfiles"), served.read_text())
def test_without_collected_css_only_the_source_is_written(self):
(self.project / "staticfiles").mkdir()
self.change_theme(self.project / "staticfiles")
self.assertEqual(list((self.project / "staticfiles").iterdir()), [])
self.change_theme(None)

View file

@ -0,0 +1,33 @@
"""The themes in dev/scss/wvc-themes compile with the pinned libsass, and the
shipped wvc-main.min.css is the default theme compiled from these sources."""
from pathlib import Path
import sass
from django.conf import settings
from django.test import SimpleTestCase
SCSS = Path(settings.BASE_DIR, "dev", "scss")
def compile_theme(theme):
# the imports the settings view compiles
return sass.compile(
string=f"@import '{SCSS}/wvc-themes/{theme}/variables';\n"
f"@import '{SCSS}/bootstrap-overrides.scss';\n"
f"@import '{SCSS}/wvc-themes/{theme}/bootswatch';",
output_style="compressed",
)
class ThemeTests(SimpleTestCase):
def test_every_theme_compiles(self):
themes = sorted(p.name for p in (SCSS / "wvc-themes").iterdir() if p.is_dir())
self.assertIn("zephyr", themes)
for theme in themes:
with self.subTest(theme=theme):
self.assertIn(".btn", compile_theme(theme))
def test_the_shipped_css_is_zephyr_from_these_sources(self):
shipped = Path(settings.BASE_DIR, "static", "css", "wvc-main.min.css").read_text()
self.assertEqual(shipped, compile_theme("zephyr"))