Skip to content

Commit

Permalink
Merge branch 'release/2.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cdown committed Jan 3, 2020
2 parents 31f69e3 + 8521b4c commit 24e11b2
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 122 deletions.
5 changes: 0 additions & 5 deletions .coveragerc

This file was deleted.

2 changes: 0 additions & 2 deletions .pylintrc

This file was deleted.

41 changes: 31 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
dist: xenial
language: python
cache: pip
install: pip install --upgrade tox
script: tox

# Drop once EOL: https://devguide.python.org/#status-of-python-branches
python:
- '2.7'
- '3.5'
- '3.6'
jobs:
include:
## Linux

install: pip install tox
script:
- 'tox'
# CPython (in official virtualenv)
- python: '3.5'
- python: '3.6'
# 3.7 is below
- python: '3.8'

# PyPy (in official virtualenv)
- python: pypy3
env: TOXENV=pypy3

# Correctness tests. "coverage" toxenv runs tests, so no need to run
# TOXENV=py38.
#
# TODO: pytype doesn't yet support 3.8, switch this with 3.8 above when it
# does

matrix:
include:
# "coverage" toxenv runs tests, so no need to run TOXENV=py37
- python: '3.7'
env: TOXENV=black,pylint,pytype,bandit,coverage

## Special jobs

# Run long Hypothesis tests for release/cron
- if: branch =~ ^release/.*$ or type = cron
python: '3.8'
env: TOXENV=py-release

notifications:
email:
- travis+tzupdate@chrisdown.name
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
include LICENSE
include MANIFEST.in
include README.rst
include requirements.txt
include .coveragerc

recursive-include * requirements.txt
Expand Down
1 change: 0 additions & 1 deletion requirements.txt

This file was deleted.

11 changes: 5 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,26 @@
with open("README.rst") as readme_f:
README = readme_f.read()

with open("requirements.txt") as requirements_f:
REQUIREMENTS = requirements_f.readlines()

setup(
name="tzupdate",
version="1.5.0",
version="2.0.0",
description="Set the system timezone based on IP geolocation",
long_description=README,
url="https://github.com/cdown/tzupdate",
license="Public Domain",
author="Chris Down",
author_email="chris@chrisdown.name",
py_modules=["tzupdate"],
install_requires=REQUIREMENTS,
entry_points={"console_scripts": ["tzupdate=tzupdate:main"]},
keywords="timezone localtime tz",
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: Public Domain",
"Programming Language :: Python :: 2",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: System",
"Topic :: System :: Networking :: Time Synchronization",
"Topic :: Utilities",
Expand Down
6 changes: 5 additions & 1 deletion tests/_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import ipaddress
from hypothesis.strategies import integers, builds

try:
_ipa_type = unicode # pytype: disable=name-error
except NameError:
_ipa_type = str

IP_ADDRESSES = builds(
ipaddress.IPv4Address, integers(min_value=0, max_value=(2 ** 32 - 1))
).map(str)
).map(_ipa_type)

FAKE_TIMEZONE = "Fake/Timezone"
FAKE_ZONEINFO_PATH = "/path/to/zoneinfo"
Expand Down
13 changes: 7 additions & 6 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
nose
hypothesis
httpretty
parameterized
mock
ipaddress
pytest>=4,<5
pytest-cov>=2,<3
hypothesis>=4,<5
httpretty>=0.9,<0.10
parameterized>=0.7,<0.8
mock>=3,<4
ipaddress>=1,<2
12 changes: 6 additions & 6 deletions tests/e2e_tests.py → tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import httpretty
import tzupdate
import mock
from nose.tools import assert_false, assert_raises
import pytest
from tests._test_utils import FAKE_SERVICES, FAKE_TIMEZONE, setup_basic_api_response


Expand All @@ -29,17 +29,17 @@ def test_print_only_no_link(link_localtime_mock, deb_tz_mock):
setup_basic_api_response()
args = ["-p"]
tzupdate.main(args, services=FAKE_SERVICES)
assert_false(link_localtime_mock.called)
assert_false(deb_tz_mock.called)
assert not link_localtime_mock.called
assert not deb_tz_mock.called


