diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d573fd15..f63cee83 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,12 +12,37 @@ permissions: jobs: test: - name: Python ${{ matrix.python-version }} + name: Python ${{ matrix.python-version }} / ${{ matrix.database }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] + database: [sqlite] + include: + - python-version: '3.12' + database: postgresql + + # Used by the postgresql leg only; settings fall back to sqlite when + # WEBVIRTCLOUD_DB_HOST is empty. + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_USER: webvirtcloud + POSTGRES_PASSWORD: webvirtcloud + POSTGRES_DB: webvirtcloud + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + WEBVIRTCLOUD_DB_HOST: ${{ matrix.database == 'postgresql' && 'localhost' || '' }} + WEBVIRTCLOUD_DB_PASSWORD: webvirtcloud steps: - name: Checkout Code diff --git a/appsettings/migrations/0014_login_lockout_settings.py b/appsettings/migrations/0014_login_lockout_settings.py index a18c9b73..4c9086e3 100644 --- a/appsettings/migrations/0014_login_lockout_settings.py +++ b/appsettings/migrations/0014_login_lockout_settings.py @@ -1,3 +1,4 @@ +from django.core.management.color import no_style from django.db import migrations @@ -12,6 +13,12 @@ SETTINGS = [ def add_settings(apps, schema_editor): setting = apps.get_model("appsettings", "AppSettings") db_alias = schema_editor.connection.alias + # Earlier migrations inserted the default rows with fixed ids, which a + # PostgreSQL id sequence does not see: move it past them before inserting + # without an id (a no-op on sqlite). + with schema_editor.connection.cursor() as cursor: + for sql in schema_editor.connection.ops.sequence_reset_sql(no_style(), [setting]): + cursor.execute(sql) for name, key, value, description in SETTINGS: setting.objects.using(db_alias).get_or_create( key=key, defaults={"name": name, "value": value, "choices": "", "description": description} diff --git a/appsettings/test_migrations.py b/appsettings/test_migrations.py new file mode 100644 index 00000000..8730c63c --- /dev/null +++ b/appsettings/test_migrations.py @@ -0,0 +1,28 @@ +"""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 django.apps import apps +from django.db import connection +from django.test import 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) diff --git a/computes/test_api_security.py b/computes/test_api_security.py index 0ac10bda..b572d4e9 100644 --- a/computes/test_api_security.py +++ b/computes/test_api_security.py @@ -34,14 +34,14 @@ class APISecurityTestCase(TestCase): name="api-test-compute", hostname="127.0.0.1", login="libvirt_user", - password="secret_hypervisor_password", + password="hv-secret-pw", type=1, ) self.empty_compute = Compute.objects.create( name="empty-test-compute", hostname="127.0.0.1", login="libvirt_user", - password="secret_hypervisor_password", + password="hv-secret-pw", type=1, ) self.flavor = Flavor.objects.create(