Skip to content

Commit

Permalink
Add guild model (#50)
Browse files Browse the repository at this point in the history
# Description

### Context
In order to improve the system we'll go back to using a database that can be accessed easily from different places. The previous Sqlite3 database will be used just as "cache" on the bot, while MongoDB will be set up to be used widely.
### This diff
* Add handler to interact with the database
* Add Guild Model again on MongoDB

# Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update

# How Has This Been Tested?

Locally tested and added unit tests

# Checklist:

- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
  • Loading branch information
akotadi committed Dec 29, 2023
1 parent 1bc2e4e commit d8e915e
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 0 deletions.
17 changes: 17 additions & 0 deletions otter_welcome_buddy/cogs/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from discord.ext.commands import Bot

from otter_welcome_buddy.common.constants import OTTER_ROLE
from otter_welcome_buddy.database.handlers.db_guild_handler import DbGuildHandler
from otter_welcome_buddy.database.models.external.guild_model import GuildModel
from otter_welcome_buddy.formatters import debug
from otter_welcome_buddy.settings import WELCOME_MESSAGES
from otter_welcome_buddy.startup.database import init_guild_table


logger = logging.getLogger(__name__)
Expand All @@ -26,8 +29,22 @@ def __init__(
@commands.Cog.listener()
async def on_ready(self) -> None:
"""Ready Event"""
init_guild_table(self.bot)

logger.info(self.debug_formatter.bot_is_ready())

@commands.Cog.listener()
async def on_guild_join(self, guild: discord.Guild) -> None:
"""Event fired when a guild is either created or the bot join into"""
if DbGuildHandler.get_guild(guild_id=guild.id) is None:
guild_model: GuildModel = GuildModel(guild_id=guild.id)
DbGuildHandler.insert_guild(guild_model=guild_model)

@commands.Cog.listener()
async def on_guild_remove(self, guild: discord.Guild) -> None:
"""Event fired when a guild is deleted or the bot is removed from it"""
DbGuildHandler.delete_guild(guild_id=guild.id)

@commands.Cog.listener()
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
"""Event fired when a user react to the welcome message, giving the entry role to him"""
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions otter_welcome_buddy/database/handlers/db_guild_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from mongoengine import DoesNotExist

from otter_welcome_buddy.database.models.external.guild_model import GuildModel


class DbGuildHandler:
"""Class to interact with the table guild via static methods"""

@staticmethod
def get_guild(guild_id: int) -> GuildModel | None:
"""Static method to get a guild by its id"""
try:
guild_model: GuildModel = GuildModel.objects(guild_id=guild_id).get()
return guild_model
except DoesNotExist:
return None

@staticmethod
def insert_guild(guild_model: GuildModel) -> GuildModel:
"""Static method to insert a guild record"""
guild_model = guild_model.save()
return guild_model

@staticmethod
def delete_guild(guild_id: int) -> None:
"""Static method to delete an interview match record by a guild_id"""
guild_model: GuildModel | None = GuildModel.objects(guild_id=guild_id).first()
if guild_model:
guild_model.delete()
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions otter_welcome_buddy/database/models/external/guild_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from mongoengine import Document
from mongoengine import IntField


class GuildModel(Document):
"""
A model that represents a guild (server) in the database.
Attributes:
guild_id (int): The identifier for the guild, is taken from discord records and is
the primary key of the object
"""

guild_id = IntField(primary_key=True, required=True)
11 changes: 11 additions & 0 deletions otter_welcome_buddy/startup/database.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import os

from discord.ext.commands import Bot
from dotenv import load_dotenv
from mongoengine import connect as mongo_connect
from pymongo import monitoring

from otter_welcome_buddy.common.constants import DATA_FILE_PATH
from otter_welcome_buddy.common.utils.database import get_cache_engine
from otter_welcome_buddy.database.dbconn import BaseModel
from otter_welcome_buddy.database.handlers.db_guild_handler import DbGuildHandler
from otter_welcome_buddy.database.models.external.guild_model import GuildModel
from otter_welcome_buddy.log.dblogger import DbCommandLogger


def init_guild_table(bot: Bot) -> None:
"""Verify that all the guilds that the bot is part of are in the database"""
for guild in bot.guilds:
if DbGuildHandler.get_guild(guild_id=guild.id) is None:
guild_model: GuildModel = GuildModel(guild_id=guild.id)
DbGuildHandler.insert_guild(guild_model=guild_model)


async def init_database() -> None:
"""Initialize the database from the existing models"""
load_dotenv()
Expand Down
57 changes: 57 additions & 0 deletions tests/cogs/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from pytest_mock import MockFixture

from otter_welcome_buddy.cogs import events
from otter_welcome_buddy.database.handlers.db_guild_handler import DbGuildHandler
from otter_welcome_buddy.database.models.external.guild_model import GuildModel

if TYPE_CHECKING:
from discord.types.gateway import MessageReactionAddEvent
Expand Down Expand Up @@ -77,3 +79,58 @@ async def test_onRawReactionAdd_addRole(
# Assert
mock_get_role.assert_called_once()
mock_add_roles.assert_called_once()


@pytest.mark.asyncio
@pytest.mark.parametrize("is_new_guild", [True, False])
async def test_onGuildJoin_insertDb(
mocker: MockFixture,
mock_bot: Bot,
mock_guild: Guild,
mock_debug_fmt,
mock_guild_model: GuildModel,
is_new_guild: bool,
):
# Arrange
mock_guild.id = 111
mock_bot.guilds = [mock_guild]
cog = events.BotEvents(mock_bot, mock_debug_fmt)

mock_get_guild = mocker.patch.object(
DbGuildHandler,
"get_guild",
return_value=None if is_new_guild else mock_guild_model,
)
mock_insert_guild = mocker.patch.object(DbGuildHandler, "insert_guild")

# Act
await cog.on_guild_join(mock_guild)

# Assert
mock_get_guild.assert_called_once_with(guild_id=mock_guild.id)
if is_new_guild:
mock_insert_guild.assert_called_once()
else:
mock_insert_guild.assert_not_called()


@pytest.mark.asyncio
async def test_onGuildRemove_deleteDb(
mocker: MockFixture,
mock_bot: Bot,
mock_guild: Guild,
mock_debug_fmt,
mock_guild_model: GuildModel,
):
# Arrange
mock_guild.id = mock_guild_model.id
mock_bot.guilds = [mock_guild]
cog = events.BotEvents(mock_bot, mock_debug_fmt)

mock_delete_guild = mocker.patch.object(DbGuildHandler, "delete_guild")

# Act
await cog.on_guild_remove(mock_guild)

# Assert
mock_delete_guild.assert_called_once_with(guild_id=mock_guild.id)
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
from discord import Member
from discord import Role
from discord.ext.commands import Bot
from mongoengine import connect as mongo_connect
from mongoengine import disconnect as mongo_disconnect
from mongomock import MongoClient

from otter_welcome_buddy.database.models.external.guild_model import GuildModel


@pytest.fixture
Expand Down Expand Up @@ -60,3 +65,23 @@ def temporary_cache():
yield db_path
if os.path.exists(db_path):
os.remove(db_path)


@pytest.fixture()
def temporary_mongo_connection():
mock_mongo_connection = mongo_connect(
"mongoenginetest",
host="mongodb://localhost",
mongo_client_class=MongoClient,
)
yield mock_mongo_connection
mongo_disconnect()


@pytest.fixture()
def mock_guild_model(temporary_mongo_connection) -> GuildModel:
mock_guild_model = GuildModel(
guild_id=123,
)
mock_guild_model.save()
return mock_guild_model
80 changes: 80 additions & 0 deletions tests/database/handlers/test_db_guild_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import pytest
from mongoengine import DoesNotExist
from mongoengine import ValidationError
from mongomock import MongoClient

from otter_welcome_buddy.database.handlers.db_guild_handler import DbGuildHandler
from otter_welcome_buddy.database.models.external.guild_model import GuildModel


def test_get_guild_succeed(temporary_mongo_connection: MongoClient) -> None:
# Arrange
mocked_guild_id: int = 123
mocked_guild_model: GuildModel = GuildModel(
guild_id=mocked_guild_id,
)
mocked_guild_model.save()

# Act
result = DbGuildHandler.get_guild(guild_id=mocked_guild_id)

# Assert
assert result is not None
assert result.id == mocked_guild_id


def test_get_guild_not_found(temporary_mongo_connection: MongoClient) -> None:
# Act
result = DbGuildHandler.get_guild(guild_id=123)

# Assert
assert result is None


def test_insert_guild_succeed(temporary_mongo_connection: MongoClient) -> None:
# Arrange
mocked_guild_id: int = 123
mocked_guild_model: GuildModel = GuildModel(
guild_id=mocked_guild_id,
)

# Act
result = DbGuildHandler.insert_guild(guild_model=mocked_guild_model)

# Assert
assert result is not None
assert result.id == mocked_guild_id


def test_insert_guild_failed(temporary_mongo_connection: MongoClient) -> None:
# Arrange
mocked_guild_model: GuildModel = GuildModel()

# Act / Assert
with pytest.raises(ValidationError):
DbGuildHandler.insert_guild(guild_model=mocked_guild_model)


def test_delete_guild_valid_id(temporary_mongo_connection: MongoClient) -> None:
# Arrange
mocked_guild_id: int = 123
mocked_guild_model: GuildModel = GuildModel(
guild_id=mocked_guild_id,
)
mocked_guild_model.save()

# Act
DbGuildHandler.delete_guild(guild_id=mocked_guild_id)

# Assert
with pytest.raises(DoesNotExist):
GuildModel.objects(guild_id=mocked_guild_id).get()


def test_delete_guild_invalid_id(temporary_mongo_connection: MongoClient) -> None:
# Act
DbGuildHandler.delete_guild(guild_id=123)

# Assert
with pytest.raises(DoesNotExist):
GuildModel.objects(guild_id=123).get()

0 comments on commit d8e915e

Please sign in to comment.