File: test_blocking_network.py

package info (click to toggle)
pytest-recording 0.13.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 272 kB
  • sloc: python: 961; makefile: 10
file content (452 lines) | stat: -rw-r--r-- 14,293 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
import json
import sys
from io import BytesIO
from socket import AF_INET, SOCK_RAW, SOCK_STREAM, socket

import pytest
import requests
import vcr.errors
from packaging import version

from pytest_recording.network import blocking_context

# Windows doesn’t have AF_NETLINK & AF_UNIX
try:
    from socket import AF_NETLINK, AF_UNIX
except ImportError:
    AF_NETLINK = None  # type: ignore[assignment]
    AF_UNIX = None  # type: ignore[assignment]


try:
    import pycurl
except ImportError as exc:
    if "No module named" not in str(exc):
        # Case with different SSL backends should be loud and visible
        # Could happen with development when environment is recreated (e.g. locally)
        raise
    pycurl = None  # type: ignore[assignment]


skip_netlink = pytest.mark.skipif(AF_NETLINK is None, reason="AF_NETLINK not available on this platform")
skip_unix = pytest.mark.skipif(AF_UNIX is None, reason="AF_UNIX not available on this platform")


def assert_network_blocking(testdir, dirname):
    result = testdir.runpytest("--record-mode=all")
    # Then all network requests in tests with block_network mark except for marked with pytest.mark.vcr should fail
    result.assert_outcomes(passed=3)

    # And a cassette is recorded for the case where pytest.mark.vcr is applied
    cassette_path = testdir.tmpdir.join("cassettes/{}/test_recording.yaml".format(dirname))
    assert cassette_path.exists()


def test_blocked_network_recording_cli_arg(testdir):
    # When record is enabled via a CLI arg
    testdir.makepyfile(
        """
import pytest
import requests

def test_no_blocking(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
@pytest.mark.vcr
def test_recording(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
def test_error(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        assert requests.get(httpbin.url + "/ip").status_code == 200
    """
    )
    assert_network_blocking(testdir, "test_blocked_network_recording_cli_arg")


def test_blocked_network_recording_vcr_config(testdir):
    # When record is enabled via the `vcr_config` fixture
    testdir.makepyfile(
        """
import pytest
import requests

@pytest.fixture(autouse=True)
def vcr_config():
    return {"record_mode": "once"}


def test_no_blocking(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
@pytest.mark.vcr
def test_recording(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
def test_error(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        assert requests.get(httpbin.url + "/ip").status_code == 200
    """
    )
    assert_network_blocking(testdir, "test_blocked_network_recording_vcr_config")


def test_blocked_network_recording_vcr_mark(testdir):
    # When record is enabled via the `vcr` mark
    testdir.makepyfile(
        """
import pytest
import requests

def test_no_blocking(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
@pytest.mark.vcr(record_mode="once")
def test_recording(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

@pytest.mark.block_network
def test_error(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        assert requests.get(httpbin.url + "/ip").status_code == 200
    """
    )
    assert_network_blocking(testdir, "test_blocked_network_recording_vcr_mark")


def test_socket_connect(testdir):
    # When socket.socket is aliased in some module
    testdir.makepyfile(
        another="""
from socket import socket, AF_INET, SOCK_STREAM

def call(port):
    s = socket(AF_INET, SOCK_STREAM)
    try:
        return s.connect(("127.0.0.1", port))
    finally:
        s.close()
"""
    )
    testdir.makepyfile(
        """
from another import call
import pytest

@pytest.mark.block_network
def test_no_blocking(httpbin):
    _, port = httpbin.url.rsplit(":", 1)
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        call(int(port))
    """
    )

    result = testdir.runpytest()
    # Then socket.socket.connect should fail
    result.assert_outcomes(passed=1)


def call(socket_name, family, type):
    s = socket(family, type)
    try:
        return s.connect(socket_name)
    finally:
        s.close()


@skip_unix
@pytest.mark.block_network(allowed_hosts=["./allowed_socket"])
def test_block_network_allowed_socket():
    # Error from actual socket call, that means it was not blocked
    with pytest.raises(IOError):
        call("./allowed_socket", AF_UNIX, SOCK_STREAM)


