Skip to content

Commit 2a48cf2

Browse files
committed
✅ Add event system tests
1 parent f1c5f64 commit 2a48cf2

File tree

9 files changed

+1804
-0
lines changed

9 files changed

+1804
-0
lines changed

tests/event_helpers.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2021-present Pycord Development
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
25+
from typing import Any
26+
from unittest.mock import AsyncMock
27+
28+
from discord.app.event_emitter import Event, EventEmitter
29+
from discord.app.state import ConnectionState
30+
31+
32+
class EventCapture:
33+
"""Helper class to capture events emitted by the EventEmitter."""
34+
35+
def __init__(self):
36+
self.events: list[Event] = []
37+
self.call_count = 0
38+
39+
async def __call__(self, event: Event) -> None:
40+
"""Called when an event is received."""
41+
self.events.append(event)
42+
self.call_count += 1
43+
44+
def assert_called_once(self):
45+
"""Assert that the event was received exactly once."""
46+
assert self.call_count == 1, f"Expected 1 event, got {self.call_count}"
47+
48+
def assert_called_with_event_type(self, event_type: type[Event]):
49+
"""Assert that the event received is of the expected type."""
50+
assert len(self.events) > 0, "No events were captured"
51+
event = self.events[-1]
52+
assert isinstance(event, event_type), f"Expected {event_type.__name__}, got {type(event).__name__}"
53+
54+
def assert_not_called(self):
55+
"""Assert that no events were received."""
56+
assert self.call_count == 0, f"Expected 0 events, got {self.call_count}"
57+
58+
def get_last_event(self) -> Event | None:
59+
"""Get the last event that was captured."""
60+
return self.events[-1] if self.events else None
61+
62+
def reset(self):
63+
"""Reset the capture state."""
64+
self.events.clear()
65+
self.call_count = 0
66+
67+
68+
async def emit_and_capture(
69+
state: ConnectionState,
70+
event_name: str,
71+
payload: Any,
72+
) -> EventCapture:
73+
"""
74+
Emit an event and capture it using an EventCapture receiver.
75+
76+
Args:
77+
state: The ConnectionState to use for emission
78+
event_name: The name of the event to emit
79+
payload: The payload to emit
80+
81+
Returns:
82+
EventCapture instance containing captured events
83+
"""
84+
capture = EventCapture()
85+
state.emitter.add_receiver(capture)
86+
87+
try:
88+
await state.emitter.emit(event_name, payload)
89+
finally:
90+
state.emitter.remove_receiver(capture)
91+
92+
return capture
93+
94+
95+
async def populate_guild_cache(state: ConnectionState, guild_id: int, guild_data: dict[str, Any]):
96+
"""
97+
Populate the cache with a guild.
98+
99+
Args:
100+
state: The ConnectionState to populate
101+
guild_id: The ID of the guild
102+
guild_data: The guild data payload
103+
"""
104+
from discord.guild import Guild
105+
106+
guild = await Guild._from_data(guild_data, state)
107+
await state.cache.add_guild(guild)

