From e771073909d500cb64cdbf17fb2912c9d77d6282 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 2 Oct 2026 19:34:11 +0200 Subject: [PATCH] test: 81 unit tests for tickets (data, storage, transcript, ui) - tests/test_data.py: models serialization, enums, roundtrip - tests/test_storage.py: CRUD JSON, cache, persistence - tests/test_transcript.py: HTML generation, escaping, attachments - tests/test_ui.py: button callbacks with mocked interactions - Fixed: transcript.py attachments access (dataclass not dict) --- tests/test_data.py | 244 +++++++++++++++++++++++++++++++ tests/test_storage.py | 130 +++++++++++++++++ tests/test_transcript.py | 133 +++++++++++++++++ tests/test_ui.py | 304 +++++++++++++++++++++++++++++++++++++++ tickets/transcript.py | 2 +- 5 files changed, 812 insertions(+), 1 deletion(-) create mode 100644 tests/test_data.py create mode 100644 tests/test_storage.py create mode 100644 tests/test_transcript.py create mode 100644 tests/test_ui.py diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..382e769 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +from tickets.data import ( + GuildTicketConfig, + TicketCategory, + TicketData, + TicketMessage, + TicketPriority, + TicketStatus, +) + + +class TestTicketStatus: + def test_open_value(self): + assert TicketStatus.OPEN.value == "open" + + def test_closed_value(self): + assert TicketStatus.CLOSED.value == "closed" + + def test_enum_inheritance(self): + assert issubclass(TicketStatus, str) + + +class TestTicketPriority: + def test_all_values(self): + assert TicketPriority.LOW.value == "low" + assert TicketPriority.NORMAL.value == "normal" + assert TicketPriority.HIGH.value == "high" + assert TicketPriority.URGENT.value == "urgent" + + +class TestTicketCategory: + def test_create_default(self): + cat = TicketCategory(id="test", name="Test") + assert cat.id == "test" + assert cat.name == "Test" + assert cat.emoji == "" + assert cat.description == "" + assert cat.staff_role_id is None + assert cat.discord_category_id is None + + def test_to_dict(self): + cat = TicketCategory( + id="support", + name="Support", + emoji="🎫", + description="Aide", + staff_role_id=123, + discord_category_id=456, + ) + d = cat.to_dict() + assert d["id"] == "support" + assert d["name"] == "Support" + assert d["emoji"] == "🎫" + assert d["description"] == "Aide" + assert d["staff_role_id"] == 123 + assert d["discord_category_id"] == 456 + + def test_from_dict(self): + d = { + "id": "support", + "name": "Support", + "emoji": "🎫", + "description": "Aide", + "staff_role_id": 123, + "discord_category_id": 456, + } + cat = TicketCategory.from_dict(d) + assert cat.id == "support" + assert cat.name == "Support" + assert cat.staff_role_id == 123 + + def test_from_dict_ignores_extra_keys(self): + d = {"id": "x", "name": "X", "unknown_key": "should be ignored"} + cat = TicketCategory.from_dict(d) + assert cat.id == "x" + + def test_roundtrip(self): + cat = TicketCategory(id="a", name="A", emoji="🔧", description="desc") + cat2 = TicketCategory.from_dict(cat.to_dict()) + assert cat2 == cat + + +class TestTicketMessage: + def test_create_minimal(self): + msg = TicketMessage(author_id=1, author_name="user", content="hello", timestamp="2026-01-01") + assert msg.attachments == [] + assert msg.is_staff is False + + def test_create_full(self): + msg = TicketMessage( + author_id=1, + author_name="user", + content="hello", + timestamp="2026-01-01", + attachments=["a.png"], + is_staff=True, + ) + assert msg.attachments == ["a.png"] + assert msg.is_staff is True + + +class TestTicketData: + def test_create_minimal(self): + t = TicketData(channel_id=1, guild_id=2, user_id=3, category_id="cat") + assert t.status == TicketStatus.OPEN + assert t.priority == TicketPriority.NORMAL + assert t.locked is False + assert t.claimed_by is None + assert t.messages == [] + assert t.reason == "" + + def test_to_dict(self): + t = TicketData( + channel_id=100, + guild_id=200, + user_id=300, + category_id="support", + status=TicketStatus.CLOSED, + priority=TicketPriority.HIGH, + claimed_by=999, + locked=True, + reason="test", + ) + d = t.to_dict() + assert d["channel_id"] == 100 + assert d["status"] == "closed" + assert d["priority"] == "high" + assert d["claimed_by"] == 999 + assert d["locked"] is True + assert d["reason"] == "test" + + def test_to_dict_with_messages(self): + t = TicketData(channel_id=1, guild_id=2, user_id=3, category_id="c") + t.messages.append(TicketMessage(author_id=1, author_name="a", content="hi", timestamp="t")) + d = t.to_dict() + assert len(d["messages"]) == 1 + assert d["messages"][0]["content"] == "hi" + + def test_from_dict(self): + d = { + "channel_id": 1, + "guild_id": 2, + "user_id": 3, + "category_id": "c", + "status": "closed", + "priority": "high", + "claimed_by": 99, + "locked": True, + "reason": "r", + "messages": [{"author_id": 1, "author_name": "a", "content": "hi", "timestamp": "t"}], + } + t = TicketData.from_dict(d) + assert t.status == TicketStatus.CLOSED + assert t.priority == TicketPriority.HIGH + assert t.claimed_by == 99 + assert t.locked is True + assert len(t.messages) == 1 + + def test_from_dict_legacy_keys(self): + d = { + "channel_id": 1, + "guild_id": 2, + "user_id": 3, + "category_id": "c", + "transcript_path": "/old/path.html", + "rating": 5, + "feedback": "ok", + } + t = TicketData.from_dict(d) + assert not hasattr(t, "transcript_path") + assert not hasattr(t, "rating") + assert not hasattr(t, "feedback") + + def test_roundtrip(self): + t = TicketData( + channel_id=10, + guild_id=20, + user_id=30, + category_id="cat", + status=TicketStatus.CLOSED, + priority=TicketPriority.URGENT, + claimed_by=50, + locked=True, + reason="test reason", + created_at="2026-01-01T00:00:00", + closed_at="2026-01-02T00:00:00", + ) + t.messages.append(TicketMessage(author_id=1, author_name="u", content="m", timestamp="t")) + t2 = TicketData.from_dict(t.to_dict()) + assert t2.channel_id == t.channel_id + assert t2.status == t.status + assert t2.priority == t.priority + assert t2.claimed_by == t.claimed_by + assert t2.locked == t.locked + assert len(t2.messages) == 1 + + +class TestGuildTicketConfig: + def test_create_default(self): + c = GuildTicketConfig(guild_id=1) + assert c.enabled is False + assert c.categories == [] + assert c.staff_role_id is None + assert c.log_channel_id is None + assert c.transcript_enabled is True + + def test_to_dict(self): + c = GuildTicketConfig( + guild_id=1, + enabled=True, + staff_role_id=100, + log_channel_id=200, + panel_channel_id=300, + panel_message_id=400, + ) + c.categories.append(TicketCategory(id="a", name="A")) + d = c.to_dict() + assert d["guild_id"] == 1 + assert d["enabled"] is True + assert d["staff_role_id"] == 100 + assert d["log_channel_id"] == 200 + assert len(d["categories"]) == 1 + + def test_from_dict(self): + d = { + "guild_id": 1, + "enabled": True, + "staff_role_id": 100, + "log_channel_id": 200, + "categories": [{"id": "a", "name": "A"}], + } + c = GuildTicketConfig.from_dict(d) + assert c.enabled is True + assert len(c.categories) == 1 + assert c.categories[0].id == "a" + + def test_roundtrip(self): + c = GuildTicketConfig(guild_id=42, enabled=True) + c.categories.append(TicketCategory(id="x", name="X", emoji="🔧")) + c2 = GuildTicketConfig.from_dict(c.to_dict()) + assert c2.guild_id == 42 + assert c2.enabled is True + assert c2.categories[0].id == "x" diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..acd3fb5 --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import json + +import pytest + +from tickets import storage +from tickets.data import GuildTicketConfig, TicketCategory, TicketData + + +@pytest.fixture(autouse=True) +def clean_storage(tmp_path): + """Isolate storage in a temp dir and reset caches.""" + storage.TICKETS_DIR = tmp_path / "tickets" + storage._CONFIG_FILE = storage.TICKETS_DIR / "config.json" + storage._config_cache.clear() + storage._tickets_cache.clear() + yield + storage._config_cache.clear() + storage._tickets_cache.clear() + + +class TestGetConfig: + def test_returns_default_when_missing(self): + config = storage.get_config(guild_id=123) + assert config.guild_id == 123 + assert config.enabled is False + assert config.categories == [] + + def test_returns_same_object(self): + c1 = storage.get_config(guild_id=1) + c2 = storage.get_config(guild_id=1) + assert c1 is c2 + + def test_different_guilds_different_configs(self): + c1 = storage.get_config(guild_id=1) + c2 = storage.get_config(guild_id=2) + assert c1.guild_id != c2.guild_id + + +class TestSaveConfig: + def test_save_creates_file(self, tmp_path): + config = storage.get_config(guild_id=1) + config.enabled = True + storage.save_config(config) + assert storage._CONFIG_FILE.exists() + + def test_save_persists(self, tmp_path): + config = storage.get_config(guild_id=1) + config.enabled = True + config.categories.append(TicketCategory(id="a", name="A")) + storage.save_config(config) + + storage._config_cache.clear() + loaded = storage.get_config(guild_id=1) + assert loaded.enabled is True + assert len(loaded.categories) == 1 + assert loaded.categories[0].id == "a" + + def test_save_multiple_guilds(self): + c1 = storage.get_config(guild_id=1) + c1.enabled = True + storage.save_config(c1) + + c2 = storage.get_config(guild_id=2) + c2.enabled = False + storage.save_config(c2) + + storage._config_cache.clear() + assert storage.get_config(guild_id=1).enabled is True + assert storage.get_config(guild_id=2).enabled is False + + +class TestSaveAndGetTicket: + def test_get_missing_returns_none(self): + assert storage.get_ticket(999) is None + + def test_save_and_get(self): + ticket = TicketData(channel_id=100, guild_id=1, user_id=2, category_id="cat") + storage.save_ticket(ticket) + loaded = storage.get_ticket(100) + assert loaded is not None + assert loaded.channel_id == 100 + assert loaded.category_id == "cat" + + def test_save_persists_to_disk(self): + ticket = TicketData(channel_id=200, guild_id=1, user_id=2, category_id="c") + storage.save_ticket(ticket) + + storage._tickets_cache.clear() + loaded = storage.get_ticket(200) + assert loaded is not None + assert loaded.user_id == 2 + + def test_save_overwrites(self): + t1 = TicketData(channel_id=300, guild_id=1, user_id=2, category_id="old") + storage.save_ticket(t1) + + t2 = TicketData(channel_id=300, guild_id=1, user_id=2, category_id="new") + storage.save_ticket(t2) + + loaded = storage.get_ticket(300) + assert loaded.category_id == "new" + + def test_multiple_tickets(self): + t1 = TicketData(channel_id=400, guild_id=1, user_id=2, category_id="a") + t2 = TicketData(channel_id=401, guild_id=1, user_id=3, category_id="b") + storage.save_ticket(t1) + storage.save_ticket(t2) + assert storage.get_ticket(400).category_id == "a" + assert storage.get_ticket(401).category_id == "b" + + +class TestDeleteTicket: + def test_delete_removes_file(self): + ticket = TicketData(channel_id=500, guild_id=1, user_id=2, category_id="c") + storage.save_ticket(ticket) + assert storage.get_ticket(500) is not None + + storage.delete_ticket(500) + assert storage.get_ticket(500) is None + + def test_delete_nonexistent(self): + storage.delete_ticket(9999) + + def test_delete_clears_cache(self): + ticket = TicketData(channel_id=600, guild_id=1, user_id=2, category_id="c") + storage.save_ticket(ticket) + storage.delete_ticket(600) + assert 600 not in storage._tickets_cache diff --git a/tests/test_transcript.py b/tests/test_transcript.py new file mode 100644 index 0000000..4483d02 --- /dev/null +++ b/tests/test_transcript.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from tickets.data import TicketData, TicketMessage +from tickets.transcript import _format_ts, generate_transcript + + +class TestFormatTs: + def test_valid_iso(self): + result = _format_ts("2026-01-15T10:30:00+00:00") + assert result == "15/01/2026 10:30:00" + + def test_valid_naive_iso(self): + result = _format_ts("2026-06-01T12:00:00") + assert result == "01/06/2026 12:00:00" + + def test_invalid_string_returns_as_is(self): + assert _format_ts("not-a-date") == "not-a-date" + + def test_empty_string(self): + assert _format_ts("") == "" + + def test_none_returns_none(self): + assert _format_ts(None) is None + + +def _make_ticket(**kwargs) -> TicketData: + defaults = dict(channel_id=123, guild_id=456, user_id=789, category_id="support") + defaults.update(kwargs) + return TicketData(**defaults) + + +class TestGenerateTranscript: + def test_returns_html(self): + result = generate_transcript(_make_ticket()) + assert result.startswith("") + assert "" in result + + def test_contains_channel_id(self): + result = generate_transcript(_make_ticket(channel_id=42)) + assert "Ticket #42" in result + + def test_empty_messages(self): + ticket = _make_ticket() + result = generate_transcript(ticket) + assert "Aucun message" in result + + def test_single_user_message(self): + ticket = _make_ticket() + ticket.messages.append( + TicketMessage(author_id=1, author_name="TestUser", content="Hello!", timestamp="2026-01-01T00:00:00") + ) + result = generate_transcript(ticket) + assert "TestUser" in result + assert "Hello!" in result + assert 'class="badge badge-staff"' not in result + + def test_staff_message_has_badge(self): + ticket = _make_ticket() + ticket.messages.append( + TicketMessage( + author_id=2, author_name="StaffUser", content="Reply", timestamp="2026-01-01T00:00:00", is_staff=True + ) + ) + result = generate_transcript(ticket) + assert "badge-staff" in result + assert "STAFF" in result + + def test_message_content_escaped(self): + ticket = _make_ticket() + ticket.messages.append( + TicketMessage(author_id=1, author_name="x", content="", timestamp="t") + ) + result = generate_transcript(ticket) + assert "