diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d8d5af1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + cache: pip + - name: Install dependencies + run: python -m pip install -r requirements.txt -r requirements-dev.txt + - name: Lint + run: ruff check . + - name: Test + run: python -m pytest diff --git a/cogs/link_fix.py b/cogs/link_fix.py index d5bf6de..ddc9638 100644 --- a/cogs/link_fix.py +++ b/cogs/link_fix.py @@ -3,18 +3,19 @@ """ import re +import asyncio from typing import List import discord_markdown_ast_parser as dmap from discord_markdown_ast_parser.parser import NodeType import logging -from database.models.Member import * +from database.models.Member import Member from database.models.Role import Role -from database.models.TextChannel import * -from database.models.Guild import * -from database.models.Event import * -from src.websites import * -from src.utils import * +from database.models.TextChannel import GuildMessageableChannel, TextChannel +from database.models.Guild import Guild, OriginalMessage +from database.models.Event import Event +from src.websites import WebsiteLink, websites +from src.utils import Typing, entrypoint_context, group_items, safe_send_coro import discore diff --git a/cogs/setup.py b/cogs/setup.py index 8471c57..32e7eaf 100644 --- a/cogs/setup.py +++ b/cogs/setup.py @@ -1,8 +1,10 @@ +import asyncio +import json import logging import aiohttp from src import utils -from database.models.Event import * +from database.models.Event import Event import discore diff --git a/database/models/Event.py b/database/models/Event.py index 39e52dc..8ff9259 100644 --- a/database/models/Event.py +++ b/database/models/Event.py @@ -9,6 +9,9 @@ import discore + +__all__ = ('Event',) + class Event(Model): """Event Model""" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0300d21 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra" +pythonpath = ["."] + +[tool.ruff] +target-version = "py314" +line-length = 120 + +[tool.ruff.lint] +select = ["E9", "F63", "F7", "F82"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..1fb9102 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest ~= 8.4 +ruff ~= 0.12 diff --git a/requirements.txt b/requirements.txt index 1d5b22a..2a686c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -discore @ git+https://github.com/Kyrela/discore +discore @ git+https://github.com/Kyrela/discore@d266d4683563b1cd03cf3b6e3149bc11b98dd358 python-i18n ~= 0.3.9 psutil ~= 7.2.2 masonite-orm @ git+https://github.com/MasoniteFramework/orm@c4959b4cadfc5207e48ae0f3918d0c1214e25865 # Temporary fix for Masonite ORM until next, hopefully stable, release pymysql ~= 1.1.1 -discord_markdown_ast_parser @ git+https://github.com/Kyrela/discord-markdown-ast-parser +discord_markdown_ast_parser @ git+https://github.com/Kyrela/discord-markdown-ast-parser@9e38d0415a900914edcbe492cbe39323e273d596 aiohttp ~= 3.13.3 diff --git a/src/websites.py b/src/websites.py index f710ee6..54559da 100644 --- a/src/websites.py +++ b/src/websites.py @@ -1,12 +1,13 @@ """ Allows fixing links from various websites. """ +import asyncio import logging import re -from typing import Type, Iterable, Callable +from typing import Type, Iterable, Callable, Self -from database.models.Event import * -from database.models.Guild import * +from database.models.Event import Event +from database.models.Guild import EmbedEzView, FxEmbedView, Guild, InstagramView, TiktokView from src import utils __all__ = ('WebsiteLink', 'websites') diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..662de6e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import discore + + +def pytest_configure(): + if not discore.config.loaded: + discore.config_init() diff --git a/tests/test_link_parsing.py b/tests/test_link_parsing.py new file mode 100644 index 0000000..3ffc2b6 --- /dev/null +++ b/tests/test_link_parsing.py @@ -0,0 +1,46 @@ +import asyncio + +import discord_markdown_ast_parser as dmap + +from cogs.link_fix import get_embeddable_urls +from database.models.Guild import FxEmbedView +from src.websites import TwitterLink, generate_regex + + +class FakeGuild: + lang = 'en' + + def __init__(self, **settings): + self.settings = settings + + def __getitem__(self, key): + return self.settings[key] + + +def test_markdown_parser_ignores_code_and_preserves_spoilers(): + nodes = dmap.parse('`https://x.com/ignored/status/1` ||https://x.com/user/status/2||') + + assert get_embeddable_urls(nodes) == [('https://x.com/user/status/2', True)] + + +def test_generated_route_requires_the_whole_url_to_match(): + regex = generate_regex('example.com', '/:username/post/:id') + + match = regex.fullmatch('https://www.example.com/alice/post/42?ref=test') + assert match is not None + assert match['username'] == 'alice' + assert match['id'] == '42' + assert regex.fullmatch('https://evil.example/example.com/alice/post/42') is None + + +def test_twitter_link_renders_expected_proxy(): + guild = FakeGuild( + twitter=True, + twitter_view=FxEmbedView.NORMAL, + twitter_tr=False, + ) + link = TwitterLink(guild, 'https://x.com/alice/status/42') + + fixed_url, label = asyncio.run(link.get_fixed_url()) + assert fixed_url == 'https://fxtwitter.com/i/status/42' + assert label == 'FxTwitter' diff --git a/tests/test_project_files.py b/tests/test_project_files.py new file mode 100644 index 0000000..bb00a08 --- /dev/null +++ b/tests/test_project_files.py @@ -0,0 +1,12 @@ +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).parents[1] + + +def test_all_locale_files_are_valid_yaml(): + for locale_file in (ROOT / 'locales').glob('*.yml'): + with locale_file.open(encoding='utf-8') as stream: + assert isinstance(yaml.safe_load(stream), dict), locale_file diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..69001f8 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,8 @@ +from src.utils import group_items + + +def test_group_items_never_exceeds_limit_when_items_fit(): + groups = group_items(['abc', 'de', 'fgh'], max_group_size=6, sep='|') + + assert groups == [('abc|de', ['abc', 'de']), ('fgh', ['fgh'])] + assert all(len(text) <= 6 for text, _ in groups)