Skip to content

Commit

Permalink
Merge pull request #11 from BoltzExchange/feat/e2e-tests
Browse files Browse the repository at this point in the history
feat: e2e tests
  • Loading branch information
michael1011 authored Feb 13, 2025
2 parents 95f20f8 + 0185caf commit 562eddd
Show file tree
Hide file tree
Showing 15 changed files with 406 additions and 26 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: CI

on:
push:
branches:
- main
pull_request:

jobs:
Expand All @@ -27,5 +29,15 @@ jobs:
- name: Run ruff checks
run: uv run ruff check .

- name: Setup PostgreSQL
run: make postgres

- name: Run tests
run: uv run pytest .
env:
DATABASE_URL: "postgresql+asyncpg://boltz:boltz@localhost:5433/fees"
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TEST_BOT_NAME: ${{ secrets.TEST_BOT_NAME }}
TEST_API_ID: ${{ secrets.TEST_API_ID }}
TEST_API_HASH: ${{ secrets.TEST_API_HASH }}
TEST_API_SESSION: ${{ secrets.TEST_API_SESSION }}
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ postgres:
-e POSTGRES_DB=$(POSTGRES_DB) \
-p $(POSTGRES_PORT):5432 \
postgres:17-alpine
docker exec boltz-fees-postgres bash -c "while ! pg_isready -U $(POSTGRES_USER) -d $(POSTGRES_DB); do sleep 1; done"