@skip_unix
@pytest.mark.block_network(allowed_hosts=["./allowed_socket"])
def test_block_network_blocked_socket():
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        call("./blocked_socket", AF_UNIX, SOCK_STREAM)


# When not AF_UNIX, AF_INET or AF_INET6 socket is used
# Then socket.socket.connect call is blocked, even if resource name is in the allowed list
@skip_netlink
@pytest.mark.block_network(allowed_hosts=["./allowed_socket", "127.0.0.1", "0"])
def test_blocked():
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        call((0, 0), AF_NETLINK, SOCK_RAW)


# When record is disabled


@pytest.mark.block_network
@pytest.mark.vcr
def test_with_vcr_mark(httpbin):
    with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException, match=r"overwrite existing cassette"):
        requests.get(httpbin.url + "/ip")
    assert socket.connect.__name__ == "network_guard"
    assert socket.connect_ex.__name__ == "network_guard"


@pytest.mark.block_network
def test_no_vcr_mark(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        requests.get(httpbin.url + "/ip")


@pytest.mark.block_network(allowed_hosts=["127.0.0.2"])
def test_no_vcr_mark_bytes():
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        with socket(AF_INET, SOCK_STREAM) as sock:
            sock.connect((b"127.0.0.1", 80))


@pytest.mark.block_network(allowed_hosts=["127.0.0.2"])
def test_no_vcr_mark_bytearray():
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        with socket(AF_INET, SOCK_STREAM) as sock:
            sock.connect((bytearray(b"127.0.0.1"), 80))


@pytest.mark.parametrize(
    "marker, cmd_options, vcr_cfg",
    (
        pytest.param(
            '@pytest.mark.block_network(allowed_hosts=["127.0.0.*", "127.0.1.1"])',
            "",
            "",
            id="block_marker",
        ),
        pytest.param(
            "",
            ("--block-network", "--allowed-hosts=127.0.0.*,127.0.1.1"),
            "",
            id="block_cmd",
        ),
        pytest.param(
            "@pytest.mark.block_network()",
            "",
            "@pytest.fixture(autouse=True)\ndef vcr_config():\n    return {'allowed_hosts': '127.0.0.*,127.0.1.1'}",
            id="vcr_cfg",
        ),
    ),
)
def test_block_network_with_allowed_hosts(testdir, marker, cmd_options, vcr_cfg):
    testdir.makepyfile(
        """
import socket
import pytest
import requests

{vcr_cfg}

{marker}
def test_allowed(httpbin):
    response = requests.get(httpbin.url + "/ip")
    assert response.status_code == 200
    assert socket.socket.connect.__name__ == "network_guard"
    assert socket.socket.connect_ex.__name__ == "network_guard"

{marker}
def test_blocked():
    with pytest.raises(RuntimeError, match="^Network is disabled$"):
        requests.get("http://example.com")
    assert socket.socket.connect.__name__ == "network_guard"
    assert socket.socket.connect_ex.__name__ == "network_guard"
    """.format(
            marker=marker,
            vcr_cfg=vcr_cfg,
        )
    )

    result = testdir.runpytest(*cmd_options)
    result.assert_outcomes(passed=2)


def test_block_network_via_cmd(testdir):
    # When `--block-network` option is passed to CMD
    testdir.makepyfile(
        """
import socket
import pytest
import requests
import vcr.errors

@pytest.mark.vcr
def test_with_vcr_mark(httpbin):
    with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException, match=r"overwrite existing cassette"):
        requests.get(httpbin.url + "/ip")
    assert socket.socket.connect.__name__ == "network_guard"
    assert socket.socket.connect_ex.__name__ == "network_guard"


def test_no_vcr_mark(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        requests.get(httpbin.url + "/ip")
    """
    )

    result = testdir.runpytest("--block-network")
    # Then all network interactions in all tests should be blocked
    result.assert_outcomes(passed=2)


def test_block_network_via_cmd_with_recording(testdir):
    # When `--block-network` option is passed to CMD and VCR recording is enabled
    testdir.makepyfile(
        """
import socket
import pytest
import requests
import vcr.errors

@pytest.mark.vcr
def test_recording(httpbin):
    assert requests.get(httpbin.url + "/ip").status_code == 200

def test_no_vcr_mark(httpbin):
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        requests.get(httpbin.url + "/ip")
    """
    )

    result = testdir.runpytest("--block-network", "--record-mode=all")
    # Then only tests with `pytest.mark.vcr` should record cassettes, other tests with network should raise errors
    result.assert_outcomes(passed=2)

    # And a cassette is recorded for the case where pytest.mark.vcr is applied
    cassette_path = testdir.tmpdir.join("cassettes/test_block_network_via_cmd_with_recording/test_recording.yaml")
    assert cassette_path.exists()


# When pycurl is used for network access
# It should be blocked as well
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network
def test_pycurl_error(httpbin):
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, httpbin.url + "/ip")  # type: ignore[attr-defined]
    c.setopt(c.WRITEDATA, buffer)  # type: ignore[attr-defined]
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        c.perform()
    c.close()


