Skip to content
Open
Changes from all 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
148 changes: 129 additions & 19 deletions tests/test_mig_shared_safeinput.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
#
# --- BEGIN_HEADER ---
#
# addheader - add license header to all code modules.
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
# test_mig_shared_safeinput - unit tests for shared safeinput validation
# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH
#
# This file is part of MiG.
#
Expand All @@ -27,16 +27,16 @@

"""Unit tests for the migrid module pointed to in the filename"""

import base64
import codecs
import importlib
import os
import sys
from past.builtins import basestring, unicode

from tests.support import MigTestCase, testmain

from mig.shared.safeinput import main as safeinput_main, InputException, \

Check failure on line 37 in tests/test_mig_shared_safeinput.py

View workflow job for this annotation

GitHub Actions / Style check python and annotate

unused import 'VALID_NAME_CHARACTERS' (90% confidence)
filter_commonname, valid_commonname
filter_commonname, valid_alphanumeric, valid_commonname, valid_path, \
valid_printable, VALID_NAME_CHARACTERS

PY2 = sys.version_info[0] == 2

Expand All @@ -50,26 +50,30 @@


def is_string_of_unicode(value):
return type(value) == type(u'')

Check warning on line 53 in tests/test_mig_shared_safeinput.py

View workflow job for this annotation

GitHub Actions / Style check python and annotate