postgres-stop:
docker stop boltz-fees-postgres
Expand Down
6 changes: 3 additions & 3 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ async def notify_subscription(

url = encode_url_params(from_asset, to_asset)
threshold_msg = (
f"went below {subscription.fee_threshold}%"
if get_fee(fees, subscription) < subscription.fee_threshold
f"has reached {subscription.fee_threshold}%"
if get_fee(fees, subscription) <= subscription.fee_threshold
else f"are above {subscription.fee_threshold}% again"
)
message = f"Fees for {from_asset} -> {to_asset} {threshold_msg}: {url}"
Expand All @@ -59,7 +59,7 @@ def check_subscription(
if fee is None or previous_fee is None:
return False
fee_threshold = subscription.fee_threshold
below = fee < fee_threshold and previous_fee >= fee_threshold
below = fee <= fee_threshold and previous_fee > fee_threshold
above = fee > fee_threshold and previous_fee <= fee_threshold
return below or above

Expand Down
1 change: 0 additions & 1 deletion commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from warnings import filterwarnings
from telegram.warnings import PTBUserWarning

# this has to run before we import the command handlers
filterwarnings(
action="ignore", message=r".*CallbackQueryHandler", category=PTBUserWarning
)
11 changes: 4 additions & 7 deletions commands/subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async def to_asset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:

async def save_threshold(
update: Update, context: ContextTypes.DEFAULT_TYPE, fee_threshold: str
):
) -> int | None:
chat = update.effective_chat
async with db_session(context) as session:
try:
Expand All @@ -127,6 +127,7 @@ async def save_threshold(
logging.info(f"Added: {subscription}")
else:
await chat.send_message("You are already subscribed!")
return ConversationHandler.END


async def threshold(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
Expand All @@ -138,15 +139,11 @@ async def threshold(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
)
return CUSTOM_THRESHOLD

await save_threshold(update, context, query.data.strip("%"))

return ConversationHandler.END
return await save_threshold(update, context, query.data)


async def custom_threshold(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
await save_threshold(update, context, update.message.text)

return ConversationHandler.END
return await save_threshold(update, context, update.message.text)


entry_point = CommandHandler("subscribe", subscribe)
Expand Down
31 changes: 31 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import asyncpg
import pytest_asyncio
from sqlalchemy import make_url
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

from db import Base
from settings import Settings


@pytest_asyncio.fixture(scope="session")
async def test_db_url():
settings = Settings()
url = make_url(settings.database_url)
conn = await asyncpg.connect(
url.set(drivername="postgresql").render_as_string(False)
)
test_db = "fees_test"
await conn.execute(f"DROP DATABASE IF EXISTS {test_db}")
await conn.execute(f"CREATE DATABASE {test_db}")
await conn.close()
return url.set(database=test_db).render_as_string(hide_password=False)


@pytest_asyncio.fixture(scope="session")
async def db_session(test_db_url):
engine = create_async_engine(test_db_url)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async_session = async_sessionmaker(engine, expire_on_commit=False)
async with async_session() as session:
yield session
4 changes: 2 additions & 2 deletions db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import declarative_base
from telegram.ext import ContextTypes

from consts import Fees
Expand All @@ -22,7 +22,7 @@ def __str__(self):
return f"Subscription(chat_id={self.chat_id}, from_asset={self.from_asset}, to_asset={self.to_asset}, fee_threshold={self.fee_threshold})"

def pretty_string(self):
return f"{self.from_asset} -> {self.to_asset} below {self.fee_threshold}%"
return f"{self.from_asset} -> {self.to_asset} at {self.fee_threshold}%"


def db_session(context: ContextTypes.DEFAULT_TYPE) -> AsyncSession:
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,13 @@ default-groups = ["dev"]
[dependency-groups]
dev = [
"pytest>=8.3.4",
"pytest-asyncio>=0.25.3",
"ruff>=0.9.2",
"telethon>=1.38.1",
]

[tool.pytest.ini_options]
pythonpath = [
"."
]
asyncio_default_fixture_loop_scope = "session"
19 changes: 7 additions & 12 deletions settings.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
from pydantic import Field
from pydantic import Field, ConfigDict
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
telegram_bot_token: str = Field(
..., env="TELEGRAM_BOT_TOKEN", description="Telegram bot token"
)
check_interval: int = Field(
60, env="CHECK_INTERVAL", description="Interval to check API (seconds)"
)
telegram_bot_token: str = Field(..., description="Telegram bot token")
check_interval: int = Field(60, description="Interval to check API (seconds)")
api_url: str = Field(
"https://api.boltz.exchange",
env="API_URL",
description="Boltz API URL for submarine swaps",
)
database_url: str = Field(
env="DATABASE_URL",
description="Database URL for PostgreSQL",
)

class Config:
env_file = ".env"
env_file_encoding = "utf-8"
model_config = ConfigDict(
env_file=".env",
extra="allow",
)
39 changes: 38 additions & 1 deletion test_bot.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import pytest
from bot import check_subscription
from sqlalchemy.ext.asyncio import AsyncSession

from bot import check_subscription, check_fees
from db import Subscription


@pytest.mark.parametrize(
"current_fees, previous_fees, from_asset, to_asset, threshold, expected, test_description",
[
(
{"BTC": {"LN": 1.2}},
{"BTC": {"LN": 1.0}},
"BTC",
"LN",
1.0,
True,
"fee exactly at threshold",
),
(
{"BTC": {"LN": 0.8}},
{"BTC": {"LN": 1.2}},
Expand All @@ -24,6 +35,15 @@
True,
"LN to BTC fee goes above threshold",
),
(
{"LN": {"BTC": 0.8}},
{"LN": {"BTC": 1.0}},
"LN",
"BTC",
1.0,
False,
"fee goes back to threshold",
),
(
{"L-BTC": {"RBTC": 1.5}},
{"L-BTC": {"RBTC": 1.3}},
Expand Down Expand Up @@ -86,3 +106,20 @@ def test_check_subscription(

result = check_subscription(current_fees, previous_fees, subscription)
assert result == expected, f"Failed case: {test_description}"


@pytest.mark.asyncio(loop_scope="session")
async def test_check_fees(db_session: AsyncSession):
current_fees = {"BTC": {"LN": 1.2}}
subscriptions = [
Subscription(chat_id=123, from_asset="BTC", to_asset="LN", fee_threshold=1.0),
Subscription(chat_id=123, from_asset="BTC", to_asset="LN", fee_threshold=0.5),
]
db_session.add_all(subscriptions)
await db_session.commit()
result = await check_fees(db_session, current_fees)
assert len(result) == 0

current_fees = {"BTC": {"LN": 0.8}}
result = await check_fees(db_session, current_fees)
assert result[0].id == subscriptions[0].id, "Failed to check fees"
67 changes: 67 additions & 0 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import subprocess
from typing import Optional

import pytest
import pytest_asyncio
from telethon.tl.custom import Conversation

from telethon import TelegramClient
from telethon.sessions import StringSession

from settings import Settings


@pytest_asyncio.fixture(autouse=True, scope="session")
def bot(test_db_url):
"""Start bot to be tested."""
os.environ["DATABASE_URL"] = test_db_url
process = subprocess.Popen(
["uv", "run", "bot.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=os.environ,
)
yield
process.terminate()
process.wait()


class TestSettings(Settings):
test_api_id: int
test_api_hash: str
test_api_session: Optional[str]
test_bot_name: str


@pytest.fixture(scope="session")
def test_settings():
return TestSettings()


@pytest_asyncio.fixture(scope="session")
async def telegram_client(test_settings):
"""Connect to Telegram user for testing."""

client = TelegramClient(
StringSession(test_settings.test_api_session),
test_settings.test_api_id,
test_settings.test_api_hash,
sequential_updates=True,
)
await client.connect()
await client.get_me()
await client.get_dialogs()

yield client

await client.disconnect()


@pytest_asyncio.fixture(scope="session")
async def conv(telegram_client: TelegramClient, test_settings) -> Conversation:
"""Open conversation with the bot."""
async with telegram_client.conversation(
f"@{test_settings.test_bot_name}", timeout=3, max_messages=10000
) as conv:
yield conv
12 changes: 12 additions & 0 deletions tests/e2e/get_session_string.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from telethon import TelegramClient
from telethon.sessions import StringSession

from tests.e2e.conftest import TestSettings

settings = TestSettings()


with TelegramClient(
StringSession(), settings.test_api_id, settings.test_api_hash
) as client:
print("Session string:", client.session.save())
Loading

0 comments on commit 562eddd

Please sign in to comment.