Skip to content

Commit

Permalink
Merge pull request #233 from yepcord/project-structure-refactoring
Browse files Browse the repository at this point in the history
Project structure refactoring
  • Loading branch information
RuslanUC authored Oct 22, 2024
2 parents df8e8b5 + 0cf1dea commit 7dfe87e
Show file tree
Hide file tree
Showing 75 changed files with 3,616 additions and 3,295 deletions.
4 changes: 0 additions & 4 deletions .coveragerc

This file was deleted.

11 changes: 6 additions & 5 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ jobs:
- name: Check out repository code
uses: actions/checkout@v4

- name: Install Poetry
run: pipx install poetry

- name: Setup Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
cache: "poetry"

- name: Install dependencies
run: poetry install --no-interaction
run: poetry install --no-interaction --all-extras

- name: Shutdown Ubuntu MySQL
if: matrix.database == 'mariadb' || matrix.database == 'mysql'
Expand Down
2,180 changes: 1,212 additions & 968 deletions poetry.lock

Large diffs are not rendered by default.

45 changes: 34 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,33 +39,33 @@ yepcord = "yepcord.cli:main"
python = "^3.9"
quart = "0.19.6"
aiofiles = "^24.1.0"
websockets = "13.0.1"
uvicorn = "^0.30.6"
websockets = "^13.1"
uvicorn = "^0.32.0"
python-magic = "^0.4.27"
pillow = "^10.4.0"
pillow = "^11.0.0"
protobuf = "4.25.3"
python-dateutil = "^2.9.0.post0"
cryptography = "^43.0.0"
cryptography = "^43.0.3"
emoji = "^2.12.1"
bcrypt = "^4.2.0"
quart-schema = "0.20.0"
pydantic = "^2.8.2"
werkzeug = "3.0.4"
aioftp = "^0.22.3"
orjson = "^3.10.7"
aioftp = { version = "^0.23.1", optional = true }
orjson = "^3.10.9"
mailers = {version = "^3.0.5", extras = ["smtp"]}
redis = "^5.0.8"
click = "^8.1.7"
maxminddb = "^2.6.2"
wget = "3.2"
tortoise-orm = {extras = ["aiosqlite", "asyncmy", "accel"], version = "^0.21.6"}
uvloop = "^0.20.0"
tortoise-orm = {extras = ["aiosqlite", "asyncmy", "accel"], version = "^0.21.7"}
uvloop = "^0.21.0"
async-timeout = "^4.0.3"
aerich = "^0.7.2"
yc-protobuf3-to-dict = "^0.3.0"
s3lite = "^0.1.8"
fast-depends = "^2.4.11"
faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.20"}
s3lite = { version = "^0.1.8", optional = true }
fast-depends = "^2.4.12"
faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.28"}

[tool.poetry.group.dev.dependencies]
pytest = "^8.2.0"
Expand All @@ -75,7 +75,30 @@ pyftpdlib = "1.5.8"
fake-s3 = "1.0.2"
types-protobuf = "^4.24.0.4"
pytest-httpx = "^0.30.0"
poethepoet = "^0.29.0"

[tool.poetry.extras]
s3 = ["s3lite"]
ftp = ["aioftp"]

[tool.poetry.group.profiling.dependencies]
viztracer = "^0.16.3"
pyinstrument = "^5.0.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.coverage.run]
omit = ["yepcord/yepcord/proto.py"]
data_file = "coverage.coverage"

[tool.poe.tasks]
migrate = "yepcord.cli migrate"
run = "yepcord.cli:main"
test = "pytest -x --cov-report=xml --cov-append --disable-warnings --cov=yepcord/yepcord"
i = "poetry install"
ie = "poetry install --all-extras"
u = "poetry update"
a = "poetry add"
ad = "poetry add --group dev"
2 changes: 1 addition & 1 deletion tests/api/test_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from yepcord.rest_api.main import app
from yepcord.yepcord.snowflake import Snowflake
from .utils import TestClientType, create_users, create_application
from ..yep_image import YEP_IMAGE
from ..utils import register_app_error_handler
from ..yep_image import YEP_IMAGE

register_app_error_handler(app)

Expand Down
14 changes: 3 additions & 11 deletions tests/api/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest_asyncio

from yepcord.rest_api.main import app
from yepcord.yepcord.utils.jwt import JWT
from yepcord.yepcord.config import Config
from yepcord.yepcord.snowflake import Snowflake
from yepcord.yepcord.utils import b64decode, b64encode
Expand All @@ -25,15 +26,6 @@ async def setup_db():
await app.ensure_async(func)()


