1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-11 05:17:55 +03:00

Adding user interface tests and refactoring user interface

This commit is contained in:
Adolfo Gómez García 2022-10-30 23:14:13 +01:00
parent 284508632c
commit adb4b5326a
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
3 changed files with 211 additions and 56 deletions

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# We use commit/rollback
from ...utils.test import UDSTestCase
from uds.core.ui.user_interface import (
gui,
UDSB,
UDSK
)
import time
from django.conf import settings
class GuiTest(UDSTestCase):
def test_globals(self):
self.assertEqual(UDSK, settings.SECRET_KEY[8:24].encode())
self.assertEqual(UDSB, b'udsprotect')
def test_convert_to_choices(self) -> None:
# Several cases
# 1. Empty list
# 2.- single string
# 3.- A list of strings
# 4.- A list of dictinaries, must be {'id': 'xxxx', 'text': 'yyy'}
# 5.- A Dictionary, Keys will be used in 'id' and values in 'text'
self.assertEqual(gui.convertToChoices([]), [])
self.assertEqual(gui.convertToChoices('aaaa'), [{'id': 'aaaa', 'text': 'aaaa'}])
self.assertEqual(gui.convertToChoices(['a', 'b']), [{'id': 'a', 'text': 'a'}, {'id': 'b', 'text': 'b'}])
self.assertEqual(gui.convertToChoices({'a': 'b', 'c': 'd'}), [{'id': 'a', 'text': 'b'}, {'id': 'c', 'text': 'd'}])
self.assertEqual(gui.convertToChoices({'a': 'b', 'c': 'd'}), [{'id': 'a', 'text': 'b'}, {'id': 'c', 'text': 'd'}])
# Expect an exception if we pass a list of dictionaries without id or text
self.assertRaises(ValueError, gui.convertToChoices, [{'a': 'b', 'c': 'd'}])
# Also if we pass a list of dictionaries with id and text, but not all of them
self.assertRaises(ValueError, gui.convertToChoices, [{'id': 'a', 'text': 'b'}, {'id': 'c', 'text': 'd'}, {'id': 'e'}])
def test_convert_to_list(self) -> None:
# Several cases
# 1. Empty list
# 2.- single string
# 3.- A list of strings
self.assertEqual(gui.convertToList([]), [])
self.assertEqual(gui.convertToList('aaaa'), ['aaaa'])
self.assertEqual(gui.convertToList(['a', 'b']), ['a', 'b'])
self.assertEqual(gui.convertToList(1), ['1'])
def test_choice_image(self) -> None:
# id, text, and base64 image
self.assertEqual(gui.choiceImage('id', 'text', 'image'), {'id': 'id', 'text': 'text', 'img': 'image'})

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# We use commit/rollback
from ...utils.test import UDSTransactionTestCase
from uds.core.ui.user_interface import (
gui,
UserInterface
)
import time
from django.conf import settings
class UserinterfaceTest(UDSTransactionTestCase):
def test_userinterface(self):
pass

View File

