Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Increase coverage. #484

Merged
merged 7 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ omit =


[report]
exclude_lines = def __repr__
exclude_lines =
def __repr__
pragma: no cover

ignore_errors = True

Expand Down
10 changes: 10 additions & 0 deletions hostpolicy/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ def clean_and_save(entity):
entity.save()


class Internals(TestCase):
"""Test internal data structures."""

def test_str(self):
"""Test that __str__ returns obj.name."""
name = "test1"
atom = HostPolicyAtom(name=name, description='test')
self.assertEqual(str(atom), f'"{name}"')


class UniqueNamespace(TestCase):
"""Atoms and Roles must jointly have unique names, so test that."""

Expand Down
28 changes: 19 additions & 9 deletions mreg/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.conf import settings

from rest_framework import exceptions
from rest_framework.permissions import IsAuthenticated, SAFE_METHODS

Expand Down Expand Up @@ -35,9 +34,10 @@ def _list_in_list(list_a, list_b):
return any(i in list_b for i in list_a)


def user_in_required_group(user):
return _list_in_list(get_settings_groups('REQUIRED_USER_GROUPS'),
user.group_list)
def user_in_required_group(user, required_groups=[]):
if not required_groups:
required_groups = get_settings_groups('REQUIRED_USER_GROUPS')
return _list_in_list(required_groups, user.group_list)


def user_is_superuser(user):
Expand Down Expand Up @@ -72,7 +72,9 @@ class IsInRequiredGroup(IsAuthenticated):
Allows only access to users in the required group.
"""

def has_permission(self, request, view):
# Note that required_user_groups is not used internally and requires a
# settings change for proper testing.
terjekv marked this conversation as resolved.
Show resolved Hide resolved
def has_permission(self, request, view): # pragma: no cover
if not super().has_permission(request, view):
return False
return request_in_settings_group(request, 'REQUIRED_USER_GROUPS')
Expand All @@ -83,7 +85,9 @@ class ReadOnlyForRequiredGroup(IsInRequiredGroup):
Allows read only access to users in the required group.
"""

def has_permission(self, request, view):
# Note that required_user_groups is not used internally and requires a
# settings change for proper testing.
def has_permission(self, request, view): # pragma: no cover
if not super().has_permission(request, view):
return False
return request.method in SAFE_METHODS
Expand Down Expand Up @@ -257,7 +261,9 @@ def has_create_permission(self, request, view, validated_serializer):
hostname = data['host'].name
elif 'host' in data:
hostname, ips = self._get_hostname_and_ips(data['host'])
else:
else: # pragma: no cover
# Testing these kinds of should-never-happen codepaths is hard.
# We have to basically mock a complete API call and then break it.
raise exceptions.PermissionDenied(f"Unhandled view: {view}")

if ips and hostname:
Expand All @@ -274,7 +280,9 @@ def has_destroy_permission(self, request, view, validated_serializer):
pass
elif hasattr(obj, 'host'):
obj = obj.host
else:
else: # pragma: no cover
# Testing these kinds of should-never-happen codepaths is hard.
# We have to basically mock a complete API call and then break it.
raise exceptions.PermissionDenied(f"Unhandled view: {view}")
if _deny_superuser_only_names(name=obj.name, view=view, request=request):
return False
Expand Down Expand Up @@ -313,7 +321,9 @@ def has_update_permission(self, request, view, validated_serializer):
if not self.has_obj_perm(request.user, data['host']):
return False
return self.has_obj_perm(request.user, obj.host)
raise exceptions.PermissionDenied(f"Unhandled view: {view}")
# Testing these kinds of should-never-happen codepaths is hard.
# We have to basically mock a complete API call and then break it.
raise exceptions.PermissionDenied(f"Unhandled view: {view}") # pragma: no cover

def _get_hostname_and_ips(self, hostobject):
ips = []
Expand Down
11 changes: 8 additions & 3 deletions mreg/api/v1/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def default(self, o):
if isinstance(o, Model):
return model_to_dict(o)

