mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-16 22:20:26 +00:00
Extend CI and local test coverage to MySQL and SQLite (#744)
* Add test runner and improve test with multi databases * Enhance cross-database compatibility and testing - Fix TRUNCATE command support detection for different databases - Add conditional PostgreSQL-specific model registration - Improve database-specific test skipping logic - Remove SQLite from TRUNCATE supported vendors list * Add docker compose for testing * Improve CI/CD with multi-database support - Add separate test workflows for SQLite, PostgreSQL, and MySQL * Add `mysqlclient` deps * fix minor - Add mysqlclient deps - upload coverage step * Fix coverage upload name conflicts in CI workflow - Add database type to coverage upload names (SQLite/PostgreSQL/MySQL)
This commit is contained in:
parent
9ef8cf2476
commit
8003b069c9
14 changed files with 385 additions and 116 deletions
30
.github/actions/setup-python-deps/action.yml
vendored
Normal file
30
.github/actions/setup-python-deps/action.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
name: 'Setup Python and Dependencies'
|
||||||
|
description: 'Common setup steps for Python and pip dependencies'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
python-version:
|
||||||
|
description: 'Python version to setup'
|
||||||
|
required: true
|
||||||
|
cache-key-prefix:
|
||||||
|
description: 'Prefix for pip cache key'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
|
||||||
|
- name: Cache pip
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ inputs.cache-key-prefix }}-${{ inputs.python-version }}-${{ hashFiles('**/pyproject.toml') }}
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install --upgrade tox tox-gh-actions
|
||||||
142
.github/workflows/test.yml
vendored
142
.github/workflows/test.yml
vendored
|
|
@ -3,67 +3,125 @@ name: Test
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test-sqlite:
|
||||||
|
name: SQLite • Python ${{ matrix.python-version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 5
|
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python and dependencies
|
||||||
|
uses: ./.github/actions/setup-python-deps
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-key-prefix: sqlite3
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
TEST_DB_BACKEND: sqlite3
|
||||||
|
run: tox -v
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
name: SQLite • Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
test-postgres:
|
||||||
|
name: PostgreSQL • Python ${{ matrix.python-version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:14
|
image: postgres:15
|
||||||
env:
|
env:
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: postgres
|
POSTGRES_DB: auditlog
|
||||||
ports:
|
ports:
|
||||||
- 5432/tcp
|
- 5432/tcp
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd pg_isready
|
--health-cmd pg_isready
|
||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Setup Python and dependencies
|
||||||
uses: actions/setup-python@v5
|
uses: ./.github/actions/setup-python-deps
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-key-prefix: postgresql
|
||||||
|
|
||||||
- name: Get pip cache dir
|
- name: Run tests
|
||||||
id: pip-cache
|
env:
|
||||||
run: |
|
TEST_DB_BACKEND: postgresql
|
||||||
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
|
TEST_DB_HOST: localhost
|
||||||
|
TEST_DB_USER: postgres
|
||||||
|
TEST_DB_PASS: postgres
|
||||||
|
TEST_DB_NAME: auditlog
|
||||||
|
TEST_DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||||
|
|
||||||
- name: Cache
|
run: tox -v
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ${{ steps.pip-cache.outputs.dir }}
|
|
||||||
key:
|
|
||||||
-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
|
|
||||||
restore-keys: |
|
|
||||||
-${{ matrix.python-version }}-v1-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Upload coverage
|
||||||
run: |
|
uses: codecov/codecov-action@v5
|
||||||
python -m pip install --upgrade pip
|
with:
|
||||||
python -m pip install --upgrade tox tox-gh-actions
|
name: PostgreSQL • Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Tox tests
|
test-mysql:
|
||||||
run: |
|
name: MySQL • Python ${{ matrix.python-version }}
|
||||||
tox -v
|
runs-on: ubuntu-latest
|
||||||
env:
|
strategy:
|
||||||
TEST_DB_HOST: localhost
|
fail-fast: false
|
||||||
TEST_DB_USER: postgres
|
matrix:
|
||||||
TEST_DB_PASS: postgres
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
TEST_DB_NAME: postgres
|
services:
|
||||||
TEST_DB_PORT: ${{ job.services.postgres.ports[5432] }}
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
env:
|
||||||
|
MYSQL_DATABASE: auditlog
|
||||||
|
MYSQL_USER: mysql
|
||||||
|
MYSQL_PASSWORD: mysql
|
||||||
|
MYSQL_ROOT_PASSWORD: mysql
|
||||||
|
ports:
|
||||||
|
- 3306/tcp
|
||||||
|
options: >-
|
||||||
|
--health-cmd="sh -c 'export MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\"; mysqladmin ping -h 127.0.0.1 --protocol=TCP -uroot --silent || exit 1'"
|
||||||
|
--health-interval=10s
|
||||||
|
--health-timeout=5s
|
||||||
|
--health-retries=20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Install MySQL client libraries
|
||||||
uses: codecov/codecov-action@v5
|
run: |
|
||||||
with:
|
sudo apt-get update
|
||||||
name: Python ${{ matrix.python-version }}
|
sudo apt-get install -y libmysqlclient-dev pkg-config mysql-client
|
||||||
|
|
||||||
|
- name: Setup Python and dependencies
|
||||||
|
uses: ./.github/actions/setup-python-deps
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-key-prefix: mysql
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
TEST_DB_BACKEND: mysql
|
||||||
|
TEST_DB_HOST: 127.0.0.1
|
||||||
|
TEST_DB_USER: root
|
||||||
|
TEST_DB_PASS: mysql
|
||||||
|
TEST_DB_NAME: auditlog
|
||||||
|
TEST_DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||||
|
run: tox -v
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
name: MySQL • Python ${{ matrix.python-version }}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
|
|
||||||
class TruncateQuery:
|
class TruncateQuery:
|
||||||
SUPPORTED_VENDORS = ("postgresql", "mysql", "sqlite", "oracle", "microsoft")
|
SUPPORTED_VENDORS = ("postgresql", "mysql", "oracle", "microsoft")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def support_truncate_statement(cls, database_vendor) -> bool:
|
def support_truncate_statement(cls, database_vendor) -> bool:
|
||||||
|
|
|
||||||
47
auditlog_tests/docker-compose.yml
Normal file
47
auditlog_tests/docker-compose.yml
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
container_name: auditlog_postgres
|
||||||
|
image: postgres:15
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: auditlog
|
||||||
|
POSTGRES_USER: ${TEST_DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${TEST_DB_PASS}
|
||||||
|
ports:
|
||||||
|
- "${TEST_DB_PORT:-5432}:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U ${TEST_DB_USER} -d auditlog
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
container_name: auditlog_mysql
|
||||||
|
platform: linux/x86_64
|
||||||
|
image: mysql:8.0
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: auditlog
|
||||||
|
MYSQL_USER: ${TEST_DB_USER}
|
||||||
|
MYSQL_PASSWORD: ${TEST_DB_PASS}
|
||||||
|
MYSQL_ROOT_PASSWORD: ${TEST_DB_PASS}
|
||||||
|
ports:
|
||||||
|
- "${TEST_DB_PORT:-3306}:3306"
|
||||||
|
expose:
|
||||||
|
- '${TEST_DB_PORT:-3306}'
|
||||||
|
volumes:
|
||||||
|
- mysql-data:/var/lib/mysql
|
||||||
|
- ./docker/db/init-mysql.sh:/docker-entrypoint-initdb.d/init.sh
|
||||||
|
healthcheck:
|
||||||
|
test: mysqladmin ping -h 127.0.0.1 -u ${TEST_DB_USER} --password=${TEST_DB_PASS}
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
|
mysql-data:
|
||||||
|
driver: local
|
||||||
6
auditlog_tests/docker/db/init-mysql.sh
Executable file
6
auditlog_tests/docker/db/init-mysql.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mysql -u root -p"$MYSQL_ROOT_PASSWORD" <<-EOSQL
|
||||||
|
GRANT ALL PRIVILEGES ON test_auditlog.* to '$MYSQL_USER';
|
||||||
|
EOSQL
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.conf import settings
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
@ -311,26 +311,36 @@ class CharfieldTextfieldModel(models.Model):
|
||||||
history = AuditlogHistoryField(delete_related=True)
|
history = AuditlogHistoryField(delete_related=True)
|
||||||
|
|
||||||
|
|
||||||
class PostgresArrayFieldModel(models.Model):
|
# Only define PostgreSQL-specific models when ArrayField is available
|
||||||
"""
|
if settings.TEST_DB_BACKEND == "postgresql":
|
||||||
Test auditlog with Postgres's ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
"""
|
|
||||||
|
|
||||||
RED = "r"
|
class PostgresArrayFieldModel(models.Model):
|
||||||
YELLOW = "y"
|
"""
|
||||||
GREEN = "g"
|
Test auditlog with Postgres's ArrayField
|
||||||
|
"""
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
RED = "r"
|
||||||
(RED, "Red"),
|
YELLOW = "y"
|
||||||
(YELLOW, "Yellow"),
|
GREEN = "g"
|
||||||
(GREEN, "Green"),
|
|
||||||
)
|
|
||||||
|
|
||||||
arrayfield = ArrayField(
|
STATUS_CHOICES = (
|
||||||
models.CharField(max_length=1, choices=STATUS_CHOICES), size=3
|
(RED, "Red"),
|
||||||
)
|
(YELLOW, "Yellow"),
|
||||||
|
(GREEN, "Green"),
|
||||||
|
)
|
||||||
|
|
||||||
history = AuditlogHistoryField(delete_related=True)
|
arrayfield = ArrayField(
|
||||||
|
models.CharField(max_length=1, choices=STATUS_CHOICES), size=3
|
||||||
|
)
|
||||||
|
|
||||||
|
history = AuditlogHistoryField(delete_related=True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
class PostgresArrayFieldModel(models.Model):
|
||||||
|
class Meta:
|
||||||
|
managed = False
|
||||||
|
|
||||||
|
|
||||||
class NoDeleteHistoryModel(models.Model):
|
class NoDeleteHistoryModel(models.Model):
|
||||||
|
|
@ -448,7 +458,8 @@ auditlog.register(AdditionalDataIncludedModel)
|
||||||
auditlog.register(DateTimeFieldModel)
|
auditlog.register(DateTimeFieldModel)
|
||||||
auditlog.register(ChoicesFieldModel)
|
auditlog.register(ChoicesFieldModel)
|
||||||
auditlog.register(CharfieldTextfieldModel)
|
auditlog.register(CharfieldTextfieldModel)
|
||||||
auditlog.register(PostgresArrayFieldModel)
|
if settings.TEST_DB_BACKEND == "postgresql":
|
||||||
|
auditlog.register(PostgresArrayFieldModel)
|
||||||
auditlog.register(NoDeleteHistoryModel)
|
auditlog.register(NoDeleteHistoryModel)
|
||||||
auditlog.register(JSONModel)
|
auditlog.register(JSONModel)
|
||||||
auditlog.register(NullableJSONModel)
|
auditlog.register(NullableJSONModel)
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@ from unittest import mock
|
||||||
|
|
||||||
import freezegun
|
import freezegun
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
from django.db import connection
|
||||||
from django.test import TestCase, TransactionTestCase
|
from django.test import TestCase, TransactionTestCase
|
||||||
|
from django.test.utils import skipIf
|
||||||
from test_app.models import SimpleModel
|
from test_app.models import SimpleModel
|
||||||
|
|
||||||
|
from auditlog.management.commands.auditlogflush import TruncateQuery
|
||||||
|
|
||||||
|
|
||||||
class AuditlogFlushTest(TestCase):
|
class AuditlogFlushTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -139,6 +143,10 @@ class AuditlogFlushWithTruncateTest(TransactionTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(err, "", msg="No stderr")
|
self.assertEqual(err, "", msg="No stderr")
|
||||||
|
|
||||||
|
@skipIf(
|
||||||
|
not TruncateQuery.support_truncate_statement(connection.vendor),
|
||||||
|
"Database does not support TRUNCATE",
|
||||||
|
)
|
||||||
def test_flush_with_truncate_and_yes(self):
|
def test_flush_with_truncate_and_yes(self):
|
||||||
obj = self.make_object()
|
obj = self.make_object()
|
||||||
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
|
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
|
||||||
|
|
@ -152,6 +160,10 @@ class AuditlogFlushWithTruncateTest(TransactionTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(err, "", msg="No stderr")
|
self.assertEqual(err, "", msg="No stderr")
|
||||||
|
|
||||||
|
@skipIf(
|
||||||
|
not TruncateQuery.support_truncate_statement(connection.vendor),
|
||||||
|
"Database does not support TRUNCATE",
|
||||||
|
)
|
||||||
def test_flush_with_truncate_with_input_yes(self):
|
def test_flush_with_truncate_with_input_yes(self):
|
||||||
obj = self.make_object()
|
obj = self.make_object()
|
||||||
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
|
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
|
||||||
|
|
|
||||||
51
auditlog_tests/test_postgresql.py
Normal file
51
auditlog_tests/test_postgresql.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""
|
||||||
|
PostgreSQL-specific tests for django-auditlog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest import skipIf
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import TestCase
|
||||||
|
from test_app.models import PostgresArrayFieldModel
|
||||||
|
|
||||||
|
|
||||||
|
@skipIf(settings.TEST_DB_BACKEND != "postgresql", "PostgreSQL-specific test")
|
||||||
|
class PostgresArrayFieldModelTest(TestCase):
|
||||||
|
databases = "__all__"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.obj = PostgresArrayFieldModel.objects.create(
|
||||||
|
arrayfield=[PostgresArrayFieldModel.RED, PostgresArrayFieldModel.GREEN],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_array_change(self):
|
||||||
|
return self.obj.history.latest().changes_display_dict["arrayfield"][1]
|
||||||
|
|
||||||
|
def test_changes_display_dict_arrayfield(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.latest_array_change,
|
||||||
|
"Red, Green",
|
||||||
|
msg="The human readable text for the two choices, 'Red, Green' is displayed.",
|
||||||
|
)
|
||||||
|
self.obj.arrayfield = [PostgresArrayFieldModel.GREEN]
|
||||||
|
self.obj.save()
|
||||||
|
self.assertEqual(
|
||||||
|
self.latest_array_change,
|
||||||
|
"Green",
|
||||||
|
msg="The human readable text 'Green' is displayed.",
|
||||||
|
)
|
||||||
|
self.obj.arrayfield = []
|
||||||
|
self.obj.save()
|
||||||
|
self.assertEqual(
|
||||||
|
self.latest_array_change,
|
||||||
|
"",
|
||||||
|
msg="The human readable text '' is displayed.",
|
||||||
|
)
|
||||||
|
self.obj.arrayfield = [PostgresArrayFieldModel.GREEN]
|
||||||
|
self.obj.save()
|
||||||
|
self.assertEqual(
|
||||||
|
self.latest_array_change,
|
||||||
|
"Green",
|
||||||
|
msg="The human readable text 'Green' is displayed.",
|
||||||
|
)
|
||||||
|
|
@ -8,6 +8,8 @@ DEBUG = True
|
||||||
|
|
||||||
SECRET_KEY = "test"
|
SECRET_KEY = "test"
|
||||||
|
|
||||||
|
TEST_DB_BACKEND = os.getenv("TEST_DB_BACKEND", "sqlite3")
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
|
|
@ -20,6 +22,7 @@ INSTALLED_APPS = [
|
||||||
"test_app",
|
"test_app",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
|
@ -28,18 +31,52 @@ MIDDLEWARE = [
|
||||||
"auditlog.middleware.AuditlogMiddleware",
|
"auditlog.middleware.AuditlogMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
DATABASES = {
|
if TEST_DB_BACKEND == "postgresql":
|
||||||
"default": {
|
DATABASES = {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"default": {
|
||||||
"NAME": os.getenv(
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
"TEST_DB_NAME", "auditlog" + os.environ.get("TOX_PARALLEL_ENV", "")
|
"NAME": os.getenv(
|
||||||
),
|
"TEST_DB_NAME", "auditlog" + os.environ.get("TOX_PARALLEL_ENV", "")
|
||||||
"USER": os.getenv("TEST_DB_USER", "postgres"),
|
),
|
||||||
"PASSWORD": os.getenv("TEST_DB_PASS", ""),
|
"USER": os.getenv("TEST_DB_USER", "postgres"),
|
||||||
"HOST": os.getenv("TEST_DB_HOST", "127.0.0.1"),
|
"PASSWORD": os.getenv("TEST_DB_PASS", ""),
|
||||||
"PORT": os.getenv("TEST_DB_PORT", "5432"),
|
"HOST": os.getenv("TEST_DB_HOST", "127.0.0.1"),
|
||||||
|
"PORT": os.getenv("TEST_DB_PORT", "5432"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
elif TEST_DB_BACKEND == "mysql":
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"NAME": os.getenv(
|
||||||
|
"TEST_DB_NAME", "auditlog" + os.environ.get("TOX_PARALLEL_ENV", "")
|
||||||
|
),
|
||||||
|
"USER": os.getenv("TEST_DB_USER", "root"),
|
||||||
|
"PASSWORD": os.getenv("TEST_DB_PASS", ""),
|
||||||
|
"HOST": os.getenv("TEST_DB_HOST", "127.0.0.1"),
|
||||||
|
"PORT": os.getenv("TEST_DB_PORT", "3306"),
|
||||||
|
"OPTIONS": {
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif TEST_DB_BACKEND == "sqlite3":
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": os.getenv(
|
||||||
|
"TEST_DB_NAME",
|
||||||
|
(
|
||||||
|
":memory:"
|
||||||
|
if os.getenv("TOX_PARALLEL_ENV")
|
||||||
|
else "test_auditlog.sqlite3"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported database backend: {TEST_DB_BACKEND}")
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@ import json
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.management import CommandError, call_command
|
from django.core.management import CommandError, call_command
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from django.test.utils import skipIf
|
||||||
from test_app.models import SimpleModel
|
from test_app.models import SimpleModel
|
||||||
|
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
|
@ -124,6 +126,7 @@ class AuditlogMigrateJsonTest(TestCase):
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(call_count, 2)
|
self.assertEqual(call_count, 2)
|
||||||
|
|
||||||
|
@skipIf(settings.TEST_DB_BACKEND != "postgresql", "PostgreSQL-specific test")
|
||||||
def test_native_postgres(self):
|
def test_native_postgres(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
log_entry = self.make_logentry()
|
log_entry = self.make_logentry()
|
||||||
|
|
@ -136,6 +139,7 @@ class AuditlogMigrateJsonTest(TestCase):
|
||||||
self.assertEqual(errbuf, "")
|
self.assertEqual(errbuf, "")
|
||||||
self.assertIsNotNone(log_entry.changes)
|
self.assertIsNotNone(log_entry.changes)
|
||||||
|
|
||||||
|
@skipIf(settings.TEST_DB_BACKEND != "postgresql", "PostgreSQL-specific test")
|
||||||
def test_native_postgres_changes_not_overwritten(self):
|
def test_native_postgres_changes_not_overwritten(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
log_entry = self.make_logentry()
|
log_entry = self.make_logentry()
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ from test_app.models import (
|
||||||
ModelPrimaryKeyModel,
|
ModelPrimaryKeyModel,
|
||||||
NoDeleteHistoryModel,
|
NoDeleteHistoryModel,
|
||||||
NullableJSONModel,
|
NullableJSONModel,
|
||||||
PostgresArrayFieldModel,
|
|
||||||
ProxyModel,
|
ProxyModel,
|
||||||
RelatedModel,
|
RelatedModel,
|
||||||
ReusableThroughRelatedModel,
|
ReusableThroughRelatedModel,
|
||||||
|
|
@ -1717,47 +1716,6 @@ class CharFieldTextFieldModelTest(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PostgresArrayFieldModelTest(TestCase):
|
|
||||||
databases = "__all__"
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.obj = PostgresArrayFieldModel.objects.create(
|
|
||||||
arrayfield=[PostgresArrayFieldModel.RED, PostgresArrayFieldModel.GREEN],
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_array_change(self):
|
|
||||||
return self.obj.history.latest().changes_display_dict["arrayfield"][1]
|
|
||||||
|
|
||||||
def test_changes_display_dict_arrayfield(self):
|
|
||||||
self.assertEqual(
|
|
||||||
self.latest_array_change,
|
|
||||||
"Red, Green",
|
|
||||||
msg="The human readable text for the two choices, 'Red, Green' is displayed.",
|
|
||||||
)
|
|
||||||
self.obj.arrayfield = [PostgresArrayFieldModel.GREEN]
|
|
||||||
self.obj.save()
|
|
||||||
self.assertEqual(
|
|
||||||
self.latest_array_change,
|
|
||||||
"Green",
|
|
||||||
msg="The human readable text 'Green' is displayed.",
|
|
||||||
)
|
|
||||||
self.obj.arrayfield = []
|
|
||||||
self.obj.save()
|
|
||||||
self.assertEqual(
|
|
||||||
self.latest_array_change,
|
|
||||||
"",
|
|
||||||
msg="The human readable text '' is displayed.",
|
|
||||||
)
|
|
||||||
self.obj.arrayfield = [PostgresArrayFieldModel.GREEN]
|
|
||||||
self.obj.save()
|
|
||||||
self.assertEqual(
|
|
||||||
self.latest_array_change,
|
|
||||||
"Green",
|
|
||||||
msg="The human readable text 'Green' is displayed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AdminPanelTest(TestCase):
|
class AdminPanelTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create_user(
|
self.user = User.objects.create_user(
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ django>=4.2,<4.3
|
||||||
sphinx
|
sphinx
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
|
mysqlclient==2.2.5
|
||||||
43
runtests.sh
Executable file
43
runtests.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Run tests against all supported databases
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Default settings
|
||||||
|
export TEST_DB_USER=${TEST_DB_USER:-testuser}
|
||||||
|
export TEST_DB_PASS=${TEST_DB_PASS:-testpass}
|
||||||
|
export TEST_DB_HOST=${TEST_DB_HOST:-127.0.0.1}
|
||||||
|
export TEST_DB_NAME=${TEST_DB_NAME:-auditlog}
|
||||||
|
|
||||||
|
# Cleanup on exit
|
||||||
|
trap 'docker compose -f auditlog_tests/docker-compose.yml down -v --remove-orphans 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
echo "Starting containers..."
|
||||||
|
docker compose -f auditlog_tests/docker-compose.yml up -d
|
||||||
|
|
||||||
|
echo "Waiting for databases..."
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
until docker compose -f auditlog_tests/docker-compose.yml exec postgres pg_isready -U ${TEST_DB_USER} -d auditlog >/dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Waiting for MySQL..."
|
||||||
|
|
||||||
|
until docker compose -f auditlog_tests/docker-compose.yml exec mysql mysqladmin ping -h 127.0.0.1 -u ${TEST_DB_USER} --password=${TEST_DB_PASS} --silent >/dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "Databases ready!"
|
||||||
|
|
||||||
|
# Run tests for each database
|
||||||
|
for backend in sqlite3 postgresql mysql; do
|
||||||
|
echo "Testing $backend..."
|
||||||
|
export TEST_DB_BACKEND=$backend
|
||||||
|
case $backend in
|
||||||
|
postgresql) export TEST_DB_PORT=5432 ;;
|
||||||
|
mysql) export TEST_DB_PORT=3306;;
|
||||||
|
sqlite3) unset TEST_DB_PORT ;;
|
||||||
|
esac
|
||||||
|
tox
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All tests completed!"
|
||||||
15
tox.ini
15
tox.ini
|
|
@ -11,7 +11,7 @@ envlist =
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv =
|
setenv =
|
||||||
COVERAGE_FILE={toxworkdir}/.coverage.{envname}
|
COVERAGE_FILE={toxworkdir}/.coverage.{envname}.{env:TEST_DB_BACKEND}
|
||||||
changedir = auditlog_tests
|
changedir = auditlog_tests
|
||||||
commands =
|
commands =
|
||||||
coverage run --source auditlog ./manage.py test
|
coverage run --source auditlog ./manage.py test
|
||||||
|
|
@ -27,7 +27,10 @@ deps =
|
||||||
codecov
|
codecov
|
||||||
freezegun
|
freezegun
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
|
mysqlclient
|
||||||
|
|
||||||
passenv=
|
passenv=
|
||||||
|
TEST_DB_BACKEND
|
||||||
TEST_DB_HOST
|
TEST_DB_HOST
|
||||||
TEST_DB_USER
|
TEST_DB_USER
|
||||||
TEST_DB_PASS
|
TEST_DB_PASS
|
||||||
|
|
@ -56,7 +59,15 @@ description = Check for missing migrations
|
||||||
changedir = auditlog_tests
|
changedir = auditlog_tests
|
||||||
deps =
|
deps =
|
||||||
Django>=4.2
|
Django>=4.2
|
||||||
psycopg2
|
psycopg2-binary
|
||||||
|
mysqlclient
|
||||||
|
passenv=
|
||||||
|
TEST_DB_BACKEND
|
||||||
|
TEST_DB_HOST
|
||||||
|
TEST_DB_USER
|
||||||
|
TEST_DB_PASS
|
||||||
|
TEST_DB_NAME
|
||||||
|
TEST_DB_PORT
|
||||||
commands =
|
commands =
|
||||||
python manage.py makemigrations --check --dry-run
|
python manage.py makemigrations --check --dry-run
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue