1# This program is free software; you can redistribute it and/or modify it under
2# the terms of the (LGPL) GNU Lesser General Public License as published by the
3# Free Software Foundation; either version 3 of the License, or (at your
4# option) any later version.
5#
6# This program is distributed in the hope that it will be useful, but WITHOUT
7# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
9# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
10#
11# You should have received a copy of the GNU Lesser General Public License
12# along with this program; if not, write to the Free Software Foundation, Inc.,
13# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
14# written by: Jeff Ortel ( jortel@redhat.com )
15
16"""
17Date & time related suds Python library unit tests.
18
19Implemented using the 'pytest' testing framework.
20
21"""
22
23if __name__ == "__main__":
24    import __init__
25    __init__.runUsingPyTest(globals())
26
27
28from suds.sax.date import (FixedOffsetTimezone, Date, DateTime, Time,
29    UtcTimezone)
30from suds.xsd.sxbuiltin import XDate, XDateTime, XTime
31import tests
32
33import pytest
34
35import datetime
36
37
38class _Dummy:
39    """Class for testing unknown object class handling."""
40    pass
41
42
43"""Invalid date strings reused for both date & datetime testing."""
44_invalid_date_strings = (
45    "",
46    "abla",
47    "12",
48    "12-01",
49    "-12-01",
50    "1900-01",
51    "+1900-10-01",  # Plus sign not allowed.
52    "1900-13-01",  # Invalid month.
53    "1900-02-30",  # Invalid day.
54    "2001-02-29",  # Not a leap year.
55    "2100-02-29",  # Not a leap year.
56    " 1900-01-01",
57    "1900- 01-01",
58    "1900-01 -01",
59    "1900-01-01 ",
60    "1900-13-011",
61    "1900-01-01X",
62    "1900-01-01T",  # 'T' is a date/time separator for DateTime.
63    # Invalid time zone indicators.
64        "1900-01-01 +17:00",
65        "1900-01-01+ 17:00",
66        "1900-01-01*17:00",
67        "1900-01-01 17:00",
68        "1900-01-01+17:",
69        "1900-01-01+170",
70        "1900-01-01+1730",
71        "1900-01-01+170:00",
72        "1900-01-01+17:00:00",
73        "1900-01-01-:4",
74        "1900-01-01-2a:00",
75        "1900-01-01-222:00",
76        "1900-01-01-12:000"
77        "1900-01-01+00:60",
78        "1900-01-01-00:99")
79
80"""Invalid date strings reused for both time & datetime testing."""
81_invalid_time_strings = (
82    "",
83    "bunga",
84    "12",
85    "::",
86    "12:",
87    "12:01",
88    "12:01:",
89    "12:01: 00",
90    "12:01:  00",
91    "23: 01:00",
92    " 23:01:00",
93    "23 :01:00",
94    "23::00",
95    "23:000:00",
96    "023:00:00",
97    "23:00:000",
98    "25:01:00",
99    "-1:01:00",
100    "24:01:00",
101    "23:-1:00",
102    "23:61:00",
103    "23:60:00",
104    "23:59:-1",
105    "23:59:61",
106    "23:59:60",
107    "7.59.13",
108    "7-59-13",
109    "-0:01:00",
110    "23:-0:00",
111    "23:59:-0",
112    "23:59:6.a",
113    "23:59:6.",
114    "23:59:6:0",
115    "23:59:6.12x",
116    "23:59:6.12x45",
117    "23:59:6.999999 ",
118    "23:59:6.999999x",
119    "T23:59:6",
120    # Invalid time zone indicators.
121        "13:27:04 -10:00",
122        "13:27:04- 10:00",
123        "13:27:04*17:00",
124        "13:27:04 17:00",
125        "13:27:04-003",
126        "13:27:04-003:00",
127        "13:27:04+00:002",
128        "13:27:04-13:60",
129        "13:27:04-121",
130        "13:27:04-1210",
131        "13:27:04-121:00",
132        "13:27:04+12:",
133        "13:27:04+12:00:00",
134        "13:27:04-:13"
135        "13:27:04-24:00"
136        "13:27:04+99:00")
137
138
139class TestDate:
140    """Tests for the suds.sax.date.Date class."""
141
142    def testConstructFromDate(self):
143        date = datetime.date(2001, 12, 10)
144        assert Date(date).value is date
145
146    def testConstructFromDateTime_naive(self):
147        date = datetime.datetime(2001, 12, 10, 10, 50, 21, 32132)
148        assert Date(date).value == datetime.date(2001, 12, 10)
149
150    @pytest.mark.parametrize("hours", (5, 20))
151    def testConstructFromDateTime_tzAware(self, hours):
152        tz = FixedOffsetTimezone(10)
153        date = datetime.datetime(2001, 12, 10, hours, 50, 21, 32132, tzinfo=tz)
154        assert Date(date).value == datetime.date(2001, 12, 10)
155
156    @pytest.mark.parametrize(("string", "y", "m", "d"), (
157        ("1900-01-01", 1900, 1, 1),
158        ("1900-1-1", 1900, 1, 1),
159        ("1900-01-01z", 1900, 1, 1),
160        ("1900-01-01Z", 1900, 1, 1),
161        ("1900-01-01-02", 1900, 1, 1),
162        ("1900-01-01+2", 1900, 1, 1),
163        ("1900-01-01+02:00", 1900, 1, 1),
164        ("1900-01-01+99:59", 1900, 1, 1),
165        ("1900-01-01-21:13", 1900, 1, 1),
166        ("2000-02-29", 2000, 2, 29)))  # Leap year.
167    def testConstructFromString(self, string, y, m, d):
168        assert Date(string).value == datetime.date(y, m, d)
169
170    @pytest.mark.parametrize("string", _invalid_date_strings)
171    def testConstructFromString_failure(self, string):
172        pytest.raises(ValueError, Date, string)
173
174    @pytest.mark.parametrize("source", (
175        None,
176        object(),
177        _Dummy(),
178        datetime.time(10, 10)))
179    def testConstructFromUnknown(self, source):
180        pytest.raises(ValueError, Date, source)
181
182    @pytest.mark.parametrize(("input", "output"), (
183        ("1900-01-01", "1900-01-01"),
184        ("2000-02-29", "2000-02-29"),
185        ("1900-1-1", "1900-01-01"),
186        ("1900-01-01z", "1900-01-01"),
187        ("1900-01-01Z", "1900-01-01"),
188        ("1900-01-01-02", "1900-01-01"),
189        ("1900-01-01+2", "1900-01-01"),
190        ("1900-01-01+02:00", "1900-01-01"),
191        ("1900-01-01+99:59", "1900-01-01"),
192        ("1900-01-01-21:13", "1900-01-01")))
193    def testConvertToString(self, input, output):
194        assert str(Date(input)) == output
195
196
197class TestDateTime:
198    """Tests for the suds.sax.date.DateTime class."""
199
200    def testConstructFromDateTime(self):
201        dt = datetime.datetime(2001, 12, 10, 1, 1)
202        assert DateTime(dt).value is dt
203        dt.replace(tzinfo=UtcTimezone())
204        assert DateTime(dt).value is dt
205
206    @pytest.mark.parametrize(
207        ("string", "y", "M", "d", "h", "m", "s", "micros"), (
208        ("2013-11-19T14:05:23.428068", 2013, 11, 19, 14, 5, 23, 428068),
209        ("2013-11-19 14:05:23.4280", 2013, 11, 19, 14, 5, 23, 428000)))
210    def testConstructFromString(self, string, y, M, d, h, m, s, micros):
211        assert DateTime(string).value == datetime.datetime(y, M, d, h, m, s,
212            micros)
213
214    @pytest.mark.parametrize("string",
215        [x + "T00:00:00" for x in _invalid_date_strings] +
216        ["2000-12-31T" + x for x in _invalid_time_strings] + [
217        # Invalid date/time separator characters.
218            "2013-11-1914:05:23.428068",
219            "2013-11-19X14:05:23.428068"])
220    def testConstructFromString_failure(self, string):
221        pytest.raises(ValueError, DateTime, string)
222
223    @pytest.mark.parametrize(
224        ("string", "y", "M", "d", "h", "m", "s", "micros"), (
225        ("2000-2-28T23:59:59.9999995", 2000, 2, 29, 0, 0, 0, 0),
226        ("2000-2-29T23:59:59.9999995", 2000, 3, 1, 0, 0, 0, 0),
227        ("2013-12-31T23:59:59.9999994", 2013, 12, 31, 23, 59, 59, 999999),
228        ("2013-12-31T23:59:59.99999949", 2013, 12, 31, 23, 59, 59, 999999),
229        ("2013-12-31T23:59:59.9999995", 2014, 1, 1, 0, 0, 0, 0)))
230    def testConstructFromString_subsecondRounding(self, string, y, M, d, h, m,
231        s, micros):
232        ref = datetime.datetime(y, M, d, h, m, s, micros)
233        assert DateTime(string).value == ref
234
235    @pytest.mark.parametrize(
236        ("string", "y", "M", "d", "h", "m", "s", "micros", "tz_h", "tz_m"), (
237        ("2013-11-19T14:05:23.428068-3",
238            2013, 11, 19, 14, 5, 23, 428068, -3, 0),
239        ("2013-11-19T14:05:23.068+03",
240            2013, 11, 19, 14, 5, 23, 68000, 3, 0),
241        ("2013-11-19T14:05:23.428068-02:00",
242            2013, 11, 19, 14, 5, 23, 428068, -2, 0),
243        ("2013-11-19T14:05:23.428068+02:00",
244            2013, 11, 19, 14, 5, 23, 428068, 2, 0),
245        ("2013-11-19T14:05:23.428068-23:59",
246            2013, 11, 19, 14, 5, 23, 428068, -23, -59)))
247    def testConstructFromString_timezone(self, string, y, M, d, h, m, s,
248        micros, tz_h, tz_m):
249        tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
250        tzinfo = FixedOffsetTimezone(tzdelta)
251        ref = datetime.datetime(y, M, d, h, m, s, micros, tzinfo=tzinfo)
252        assert DateTime(string).value == ref
253
254    @pytest.mark.parametrize("source", (
255        None,
256        object(),
257        _Dummy(),
258        datetime.date(2010, 10, 27),
259        datetime.time(10, 10)))
260    def testConstructFromUnknown(self, source):
261        pytest.raises(ValueError, DateTime, source)
262
263    @pytest.mark.parametrize(("input", "output"), (
264        ("2013-11-19T14:05:23.428068", "2013-11-19T14:05:23.428068"),
265        ("2013-11-19 14:05:23.4280", "2013-11-19T14:05:23.428000"),
266        ("2013-12-31T23:59:59.9999995", "2014-01-01T00:00:00"),
267        ("2013-11-19T14:05:23.428068-3", "2013-11-19T14:05:23.428068-03:00"),
268        ("2013-11-19T14:05:23.068+03", "2013-11-19T14:05:23.068000+03:00"),
269        ("2013-11-19T14:05:23.4-02:00", "2013-11-19T14:05:23.400000-02:00"),
270        ("2013-11-19T14:05:23.410+02:00", "2013-11-19T14:05:23.410000+02:00"),
271        ("2013-11-19T14:05:23.428-23:59", "2013-11-19T14:05:23.428000-23:59")))
272    def testConvertToString(self, input, output):
273        assert str(DateTime(input)) == output
274
275
276class TestTime:
277    """Tests for the suds.sax.date.Time class."""
278
279    def testConstructFromTime(self):
280        time = datetime.time(1, 1)
281        assert Time(time).value is time
282        time.replace(tzinfo=UtcTimezone())
283        assert Time(time).value is time
284
285    @pytest.mark.parametrize(("string", "h", "m", "s", "micros"), (
286        ("10:59:47", 10, 59, 47, 0),
287        ("9:9:13", 9, 9, 13, 0),
288        ("18:0:09.2139", 18, 0, 9, 213900),
289        ("18:0:09.02139", 18, 0, 9, 21390),
290        ("18:0:09.002139", 18, 0, 9, 2139),
291        ("0:00:00.00013", 0, 0, 0, 130),
292        ("0:00:00.000001", 0, 0, 0, 1),
293        ("0:00:00.000000", 0, 0, 0, 0),
294        ("23:59:6.999999", 23, 59, 6, 999999),
295        ("1:13:50.0", 1, 13, 50, 0)))
296    def testConstructFromString(self, string, h, m, s, micros):
297        assert Time(string).value == datetime.time(h, m, s, micros)
298
299    @pytest.mark.parametrize("string", _invalid_time_strings)
300    def testConstructFromString_failure(self, string):
301        pytest.raises(ValueError, Time, string)
302
303    @pytest.mark.parametrize(("string", "h", "m", "s", "micros"), (
304        ("0:0:0.0000000", 0, 0, 0, 0),
305        ("0:0:0.0000001", 0, 0, 0, 0),
306        ("0:0:0.0000004", 0, 0, 0, 0),
307        ("0:0:0.0000005", 0, 0, 0, 1),
308        ("0:0:0.0000006", 0, 0, 0, 1),
309        ("0:0:0.0000009", 0, 0, 0, 1),
310        ("0:0:0.5", 0, 0, 0, 500000),
311        ("0:0:0.5000004", 0, 0, 0, 500000),
312        ("0:0:0.5000005", 0, 0, 0, 500001),
313        ("0:0:0.50000050", 0, 0, 0, 500001),
314        ("0:0:0.50000051", 0, 0, 0, 500001),
315        ("0:0:0.50000055", 0, 0, 0, 500001),
316        ("0:0:0.50000059", 0, 0, 0, 500001),
317        ("0:0:0.5000006", 0, 0, 0, 500001),
318        ("0:0:0.9999990", 0, 0, 0, 999999),
319        ("0:0:0.9999991", 0, 0, 0, 999999),
320        ("0:0:0.9999994", 0, 0, 0, 999999),
321        ("0:0:0.99999949", 0, 0, 0, 999999),
322        ("0:0:0.9999995", 0, 0, 1, 0),
323        ("0:0:0.9999996", 0, 0, 1, 0),
324        ("0:0:0.9999999", 0, 0, 1, 0)))
325    def testConstructFromString_subsecondRounding(self, string, h, m, s,
326        micros):
327        assert Time(string).value == datetime.time(h, m, s, micros)
328
329    @pytest.mark.parametrize(
330        ("string", "h", "m", "s", "micros", "tz_h", "tz_m"), (
331        ("18:0:09.2139z", 18, 0, 9, 213900, 0, 0),
332        ("18:0:09.2139Z", 18, 0, 9, 213900, 0, 0),
333        ("18:0:09.2139+3", 18, 0, 9, 213900, 3, 0),
334        ("18:0:09.2139-3", 18, 0, 9, 213900, -3, 0),
335        ("18:0:09.2139-03", 18, 0, 9, 213900, -3, 0),
336        ("18:0:09.2139+9:3", 18, 0, 9, 213900, 9, 3),
337        ("18:0:09.2139+10:31", 18, 0, 9, 213900, 10, 31),
338        ("18:0:09.2139-10:31", 18, 0, 9, 213900, -10, -31)))
339    def testConstructFromString_timezone(self, string, h, m, s, micros, tz_h,
340        tz_m):
341        tzdelta = datetime.timedelta(hours=tz_h, minutes=tz_m)
342        tzinfo = FixedOffsetTimezone(tzdelta)
343        ref = datetime.time(h, m, s, micros, tzinfo=tzinfo)
344        assert Time(string).value == ref
345
346    @pytest.mark.parametrize("source", (
347        None,
348        object(),
349        _Dummy(),
350        datetime.date(2010, 10, 27),
351        datetime.datetime(2010, 10, 27, 10, 10)))
352    def testConstructFromUnknown(self, source):
353        pytest.raises(ValueError, Time, source)
354
355    @pytest.mark.parametrize(("input", "output"), (
356        ("14:05:23.428068", "14:05:23.428068"),
357        ("14:05:23.4280", "14:05:23.428000"),
358        ("23:59:59.9999995", "00:00:00"),
359        ("14:05:23.428068-3", "14:05:23.428068-03:00"),
360        ("14:05:23.068+03", "14:05:23.068000+03:00"),
361        ("14:05:23.4-02:00", "14:05:23.400000-02:00"),
362        ("14:05:23.410+02:00", "14:05:23.410000+02:00"),
363        ("14:05:23.428-23:59", "14:05:23.428000-23:59")))
364    def testConvertToString(self, input, output):
365        assert str(Time(input)) == output
366
367
368class TestXDate:
369    """
370    Tests for the suds.xsd.sxbuiltin.XDate class.
371
372    Python object <--> string conversion details already tested in TestDate.
373
374    """
375
376    def testTranslateEmptyStringToPythonObject(self):
377        assert XDate.translate("") == None
378
379    def testTranslateStringToPythonObject(self):
380        assert XDate.translate("1941-12-7") == datetime.date(1941, 12, 7)
381
382    def testTranslatePythonObjectToString(self):
383        date = datetime.date(2013, 7, 24)
384        translated = XDate.translate(date, topython=False)
385        assert isinstance(translated, str)
386        assert translated == "2013-07-24"
387
388    def testTranslatePythonObjectToString_datetime(self):
389        dt = datetime.datetime(2013, 7, 24, 11, 59, 4)
390        translated = XDate.translate(dt, topython=False)
391        assert isinstance(translated, str)
392        assert translated == "2013-07-24"
393
394    @pytest.mark.parametrize("source", (
395        None,
396        object(),
397        _Dummy(),
398        datetime.time()))
399    def testTranslatePythonObjectToString_failed(self, source):
400        assert XDate.translate(source, topython=False) is source
401
402
403class TestXDateTime:
404    """
405    Tests for the suds.xsd.sxbuiltin.XDateTime class.
406
407    Python object <--> string conversion details already tested in
408    TestDateTime.
409
410    """
411
412    def testTranslateEmptyStringToPythonObject(self):
413        assert XDateTime.translate("") == None
414
415    def testTranslateStringToPythonObject(self):
416        dt = datetime.datetime(1941, 12, 7, 10, 30, 22, 454000)
417        assert XDateTime.translate("1941-12-7T10:30:22.454") == dt
418
419    def testTranslatePythonObjectToString(self):
420        dt = datetime.datetime(2021, 12, 31, 11, 25, tzinfo=UtcTimezone())
421        translated = XDateTime.translate(dt, topython=False)
422        assert isinstance(translated, str)
423        assert translated == "2021-12-31T11:25:00+00:00"
424
425    @pytest.mark.parametrize("source", (
426        None,
427        object(),
428        _Dummy(),
429        datetime.time(22, 47, 9, 981),
430        datetime.date(2101, 1, 1)))
431    def testTranslatePythonObjectToString_failed(self, source):
432        assert XDateTime.translate(source, topython=False) is source
433
434
435class TestXTime:
436    """
437    Tests for the suds.xsd.sxbuiltin.XTime class.
438
439    Python object <--> string conversion details already tested in
440    TestDateTime.
441
442    """
443
444    def testTranslateEmptyStringToPythonObject(self):
445        assert XTime.translate("") == None
446
447    def testTranslateStringToPythonObject(self):
448        assert XTime.translate("10:30:22") == datetime.time(10, 30, 22)
449
450    def testTranslatePythonObjectToString(self):
451        time = datetime.time(16, 53, 12, tzinfo=FixedOffsetTimezone(4))
452        translated = XTime.translate(time, topython=False)
453        assert isinstance(translated, str)
454        assert translated == "16:53:12+04:00"
455
456    @pytest.mark.parametrize("source", (
457        None,
458        object(),
459        _Dummy(),
460        datetime.date(2101, 1, 1),
461        datetime.datetime(2101, 1, 1, 22, 47, 9, 981)))
462    def testTranslatePythonObjectToString_failed(self, source):
463        assert XTime.translate(source, topython=False) is source
464