return super().default(o)
# TODO: #485 We should never ever hit this. Should we fail?
return super().default(o) # pragma: no cover
terjekv marked this conversation as resolved.
Show resolved Hide resolved


class HistoryLog:
Expand Down Expand Up @@ -47,9 +48,11 @@ def save_log(self, action, serializer, data, orig_data=None):
model=model,
action=action,
data=json_data)

# We should never fail at performing a clean on the testdata itself.
try:
history.full_clean()
except ValidationError as e:
except ValidationError as e: # pragma: no cover
print(e)
return
history.save()
Expand Down Expand Up @@ -82,9 +85,11 @@ def save_log_m2m_alteration(self, method, instance):
model=model,
action=action,
data=data)

# We should never fail at performing a clean on the testdata itself.
try:
history.full_clean()
except ValidationError as e:
except ValidationError as e: # pragma: no cover
print(e)
return
history.save()
Expand Down
8 changes: 6 additions & 2 deletions mreg/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ def to_internal_value(self, data):
if data and isinstance(data, str):
validate_normalizeable_mac_address(data)
return normalize_mac(data)
return data
# We never get here. What other than str can the data be?
# TODO: Check data paths.
return data # pragma: no cover
terjekv marked this conversation as resolved.
Show resolved Hide resolved

class IpaddressSerializer(ValidationMixin, serializers.ModelSerializer):
macaddress = MacAddressSerializerField(required=False, allow_blank=True)
Expand Down Expand Up @@ -243,7 +245,9 @@ def validate(self, data):

def _get_ip(attr):
ip = data.get(attr) or getattr(self.instance, attr)
if isinstance(ip, str):
# ip should be an object at this point, and it's never a string.
# TODO: Check data path and remove?
if isinstance(ip, str): # pragma: no cover
terjekv marked this conversation as resolved.
Show resolved Hide resolved
return ipaddress.ip_address(ip)
return ip
start_ip = _get_ip('start_ip')
Expand Down
33 changes: 32 additions & 1 deletion mreg/api/v1/tests/test_host_permissions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
from django.contrib.auth.models import Group
from rest_framework import exceptions

from mreg.api.permissions import get_settings_groups, user_in_required_group
from mreg.models import ForwardZone, Host, Ipaddress, NetGroupRegexPermission, Network, PtrOverride, User

from mreg.models import ForwardZone, Host, Ipaddress, NetGroupRegexPermission, Network, PtrOverride

from .tests import MregAPITestCase


class Internals(MregAPITestCase):
terjekv marked this conversation as resolved.
Show resolved Hide resolved
"""Test internal structures in permissions."""
def test_missing_group_settings(self):
"""Ensure that missing group settings are caught if requested."""
with self.assertRaises(exceptions.APIException):
get_settings_groups("NO_SUCH_SETTINGS_GROUP")

def test_user_in_required_groups(self):
"""Ensure that user_in_required_groups works."""
testuser, _ = User.objects.get_or_create(username="testuser")
testgroup, _ = Group.objects.get_or_create(name="testgroup")

with self.assertRaises(exceptions.APIException):
user_in_required_group(testuser)

# Note that required groups contains the names of the groups, not objects.
# See the User definition in mreg/models_auth.py
self.assertFalse(user_in_required_group(testuser, required_groups=[testgroup.name]))
# The caching of group list requires us to manually expunge the cache.
# This operation has no API, so right no we do it the hard way.
testuser._group_list = None
testuser.groups.add(testgroup)
self.assertTrue(user_in_required_group(testuser, required_groups=[testgroup.name]))

testuser.delete()
testgroup.delete()


class HostsNoRights(MregAPITestCase):

def setUp(self):
Expand Down
2 changes: 2 additions & 0 deletions mreg/api/v1/tests/test_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def setUp(self):
def test_create_label(self):
# Create a normal label
self.assert_post('/api/v1/labels/', {'name':'normal_label','description':'A normal label'})
# Creating a label with the same name should fail
self.assert_post_and_409('/api/v1/labels/', {'name':'normal_label','description':'A normal label redone'})
# Verify that a description is required
self.assert_post_and_400('/api/v1/labels/', {'name':'testlabel2'})
# Verify that spaces in the label name isn't allowed
Expand Down
6 changes: 5 additions & 1 deletion mreg/api/v1/tests/test_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ def test_networks_list_200_ok(self):
self.assertEqual(response.data['count'], 4)
self.assertEqual(len(response.data['results']), 4)