@mock.patch("tzupdate.write_debian_timezone")
@mock.patch("tzupdate.link_localtime")
def test_print_sys_tz_no_link(link_localtime_mock, deb_tz_mock):
args = ["--print-system-timezone"]
tzupdate.main(args, services=FAKE_SERVICES)
assert_false(link_localtime_mock.called)
assert_false(deb_tz_mock.called)
assert not link_localtime_mock.called
assert not deb_tz_mock.called


@httpretty.activate
Expand Down Expand Up @@ -91,5 +91,5 @@ def test_timeout_results_in_exception(process_mock):
# should time out
setup_basic_api_response()
args = ["-s", "0.01"]
with assert_raises(tzupdate.TimezoneAcquisitionError):
with pytest.raises(tzupdate.TimezoneAcquisitionError):
tzupdate.main(args, services=FAKE_SERVICES)
90 changes: 49 additions & 41 deletions tests/unit_tests.py → tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,37 @@
import os
import errno
import mock
import pytest
from tests._test_utils import (
IP_ADDRESSES,
FAKE_SERVICES,
FAKE_TIMEZONE,
FAKE_ZONEINFO_PATH,
setup_basic_api_response,
)
from nose.tools import (
assert_raises,
eq_ as eq,
assert_true,
assert_is_none,
assert_in,
assert_false,
)
from parameterized import parameterized
from hypothesis import given, settings
from hypothesis import given, settings, HealthCheck, assume
from hypothesis.strategies import sampled_from, none, one_of, text, integers


ERROR_STATUSES = [s for s in httpretty.http.STATUSES if 400 <= s <= 599]
SUPPRESSED_CHECKS = [HealthCheck.too_slow]

settings.register_profile("base", settings(suppress_health_check=SUPPRESSED_CHECKS))
settings.register_profile(
"release", settings(max_examples=1000, suppress_health_check=SUPPRESSED_CHECKS)
)
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "base"))


@httpretty.activate
@given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES))
@settings(max_examples=20)
def test_get_timezone_for_ip(ip, service):
fake_queue = mock.Mock()
setup_basic_api_response()
tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue)
tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue)

if ip is not None:
assert_in(ip, httpretty.last_request().path)
assert ip in httpretty.last_request().path

fake_queue.put.assert_called_once_with(FAKE_TIMEZONE)

Expand All @@ -51,23 +49,23 @@ def test_get_sys_timezone():

@httpretty.activate
@given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES))
@settings(max_examples=20)
def test_get_timezone_for_ip_empty_resp(ip, service):
fake_queue = mock.Mock()
setup_basic_api_response(empty_resp=True)
assert_is_none(
tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue)
assert (
tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue)
is None
)


@httpretty.activate
@given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES))
@settings(max_examples=20)
def test_get_timezone_for_ip_empty_val(ip, service):
fake_queue = mock.Mock()
setup_basic_api_response(empty_val=True)
assert_is_none(
tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue)
assert (
tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue)
is None
)


Expand All @@ -77,12 +75,12 @@ def test_get_timezone_for_ip_empty_val(ip, service):
sampled_from(FAKE_SERVICES),
sampled_from(ERROR_STATUSES),
)
@settings(max_examples=20)
def test_get_timezone_for_ip_doesnt_raise(ip, service, status):
fake_queue = mock.Mock()
setup_basic_api_response(status=status)
assert_is_none(
tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue)
assert (
tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue)
is None
)


Expand All @@ -97,17 +95,31 @@ def test_link_localtime(isfile_mock, symlink_mock, unlink_mock):
FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH
)

assert_true(unlink_mock.called_once_with([expected]))
assert_true(
symlink_mock.called_once_with([expected, tzupdate.DEFAULT_LOCALTIME_PATH])
)
assert unlink_mock.called_once_with([expected])
assert symlink_mock.called_once_with([expected, tzupdate.DEFAULT_LOCALTIME_PATH])

assert zoneinfo_tz_path == expected


@given(text())
def test_link_localtime_traversal_attack_root(questionable_timezone):
assume(tzupdate.DEFAULT_ZONEINFO_PATH not in questionable_timezone)
questionable_timezone = "/" + questionable_timezone

