mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-09 01:03:24 +00:00
Compare commits
15 Commits
04fc6cf626
...
python-ssl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72d81cc9d | ||
|
|
470dd96c68 | ||
|
|
612f597aa9 | ||
|
|
47be90add8 | ||
|
|
ab3d8bf7c9 | ||
|
|
ec4fb9b0b3 | ||
|
|
f83f83a0c4 | ||
|
|
ef2e1d895a | ||
|
|
384d47714e | ||
|
|
3547ed966f | ||
|
|
f1b921c211 | ||
|
|
ea5e20edc7 | ||
|
|
b09c271a76 | ||
|
|
ef7cb8cf50 | ||
|
|
c78b0c81e9 |
37
.github/workflows/main.yml
vendored
37
.github/workflows/main.yml
vendored
@@ -2,25 +2,38 @@ name: Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runs-on }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.8"]
|
include:
|
||||||
|
- python-version: "3.7"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "3.8"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "3.9"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "3.10"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
- python-version: "3.11"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
- python-version: "pypy-3.7"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "pypy-3.8"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "pypy-3.9"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
- python-version: "pypy-3.10"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
- python-version: "pypy-3.11"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install libgnutls28-dev
|
|
||||||
run: |
|
|
||||||
sudo apt update -q
|
|
||||||
sudo apt install -q -y libgnutls28-dev libcurl4-gnutls-dev
|
|
||||||
|
|
||||||
- uses: actions/checkout@v3.5.2
|
- uses: actions/checkout@v3.5.2
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
@@ -33,6 +46,12 @@ jobs:
|
|||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip install codecov tox tox-gh-actions
|
pip install codecov tox tox-gh-actions
|
||||||
|
|
||||||
|
- name: "Debug OpenSSL version used with Python ${{ matrix.python-version }}"
|
||||||
|
run: |
|
||||||
|
which python
|
||||||
|
python --version
|
||||||
|
python -c 'import ssl; print(ssl.OPENSSL_VERSION_INFO)'
|
||||||
|
|
||||||
- name: Run tests with tox
|
- name: Run tests with tox
|
||||||
run: tox
|
run: tox
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from assertions import assert_cassette_empty, assert_is_json
|
|||||||
|
|
||||||
import vcr
|
import vcr
|
||||||
from vcr.patch import force_reset
|
from vcr.patch import force_reset
|
||||||
|
from vcr.stubs.compat import get_headers
|
||||||
|
|
||||||
urllib3 = pytest.importorskip("urllib3")
|
urllib3 = pytest.importorskip("urllib3")
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ def test_headers(tmpdir, httpbin_both, verify_pool_mgr):
|
|||||||
headers = verify_pool_mgr.request("GET", url).headers
|
headers = verify_pool_mgr.request("GET", url).headers
|
||||||
|
|
||||||
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
with vcr.use_cassette(str(tmpdir.join("headers.yaml"))):
|
||||||
assert headers == verify_pool_mgr.request("GET", url).headers
|
new_headers = verify_pool_mgr.request("GET", url).headers
|
||||||
|
assert sorted(get_headers(headers)) == sorted(get_headers(new_headers))
|
||||||
|
|
||||||
|
|
||||||
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
def test_body(tmpdir, httpbin_both, verify_pool_mgr):
|
||||||
@@ -145,18 +147,18 @@ def test_https_with_cert_validation_disabled(tmpdir, httpbin_secure, pool_mgr):
|
|||||||
|
|
||||||
|
|
||||||
def test_urllib3_force_reset():
|
def test_urllib3_force_reset():
|
||||||
cpool = urllib3.connectionpool
|
conn = urllib3.connection
|
||||||
http_original = cpool.HTTPConnection
|
http_original = conn.HTTPConnection
|
||||||
https_original = cpool.HTTPSConnection
|
https_original = conn.HTTPSConnection
|
||||||
verified_https_original = cpool.VerifiedHTTPSConnection
|
verified_https_original = conn.VerifiedHTTPSConnection
|
||||||
with vcr.use_cassette(path="test"):
|
with vcr.use_cassette(path="test"):
|
||||||
first_cassette_HTTPConnection = cpool.HTTPConnection
|
first_cassette_HTTPConnection = conn.HTTPConnection
|
||||||
first_cassette_HTTPSConnection = cpool.HTTPSConnection
|
first_cassette_HTTPSConnection = conn.HTTPSConnection
|
||||||
first_cassette_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
first_cassette_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||||
with force_reset():
|
with force_reset():
|
||||||
assert cpool.HTTPConnection is http_original
|
assert conn.HTTPConnection is http_original
|
||||||
assert cpool.HTTPSConnection is https_original
|
assert conn.HTTPSConnection is https_original
|
||||||
assert cpool.VerifiedHTTPSConnection is verified_https_original
|
assert conn.VerifiedHTTPSConnection is verified_https_original
|
||||||
assert cpool.HTTPConnection is first_cassette_HTTPConnection
|
assert conn.HTTPConnection is first_cassette_HTTPConnection
|
||||||
assert cpool.HTTPSConnection is first_cassette_HTTPSConnection
|
assert conn.HTTPSConnection is first_cassette_HTTPSConnection
|
||||||
assert cpool.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
assert conn.VerifiedHTTPSConnection is first_cassette_VerifiedHTTPSConnection
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ def test_cookies(tmpdir, httpbin):
|
|||||||
with vcr.use_cassette(testfile):
|
with vcr.use_cassette(testfile):
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
s.get(httpbin.url + "/cookies/set?k1=v1&k2=v2")
|
||||||
|
assert s.cookies.keys() == ["k1", "k2"]
|
||||||
|
|
||||||
r2 = s.get(httpbin.url + "/cookies")
|
r2 = s.get(httpbin.url + "/cookies")
|
||||||
assert len(r2.json()["cookies"]) == 2
|
assert sorted(r2.json()["cookies"].keys()) == ["k1", "k2"]
|
||||||
|
|
||||||
|
|
||||||
def test_amazon_doctype(tmpdir):
|
def test_amazon_doctype(tmpdir):
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def test_vcr_use_cassette():
|
|||||||
|
|
||||||
|
|
||||||
def test_vcr_before_record_request_params():
|
def test_vcr_before_record_request_params():
|
||||||
base_path = "http://httpbin.org/"
|
base_path = "http://whatever.test/"
|
||||||
|
|
||||||
def before_record_cb(request):
|
def before_record_cb(request):
|
||||||
if request.path != "/get":
|
if request.path != "/get":
|
||||||
|
|||||||
13
tox.ini
13
tox.ini
@@ -3,8 +3,8 @@ skip_missing_interpreters=true
|
|||||||
envlist =
|
envlist =
|
||||||
cov-clean,
|
cov-clean,
|
||||||
lint,
|
lint,
|
||||||
{py37,py38,py39,py310,py311}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
|
{py37,py38,py39,py310,py311}-{requests-urllib3-1,requests-urllib3-2,httplib2,urllib3-1,urllib3-2,tornado4,boto3,aiohttp,httpx},
|
||||||
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
|
{pypy3}-{requests-urllib3-1,requests-urllib3-2,httplib2,urllib3-1,urllib3-2,tornado4,boto3},
|
||||||
{py310}-httpx019,
|
{py310}-httpx019,
|
||||||
cov-report
|
cov-report
|
||||||
|
|
||||||
@@ -85,11 +85,10 @@ deps =
|
|||||||
PyYAML
|
PyYAML
|
||||||
ipaddress
|
ipaddress
|
||||||
requests: requests>=2.22.0
|
requests: requests>=2.22.0
|
||||||
requests: urllib3<2
|
|
||||||
httplib2: httplib2
|
httplib2: httplib2
|
||||||
urllib3: urllib3<2
|
urllib3-1: urllib3<2
|
||||||
|
urllib3-2: urllib3<3
|
||||||
boto3: boto3
|
boto3: boto3
|
||||||
boto3: urllib3
|
|
||||||
aiohttp: aiohttp
|
aiohttp: aiohttp
|
||||||
aiohttp: pytest-asyncio
|
aiohttp: pytest-asyncio
|
||||||
aiohttp: pytest-aiohttp
|
aiohttp: pytest-aiohttp
|
||||||
@@ -101,8 +100,8 @@ deps =
|
|||||||
httpx019: httpx==0.19
|
httpx019: httpx==0.19
|
||||||
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
|
{py37,py38,py39,py310}-{httpx}: pytest-asyncio
|
||||||
depends =
|
depends =
|
||||||
lint,{py37,py38,py39,py310,py311,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp},{py37,py38,py39,py310,py311}-{httpx}: cov-clean
|
lint,{py37,py38,py39,py310,py311,pypy3}-{requests-urllib3-1,requests-urllib3-2,httplib2,urllib3-1,urllib3-2,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp},{py37,py38,py39,py310,py311}-{httpx}: cov-clean
|
||||||
cov-report: lint,{py37,py38,py39,py310,py311,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp}
|
cov-report: lint,{py37,py38,py39,py310,py311,pypy3}-{requests-urllib3-1,requests-urllib3-2,httplib2,urllib3-1,urllib3-2,tornado4,boto3},{py37,py38,py39,py310,py311}-{aiohttp}
|
||||||
passenv =
|
passenv =
|
||||||
AWS_ACCESS_KEY_ID
|
AWS_ACCESS_KEY_ID
|
||||||
AWS_DEFAULT_REGION
|
AWS_DEFAULT_REGION
|
||||||
|
|||||||
32
vcr/patch.py
32
vcr/patch.py
@@ -32,15 +32,17 @@ else:
|
|||||||
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
|
_cpoolBoto3HTTPSConnection = AWSHTTPSConnection
|
||||||
|
|
||||||
cpool = None
|
cpool = None
|
||||||
|
conn = None
|
||||||
# Try to save the original types for urllib3
|
# Try to save the original types for urllib3
|
||||||
try:
|
try:
|
||||||
|
import urllib3.connection as conn
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
_VerifiedHTTPSConnection = conn.VerifiedHTTPSConnection
|
||||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
_connHTTPConnection = conn.HTTPConnection
|
||||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
_connHTTPSConnection = conn.HTTPSConnection
|
||||||
|
|
||||||
# Try to save the original types for requests
|
# Try to save the original types for requests
|
||||||
try:
|
try:
|
||||||
@@ -198,7 +200,7 @@ class CassettePatcherBuilder:
|
|||||||
from .stubs import requests_stubs
|
from .stubs import requests_stubs
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
return ()
|
return ()
|
||||||
return self._urllib3_patchers(cpool, requests_stubs)
|
return self._urllib3_patchers(cpool, conn, requests_stubs)
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _boto3(self):
|
def _boto3(self):
|
||||||
@@ -248,12 +250,13 @@ class CassettePatcherBuilder:
|
|||||||
|
|
||||||
def _urllib3(self):
|
def _urllib3(self):
|
||||||
try:
|
try:
|
||||||
|
import urllib3.connection as conn
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
return ()
|
return ()
|
||||||
from .stubs import urllib3_stubs
|
from .stubs import urllib3_stubs
|
||||||
|
|
||||||
return self._urllib3_patchers(cpool, urllib3_stubs)
|
return self._urllib3_patchers(cpool, conn, urllib3_stubs)
|
||||||
|
|
||||||
@_build_patchers_from_mock_triples_decorator
|
@_build_patchers_from_mock_triples_decorator
|
||||||
def _httplib2(self):
|
def _httplib2(self):
|
||||||
@@ -330,7 +333,7 @@ class CassettePatcherBuilder:
|
|||||||
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
new_sync_client_send = sync_vcr_send(self._cassette, _HttpxSyncClient_send)
|
||||||
yield httpx.Client, "send", new_sync_client_send
|
yield httpx.Client, "send", new_sync_client_send
|
||||||
|
|
||||||
def _urllib3_patchers(self, cpool, stubs):
|
def _urllib3_patchers(self, cpool, conn, stubs):
|
||||||
http_connection_remover = ConnectionRemover(
|
http_connection_remover = ConnectionRemover(
|
||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPConnection)
|
||||||
)
|
)
|
||||||
@@ -338,9 +341,9 @@ class CassettePatcherBuilder:
|
|||||||
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
self._get_cassette_subclass(stubs.VCRRequestsHTTPSConnection)
|
||||||
)
|
)
|
||||||
mock_triples = (
|
mock_triples = (
|
||||||
(cpool, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
(conn, "VerifiedHTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||||
(cpool, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
(conn, "HTTPConnection", stubs.VCRRequestsHTTPConnection),
|
||||||
(cpool, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
(conn, "HTTPSConnection", stubs.VCRRequestsHTTPSConnection),
|
||||||
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
|
(cpool, "is_connection_dropped", mock.Mock(return_value=False)), # Needed on Windows only
|
||||||
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
|
(cpool.HTTPConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPConnection),
|
||||||
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
|
(cpool.HTTPSConnectionPool, "ConnectionCls", stubs.VCRRequestsHTTPSConnection),
|
||||||
@@ -410,16 +413,17 @@ def reset_patchers():
|
|||||||
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
|
yield mock.patch.object(httplib, "HTTPSConnection", _HTTPSConnection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import urllib3.connection as conn
|
||||||
import urllib3.connectionpool as cpool
|
import urllib3.connectionpool as cpool
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
yield mock.patch.object(cpool, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
yield mock.patch.object(conn, "VerifiedHTTPSConnection", _VerifiedHTTPSConnection)
|
||||||
yield mock.patch.object(cpool, "HTTPConnection", _cpoolHTTPConnection)
|
yield mock.patch.object(conn, "HTTPConnection", _connHTTPConnection)
|
||||||
yield mock.patch.object(cpool, "HTTPSConnection", _cpoolHTTPSConnection)
|
yield mock.patch.object(conn, "HTTPSConnection", _connHTTPSConnection)
|
||||||
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
if hasattr(cpool.HTTPConnectionPool, "ConnectionCls"):
|
||||||
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _cpoolHTTPConnection)
|
yield mock.patch.object(cpool.HTTPConnectionPool, "ConnectionCls", _connHTTPConnection)
|
||||||
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _cpoolHTTPSConnection)
|
yield mock.patch.object(cpool.HTTPSConnectionPool, "ConnectionCls", _connHTTPSConnection)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# unpatch botocore with awsrequest
|
# unpatch botocore with awsrequest
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ def parse_headers(header_list):
|
|||||||
|
|
||||||
|
|
||||||
def serialize_headers(response):
|
def serialize_headers(response):
|
||||||
|
headers = response.headers if response.msg is None else response.msg
|
||||||
out = {}
|
out = {}
|
||||||
for key, values in compat.get_headers(response.msg):
|
for key, values in compat.get_headers(headers):
|
||||||
out.setdefault(key, [])
|
out.setdefault(key, [])
|
||||||
out[key].extend(values)
|
out[key].extend(values)
|
||||||
return out
|
return out
|
||||||
@@ -67,6 +68,7 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
self.version = None
|
self.version = None
|
||||||
self._content = BytesIO(self.recorded_response["body"]["string"])
|
self._content = BytesIO(self.recorded_response["body"]["string"])
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
self._original_response = self # for requests.session.Session cookie extraction
|
||||||
|
|
||||||
headers = self.recorded_response["headers"]
|
headers = self.recorded_response["headers"]
|
||||||
# Since we are loading a response that has already been serialized, our
|
# Since we are loading a response that has already been serialized, our
|
||||||
@@ -143,6 +145,28 @@ class VCRHTTPResponse(HTTPResponse):
|
|||||||
def readable(self):
|
def readable(self):
|
||||||
return self._content.readable()
|
return self._content.readable()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length_remaining(self):
|
||||||
|
return self._content.getbuffer().nbytes - self._content.tell()
|
||||||
|
|
||||||
|
def get_redirect_location(self):
|
||||||
|
"""
|
||||||
|
Returns (a) redirect location string if we got a redirect
|
||||||
|
status code and valid location, (b) None if redirect status and
|
||||||
|
no location, (c) False if not a redirect status code.
|
||||||
|
See https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html .
|
||||||
|
"""
|
||||||
|
if not (300 <= self.status <= 399):
|
||||||
|
return False
|
||||||
|
return self.getheader("Location")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._content.getbuffer().tobytes()
|
||||||
|
|
||||||
|
def drain_conn(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VCRConnection:
|
class VCRConnection:
|
||||||
# A reference to the cassette that's currently being patched in
|
# A reference to the cassette that's currently being patched in
|
||||||
@@ -248,12 +272,13 @@ class VCRConnection:
|
|||||||
|
|
||||||
# get the response
|
# get the response
|
||||||
response = self.real_connection.getresponse()
|
response = self.real_connection.getresponse()
|
||||||
|
response_data = response.data if hasattr(response, "data") else response.read()
|
||||||
|
|
||||||
# put the response into the cassette
|
# put the response into the cassette
|
||||||
response = {
|
response = {
|
||||||
"status": {"code": response.status, "message": response.reason},
|
"status": {"code": response.status, "message": response.reason},
|
||||||
"headers": serialize_headers(response),
|
"headers": serialize_headers(response),
|
||||||
"body": {"string": response.read()},
|
"body": {"string": response_data},
|
||||||
}
|
}
|
||||||
self.cassette.append(self._vcr_request, response)
|
self.cassette.append(self._vcr_request, response)
|
||||||
return VCRHTTPResponse(response)
|
return VCRHTTPResponse(response)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Stubs for requests"""
|
"""Stubs for requests"""
|
||||||
|
|
||||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Stubs for urllib3"""
|
"""Stubs for urllib3"""
|
||||||
|
|
||||||
from urllib3.connectionpool import HTTPConnection, VerifiedHTTPSConnection
|
from urllib3.connection import HTTPConnection, VerifiedHTTPSConnection
|
||||||
|
|
||||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user