def generateEmailVerificationToken(user_id: int, email: str, key: bytes):
key = new(key, str(user_id).encode('utf-8'), sha256).digest()
t = int(time())
sig = b64encode(new(key, f"{user_id}:{email}:{t}".encode('utf-8'), sha256).digest())
token = b64encode(dumps({"id": user_id, "email": email, "time": t}))
token += f".{sig}"
return token


@pt.mark.asyncio
async def test_login_nonexistent_user():
client: TestClientType = app.test_client()
Expand Down Expand Up @@ -109,15 +101,15 @@ async def test_verify_email():
client: TestClientType = app.test_client()
user = (await create_users(client, 1))[0]

token = generateEmailVerificationToken(int(user["id"]), user["email"], b64decode(Config.KEY))
token = JWT.encode({"id": int(user["id"]), "email": user["email"]}, b64decode(Config.KEY), expires_after=600)

resp = await client.post("/api/v9/auth/verify", json={"token": ""})
assert resp.status_code == 400
resp = await client.post("/api/v9/auth/verify", json={'token': "1"})
assert resp.status_code == 400

resp = await client.post("/api/v9/auth/verify", json={'token': token})
assert resp.status_code == 200
assert resp.status_code == 200, await resp.get_json()
json = await resp.get_json()
assert json["token"]
assert json["user_id"] == user["id"]
Expand Down
4 changes: 2 additions & 2 deletions tests/api/test_guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pytest_asyncio

from yepcord.rest_api.main import app
from yepcord.yepcord.classes.other import MFA
from yepcord.yepcord.utils.mfa import MFA
from yepcord.yepcord.enums import ChannelType
from yepcord.yepcord.snowflake import Snowflake
from tests.api.utils import TestClientType, create_users, create_guild, create_invite, enable_mfa, create_guild_channel, \
Expand Down Expand Up @@ -306,7 +306,7 @@ async def test_delete_guild():
resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": "wrong"})
assert resp.status_code == 400

resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": mfa.getCode()})
resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": mfa.get_code()})
assert resp.status_code == 204


Expand Down
19 changes: 10 additions & 9 deletions tests/api/test_mfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import pytest_asyncio

from yepcord.rest_api.main import app
from yepcord.yepcord.classes.other import MFA, JWT
from yepcord.yepcord.utils.jwt import JWT
from yepcord.yepcord.config import Config
from yepcord.yepcord.utils import b64decode
from yepcord.yepcord.utils.mfa import MFA
from .utils import TestClientType, create_users, enable_mfa
from ..utils import register_app_error_handler

Expand All @@ -23,7 +24,7 @@ async def setup_db():
def generateMfaVerificationKey(nonce: str, mfa_key: str, key: bytes):
if not (payload := JWT.decode(nonce, key + b64decode(mfa_key))):
return
token = JWT.encode({"code": payload["code"]}, key)
token = JWT.encode({"code": payload["c"]}, key)
signature = token.split(".")[2]
return signature.replace("-", "").replace("_", "")[:8].upper()

Expand Down Expand Up @@ -58,21 +59,21 @@ async def test_mfa_enable():
secret = "a" * 16
mfa = MFA(secret, 0)

code = mfa.getCode()
code = mfa.get_code()
invalid_code = (code + str((int(code[-1]) + 1) % 10))[1:]
resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers,
json={'code': invalid_code, 'secret': secret, 'password': user["password"]})
assert resp.status_code == 400

resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers,
json={'code': mfa.getCode(), 'secret': secret, 'password': user["password"]})
json={'code': mfa.get_code(), 'secret': secret, 'password': user["password"]})
assert resp.status_code == 200
json = await resp.get_json()
assert json["token"]
user["token"] = json["token"]
headers = {"Authorization": user["token"]}
resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers,
json={'code': mfa.getCode(), 'secret': secret, 'password': user["password"]})
json={'code': mfa.get_code(), 'secret': secret, 'password': user["password"]})
assert resp.status_code == 404

check_codes(json["backup_codes"], user_id)
Expand Down Expand Up @@ -146,7 +147,7 @@ async def test_login_with_mfa():
assert json["mfa"]
assert (ticket := json["ticket"])

code = mfa.getCode()
code = mfa.get_code()
invalid_code = (code + str((int(code[-1]) + 1) % 10))[1:]

resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": "", "code": ""}) # No ticket
Expand All @@ -158,7 +159,7 @@ async def test_login_with_mfa():
resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": invalid_code}) # Invalid code
assert resp.status_code == 400

resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": mfa.getCode()})
resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": mfa.get_code()})
assert resp.status_code == 200
json = await resp.get_json()
assert json["token"]
Expand All @@ -178,12 +179,12 @@ async def test_disable_mfa():
resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': ""})
assert resp.status_code == 400

resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.getCode()})
resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.get_code()})
assert resp.status_code == 200
json = await resp.get_json()
assert json["token"]
user["token"] = json["token"]
headers = {"Authorization": user["token"]}

resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.getCode()})
resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.get_code()})
assert resp.status_code == 404
33 changes: 32 additions & 1 deletion tests/api/test_user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import pytest_asyncio

from yepcord.rest_api.main import app
from .utils import TestClientType, create_users, create_guild
from yepcord.yepcord.enums import ChannelType
from .utils import TestClientType, create_users, create_guild, create_invite
from ..yep_image import YEP_IMAGE
from ..utils import register_app_error_handler

Expand Down Expand Up @@ -67,6 +68,7 @@ async def test_get_my_profile():
assert resp.status_code == 200
json = await resp.get_json()
assert json["user"]["id"] == user_id
assert len(json["mutual_guilds"]) > 0


@pt.mark.asyncio
Expand Down Expand Up @@ -166,3 +168,32 @@ async def test_hypesquad_change_house():

resp = await client.post("/api/v9/hypesquad/online", headers=headers, json={'house_id': 4})
assert resp.status_code == 400


@pt.mark.asyncio
async def test_get_other_profile():
client: TestClientType = app.test_client()
user, user2 = await create_users(client, 2)
guild = await create_guild(client, user, "Test Guild")
headers = {"Authorization": user["token"]}
channel = [channel for channel in guild["channels"] if channel["type"] == ChannelType.GUILD_TEXT][0]
invite = await create_invite(client, user, channel["id"])

resp = await client.post(f"/api/v9/invites/{invite['code']}", headers={"Authorization": user2["token"]})
assert resp.status_code == 200

resp = await client.get(f"/api/v9/users/{user2['id']}/profile?with_mutual_guilds=true", headers=headers)
assert resp.status_code == 200
json = await resp.get_json()
assert json["user"]["id"] == user2["id"]
assert len(json["mutual_guilds"]) > 0
assert json["mutual_guilds"] == [{"id": guild["id"], "nick": None}]

resp = await client.patch(f"/api/v9/guilds/{guild['id']}/members/{user2['id']}", headers=headers,
json={"nick": "TEST"})
assert resp.status_code == 200

resp = await client.get(f"/api/v9/users/{user2['id']}/profile?with_mutual_guilds=true", headers=headers)
assert resp.status_code == 200
json = await resp.get_json()
assert json["mutual_guilds"] == [{"id": guild["id"], "nick": "TEST"}]
13 changes: 8 additions & 5 deletions tests/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
from hashlib import sha256
from typing import Optional, Union
from typing import Optional, Union, Awaitable, Callable

from quart.typing import TestWebsocketConnectionProtocol

from yepcord.rest_api.main import app as _app
from yepcord.yepcord.classes.other import MFA
from yepcord.yepcord.utils.mfa import MFA
from yepcord.yepcord.enums import ChannelType, GatewayOp
from yepcord.yepcord.snowflake import Snowflake
from yepcord.yepcord.utils import getImage
from tests.yep_image import YEP_IMAGE

TestClientType = _app.test_client_class

#User.y.hash_password = lambda password, user_id: password
#User.check_password = lambda self, password: self.password == password


async def create_user(app: TestClientType, email: str, password: str, username: str, *, exp_code: int=200) -> Optional[str]:
response = await app.post('/api/v9/auth/register', json={
Expand Down Expand Up @@ -55,7 +58,7 @@ async def create_users(app: TestClientType, count: int = 1) -> list[dict]:

async def enable_mfa(app: TestClientType, user: dict, mfa: MFA) -> None:
resp = await app.post("/api/v9/users/@me/mfa/totp/enable", headers={"Authorization": user["token"]},
json={"code": mfa.getCode(), "secret": mfa.key, "password": user["password"]})
json={"code": mfa.get_code(), "secret": mfa.key, "password": user["password"]})
assert resp.status_code == 200, (resp.status_code, await resp.get_json())
json = await resp.get_json()
assert json["token"]
Expand Down Expand Up @@ -390,7 +393,7 @@ def __init__(self, token: str):
self.heartbeatTask: Optional[asyncio.Task] = None
self.mainTask: Optional[asyncio.Task] = None

self.handlers = {
self.handlers: dict[int, Callable[[TestWebsocketConnectionProtocol, Optional[dict]], Awaitable[None]]] = {
GatewayOp.HELLO: self.handle_hello
}
self.listeners: list[GatewayClient.EventListener] = []
Expand All @@ -405,7 +408,7 @@ async def run(self, ws: TestWebsocketConnectionProtocol, task=True) -> None:
return

while self.running:
msg = await ws.receive_json()
msg: dict = await ws.receive_json()
if msg["op"] in self.handlers:
await self.handlers[msg["op"]](ws, msg.get("data"))

Expand Down
Loading

0 comments on commit 7dfe87e

Please sign in to comment.