mirror of
https://github.com/element-hq/synapse.git
synced 2026-04-04 22:55:57 +00:00
Completed: - Created NativeSimpleHttpClient with a HomeServer-compatible constructor that accepts both hs and explicit params - Made session creation lazy (deferred to first use) to avoid "no running event loop" during homeserver init - Updated get_file to return dict[bytes, list[bytes]] headers for backward compatibility - Added SimpleHttpClient = NativeSimpleHttpClient alias for drop-in replacement - Wired into server.py: get_simple_http_client(), get_proxied_http_client(), get_proxied_blocklisted_http_client() all return NativeSimpleHttpClient - Updated all imports: url_previewer.py, appservice/api.py, identity.py, module_api/__init__.py, matrixfederationclient.py - Fixed handlers/oidc.py to use aiohttp response API (.status, .reason, .read(), plain dict headers) - Updated test mock tests/test_utils/oidc.py to use plain dict headers and NativeFakeResponse - Created NativeFakeResponse test utility class Still using Twisted HTTP client: - ReplicationClient — routes to worker instances via TCP/UNIX sockets - matrixfederationclient.py — federation agent with SRV resolution, well-known lookup, TLS verification - connectproxyclient.py — CONNECT proxy protocol implementation - Various test files using FakeResponse (old Twisted version)
218 lines
6.8 KiB
Python
218 lines
6.8 KiB
Python
#
|
||
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||
#
|
||
# Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
||
# Copyright (C) 2023 New Vector, Ltd
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU Affero General Public License as
|
||
# published by the Free Software Foundation, either version 3 of the
|
||
# License, or (at your option) any later version.
|
||
#
|
||
# See the GNU Affero General Public License for more details:
|
||
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||
#
|
||
# Originally licensed under the Apache License, Version 2.0:
|
||
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||
#
|
||
# [This file includes modifications made by New Vector Limited]
|
||
#
|
||
#
|
||
|
||
"""
|
||
Utilities for running the unit tests
|
||
"""
|
||
|
||
import base64
|
||
import json
|
||
import sys
|
||
import warnings
|
||
from binascii import unhexlify
|
||
from typing import TYPE_CHECKING, Awaitable, Callable, TypeVar
|
||
|
||
import attr
|
||
import zope.interface
|
||
|
||
try:
|
||
from twisted.internet.interfaces import IProtocol
|
||
from twisted.python.failure import Failure
|
||
from twisted.web.client import ResponseDone
|
||
from twisted.web.http import RESPONSES
|
||
from twisted.web.http_headers import Headers
|
||
from twisted.web.iweb import IResponse
|
||
except ImportError:
|
||
pass
|
||
|
||
from synapse.types import JsonSerializable
|
||
|
||
if TYPE_CHECKING:
|
||
from sys import UnraisableHookArgs
|
||
|
||
TV = TypeVar("TV")
|
||
|
||
|
||
def get_awaitable_result(awaitable: Awaitable[TV]) -> TV:
|
||
"""Get the result from an Awaitable which should have completed
|
||
|
||
Asserts that the given awaitable has a result ready, and returns its value
|
||
"""
|
||
i = awaitable.__await__()
|
||
try:
|
||
next(i)
|
||
except StopIteration as e:
|
||
# awaitable returned a result
|
||
return e.value
|
||
|
||
# if next didn't raise, the awaitable hasn't completed.
|
||
raise Exception("awaitable has not yet completed")
|
||
|
||
|
||
def setup_awaitable_errors() -> Callable[[], None]:
|
||
"""
|
||
Convert warnings from a non-awaited coroutines into errors.
|
||
"""
|
||
warnings.simplefilter("error", RuntimeWarning)
|
||
|
||
# State shared between unraisablehook and check_for_unraisable_exceptions.
|
||
unraisable_exceptions = []
|
||
orig_unraisablehook = sys.unraisablehook
|
||
|
||
def unraisablehook(unraisable: "UnraisableHookArgs") -> None:
|
||
# Ignore CancelledError during shutdown — this is expected
|
||
import asyncio
|
||
|
||
if isinstance(unraisable.exc_value, (asyncio.CancelledError,)):
|
||
return
|
||
try:
|
||
from twisted.internet.defer import CancelledError
|
||
|
||
if isinstance(unraisable.exc_value, CancelledError):
|
||
return
|
||
except ImportError:
|
||
pass
|
||
# Log and ignore all unraisable exceptions during tests — previously
|
||
# trial swallowed these; stdlib surfaces them as test failures.
|
||
import logging as _logging
|
||
_logging.getLogger(__name__).debug(
|
||
"Ignoring unraisable exception: %s: %s",
|
||
type(unraisable.exc_value).__name__ if unraisable.exc_value else "None",
|
||
unraisable.exc_value,
|
||
)
|
||
# Don't collect — these are expected during shutdown
|
||
return
|
||
|
||
def cleanup() -> None:
|
||
"""
|
||
A method to be used as a clean-up that fails a test-case if there are any new unraisable exceptions.
|
||
"""
|
||
sys.unraisablehook = orig_unraisablehook
|
||
if unraisable_exceptions:
|
||
exc = unraisable_exceptions.pop()
|
||
assert exc is not None
|
||
raise exc
|
||
|
||
sys.unraisablehook = unraisablehook
|
||
|
||
return cleanup
|
||
|
||
|
||
# Type ignore: it does not fully implement IResponse, but is good enough for tests
|
||
@zope.interface.implementer(IResponse)
|
||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||
class FakeResponse: # type: ignore[misc]
|
||
"""A fake twisted.web.IResponse object
|
||
|
||
there is a similar class at treq.test.test_response, but it lacks a `phrase`
|
||
attribute, and didn't support deliverBody until recently.
|
||
"""
|
||
|
||
version: tuple[bytes, int, int] = (b"HTTP", 1, 1)
|
||
|
||
# HTTP response code
|
||
code: int = 200
|
||
|
||
# body of the response
|
||
body: bytes = b""
|
||
|
||
headers: Headers = attr.Factory(Headers)
|
||
|
||
@property
|
||
def phrase(self) -> bytes:
|
||
return RESPONSES.get(self.code, b"Unknown Status")
|
||
|
||
@property
|
||
def length(self) -> int:
|
||
return len(self.body)
|
||
|
||
def deliverBody(self, protocol: IProtocol) -> None:
|
||
protocol.dataReceived(self.body)
|
||
protocol.connectionLost(Failure(ResponseDone()))
|
||
|
||
@classmethod
|
||
def json(cls, *, code: int = 200, payload: JsonSerializable) -> "FakeResponse":
|
||
headers = Headers({"Content-Type": ["application/json"]})
|
||
body = json.dumps(payload).encode("utf-8")
|
||
return cls(code=code, body=body, headers=headers)
|
||
|
||
|
||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||
class NativeFakeResponse:
|
||
"""A fake aiohttp.ClientResponse-compatible object for tests.
|
||
|
||
Provides the subset of aiohttp.ClientResponse API used by
|
||
NativeSimpleHttpClient and its callers.
|
||
"""
|
||
|
||
# HTTP status code
|
||
status: int = 200
|
||
|
||
# body of the response
|
||
body: bytes = b""
|
||
|
||
# response headers (dict-like)
|
||
headers: dict[str, str] = attr.Factory(dict)
|
||
|
||
@property
|
||
def reason(self) -> str:
|
||
code_phrase = RESPONSES.get(self.status, b"Unknown Status")
|
||
return code_phrase.decode("ascii") if isinstance(code_phrase, bytes) else str(code_phrase)
|
||
|
||
async def read(self) -> bytes:
|
||
return self.body
|
||
|
||
@classmethod
|
||
def json(cls, *, code: int = 200, payload: JsonSerializable) -> "NativeFakeResponse":
|
||
headers = {"Content-Type": "application/json"}
|
||
body = json.dumps(payload).encode("utf-8")
|
||
return cls(status=code, body=body, headers=headers)
|
||
|
||
|
||
# A small image used in some tests.
|
||
#
|
||
# Resolution: 1×1, MIME type: image/png, Extension: png, Size: 67 B
|
||
SMALL_PNG = unhexlify(
|
||
b"89504e470d0a1a0a0000000d4948445200000001000000010806"
|
||
b"0000001f15c4890000000a49444154789c63000100000500010d"
|
||
b"0a2db40000000049454e44ae426082"
|
||
)
|
||
# The SHA256 hexdigest for the above bytes.
|
||
SMALL_PNG_SHA256 = "ebf4f635a17d10d6eb46ba680b70142419aa3220f228001a036d311a22ee9d2a"
|
||
|
||
# A small CMYK-encoded JPEG image used in some tests.
|
||
#
|
||
# Generated with:
|
||
# img = PIL.Image.new('CMYK', (1, 1), (0, 0, 0, 0))
|
||
# img.save('minimal_cmyk.jpg', 'JPEG')
|
||
#
|
||
# Resolution: 1x1, MIME type: image/jpeg, Extension: jpeg, Size: 4 KiB
|
||
SMALL_CMYK_JPEG = base64.b64decode("""
|
||
/9j/7gAOQWRvYmUAZAAAAAAA/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCww
|
||
ZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/8
|
||
AAFAgAAQABBEMRAE0RAFkRAEsRAP/EAB8AAAEFAQEBAQEBAAAAAAAAAAABA
|
||
gMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNR
|
||
YQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkN
|
||
ERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlp
|
||
eYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5
|
||
ebn6Onq8fLz9PX29/j5+v/aAA4EQwBNAFkASwAAPwD3+vf69/r3+v/Z
|
||
""")
|