do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`


class MigSharedSafeinput(MigTestCase):
def _hex_wrap(val):
"""Insert a clearly marked hex representation of val"""
# Please keep aligned with helper in mig/shared/functionality/autocreate.py
return ".X%s" % base64.b16encode(val.encode('utf8')).decode('utf8')

def test_existing_main(self):
def raise_on_error_exit(exit_code):
if exit_code != 0:
if raise_on_error_exit.last_print is not None:
identifying_message = raise_on_error_exit.last_print
else:
identifying_message = 'unknown'
raise AssertionError(
'failure in unittest/testcore: %s' % (identifying_message,))
raise_on_error_exit.last_print = None

def record_last_print(value):
raise_on_error_exit.last_print = value
class TestMigSharedSafeInput(MigTestCase):
"""Test mig.shared.safeinput functions"""

safeinput_main(_exit=raise_on_error_exit, _print=record_last_print)
# Core functionality test constants
INVALID_DOLLAR_NAME = "invalid$name"
VALID_EXTRA_CHARS_NAME = "user-name_123"
PRINTABLE_CHARS = "abc123!@#"
ACCENTED_VALID = "Renée Müller"
ACCENTED_INVALID_EXOTIC = "Źaćâř"
DECOMPOSED_UNICODE = u"å" # a + combining ring above

# Commonname specific test constants
APOSTROPHE_FULL_NAME = "John O'Connor"
APOSTROPHE_FULL_NAME_SKIP = "John OConnor"
APOSTROPHE_FULL_NAME_HEX = "John O.X27Connor"

COMMONNAME_PERMITTED = (
'Firstname Lastname',
Expand All @@ -84,7 +88,16 @@
'Test Invalid ?',
'Test HTML Invalid <code/>')

def _provide_configuration(self):

Check failure on line 91 in tests/test_mig_shared_safeinput.py

View workflow job for this annotation

GitHub Actions / Style check python and annotate

unused method '_provide_configuration' (60% confidence)
"""Provide test configuration"""
return 'testconfig'

def before_each(self):

Check failure on line 95 in tests/test_mig_shared_safeinput.py

View workflow job for this annotation

GitHub Actions / Style check python and annotate

unused method 'before_each' (60% confidence)
"""Test case setup handler"""
return

def test_commonname_valid(self):
"""Test valid_commonname with acceptable and prohibited names"""
for test_cn in self.COMMONNAME_PERMITTED:
saw_raise = False
try:
Expand All @@ -102,6 +115,7 @@
self.assertTrue(saw_raise)

def test_commonname_filter(self):
"""Test filter_commonname name sanitization"""
for test_cn in self.COMMONNAME_PERMITTED:
test_cn_unicode = as_string_of_unicode(test_cn)
filtered_cn = filter_commonname(test_cn)
Expand All @@ -111,6 +125,102 @@
test_cn_unicode = as_string_of_unicode(test_cn)
filtered_cn = filter_commonname(test_cn)
self.assertNotEqual(filtered_cn, test_cn_unicode)
self.assertTrue(len(filtered_cn) < len(test_cn_unicode))
# With default skip all chars in filtered_cn must be in original
overlap = [i for i in filtered_cn if i in test_cn_unicode]
self.assertEqual(''.join(overlap), filtered_cn)

def test_commonname_filter_hexlify_illegal(self):
"""Test filter_commonname with hex encoding of illegal chars"""
for test_cn in self.COMMONNAME_PERMITTED:
test_cn_unicode = as_string_of_unicode(test_cn)
filtered_cn = filter_commonname(test_cn, illegal_handler=_hex_wrap)
# Valid should remain unchanged with hexlify illegal_handler
self.assertEqual(filtered_cn, test_cn_unicode)

for test_cn in self.COMMONNAME_PROHIBITED:
test_cn_unicode = as_string_of_unicode(test_cn)
filtered_cn = filter_commonname(test_cn, illegal_handler=_hex_wrap)
# Invalid should be replaced with hexlify illegal_handler
self.assertNotEqual(filtered_cn, test_cn_unicode)
self.assertIn('.X', filtered_cn)
self.assertTrue(len(filtered_cn) > len(test_cn_unicode))

def test_filter_commonname_apostrophe_name_skip_illegal(self):
"""Test apostrophe handling with skip illegal_handler"""
result = filter_commonname(self.APOSTROPHE_FULL_NAME,
illegal_handler=None)
self.assertNotEqual(result, self.APOSTROPHE_FULL_NAME)
self.assertNotIn("'", result)
self.assertEqual(result, self.APOSTROPHE_FULL_NAME_SKIP)

def test_filter_commonname_apostrophe_name_hexlify_illegal(self):
"""Test apostrophe handling with hex encode illegal_handler"""
result = filter_commonname(self.APOSTROPHE_FULL_NAME,
illegal_handler=_hex_wrap)
self.assertNotEqual(result, self.APOSTROPHE_FULL_NAME)
self.assertNotIn("'", result)
self.assertEqual(result, self.APOSTROPHE_FULL_NAME_HEX)

# NOTE: indirect tests for __valid_contents using some of its wrappers

def test_valid_printable_lengths(self):
"""Test printable character validation"""
# Valid cases
valid_printable(self.PRINTABLE_CHARS, min_length=5)

# Length violations
with self.assertRaises(InputException):
valid_printable("a", min_length=2)

with self.assertRaises(InputException):
valid_printable("a" * 201, max_length=200)

def test_valid_alphanumeric_with_extras(self):
"""Test alphanumeric validation with extra characters"""
# Valid cases
valid_alphanumeric(self.VALID_EXTRA_CHARS_NAME, extra_chars="-_")

# Invalid characters
with self.assertRaises(InputException):
valid_alphanumeric(self.INVALID_DOLLAR_NAME)

def test_valid_commonname_accent_handling(self):
"""Test accented character handling validation"""
# Common accents should pass with COMMON_ACCENTED
valid_commonname(self.ACCENTED_VALID)

# Exotic accents should fail with NO_ACCENTED
with self.assertRaises(InputException):
valid_printable(self.ACCENTED_INVALID_EXOTIC)

def test_valid_path_unicode_normalization(self):
"""Test unicode decomposition handling"""
# Make sure unicode normalization doesn't raise exception
self.assertEqual(valid_path(self.DECOMPOSED_UNICODE), None)


class TestMigSharedSafeInput__legacy(MigTestCase):
"""Legacy tests for safeinput module self-checks"""

# TODO: migrate all legacy self-check functionality into the above?
def test_existing_main(self):
"""Run built-in self-tests and check output"""
def raise_on_error_exit(exit_code):
if exit_code != 0:
if raise_on_error_exit.last_print is not None:
identifying_message = raise_on_error_exit.last_print
else:
identifying_message = 'unknown'
raise AssertionError(
'failure in unittest/testcore: %s' % (identifying_message,))
raise_on_error_exit.last_print = None

def record_last_print(value):
"""Keep track of printed output"""
raise_on_error_exit.last_print = value

safeinput_main(_exit=raise_on_error_exit, _print=record_last_print)


if __name__ == '__main__':
Expand Down