Skip to content

Commit

Permalink
Merge pull request #100 from placeTW/mock-apis
Browse files Browse the repository at this point in the history
Mock API testing, postprocess refactor
  • Loading branch information
howardt12345 authored Aug 4, 2024
2 parents f39552f + 52cc973 commit e5db37a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 45 deletions.
14 changes: 11 additions & 3 deletions commands/fetch_entry/fetch_entry_cmd.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import aiohttp
import discord
from discord import app_commands
from modules import async_utils, postprocess
from modules import async_utils
from . import postprocess
import typing
from discord.app_commands import Choice
from .fetch_entry_main import _fetch_entry_with_json, send_fetch_response
Expand All @@ -27,6 +28,7 @@ async def fetch_entry(
entry: Choice[str],
lang: Choice[str],
field: Choice[str] = None,
is_ephemeral: bool = True,
):
"""This function fetches an entry's field as needed.
If field is empty, the entire entry is returned.
Expand All @@ -36,8 +38,9 @@ async def fetch_entry(
entry (str): The entry to fetch.
lang (str): The language of the entry to fetch.
field (str, optional): Field to fetch: title, blurb, description,
or links.
If not passed, return the entire entry.
or links. If not passed, return the entire entry.
is_ephemeral (bool, optional): Whether the response should be ephemeral
(only visible to the user who triggered the command). Defaults to True.
"""
# * assemble values
selected_lang = lang.value # lang always exists
Expand All @@ -49,12 +52,17 @@ async def fetch_entry(
interaction, selected_entry, selected_lang, selected_field
)

if not res:
await interaction.response.send_message("Failed to fetch entry.", ephemeral=True)
return

await send_fetch_response(
interaction,
res,
selected_entry,
selected_lang,
selected_field,
is_ephermeral=is_ephemeral,
)


Expand Down
19 changes: 17 additions & 2 deletions commands/fetch_entry/fetch_entry_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import discord

from commands.entry_consts.consts import I18N_JSON_URL
from modules import async_utils, postprocess
from modules import async_utils
from . import postprocess
from commands.entry_consts.consts import (

SUPPORTED_LANGUAGE_CODES,
)


async def get_json(how="url", json_url="") -> dict:
Expand Down Expand Up @@ -51,7 +56,17 @@ async def _fetch_entry_with_json(
)
# * if some error happens, notify user and stop
if result_json is None:
return
print("Failed to fetch JSON.")
return None
if entry not in result_json:
print("Entry not found.")
return None
if lang not in SUPPORTED_LANGUAGE_CODES.keys():
print("Language not found.")
return None
if field is not None and field not in result_json[entry]:
print("Field not found.")
return None

if field is None: # * return entire entry
result = result_json[entry]
Expand Down
5 changes: 3 additions & 2 deletions commands/fetch_entry/fetch_entry_ui.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
import discord
from discord import app_commands
from modules import async_utils, postprocess
from modules import async_utils
from . import postprocess
import typing
from discord.app_commands import Choice
from .fetch_entry_main import _fetch_entry_with_json, send_fetch_response
Expand Down Expand Up @@ -127,7 +128,7 @@ async def on_timeout(self) -> None:
def register_commands(tree, guilds: list[discord.Object]):
@tree.command(
name="ui-fetch",
description="Eady-to-use entry selector",
description="Easy-to-use entry selector",
guilds=guilds,
)
async def ui_fetch(interaction: discord.Interaction):
Expand Down
File renamed without changes.
102 changes: 79 additions & 23 deletions tests/test_fetch_entry.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,102 @@
import commands.fetch_entry.fetch_entry_main as fetch
from modules.async_utils import _async_get_json
from commands.fetch_entry import postprocess
import pytest
from commands.entry_consts.consts import SUPPORTED_LANGUAGE_CODES, SUPPORTED_ART2023_IDS

LANGS_TO_TEST = ["cz", "en", "et", "fr", "lt", "lv", "ua", "zh"]
LANGS_TO_TEST = list(SUPPORTED_LANGUAGE_CODES.keys())
ENTRIES_TO_TEST = list(SUPPORTED_ART2023_IDS.keys())

