|
18 | 18 | import re
|
19 | 19 | from secrets import token_hex
|
20 | 20 | import socket
|
| 21 | +from types import SimpleNamespace |
| 22 | +from typing import Optional |
| 23 | +from unittest.mock import Mock |
21 | 24 |
|
22 | 25 | import pytest
|
23 | 26 |
|
|
36 | 39 | LOCALHOST_ALIASES = socket.gethostbyname_ex('localhost')[1]
|
37 | 40 |
|
38 | 41 |
|
| 42 | +@pytest.fixture |
| 43 | +def mock_socket(monkeypatch: pytest.MonkeyPatch): |
| 44 | + """Reference implementation of socket functions, with some of their real |
| 45 | + observed quirks. |
| 46 | +
|
| 47 | + Yes, it is horribly quirky. It is based on one linux system, and other |
| 48 | + systems or different /etc/hosts setup will result in different behaviour. |
| 49 | + """ |
| 50 | + this_name = 'NCC1701' |
| 51 | + this_fqdn = f'{this_name}.starfleet.gov' |
| 52 | + this_ip = '12.345.67.89' |
| 53 | + this_ex = (this_fqdn, [this_name], [this_ip]) |
| 54 | + |
| 55 | + localhost = 'localhost' |
| 56 | + localhost_fqdn = f'{localhost}.localdomain' |
| 57 | + localhost_ip = '127.0.0.1' |
| 58 | + localhost_aliases_v4 = [ |
| 59 | + localhost_fqdn, |
| 60 | + f'{localhost}4', |
| 61 | + f'{localhost}4.localdomain4', |
| 62 | + ] |
| 63 | + localhost_aliases_v6 = [ |
| 64 | + localhost_fqdn, |
| 65 | + f'{localhost}6', |
| 66 | + f'{localhost}6.localdomain6', |
| 67 | + ] |
| 68 | + localhost_ex = ( |
| 69 | + localhost, |
| 70 | + [*localhost_aliases_v4, *localhost_aliases_v6], |
| 71 | + [localhost_ip, localhost_ip], |
| 72 | + ) |
| 73 | + |
| 74 | + def _getfqdn(x: Optional[str] = None): |
| 75 | + if x: |
| 76 | + x = x.lower() |
| 77 | + if not x or this_fqdn.lower().startswith(x) or x == this_ip: |
| 78 | + return this_fqdn |
| 79 | + if x in {localhost, localhost_fqdn, localhost_ip}: |
| 80 | + return localhost_fqdn |
| 81 | + return x |
| 82 | + |
| 83 | + def _gethostbyaddr(x: str): |
| 84 | + x = x.lower() |
| 85 | + if this_fqdn.lower().startswith(x) or x == this_ip: |
| 86 | + return this_ex |
| 87 | + if x in {localhost, localhost_fqdn, '::1', *localhost_aliases_v6}: |
| 88 | + return (localhost, localhost_aliases_v6, ['::1']) |
| 89 | + if x in {localhost_ip, *localhost_aliases_v4}: |
| 90 | + return (localhost, localhost_aliases_v4, [localhost_ip]) |
| 91 | + raise socket.gaierror("oopsie") |
| 92 | + |
| 93 | + def _gethostbyname_ex(x: str): |
| 94 | + x = x.lower() |
| 95 | + if x in {this_fqdn.lower(), this_name.lower()}: |
| 96 | + return this_ex |
| 97 | + if this_fqdn.lower().startswith(x): |
| 98 | + return (this_fqdn, [], [this_ip]) |
| 99 | + if x in {localhost, localhost_fqdn}: |
| 100 | + return localhost_ex |
| 101 | + if x in localhost_aliases_v6: |
| 102 | + return (localhost, localhost_aliases_v6, ['::1']) |
| 103 | + if x in localhost_aliases_v4: |
| 104 | + return (localhost, localhost_aliases_v4, [localhost_ip]) |
| 105 | + raise socket.gaierror("oopsie") |
| 106 | + |
| 107 | + mock_getfqdn = Mock(side_effect=_getfqdn) |
| 108 | + monkeypatch.setattr('cylc.flow.hostuserutil.socket.getfqdn', mock_getfqdn) |
| 109 | + mock_gethostbyaddr = Mock(side_effect=_gethostbyaddr) |
| 110 | + monkeypatch.setattr( |
| 111 | + 'cylc.flow.hostuserutil.socket.gethostbyaddr', mock_gethostbyaddr |
| 112 | + ) |
| 113 | + mock_gethostbyname_ex = Mock(side_effect=_gethostbyname_ex) |
| 114 | + monkeypatch.setattr( |
| 115 | + 'cylc.flow.hostuserutil.socket.gethostbyname_ex', mock_gethostbyname_ex |
| 116 | + ) |
| 117 | + return SimpleNamespace( |
| 118 | + this_fqdn=this_fqdn, |
| 119 | + this_ip=this_ip, |
| 120 | + this_ex=this_ex, |
| 121 | + localhost_ex=localhost_ex, |
| 122 | + getfqdn=mock_getfqdn, |
| 123 | + gethostbyaddr=mock_gethostbyaddr, |
| 124 | + gethostbyname_ex=mock_gethostbyname_ex, |
| 125 | + ) |
| 126 | + |
| 127 | + |
39 | 128 | def test_is_remote_user_on_current_user():
|
40 | 129 | """is_remote_user with current user."""
|
41 | 130 | assert not is_remote_user(None)
|
@@ -101,3 +190,30 @@ def test_get_host_info__basic():
|
101 | 190 | with pytest.raises(IOError) as exc:
|
102 | 191 | hu._get_host_info(bad_host)
|
103 | 192 | assert bad_host in str(exc.value)
|
| 193 | + |
| 194 | + |
| 195 | +def test_get_host_info__advanced(mock_socket): |
| 196 | + hu = HostUtil(expire=3600) |
| 197 | + assert mock_socket.gethostbyname_ex.call_count == 0 |
| 198 | + assert hu._get_host_info() == mock_socket.this_ex |
| 199 | + assert mock_socket.gethostbyname_ex.call_count == 1 |
| 200 | + # Test caching: |
| 201 | + hu._get_host_info() |
| 202 | + assert mock_socket.gethostbyname_ex.call_count == 1 |
| 203 | + # Test variations of host name: |
| 204 | + assert hu._get_host_info('NCC1701') == mock_socket.this_ex |
| 205 | + assert hu._get_host_info('ncc1701.starfleet') == mock_socket.this_ex |
| 206 | + # (Note:) |
| 207 | + assert ( |
| 208 | + mock_socket.gethostbyname_ex('ncc1701.starfleet') |
| 209 | + != mock_socket.this_ex |
| 210 | + ) |
| 211 | + assert hu._get_host_info('localhost4') == mock_socket.localhost_ex |
| 212 | + assert hu._get_host_info('localhost6') == mock_socket.localhost_ex |
| 213 | + # Test IP address: |
| 214 | + assert hu._get_host_info(mock_socket.this_ip) == mock_socket.this_ex |
| 215 | + assert hu._get_host_info('127.0.0.1') == mock_socket.localhost_ex |
| 216 | + # Test error: |
| 217 | + with pytest.raises(IOError): |
| 218 | + hu._get_host_info('nonexist') |
| 219 | + assert 'nonexist' not in hu._host_exs |
0 commit comments