def test_networks_get_401_unauthorized(self):
"""GET on an existing ip-network while unauthorized should return 401 Unauthorized."""
self.client.logout()
self.assert_get_and_401('/networks/%s' % self.network_sample.network)

def test_networks_patch_204_no_content(self):
"""Patching an existing and valid entry should return 204 and Location"""
response = self.assert_patch('/networks/%s' % self.network_sample.network,
Expand Down Expand Up @@ -452,7 +457,6 @@ def test_client_must_be_logged_in(self):
self.client.logout()
self.assert_get_and_401('/networks/')


class NetworkExcludedRanges(MregAPITestCase):
"""Tests for NetworkExcludedRange objects and that they are enforced
for Ipaddress and PtrOverride"""
Expand Down
11 changes: 11 additions & 0 deletions mreg/api/v1/tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from rest_framework.test import APIClient

from .tests import MregAPITestCase


Expand All @@ -14,6 +16,15 @@ def test_get(self):
ret2 = self.assert_get('/permissions/netgroupregex/{}'.format(ret1.json()['id']))
self.assertEqual(ret1.json(), ret2.json())

def test_get_at_different_privilege_levels(self):
"""Verify get at different privilege levels."""
ret1 = self.assert_post('/permissions/netgroupregex/', self.data)
self.client = self.get_token_client(superuser=False, adminuser=False)
ret2 = self.assert_get('/permissions/netgroupregex/{}'.format(ret1.json()['id']))
self.assertEqual(ret1.json(), ret2.json())
self.client = APIClient()
self.assert_get_and_401('/permissions/netgroupregex/{}'.format(ret1.json()['id']))

def test_list(self):
ret1 = self.assert_post('/permissions/netgroupregex/', self.data)
data = self.assert_get('/permissions/netgroupregex/').json()
Expand Down
44 changes: 42 additions & 2 deletions mreg/api/v1/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Network, PtrOverride, ReverseZone, Txt,
ExpiringToken)

from mreg.utils import nonify

class MissingSettings(Exception):
pass
Expand Down Expand Up @@ -182,6 +183,28 @@ def create_reverse_zone(name='10.10.in-addr.arpa', primary_ns='ns.example.org',
return ReverseZone.objects.create(name=name, primary_ns=primary_ns, email=email)


class APITestInternals(MregAPITestCase):
"""Test internal methods."""

def test_create_path(self):
"""Test that _create_path generates correct paths."""
target = "/api/v1/target"
self.assertEqual(self._create_path("/api/v1/target"), target)
self.assertEqual(self._create_path("/target"), target)
self.assertEqual(self._create_path("target"), target)

def test_nonify(self):
"""Test that nonify works."""
target = 42
self.assertEqual(nonify(target), target)
self.assertEqual(nonify(-1), "")

def test_add_user_to_group(self):
"""Test that add_user_to_group works."""
with self.assertRaises(MissingSettings):
self.add_user_to_groups("nosuchgroup")


class APITokenAuthenticationTestCase(MregAPITestCase):
"""Test various token authentication operations."""

Expand Down Expand Up @@ -209,7 +232,7 @@ def test_token_reissue(self):
old = ExpiringToken.objects.get(user=self.user)
self.assert_post_and_200("/api/token-auth/",
{"username": self.user,
"password":"test"})
"password": "test"})
new = ExpiringToken.objects.get(user=self.user)
assert old.key == new.key
self.assertLess(old.last_used, new.last_used)
Expand Down Expand Up @@ -255,6 +278,7 @@ def test_login_with_invalid_credentials(self):
self.assert_post_and_400("/api/token-auth/", {"who":"someone","why":"because"})
self.assert_post_and_400("/api/token-auth/", {})