with pytest.raises(tzupdate.DirectoryTraversalError):
tzupdate.link_localtime(
questionable_timezone,
tzupdate.DEFAULT_ZONEINFO_PATH,
tzupdate.DEFAULT_LOCALTIME_PATH,
)

eq(zoneinfo_tz_path, expected)

@given(text())
def test_link_localtime_traversal_attack_dotdot(questionable_timezone):
assume(tzupdate.DEFAULT_ZONEINFO_PATH not in questionable_timezone)
questionable_timezone = "../../../" + questionable_timezone

@parameterized(["/foo/bar", "../../../../foo/bar"])
def test_link_localtime_traversal_attack(questionable_timezone):
with assert_raises(tzupdate.DirectoryTraversalError):
with pytest.raises(tzupdate.DirectoryTraversalError):
tzupdate.link_localtime(
questionable_timezone,
tzupdate.DEFAULT_ZONEINFO_PATH,
Expand All @@ -118,7 +130,7 @@ def test_link_localtime_traversal_attack(questionable_timezone):
@mock.patch("tzupdate.os.path.isfile")
def test_link_localtime_timezone_not_available(isfile_mock):
isfile_mock.return_value = False
with assert_raises(tzupdate.TimezoneNotLocallyAvailableError):
with pytest.raises(tzupdate.TimezoneNotLocallyAvailableError):
tzupdate.link_localtime(
FAKE_TIMEZONE,
tzupdate.DEFAULT_ZONEINFO_PATH,
Expand All @@ -131,14 +143,14 @@ def test_link_localtime_timezone_not_available(isfile_mock):
def test_link_localtime_permission_denied(isfile_mock, unlink_mock):
isfile_mock.return_value = True
unlink_mock.side_effect = OSError(errno.EACCES, "Permission denied yo")
with assert_raises(OSError) as raise_cm:
with pytest.raises(OSError) as raise_cm:
tzupdate.link_localtime(
FAKE_TIMEZONE,
tzupdate.DEFAULT_ZONEINFO_PATH,
tzupdate.DEFAULT_LOCALTIME_PATH,
)

eq(raise_cm.exception.errno, errno.EACCES)
assert raise_cm.value.errno == errno.EACCES


@mock.patch("tzupdate.os.unlink")
Expand All @@ -148,14 +160,14 @@ def test_link_localtime_oserror_not_permission(isfile_mock, unlink_mock):
code = errno.ENOSPC
unlink_mock.side_effect = OSError(code, "No space yo")

with assert_raises(OSError) as thrown_exc:
with pytest.raises(OSError) as thrown_exc:
tzupdate.link_localtime(
FAKE_TIMEZONE,
tzupdate.DEFAULT_ZONEINFO_PATH,
tzupdate.DEFAULT_LOCALTIME_PATH,
)

eq(thrown_exc.exception.errno, code)
assert thrown_exc.value.errno == code


@mock.patch("tzupdate.os.unlink")
Expand All @@ -175,7 +187,6 @@ def test_link_localtime_localtime_missing_no_raise(


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_exists_not_forced(timezone, tz_path):
mo = mock.mock_open()
with mock.patch("tzupdate.open", mo, create=True):
Expand All @@ -186,7 +197,6 @@ def test_debian_tz_path_exists_not_forced(timezone, tz_path):


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_doesnt_exist_not_forced(timezone, tz_path):
mo = mock.mock_open()
mo.side_effect = OSError(errno.ENOENT, "")
Expand All @@ -196,19 +206,17 @@ def test_debian_tz_path_doesnt_exist_not_forced(timezone, tz_path):


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_other_error_raises(timezone, tz_path):
mo = mock.mock_open()
code = errno.EPERM
mo.side_effect = OSError(code, "")
with mock.patch("tzupdate.open", mo, create=True):
with assert_raises(OSError) as thrown_exc:
with pytest.raises(OSError) as thrown_exc:
tzupdate.write_debian_timezone(timezone, tz_path, must_exist=True)
eq(thrown_exc.exception.errno, code)
assert thrown_exc.value.errno == code


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_doesnt_exist_forced(timezone, tz_path):
mo = mock.mock_open()
with mock.patch("tzupdate.open", mo, create=True):
Expand Down
Loading

0 comments on commit 24e11b2

Please sign in to comment.