@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
def test_pycurl_work(httpbin):
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, httpbin.url + "/ip")  # type: ignore[attr-defined]
    c.setopt(c.WRITEDATA, buffer)  # type: ignore[attr-defined]
    c.perform()
    c.close()
    assert json.loads(buffer.getvalue()) == {"origin": "127.0.0.1"}


# When pycurl is used for network access
# It should be blocked as well


@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network(allowed_hosts=["127.0.0.*", "127.0.1.1"])
def test_pycurl_with_allowed_hosts_allowed(httpbin):
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, httpbin.url + "/ip")  # type: ignore[attr-defined]
    c.setopt(c.WRITEDATA, buffer)  # type: ignore[attr-defined]
    c.perform()
    c.close()
    assert json.loads(buffer.getvalue()) == {"origin": "127.0.0.1"}


@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network(allowed_hosts=["127.0.0.*", "127.0.1.1"])
def test_pycurl_with_allowed_hosts_blocked(httpbin):
    buffer = BytesIO()
    c = pycurl.Curl()
    c.setopt(c.URL, "http://example.com")  # type: ignore[attr-defined]
    c.setopt(c.WRITEDATA, buffer)  # type: ignore[attr-defined]
    with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
        c.perform()
    c.close()


@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
def test_pycurl_setattr():
    # When pycurl is used for network access
    # And an attribute is set on an instance
    curl = pycurl.Curl()
    curl.attr = 42  # type: ignore[attr-defined]
    # Then it should be proxied to the original Curl instance itself
    assert curl.handle.attr == 42  # type: ignore[attr-defined]


@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
def test_pycurl_url_error():
    # When pycurl is used for network access
    # And a wrapper may fail on URL manipulation due to missing URL
    curl = pycurl.Curl()
    # Then original pycurl error must be raised
    with pytest.raises(pycurl.error, match="No URL set"):
        curl.perform()


# When pycurl is patched
# Patched module should be hashable - use case for auto-reloaders and similar (e.g. in Django)
# The patch should behave as close to real modules as possible
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network
def test_sys_modules():
    set(sys.modules.values())


# When a critical error happened and the `network.disable` ctx manager is interrupted on `yield`
# Then socket and pycurl should be unpatched anyway
# NOTE. In reality, it is not likely to happen - e.g. if pytest will partially crash and will not call the teardown
# part of the generator, but this try/finally implementation could also guard against errors on manual


def test_critical_error():
    try:
        with blocking_context():
            assert socket.connect.__name__ == "network_guard"
            assert socket.connect_ex.__name__ == "network_guard"
            raise ValueError
    except ValueError:
        pass
    assert socket.connect.__name__ == "connect"
    assert socket.connect_ex.__name__ == "connect_ex"


IS_PYTEST_ABOVE_54 = version.parse(pytest.__version__) >= version.parse("5.4.0")


@pytest.mark.parametrize("args", ("foo=42", "42"))
def test_invalid_input_arguments(testdir, args):
    # When the `block_network` mark receives an unknown argument
    testdir.makepyfile(
        """
import pytest
import requests

@pytest.mark.block_network({})
def test_request():
    requests.get("https://google.com")
    """.format(args)
    )
    result = testdir.runpytest()
    # Then there should be an error
    if IS_PYTEST_ABOVE_54:
        result.assert_outcomes(errors=1)
    else:
        result.assert_outcomes(error=1)
    expected = "Invalid arguments to `block_network`. It accepts only the following keyword arguments: `allowed_hosts`."
    assert expected in result.stdout.str()