1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
import datetime
import warnings
import zoneinfo
from collections import namedtuple
from decimal import Decimal
from exchangelib.extended_properties import ExternId
from exchangelib.fields import (
Base64Field,
BooleanField,
CharField,
CharListField,
Choice,
ChoiceField,
DateField,
DateOrDateTimeField,
DateTimeField,
DecimalField,
EnumField,
EnumListField,
ExtendedPropertyField,
ExtendedPropertyListField,
IntegerField,
InvalidChoiceForVersion,
InvalidFieldForVersion,
TextField,
TimeZoneField,
)
from exchangelib.indexed_properties import SingleFieldIndexedElement
from exchangelib.util import TNS, to_xml
from exchangelib.version import EXCHANGE_2007, EXCHANGE_2010, EXCHANGE_2013, Version
from .common import TimedTestCase
class FieldTest(TimedTestCase):
def test_value_validation(self):
field = TextField("foo", field_uri="bar", is_required=True, default=None)
with self.assertRaises(ValueError) as e:
field.clean(None) # Must have a default value on None input
self.assertEqual(str(e.exception), "'foo' is a required field with no default")
field = TextField("foo", field_uri="bar", is_required=True, default="XXX")
self.assertEqual(field.clean(None), "XXX")
field = CharListField("foo", field_uri="bar")
with self.assertRaises(TypeError) as e:
field.clean("XXX") # Must be a list type
self.assertEqual(str(e.exception), "Field 'foo' value 'XXX' must be of type <class 'list'>")
field = CharListField("foo", field_uri="bar")
with self.assertRaises(TypeError) as e:
field.clean([1, 2, 3]) # List items must be correct type
self.assertEqual(str(e.exception), "Field 'foo' value 1 must be of type <class 'str'>")
field = CharField("foo", field_uri="bar")
with self.assertRaises(TypeError) as e:
field.clean(1) # Value must be correct type
self.assertEqual(str(e.exception), "Field 'foo' value 1 must be of type <class 'str'>")
with self.assertRaises(ValueError) as e:
field.clean("X" * 256) # Value length must be within max_length
self.assertEqual(
str(e.exception),
"'foo' value 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' exceeds length 255",
)
field = DateTimeField("foo", field_uri="bar")
with self.assertRaises(ValueError) as e:
field.clean(datetime.datetime(2017, 1, 1)) # Datetime values must be timezone aware
self.assertEqual(
str(e.exception), "Value datetime.datetime(2017, 1, 1, 0, 0) on field 'foo' must be timezone aware"
)
field = ChoiceField("foo", field_uri="bar", choices=[Choice("foo"), Choice("bar")])
with self.assertRaises(ValueError) as e:
field.clean("XXX") # Value must be a valid choice
self.assertEqual(str(e.exception), "Invalid choice 'XXX' for field 'foo'. Valid choices are ['bar', 'foo']")
# A few tests on extended properties that override base methods
field = ExtendedPropertyField("foo", value_cls=ExternId, is_required=True)
with self.assertRaises(ValueError) as e:
field.clean(None) # Value is required
self.assertEqual(str(e.exception), "'foo' is a required field")
with self.assertRaises(TypeError) as e:
field.clean(123) # Correct type is required
self.assertEqual(str(e.exception), "Field 'ExternId' value 123 must be of type <class 'str'>")
self.assertEqual(field.clean("XXX"), "XXX") # We can clean a simple value and keep it as a simple value
self.assertEqual(field.clean(ExternId("XXX")), ExternId("XXX")) # We can clean an ExternId instance as well
class ExternIdArray(ExternId):
property_type = "StringArray"
field = ExtendedPropertyListField("foo", value_cls=ExternIdArray, is_required=True)
with self.assertRaises(ValueError) as e:
field.clean(None) # Value is required
self.assertEqual(str(e.exception), "'foo' is a required field")
with self.assertRaises(TypeError) as e:
field.clean(123) # Must be an iterable
self.assertEqual(str(e.exception), "Field 'ExternIdArray' value 123 must be of type <class 'list'>")
with self.assertRaises(TypeError) as e:
field.clean([123]) # Correct type is required
self.assertEqual(str(e.exception), "Field 'ExternIdArray' list value 123 must be of type <class 'str'>")
# Test min/max on IntegerField
field = IntegerField("foo", field_uri="bar", min=5, max=10)
with self.assertRaises(ValueError) as e:
field.clean(2)
self.assertEqual(str(e.exception), "Value 2 on field 'foo' must be greater than 5")
with self.assertRaises(ValueError) as e:
field.clean(12)
self.assertEqual(str(e.exception), "Value 12 on field 'foo' must be less than 10")
# Test min/max on DecimalField
field = DecimalField("foo", field_uri="bar", min=5, max=10)
with self.assertRaises(ValueError) as e:
field.clean(Decimal(2))
self.assertEqual(str(e.exception), "Value Decimal('2') on field 'foo' must be greater than 5")
with self.assertRaises(ValueError) as e:
field.clean(Decimal(12))
self.assertEqual(str(e.exception), "Value Decimal('12') on field 'foo' must be less than 10")
# Test enum validation
field = EnumField("foo", field_uri="bar", enum=["a", "b", "c"])
with self.assertRaises(ValueError) as e:
field.clean(0) # Enums start at 1
self.assertEqual(str(e.exception), "Value 0 on field 'foo' must be greater than 1")
with self.assertRaises(ValueError) as e:
field.clean(4) # Spills over list
self.assertEqual(str(e.exception), "Value 4 on field 'foo' must be less than 3")
with self.assertRaises(ValueError) as e:
field.clean("d") # Value not in enum
self.assertEqual(str(e.exception), "Value 'd' on field 'foo' must be one of ['a', 'b', 'c']")
# Test enum list validation
field = EnumListField("foo", field_uri="bar", enum=["a", "b", "c"])
with self.assertRaises(ValueError) as e:
field.clean([])
self.assertEqual(str(e.exception), "Value [] on field 'foo' must not be empty")
with self.assertRaises(ValueError) as e:
field.clean([0])
self.assertEqual(str(e.exception), "Value 0 on field 'foo' must be greater than 1")
with self.assertRaises(ValueError) as e:
field.clean([1, 1]) # Values must be unique
self.assertEqual(str(e.exception), "List entries [1, 1] on field 'foo' must be unique")
with self.assertRaises(ValueError) as e:
field.clean(["d"])
self.assertEqual(str(e.exception), "List value 'd' on field 'foo' must be one of ['a', 'b', 'c']")
def test_garbage_input(self):
# Test that we can survive garbage input for common field types
tz = zoneinfo.ZoneInfo("Europe/Copenhagen")
account = namedtuple("Account", ["default_timezone"])(default_timezone=tz)
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:Foo>THIS_IS_GARBAGE</t:Foo>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
for field_cls in (Base64Field, BooleanField, IntegerField, DateField, DateTimeField, DecimalField):
field = field_cls("foo", field_uri="item:Foo", is_required=True, default="DUMMY")
self.assertEqual(field.from_xml(elem=elem, account=account), None)
# Test MS timezones
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:Foo Id="THIS_IS_GARBAGE"></t:Foo>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
field = TimeZoneField("foo", field_uri="item:Foo", default="DUMMY")
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
tz = field.from_xml(elem=elem, account=account)
self.assertEqual(tz, None)
self.assertEqual(
str(w[0].message),
"""\
Cannot convert value 'THIS_IS_GARBAGE' on field 'foo' to type 'EWSTimeZone' (unknown timezone ID).
You can fix this by adding a custom entry into the timezone translation map:
from exchangelib.winzone import MS_TIMEZONE_TO_IANA_MAP, CLDR_TO_MS_TIMEZONE_MAP
# Replace "Some_Region/Some_Location" with a reasonable value from CLDR_TO_MS_TIMEZONE_MAP.keys()
MS_TIMEZONE_TO_IANA_MAP['THIS_IS_GARBAGE'] = "Some_Region/Some_Location"
# Your code here""",
)
def test_versioned_field(self):
field = TextField("foo", field_uri="bar", supported_from=EXCHANGE_2010)
with self.assertRaises(InvalidFieldForVersion):
field.clean("baz", version=Version(EXCHANGE_2007))
field.clean("baz", version=Version(EXCHANGE_2010))
field.clean("baz", version=Version(EXCHANGE_2013))
def test_versioned_choice(self):
field = ChoiceField("foo", field_uri="bar", choices={Choice("c1"), Choice("c2", supported_from=EXCHANGE_2010)})
with self.assertRaises(ValueError):
field.clean("XXX") # Value must be a valid choice
field.clean("c2", version=None)
with self.assertRaises(InvalidChoiceForVersion):
field.clean("c2", version=Version(EXCHANGE_2007))
field.clean("c2", version=Version(EXCHANGE_2010))
field.clean("c2", version=Version(EXCHANGE_2013))
def test_date_or_datetime_field(self):
# Test edge cases with timezone info on date strings
tz = zoneinfo.ZoneInfo("Europe/Copenhagen")
account = namedtuple("Account", ["default_timezone"])(default_timezone=tz)
field = DateOrDateTimeField("foo", field_uri="calendar:Start")
# TZ-aware date string
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:Start>2017-06-21Z</t:Start>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(field.from_xml(elem=elem, account=account), datetime.date(2017, 6, 21))
# TZ-aware date string
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:Start>2017-06-21+01:00</t:Start>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(field.from_xml(elem=elem, account=account), datetime.date(2017, 6, 21))
def test_naive_datetime(self):
# Test that we can survive naive datetimes on a datetime field
tz = zoneinfo.ZoneInfo("Europe/Copenhagen")
utc = zoneinfo.ZoneInfo("UTC")
account = namedtuple("Account", ["default_timezone"])(default_timezone=tz)
default_value = datetime.datetime(2017, 1, 2, 3, 4, tzinfo=tz)
field = DateTimeField("foo", field_uri="item:DateTimeSent", default=default_value)
# TZ-aware datetime string
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:DateTimeSent>2017-06-21T18:40:02Z</t:DateTimeSent>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(
field.from_xml(elem=elem, account=account), datetime.datetime(2017, 6, 21, 18, 40, 2, tzinfo=utc)
)
# Naive datetime string is localized to tz of the account
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:DateTimeSent>2017-06-21T18:40:02</t:DateTimeSent>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(
field.from_xml(elem=elem, account=account), datetime.datetime(2017, 6, 21, 18, 40, 2, tzinfo=tz)
)
# Garbage string returns None
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
<t:DateTimeSent>THIS_IS_GARBAGE</t:DateTimeSent>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(field.from_xml(elem=elem, account=account), None)
# Element not found returns default value
payload = b"""\
<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
<t:Item>
</t:Item>
</Envelope>"""
elem = to_xml(payload).find(f"{{{TNS}}}Item")
self.assertEqual(field.from_xml(elem=elem, account=account), default_value)
def test_single_field_indexed_element(self):
# A SingleFieldIndexedElement must have only one field defined
class TestField(SingleFieldIndexedElement):
a = CharField()
b = CharField()
with self.assertRaises(ValueError) as e:
TestField.value_field(version=Version(EXCHANGE_2013))
self.assertEqual(
e.exception.args[0],
"Class <class 'tests.test_field.FieldTest.test_single_field_indexed_element.<locals>.TestField'> "
"must have only one value field (found ('a', 'b'))",
)
|