class APIAutoupdateZonesTestCase(MregAPITestCase):
"""This class tests the autoupdate of zones' updated_at whenever
various models are added/deleted/renamed/changed etc."""
Expand Down Expand Up @@ -567,10 +591,26 @@ def test_zone_txts_added(self):
class APIHostsIdna(MregAPITestCase):

data_v4 = {'name': 'æøå.example.org', "ipaddress": '10.10.0.1'}
data_v4_long = {
'name': 'endørtilhøjreibaggrundenførerudtilforstuenenandendørtilvenstre.example.org',
"ipaddress": '10.10.0.2'
}
data_v4_codepoint = {
'name': 'Endørtilhøjreibaggrundenførerudtilforstuen.example.org',
"ipaddress": '10.10.0.2'
}

def _add_data(self, data):
self.assert_post_and_201('/hosts/', data)

def test_long_idna(self):
"""Test creating a host with a too long IDNA."""
self.assert_post_and_400('/hosts/', self.data_v4_long)

def test_codepoint_idna(self):
"""Test creating a host with a codepoint issue using IDNA."""
self.assert_post_and_400('/hosts/', self.data_v4_codepoint)

def test_hosts_idna_forward(self):
"""Test that a hostname outside ASCII 128 is handled properly"""
zone = create_forward_zone()
Expand Down Expand Up @@ -849,7 +889,7 @@ def test_ptr_override_ipv6_patch_204(self):

@skip("This test crashes the database and is skipped "
"until the underlying problem has been fixed")
def test_ptr_override_reject_invalid_ipv4_400(self):
def test_ptr_override_reject_invalid_ipv4_400(self): # pragma: no cover (skipped test)
ptr_override_data = {'host': self.host.id,
'ipaddress': '10.0.0.400'}
self.assert_post_and_400("/ptroverrides/", ptr_override_data)
Expand Down
2 changes: 1 addition & 1 deletion mreg/api/v1/tests/tests_zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ def test_zones_ns_patch_400_bad_request(self):
{'garbage': self.ns_one.name})

@skip("Not testable, yet")
def test_zones_ns_patch_404_not_found(self):
def test_zones_ns_patch_404_not_found(self): # pragma: no cover
terjekv marked this conversation as resolved.
Show resolved Hide resolved
""""Patching the list of nameservers with a non-existing nameserver should return 404"""
self.assert_post(self.zonepath, self.post_data)
self.assert_patch_and_404(self.ns_path(self.post_data['name']),
Expand Down
2 changes: 1 addition & 1 deletion mreg/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def update_serialno(self, force=False):
try:
with transaction.atomic():
self.save()
except DatabaseError:
except DatabaseError: # pragma: no cover
pass


Expand Down
1 change: 1 addition & 0 deletions mreg/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _signal_history(resource, name, action, model, model_id, data):
model=model,
action=action,
data=data)

try:
history.full_clean()
except ValidationError:
Expand Down
2 changes: 1 addition & 1 deletion mreg/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def test_ipv6_quick_list_unused(self):
clean_and_save(NetworkExcludedRange(network=n, start_ip='2001:DB8:BAD:BAD::0', end_ip='2001:DB8:BAD:BAD::ffff:ffff:ffff'))
clean_and_save(NetworkExcludedRange(network=n, start_ip='2001:DB8:BAD:BAD:2:0:0:0', end_ip='2001:DB8:BAD:BAD:ffff:ffff:ffff:ffff'))
unused_count_should_be = 0x1000000000000 - len(n.get_reserved_ipaddresses())
def handler(signum, frame):
def handler(signum, frame): # pragma: no cover
raise Exception("timeout")
signal.signal(signal.SIGALRM, handler)
signal.alarm(5)
Expand Down
6 changes: 1 addition & 5 deletions mreg/validators.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import re
import string

import idna
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator

import idna

from rest_framework import serializers


from .utils import get_network_from_zonename


# TODO: Implement validation for retry, refresh, expire


Expand Down