tests/events/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Event tests for py-cord."""
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2021-present Pycord Development
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
25+
import pytest
26+
27+
from discord.events.channel import (
28+
ChannelCreate,
29+
ChannelDelete,
30+
ChannelPinsUpdate,
31+
GuildChannelUpdate,
32+
)
33+
from tests.event_helpers import emit_and_capture, populate_guild_cache
34+
from tests.fixtures import create_channel_payload, create_guild_payload, create_mock_state
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_channel_create():
39+
"""Test that CHANNEL_CREATE event is emitted correctly."""
40+
# Setup
41+
state = create_mock_state()
42+
guild_id = 111111111
43+
channel_id = 222222222
44+
45+
# Populate cache with guild
46+
guild_data = create_guild_payload(guild_id)
47+
await populate_guild_cache(state, guild_id, guild_data)
48+
49+
# Create channel payload
50+
channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="test-channel")
51+
52+
# Emit event and capture
53+
capture = await emit_and_capture(state, "CHANNEL_CREATE", channel_data)
54+
55+
# Assertions
56+
capture.assert_called_once()
57+
capture.assert_called_with_event_type(ChannelCreate)
58+
59+
event = capture.get_last_event()
60+
assert event is not None
61+
assert event.id == channel_id
62+
assert event.name == "test-channel"
63+
64+
65+
@pytest.mark.asyncio
66+
async def test_channel_delete():
67+
"""Test that CHANNEL_DELETE event is emitted correctly."""
68+
# Setup
69+
state = create_mock_state()
70+
guild_id = 111111111
71+
channel_id = 222222222
72+
73+
# Populate cache with guild and channel
74+
guild_data = create_guild_payload(guild_id)
75+
await populate_guild_cache(state, guild_id, guild_data)
76+
77+
# Create channel first
78+
channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="test-channel")
79+
await state.emitter.emit("CHANNEL_CREATE", channel_data)
80+
81+
# Now delete it
82+
capture = await emit_and_capture(state, "CHANNEL_DELETE", channel_data)
83+
84+
# Assertions
85+
capture.assert_called_once()
86+
capture.assert_called_with_event_type(ChannelDelete)
87+
88+
event = capture.get_last_event()
89+
assert event is not None
90+
assert event.id == channel_id
91+
assert event.name == "test-channel"
92+
93+
94+
@pytest.mark.asyncio
95+
async def test_channel_pins_update():
96+
"""Test that CHANNEL_PINS_UPDATE event is emitted correctly."""
97+
# Setup
98+
state = create_mock_state()
99+
guild_id = 111111111
100+
channel_id = 222222222
101+
102+
# Populate cache with guild and channel
103+
guild_data = create_guild_payload(guild_id)
104+
await populate_guild_cache(state, guild_id, guild_data)
105+
106+
channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="test-channel")
107+
await state.emitter.emit("CHANNEL_CREATE", channel_data)
108+
109+
# Create pins update payload
110+
pins_data = {
111+
"guild_id": str(guild_id),
112+
"channel_id": str(channel_id),
113+
"last_pin_timestamp": "2024-01-01T00:00:00+00:00",
114+
}
115+
116+
# Emit event and capture
117+
capture = await emit_and_capture(state, "CHANNEL_PINS_UPDATE", pins_data)
118+
119+
# Assertions
120+
capture.assert_called_once()
121+
capture.assert_called_with_event_type(ChannelPinsUpdate)
122+
123+
event = capture.get_last_event()
124+
assert event is not None
125+
assert event.channel.id == channel_id
126+
assert event.last_pin is not None
127+
128+
129+
@pytest.mark.asyncio
130+
async def test_channel_update():
131+
"""Test that CHANNEL_UPDATE event triggers GUILD_CHANNEL_UPDATE."""
132+
# Setup
133+
state = create_mock_state()
134+
guild_id = 111111111
135+
channel_id = 222222222
136+
137+
# Populate cache with guild and channel
138+
guild_data = create_guild_payload(guild_id)
139+
await populate_guild_cache(state, guild_id, guild_data)
140+
141+
channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="test-channel")
142+
await state.emitter.emit("CHANNEL_CREATE", channel_data)
143+
144+
# Update channel
145+
updated_channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="updated-channel")
146+
147+
# Emit event and capture
148+
capture = await emit_and_capture(state, "CHANNEL_UPDATE", updated_channel_data)
149+
150+
# Assertions - CHANNEL_UPDATE dispatches GUILD_CHANNEL_UPDATE
151+
# The original event doesn't return anything but emits a sub-event
152+
assert capture.call_count >= 0 # May emit GUILD_CHANNEL_UPDATE
153+
154+
155+
@pytest.mark.asyncio
156+
async def test_channel_create_without_guild():
157+
"""Test that CHANNEL_CREATE returns None when guild is not found."""
158+
# Setup
159+
state = create_mock_state()
160+
guild_id = 111111111
161+
channel_id = 222222222
162+
163+
# Don't populate cache with guild
164+
165+
# Create channel payload
166+
channel_data = create_channel_payload(channel_id=channel_id, guild_id=guild_id, name="test-channel")
167+
168+
# Emit event and capture
169+
capture = await emit_and_capture(state, "CHANNEL_CREATE", channel_data)
170+
171+
# Assertions - should not emit event if guild not found
172+
capture.assert_not_called()

0 commit comments

Comments
 (0)