django-auditlog/auditlog_tests/test_commands.py
Youngkwang Yang 8003b069c9
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)
2025-08-17 16:50:23 +02:00

202 lines
7.3 KiB
Python

"""Tests for auditlog.management.commands"""
import datetime
from io import StringIO
from unittest import mock
import freezegun
from django.core.management import call_command
from django.db import connection
from django.test import TestCase, TransactionTestCase
from django.test.utils import skipIf
from test_app.models import SimpleModel
from auditlog.management.commands.auditlogflush import TruncateQuery
class AuditlogFlushTest(TestCase):
def setUp(self):
input_patcher = mock.patch("builtins.input")
self.mock_input = input_patcher.start()
self.addCleanup(input_patcher.stop)
def make_object(self):
return SimpleModel.objects.create(text="I am a simple model.")
def call_command(self, *args, **kwargs):
outbuf = StringIO()
errbuf = StringIO()
call_command("auditlogflush", *args, stdout=outbuf, stderr=errbuf, **kwargs)
return outbuf.getvalue().strip(), errbuf.getvalue().strip()
def test_flush_yes(self):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
out, err = self.call_command("--yes")
self.assertEqual(obj.history.count(), 0, msg="There are no log entries.")
self.assertEqual(
out, "Deleted 1 objects.", msg="Output shows deleted 1 object."
)
self.assertEqual(err, "", msg="No stderr")
def test_flush_no(self):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
self.mock_input.return_value = "N\n"
out, err = self.call_command()
self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.")
self.assertEqual(
out,
"This action will clear all log entries from the database.\nAborted.",
msg="Output shows warning and aborted.",
)
self.assertEqual(err, "", msg="No stderr")
def test_flush_input_yes(self):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
self.mock_input.return_value = "Y\n"
out, err = self.call_command()
self.assertEqual(obj.history.count(), 0, msg="There are no log entries.")
self.assertEqual(
out,
"This action will clear all log entries from the database.\nDeleted 1 objects.",
msg="Output shows warning and deleted 1 object.",
)
self.assertEqual(err, "", msg="No stderr")
def test_before_date_input(self):
self.mock_input.return_value = "N\n"
out, err = self.call_command("--before-date=2000-01-01")
self.assertEqual(
out,
(
"This action will clear all log entries before "
"2000-01-01 from the database.\nAborted."
),
msg="Output shows warning with date and then aborted.",
)
self.assertEqual(err, "", msg="No stderr")
def test_before_date(self):
with freezegun.freeze_time("1999-12-31"):
obj = self.make_object()
with freezegun.freeze_time("2000-01-02"):
obj.text = "I have new text"
obj.save()
self.assertEqual(
{v["timestamp"] for v in obj.history.values("timestamp")},
{
datetime.datetime(1999, 12, 31, tzinfo=datetime.timezone.utc),
datetime.datetime(2000, 1, 2, tzinfo=datetime.timezone.utc),
},
msg="Entries exist for 1999-12-31 and 2000-01-02",
)
out, err = self.call_command("--yes", "--before-date=2000-01-01")
self.assertEqual(
{v["timestamp"] for v in obj.history.values("timestamp")},
{
datetime.datetime(2000, 1, 2, tzinfo=datetime.timezone.utc),
},
msg="An entry exists only for 2000-01-02",
)
self.assertEqual(
out, "Deleted 1 objects.", msg="Output shows deleted 1 object."
)
self.assertEqual(err, "", msg="No stderr")
class AuditlogFlushWithTruncateTest(TransactionTestCase):
def setUp(self):
input_patcher = mock.patch("builtins.input")
self.mock_input = input_patcher.start()
self.addCleanup(input_patcher.stop)
def make_object(self):
return SimpleModel.objects.create(text="I am a simple model.")
def call_command(self, *args, **kwargs):
outbuf = StringIO()
errbuf = StringIO()
call_command("auditlogflush", *args, stdout=outbuf, stderr=errbuf, **kwargs)
return outbuf.getvalue().strip(), errbuf.getvalue().strip()
def test_flush_with_both_truncate_and_before_date_options(self):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
out, err = self.call_command("--truncate", "--before-date=2000-01-01")
self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.")
self.assertEqual(
out,
"Truncate deletes all log entries and can not be passed with before-date.",
msg="Output shows error",
)
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):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
out, err = self.call_command("--truncate", "--y")
self.assertEqual(obj.history.count(), 0, msg="There is no log entry.")
self.assertEqual(
out,
"Truncated log entry table.",
msg="Output shows table gets truncate",
)
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):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
self.mock_input.return_value = "Y\n"
out, err = self.call_command("--truncate")
self.assertEqual(obj.history.count(), 0, msg="There is no log entry.")
self.assertEqual(
out,
"This action will clear all log entries from the database.\nTruncated log entry table.",
msg="Output shows warning and table gets truncate",
)
self.assertEqual(err, "", msg="No stderr")
@mock.patch(
"django.db.connection.vendor",
new_callable=mock.PropertyMock(return_value="unknown"),
)
@mock.patch(
"django.db.connection.display_name",
new_callable=mock.PropertyMock(return_value="Unknown"),
)
def test_flush_with_truncate_for_unsupported_database_vendor(
self, mocked_vendor, mocked_db_name
):
obj = self.make_object()
self.assertEqual(obj.history.count(), 1, msg="There is one log entry.")
out, err = self.call_command("--truncate", "--y")
self.assertEqual(obj.history.count(), 1, msg="There is still one log entry.")
self.assertEqual(
out,
"Database Unknown does not support truncate statement.",
msg="Output shows error",
)
self.assertEqual(err, "", msg="No stderr")