Files
devplacepy/tests/unit/services/community/fakes.py
T
retoor 6f80a820da
DevPlace CI / test (push) Failing after 3m13s
Zo doen wijdat in oost
2026-10-16 07:20:44 +02:00

229 lines
8.2 KiB
Python

# retoor <retoor@molodetz.nl>
import json
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
class FakeResponse:
def __init__(self, status: int, body):
self.status = status
self.body = body if isinstance(body, str) else json.dumps(body)
self.headers = {}
async def text(self):
return self.body
async def json(self):
return json.loads(self.body)
class FakeFetcher:
def __init__(self, routes: dict):
self.routes = routes
self.requests: list[str] = []
self.session = None
self.posts: list[tuple] = []
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return None
@staticmethod
def key(url: str, params: dict | None) -> str:
parts = urlsplit(url)
query = dict(parse_qsl(parts.query))
query.update({name: str(value) for name, value in (params or {}).items()})
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(sorted(query.items())), ""))
async def get(self, url, params=None, timeout=None, **kwargs):
key = self.key(url, params)
self.requests.append(key)
route = self.routes.get(key)
if route is None:
return FakeResponse(404, "not found")
if callable(route):
route = route()
status, body = route if isinstance(route, tuple) else (200, route)
return FakeResponse(status, body)
async def post(self, url, data=None, timeout=None, **kwargs):
self.posts.append((url, dict(data or {})))
route = self.routes.get("post:" + url)
if callable(route):
route = route(dict(data or {}))
status, body = route if isinstance(route, tuple) else (200, route or "")
return FakeResponse(status, body)
async def render_html(self, url, session=None):
route = self.routes.get("render:" + url)
if route is None:
response = await self.get(url)
return response.status, response.body
return 200, route
def page(self, session=None):
return FakePage(self)
async def render(self, url, collect_links=False):
response = await self.get(url)
return {"status": response.status, "text": response.body, "links": [], "rendered": False}
def json_post(number: int) -> dict:
return {
"id": 1000 + number,
"title": f"Post number {number} about asyncio",
"content_html": f"<p>This is the body of post {number}, long enough to sync.</p>",
"created_at": f"2026-10-{number:02d}T10:00:00Z",
"author": {"id": 7 + number % 2, "username": f"writer{number % 2}", "url": f"https://forum.test/u/{number % 2}"},
"url": f"https://forum.test/posts/{1000 + number}",
"comments": [
{
"id": 5000 + number * 10 + index,
"body": f"Comment {index} on post {number}",
"created_at": f"2026-10-{number:02d}T11:0{index}:00Z",
"user": {"id": 20 + index, "username": f"replier{index}"},
}
for index in range(2)
],
}
def json_community() -> dict:
posts = [json_post(number) for number in range(1, 9)]
return {
"https://forum.test/": '<html lang="en"><head><title>Forum Test</title>'
'<meta property="og:description" content="A friendly forum about code."></head>'
"<body><p>Join 1,234 members discussing code every single day on this community site.</p></body></html>",
"https://forum.test/api/posts": {"items": posts, "total": 8},
"https://forum.test/api/posts?page=2": {"items": [json_post(9), json_post(10)], "total": 8},
"https://forum.test/about": "<html><body><p>About: 2,500 members strong.</p></body></html>",
}
RSS = """<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel>
<title>Blog Test</title><description>A blog</description>
{items}
</channel></rss>"""
RSS_ITEM = """<item><title>Entry {n} on testing</title><link>https://blog.test/p/{n}</link>
<guid>https://blog.test/p/{n}</guid><pubDate>Tue, {n:02d} Oct 2026 10:00:00 GMT</pubDate>
<dc:creator>Alice</dc:creator><description>&lt;p&gt;Body of entry {n} with enough text.&lt;/p&gt;</description></item>"""
def detail_page(kind: str, n: int, comments: int = 2) -> str:
rows = "".join(
f'<div class="comment" id="c{n}-{index}"><span class="author">replier{index}</span>'
f'<time datetime="2026-10-{n:02d}T12:0{index}:00Z">t</time>'
f'<div class="comment-body">Comment {index} on {kind} {n} with words</div>'
+ (
f'<div class="comment" id="c{n}-{index}-r"><span class="author">deep{index}</span>'
f'<div class="comment-body">Nested reply to comment {index} on {n}</div></div>'
if index == 0
else ""
)
+ "</div>"
for index in range(comments)
)
body = f"Full body of {kind} {n}. " + "It goes on at length about the topic. " * 6
return f"<html><body><article>{body}</article><section>{rows}</section></body></html>"
def rss_community() -> dict:
items = "".join(RSS_ITEM.format(n=n) for n in range(1, 7))
pages = {f"https://blog.test/p/{n}": detail_page("entry", n) for n in range(1, 7)}
return {
**pages,
"https://blog.test/": '<html><head><link rel="alternate" type="application/rss+xml" href="/feed.xml">'
"<title>Blog Test</title></head><body>hi</body></html>",
"https://blog.test/feed.xml": RSS.format(items=items),
}
HTML_ITEM = """<article class="post"><h2><a href="/t/{n}">Thread {n} on shells and terminals</a></h2>
<span class="author">user{n}</span><time datetime="2026-10-{n:02d}T09:00:00Z">date</time>
<p>Excerpt of thread {n} which is certainly long enough to count as content.</p></article>"""
def html_community() -> dict:
items = "".join(HTML_ITEM.format(n=n) for n in range(1, 7))
pages = {f"https://board.test/t/{n}": detail_page("thread", n) for n in range(1, 7)}
return {
**pages,
"https://board.test/": f"<html><head><title>Board</title></head><body><main>{items}</main></body></html>",
}
async def no_sleep(_seconds):
return None
class FakePage:
def __init__(self, fetcher):
self.fetcher = fetcher
self.filled: dict[str, str] = {}
self.url = ""
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return None
async def goto(self, url, **kwargs):
self.url = url
status, self.html = await self.fetcher.render_html(url)
return type("Resp", (), {"status": status})()
async def wait_for_timeout(self, _ms):
return None
async def content(self):
return self.html
async def fill(self, selector, value):
self.filled[selector] = value
async def click(self, selector):
self.fetcher.posts.append(("click", dict(self.filled)))
SPA_SHELL = "<html><head><title>Spa</title></head><body><div id=app></div></body></html>"
def spa_community() -> dict:
items = "".join(HTML_ITEM.format(n=n) for n in range(1, 7))
return {
"https://spa.test/": SPA_SHELL,
"render:https://spa.test/": f"<html><body><main>{items}</main></body></html>",
}
WALL = (
'<html><body><form action="/login" method="post"><input type="hidden" name="csrf" value="t1">'
'<input type="text" name="user"><input type="password" name="pass"></form></body></html>'
)
CLUB_HOME = (
'<html><head><title>Club</title><link rel="alternate" type="application/rss+xml" href="/feed.xml"></head>'
"<body><p>Members only area with plenty of text on the page for the club.</p></body></html>"
)
def club_community(state: dict) -> dict:
items = "".join(RSS_ITEM.format(n=n).replace("blog.test", "club.test") for n in range(1, 7))
feed = RSS.format(items=items)
def login(data):
state["in"] = data.get("user") == "bob" and data.get("pass") == "pw" and data.get("csrf") == "t1"
return (200, "welcome" if state["in"] else WALL)
return {
"https://club.test/": lambda: CLUB_HOME if state.get("in") else WALL,
"https://club.test/login": WALL,
"https://club.test/feed.xml": lambda: feed if state.get("in") else (401, "login"),
"post:https://club.test/login": login,
}