@pytest.mark.parametrize("entry", ["capoo"])
@pytest.mark.parametrize("lang", ["en", "lt", "et"])
@pytest.mark.parametrize("lang", LANGS_TO_TEST)
async def test_placetw_art_schema(lang: str):
"""Tests that the API can be fetched and that the schema is correct."""
url = f"https://placetw.com/locales/{lang}/art-pieces.json"
result = await _async_get_json(url)
assert type(result) is dict
# assert that result's values has the following keys and types:
# "title" (str), "blurb" (str), "desc" (str), "links" (list)
for art_name, art_dict in result.items():
assert "title" in art_dict
assert type(art_dict["title"]) is str
assert "blurb" in art_dict
assert type(art_dict["blurb"]) is str
assert "desc" in art_dict
assert type(art_dict["desc"]) is str
assert "links" in art_dict
assert type(art_dict["links"]) is list

@pytest.fixture
def mock_art_piece_json(monkeypatch):
# mock _fetch_entry_with_json to return a dict
async def mock_get_json(*args, **kwargs) -> dict:
return {
art_key : { # the contents don't matter here
"title": art_key,
"blurb": art_desc,
"desc": art_desc,
"links": ["https://nonexistent_link.com"],
} for art_key, art_desc in SUPPORTED_ART2023_IDS.items()
}
monkeypatch.setattr(fetch, "get_json", mock_get_json)

@pytest.mark.parametrize("entry", ENTRIES_TO_TEST)
@pytest.mark.parametrize("lang", LANGS_TO_TEST)
@pytest.mark.parametrize("field", ["title", "blurb", "desc", None])
async def test__fetch_entry_with_json_valid_input_returns_str(
entry, lang, field
async def test__fetch_entry_with_json_valid_input_return_val(
entry, lang, field, mock_art_piece_json
):
"""
Checks that _fetch_entry_with_json returns string when
correct input is given.
Checks that _fetch_entry_with_json returns the expected type.
"""
# check case where field is not "links" (should return a string)
result = await fetch._fetch_entry_with_json(
interaction=None, entry=entry, lang=lang, field=field
)
assert type(result) is str


@pytest.mark.parametrize("entry", ["capoo"])
@pytest.mark.parametrize("lang", LANGS_TO_TEST)
async def test__fetch_entry_with_json_valid_input_links_returns_list(
entry, lang
):
"""
Checks that _fetch_entry_with_json returns string when
correct input is given.
"""
# check case where field is "links" (should return a list instead)
result = await fetch._fetch_entry_with_json(
interaction=None, entry=entry, lang=lang, field="links"
)
assert type(result) is list

@pytest.mark.parametrize("entry,lang,field",
[
("bad_entry", "en", None),
("capoo", "bad_lang", "title"),
("capoo", "en", "bad_field"),
]
)
async def test__fetch_entry_with_json_invalid_input_return_val(entry, lang, field, mock_art_piece_json):
# make sure that invalid input returns None
result = await fetch._fetch_entry_with_json(
interaction=None, entry=entry, lang=lang, field=field
)
assert result is None

@pytest.mark.parametrize("lang", ["aaaa", "bbbb", None, 3])
async def test__fetch_entry_with_json_invalid_lang_returns_none(lang):
@pytest.mark.xfail # need to fix get_json to return None on error
async def test__fetch_entry_with_json_error(monkeypatch):
"""
Checks that _fetch_entry_with_json returns None when
invalid input is given.
Checks that _fetch_entry_with_json returns None when get_json returns None (i.e. error).
"""
# monkkeypatch get_json to return None
async def mock_get_json(*args, **kwargs):
return None
monkeypatch.setattr(fetch, "get_json", mock_get_json)
result = await fetch._fetch_entry_with_json(
interaction=None, entry="capoo", lang=lang, field="title"
interaction=None, entry="capoo", lang="en", field=None
)
assert result is None
assert result in None

def test_postprocess_fetch_field():
assert postprocess.postprocess_fetch_field("hi") == "hi"
assert postprocess.postprocess_fetch_field("") == ""
assert (
postprocess.postprocess_fetch_field(["a", "b", "c"]) == "* a\n* b\n* c"
)


def test_postprocess_fetch_item_returns_str():
"""Since discord msgs only accept strings, this function should return only strings."""
input_dict = {"title": "a", "blurb": "b", "desc": "c", "links": "d"}
assert type(postprocess.postprocess_fetch_item(input_dict)) is str
15 changes: 0 additions & 15 deletions tests/test_postprocess.py

This file was deleted.

0 comments on commit e5db37a

Please sign in to comment.