@ -42,6 +42,7 @@ from collections import abc
from django.utils.translation import get_language, gettext as _, gettext_noop from django.utils.translation import get_language, gettext as _, gettext_noop
from django.conf import settings from django.conf import settings
from numpy import isin
from uds.core.managers import cryptoManager from uds.core.managers import cryptoManager
from uds.core.util.decorators import deprecatedClassValue from uds.core.util.decorators import deprecatedClassValue
@ -99,19 +100,21 @@ class gui:
# Values dict type # Values dict type
ValuesType = typing.Optional[typing.Dict[str, str]] ValuesType = typing.Optional[typing.Dict[str, str]]
class ChoiceType(typing.TypedDict):
id: str
text: str
ValuesDictType = typing.Dict[ ValuesDictType = typing.Dict[
str, str,
typing.Union[str, bool, typing.List[str], typing.List[typing.Dict[str, str]]], typing.Union[str, bool, typing.List[str], typing.List[ChoiceType]],
] ]
ChoiceType = typing.Dict[str, str]
# : True string value # : True string value
TRUE: typing.ClassVar[str] = 'true' TRUE: typing.ClassVar[str] = 'true'
# : False string value # : False string value
FALSE: typing.ClassVar[str] = 'false' FALSE: typing.ClassVar[str] = 'false'
class Tab(enum.Enum): class Tab(enum.Enum):
ADVANCED = gettext_noop('Advanced') ADVANCED = gettext_noop('Advanced')
PARAMETERS = gettext_noop('Parameters') PARAMETERS = gettext_noop('Parameters')
@ -123,24 +126,28 @@ class gui:
def __str__(self) -> str: def __str__(self) -> str:
return str(self.value) return str(self.value)
# : For backward compatibility, will be removed in future versions # : For backward compatibility, will be removed in future versions
# For now, will log an warning if used # For now, will log an warning if used
@deprecatedClassValue('gui.Tab.ADVANCED') @deprecatedClassValue('gui.Tab.ADVANCED')
def ADVANCED_TAB(cls) -> str: def ADVANCED_TAB(cls) -> str:
return str(gui.Tab.ADVANCED) return str(gui.Tab.ADVANCED)
@deprecatedClassValue('gui.Tab.PARAMETERS') @deprecatedClassValue('gui.Tab.PARAMETERS')
def PARAMETERS_TAB(cls) -> str: def PARAMETERS_TAB(cls) -> str:
return str(gui.Tab.PARAMETERS) return str(gui.Tab.PARAMETERS)
@deprecatedClassValue('gui.Tab.CREDENTIALS') @deprecatedClassValue('gui.Tab.CREDENTIALS')
def CREDENTIALS_TAB(cls) -> str: def CREDENTIALS_TAB(cls) -> str:
return str(gui.Tab.CREDENTIALS) return str(gui.Tab.CREDENTIALS)
@deprecatedClassValue('gui.Tab.TUNNEL') @deprecatedClassValue('gui.Tab.TUNNEL')
def TUNNEL_TAB(cls) -> str: def TUNNEL_TAB(cls) -> str:
return str(gui.Tab.TUNNEL) return str(gui.Tab.TUNNEL)
@deprecatedClassValue('gui.Tab.DISPLAY') @deprecatedClassValue('gui.Tab.DISPLAY')
def DISPLAY_TAB(cls) -> str: def DISPLAY_TAB(cls) -> str:
return str(gui.Tab.DISPLAY) return str(gui.Tab.DISPLAY)
@deprecatedClassValue('gui.Tab.MFA') @deprecatedClassValue('gui.Tab.MFA')
def MFA_TAB(cls) -> str: def MFA_TAB(cls) -> str:
return str(gui.Tab.MFA) return str(gui.Tab.MFA)
@ -151,42 +158,10 @@ class gui:
typing.Callable[[typing.Dict[str, str]], typing.List[typing.Dict[str, str]]], typing.Callable[[typing.Dict[str, str]], typing.List[typing.Dict[str, str]]],
] = {} ] = {}
# Helpers
@staticmethod @staticmethod
def convertToChoices( def choiceItem(
vals: typing.Union[typing.Iterable[typing.Union[str, typing.Dict[str, str]]], typing.Dict[str, str]] id_: typing.Union[str, int], text: typing.Union[str, int]
) -> typing.List[typing.Dict[str, str]]: ) -> 'gui.ChoiceType':
"""
Helper to convert from array of strings (or dictionaries) to the same dict used in choice,
multichoice, ..
"""
if not vals:
return []
# Helper to convert an item to a dict
def choiceFromValue(val: typing.Union[str, typing.Dict[str, str]]) -> typing.Dict[str, str]:
if isinstance(val, str):
return {'id': val, 'text': val}
return copy.deepcopy(val)
# If is a dict
if isinstance(vals, abc.Mapping):
return [{'id': str(k), 'text': v} for k, v in vals.items()]
# If is an iterator
if isinstance(vals, abc.Iterable):
return [choiceFromValue(v) for v in vals]
raise ValueError('Invalid type for convertToChoices: {}'.format(type(vals)))
@staticmethod
def convertToList(vals: typing.Iterable[str]) -> typing.List[str]:
if vals:
return [str(v) for v in vals]
return []
@staticmethod
def choiceItem(id_: typing.Union[str, int], text: str) -> 'gui.ChoiceType':
""" """
Helper method to create a single choice item. Helper method to create a single choice item.
@ -204,6 +179,58 @@ class gui:
""" """
return {'id': str(id_), 'text': str(text)} return {'id': str(id_), 'text': str(text)}
# Helpers
@staticmethod
def convertToChoices(
vals: typing.Union[
typing.Iterable[typing.Union[str, typing.Dict[str, str]]],
typing.Dict[str, str],
None,
]
) -> typing.List['gui.ChoiceType']:
"""
Helper to convert from array of strings (or dictionaries) to the same dict used in choice,
multichoice, ..
"""
if not vals:
return []
# Helper to convert an item to a dict
def choiceFromValue(
val: typing.Union[str, int, typing.Dict[str, str]]
) -> 'gui.ChoiceType':
if isinstance(val, dict):
if 'id' not in val or 'text' not in val:
raise ValueError('Invalid choice dict: {}'.format(val))
return gui.choiceItem(val['id'], val['text'])
# If val is not a dict, and it has not 'id' and 'text', raise an exception
return gui.choiceItem(val, val)
# If is a dict
if isinstance(vals, abc.Mapping):
return [gui.choiceItem(str(k), v) for k, v in vals.items()]
# if single value, convert to list
if not isinstance(vals, abc.Iterable) or isinstance(vals, str):
vals = [vals]
# If is an iterable
if isinstance(vals, abc.Iterable):
return [choiceFromValue(v) for v in vals]
# This should never happen
raise RuntimeError('Invalid type for convertToChoices: {}'.format(type(vals)))
@staticmethod
def convertToList(
vals: typing.Union[str, int, typing.Iterable]
) -> typing.List[str]:
if vals:
if isinstance(vals, (str, int)):
return [str(vals)]
return [str(v) for v in vals]
return []
@staticmethod @staticmethod
def choiceImage( def choiceImage(
id_: typing.Union[str, int], text: str, img: str id_: typing.Union[str, int], text: str, img: str
@ -211,7 +238,7 @@ class gui:
return {'id': str(id_), 'text': str(text), 'img': img} return {'id': str(id_), 'text': str(text), 'img': img}
@staticmethod @staticmethod
def sortedChoices(choices): def sortedChoices(choices: typing.Iterable):
return sorted(choices, key=lambda item: item['text'].lower()) return sorted(choices, key=lambda item: item['text'].lower())
@staticmethod @staticmethod
@ -287,6 +314,7 @@ class gui:
so if you use both, the used one will be "value". This is valid for so if you use both, the used one will be "value". This is valid for
all form fields. all form fields.
""" """
class Types(enum.Enum): class Types(enum.Enum):
TEXT = 'text' TEXT = 'text'
TEXT_AUTOCOMPLETE = 'text-autocomplete' TEXT_AUTOCOMPLETE = 'text-autocomplete'
@ -312,7 +340,9 @@ class gui:
def __init__(self, **options) -> None: def __init__(self, **options) -> None:
# Added defaultValue as alias for defvalue # Added defaultValue as alias for defvalue
defvalue = options.get('defvalue', options.get('defaultValue', options.get('defValue', ''))) defvalue = options.get(
'defvalue', options.get('defaultValue', options.get('defValue', ''))
)
if callable(defvalue): if callable(defvalue):
defvalue = defvalue() defvalue = defvalue()
self._data = { self._data = {
@ -604,9 +634,7 @@ class gui:
datetime.date: the date that this object holds, or "min" | "max" on error datetime.date: the date that this object holds, or "min" | "max" on error
""" """
try: try:
return datetime.datetime.strptime( return datetime.datetime.strptime(self.value, '%Y-%m-%d') # ISO Format
self.value, '%Y-%m-%d'
) # ISO Format
except Exception: except Exception:
return datetime.datetime.min if min else datetime.datetime.max return datetime.datetime.min if min else datetime.datetime.max
@ -827,10 +855,7 @@ class gui:
def __init__(self, **options): def __init__(self, **options):
super().__init__(**options) super().__init__(**options)
vals = options.get('values') self._data['values'] = gui.convertToChoices(options.get('values'))
if vals and isinstance(vals, (dict, list, tuple)):
options['values'] = gui.convertToChoices(options['values'])
self._data['values'] = options.get('values', [])
if 'fills' in options: if 'fills' in options:
# Save fnc to register as callback # Save fnc to register as callback
fills = options['fills'] fills = options['fills']
@ -840,7 +865,7 @@ class gui:
gui.callbacks[fills['callbackName']] = fnc gui.callbacks[fills['callbackName']] = fnc
self._type(gui.InputField.Types.CHOICE) self._type(gui.InputField.Types.CHOICE)
def setValues(self, values: typing.List[typing.Dict[str, typing.Any]]): def setValues(self, values: typing.List['gui.ChoiceType']):
""" """
Set the values for this choice field Set the values for this choice field
""" """
@ -1177,15 +1202,13 @@ class UserInterface(metaclass=UserInterfaceType):
if k in self._gui: if k in self._gui:
try: try:
if v.startswith(MULTIVALUE_FIELD): if v.startswith(MULTIVALUE_FIELD):
val = pickle.loads(v[1:]) # nosec: secure pickled by us for sure val = pickle.loads( # nosec: safe pickle, controlled
v[1:]
) # nosec: secure pickled by us for sure
elif v.startswith(OLD_PASSWORD_FIELD): elif v.startswith(OLD_PASSWORD_FIELD):
val = cryptoManager().AESDecrypt(v[1:], UDSB, True).decode() val = cryptoManager().AESDecrypt(v[1:], UDSB, True).decode()
elif v.startswith(PASSWORD_FIELD): elif v.startswith(PASSWORD_FIELD):
val = ( val = cryptoManager().AESDecrypt(v[1:], UDSK, True).decode()
cryptoManager()
.AESDecrypt(v[1:], UDSK, True)
.decode()
)
else: else:
val = v val = v
# Ensure "legacy bytes" values are loaded correctly as unicode # Ensure "legacy bytes" values are loaded correctly as unicode