• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17"""Tests for apitools.base.protorpclite.protojson."""
18import datetime
19import json
20import unittest
21
22from apitools.base.protorpclite import message_types
23from apitools.base.protorpclite import messages
24from apitools.base.protorpclite import protojson
25from apitools.base.protorpclite import test_util
26
27
28class CustomField(messages.MessageField):
29    """Custom MessageField class."""
30
31    type = int
32    message_type = message_types.VoidMessage
33
34    def __init__(self, number, **kwargs):
35        super(CustomField, self).__init__(self.message_type, number, **kwargs)
36
37    def value_to_message(self, value):
38        return self.message_type()  # pylint:disable=not-callable
39
40
41class MyMessage(messages.Message):
42    """Test message containing various types."""
43
44    class Color(messages.Enum):
45
46        RED = 1
47        GREEN = 2
48        BLUE = 3
49
50    class Nested(messages.Message):
51
52        nested_value = messages.StringField(1)
53
54    class NestedDatetime(messages.Message):
55
56        nested_dt_value = message_types.DateTimeField(1)
57
58    a_string = messages.StringField(2)
59    an_integer = messages.IntegerField(3)
60    a_float = messages.FloatField(4)
61    a_boolean = messages.BooleanField(5)
62    an_enum = messages.EnumField(Color, 6)
63    a_nested = messages.MessageField(Nested, 7)
64    a_repeated = messages.IntegerField(8, repeated=True)
65    a_repeated_float = messages.FloatField(9, repeated=True)
66    a_datetime = message_types.DateTimeField(10)
67    a_repeated_datetime = message_types.DateTimeField(11, repeated=True)
68    a_custom = CustomField(12)
69    a_repeated_custom = CustomField(13, repeated=True)
70    a_nested_datetime = messages.MessageField(NestedDatetime, 14)
71
72
73class ModuleInterfaceTest(test_util.ModuleInterfaceTest,
74                          test_util.TestCase):
75
76    MODULE = protojson
77
78
79# TODO(rafek): Convert this test to the compliance test in test_util.
80class ProtojsonTest(test_util.TestCase,
81                    test_util.ProtoConformanceTestBase):
82    """Test JSON encoding and decoding."""
83
84    PROTOLIB = protojson
85
86    def CompareEncoded(self, expected_encoded, actual_encoded):
87        """JSON encoding will be laundered to remove string differences."""
88        self.assertEquals(json.loads(expected_encoded),
89                          json.loads(actual_encoded))
90
91    encoded_empty_message = '{}'
92
93    encoded_partial = """{
94    "double_value": 1.23,
95    "int64_value": -100000000000,
96    "int32_value": 1020,
97    "string_value": "a string",
98    "enum_value": "VAL2"
99    }
100    """
101
102    # pylint:disable=anomalous-unicode-escape-in-string
103    encoded_full = """{
104    "double_value": 1.23,
105    "float_value": -2.5,
106    "int64_value": -100000000000,
107    "uint64_value": 102020202020,
108    "int32_value": 1020,
109    "bool_value": true,
110    "string_value": "a string\u044f",
111    "bytes_value": "YSBieXRlc//+",
112    "enum_value": "VAL2"
113    }
114    """
115
116    encoded_repeated = """{
117    "double_value": [1.23, 2.3],
118    "float_value": [-2.5, 0.5],
119    "int64_value": [-100000000000, 20],
120    "uint64_value": [102020202020, 10],
121    "int32_value": [1020, 718],
122    "bool_value": [true, false],
123    "string_value": ["a string\u044f", "another string"],
124    "bytes_value": ["YSBieXRlc//+", "YW5vdGhlciBieXRlcw=="],
125    "enum_value": ["VAL2", "VAL1"]
126    }
127    """
128
129    encoded_nested = """{
130    "nested": {
131      "a_value": "a string"
132    }
133    }
134    """
135
136    encoded_repeated_nested = """{
137    "repeated_nested": [{"a_value": "a string"},
138                        {"a_value": "another string"}]
139    }
140    """
141
142    unexpected_tag_message = '{"unknown": "value"}'
143
144    encoded_default_assigned = '{"a_value": "a default"}'
145
146    encoded_nested_empty = '{"nested": {}}'
147
148    encoded_repeated_nested_empty = '{"repeated_nested": [{}, {}]}'
149
150    encoded_extend_message = '{"int64_value": [400, 50, 6000]}'
151
152    encoded_string_types = '{"string_value": "Latin"}'
153
154    encoded_invalid_enum = '{"enum_value": "undefined"}'
155
156    def testConvertIntegerToFloat(self):
157        """Test that integers passed in to float fields are converted.
158
159        This is necessary because JSON outputs integers for numbers
160        with 0 decimals.
161
162        """
163        message = protojson.decode_message(MyMessage, '{"a_float": 10}')
164
165        self.assertTrue(isinstance(message.a_float, float))
166        self.assertEquals(10.0, message.a_float)
167
168    def testConvertStringToNumbers(self):
169        """Test that strings passed to integer fields are converted."""
170        message = protojson.decode_message(MyMessage,
171                                           """{"an_integer": "10",
172                                           "a_float": "3.5",
173                                           "a_repeated": ["1", "2"],
174                                           "a_repeated_float": ["1.5", "2", 10]
175                                           }""")
176
177        self.assertEquals(MyMessage(an_integer=10,
178                                    a_float=3.5,
179                                    a_repeated=[1, 2],
180                                    a_repeated_float=[1.5, 2.0, 10.0]),
181                          message)
182
183    def testWrongTypeAssignment(self):
184        """Test when wrong type is assigned to a field."""
185        self.assertRaises(messages.ValidationError,
186                          protojson.decode_message,
187                          MyMessage, '{"a_string": 10}')
188        self.assertRaises(messages.ValidationError,
189                          protojson.decode_message,
190                          MyMessage, '{"an_integer": 10.2}')
191        self.assertRaises(messages.ValidationError,
192                          protojson.decode_message,
193                          MyMessage, '{"an_integer": "10.2"}')
194
195    def testNumericEnumeration(self):
196        """Test that numbers work for enum values."""
197        message = protojson.decode_message(MyMessage, '{"an_enum": 2}')
198
199        expected_message = MyMessage()
200        expected_message.an_enum = MyMessage.Color.GREEN
201
202        self.assertEquals(expected_message, message)
203
204    def testNumericEnumerationNegativeTest(self):
205        """Test with an invalid number for the enum value."""
206        # The message should successfully decode.
207        message = protojson.decode_message(MyMessage,
208                                           '{"an_enum": 89}')
209
210        expected_message = MyMessage()
211
212        self.assertEquals(expected_message, message)
213        # The roundtrip should result in equivalent encoded
214        # message.
215        self.assertEquals('{"an_enum": 89}', protojson.encode_message(message))
216
217    def testAlphaEnumeration(self):
218        """Test that alpha enum values work."""
219        message = protojson.decode_message(MyMessage, '{"an_enum": "RED"}')
220
221        expected_message = MyMessage()
222        expected_message.an_enum = MyMessage.Color.RED
223
224        self.assertEquals(expected_message, message)
225
226    def testAlphaEnumerationNegativeTest(self):
227        """The alpha enum value is invalid."""
228        # The message should successfully decode.
229        message = protojson.decode_message(MyMessage,
230                                           '{"an_enum": "IAMINVALID"}')
231
232        expected_message = MyMessage()
233
234        self.assertEquals(expected_message, message)
235        # The roundtrip should result in equivalent encoded message.
236        self.assertEquals('{"an_enum": "IAMINVALID"}',
237                          protojson.encode_message(message))
238
239    def testEnumerationNegativeTestWithEmptyString(self):
240        """The enum value is an empty string."""
241        # The message should successfully decode.
242        message = protojson.decode_message(MyMessage, '{"an_enum": ""}')
243
244        expected_message = MyMessage()
245
246        self.assertEquals(expected_message, message)
247        # The roundtrip should result in equivalent encoded message.
248        self.assertEquals('{"an_enum": ""}', protojson.encode_message(message))
249
250    def testNullValues(self):
251        """Test that null values overwrite existing values."""
252        self.assertEquals(MyMessage(),
253                          protojson.decode_message(MyMessage,
254                                                   ('{"an_integer": null,'
255                                                    ' "a_nested": null,'
256                                                    ' "an_enum": null'
257                                                    '}')))
258
259    def testEmptyList(self):
260        """Test that empty lists are ignored."""
261        self.assertEquals(MyMessage(),
262                          protojson.decode_message(MyMessage,
263                                                   '{"a_repeated": []}'))
264
265    def testNotJSON(self):
266        """Test error when string is not valid JSON."""
267        self.assertRaises(
268            ValueError,
269            protojson.decode_message, MyMessage,
270            '{this is not json}')
271
272    def testDoNotEncodeStrangeObjects(self):
273        """Test trying to encode a strange object.
274
275        The main purpose of this test is to complete coverage. It
276        ensures that the default behavior of the JSON encoder is
277        preserved when someone tries to serialized an unexpected type.
278
279        """
280        class BogusObject(object):
281
282            def check_initialized(self):
283                pass
284
285        self.assertRaises(TypeError,
286                          protojson.encode_message,
287                          BogusObject())
288
289    def testMergeEmptyString(self):
290        """Test merging the empty or space only string."""
291        message = protojson.decode_message(test_util.OptionalMessage, '')
292        self.assertEquals(test_util.OptionalMessage(), message)
293
294        message = protojson.decode_message(test_util.OptionalMessage, ' ')
295        self.assertEquals(test_util.OptionalMessage(), message)
296
297    def testProtojsonUnrecognizedFieldName(self):
298        """Test that unrecognized fields are saved and can be accessed."""
299        decoded = protojson.decode_message(
300            MyMessage,
301            ('{"an_integer": 1, "unknown_val": 2}'))
302        self.assertEquals(decoded.an_integer, 1)
303        self.assertEquals(1, len(decoded.all_unrecognized_fields()))
304        self.assertEquals('unknown_val', decoded.all_unrecognized_fields()[0])
305        self.assertEquals((2, messages.Variant.INT64),
306                          decoded.get_unrecognized_field_info('unknown_val'))
307
308    def testProtojsonUnrecognizedFieldNumber(self):
309        """Test that unrecognized fields are saved and can be accessed."""
310        decoded = protojson.decode_message(
311            MyMessage,
312            '{"an_integer": 1, "1001": "unknown", "-123": "negative", '
313            '"456_mixed": 2}')
314        self.assertEquals(decoded.an_integer, 1)
315        self.assertEquals(3, len(decoded.all_unrecognized_fields()))
316        self.assertFalse(1001 in decoded.all_unrecognized_fields())
317        self.assertTrue('1001' in decoded.all_unrecognized_fields())
318        self.assertEquals(('unknown', messages.Variant.STRING),
319                          decoded.get_unrecognized_field_info('1001'))
320        self.assertTrue('-123' in decoded.all_unrecognized_fields())
321        self.assertEquals(('negative', messages.Variant.STRING),
322                          decoded.get_unrecognized_field_info('-123'))
323        self.assertTrue('456_mixed' in decoded.all_unrecognized_fields())
324        self.assertEquals((2, messages.Variant.INT64),
325                          decoded.get_unrecognized_field_info('456_mixed'))
326
327    def testProtojsonUnrecognizedNull(self):
328        """Test that unrecognized fields that are None are skipped."""
329        decoded = protojson.decode_message(
330            MyMessage,
331            '{"an_integer": 1, "unrecognized_null": null}')
332        self.assertEquals(decoded.an_integer, 1)
333        self.assertEquals(decoded.all_unrecognized_fields(), [])
334
335    def testUnrecognizedFieldVariants(self):
336        """Test that unrecognized fields are mapped to the right variants."""
337        for encoded, expected_variant in (
338                ('{"an_integer": 1, "unknown_val": 2}',
339                 messages.Variant.INT64),
340                ('{"an_integer": 1, "unknown_val": 2.0}',
341                 messages.Variant.DOUBLE),
342                ('{"an_integer": 1, "unknown_val": "string value"}',
343                 messages.Variant.STRING),
344                ('{"an_integer": 1, "unknown_val": [1, 2, 3]}',
345                 messages.Variant.INT64),
346                ('{"an_integer": 1, "unknown_val": [1, 2.0, 3]}',
347                 messages.Variant.DOUBLE),
348                ('{"an_integer": 1, "unknown_val": [1, "foo", 3]}',
349                 messages.Variant.STRING),
350                ('{"an_integer": 1, "unknown_val": true}',
351                 messages.Variant.BOOL)):
352            decoded = protojson.decode_message(MyMessage, encoded)
353            self.assertEquals(decoded.an_integer, 1)
354            self.assertEquals(1, len(decoded.all_unrecognized_fields()))
355            self.assertEquals(
356                'unknown_val', decoded.all_unrecognized_fields()[0])
357            _, decoded_variant = decoded.get_unrecognized_field_info(
358                'unknown_val')
359            self.assertEquals(expected_variant, decoded_variant)
360
361    def testDecodeDateTime(self):
362        for datetime_string, datetime_vals in (
363                ('2012-09-30T15:31:50.262', (2012, 9, 30, 15, 31, 50, 262000)),
364                ('2012-09-30T15:31:50', (2012, 9, 30, 15, 31, 50, 0))):
365            message = protojson.decode_message(
366                MyMessage, '{"a_datetime": "%s"}' % datetime_string)
367            expected_message = MyMessage(
368                a_datetime=datetime.datetime(*datetime_vals))
369
370            self.assertEquals(expected_message, message)
371
372    def testDecodeInvalidDateTime(self):
373        self.assertRaises(messages.DecodeError, protojson.decode_message,
374                          MyMessage, '{"a_datetime": "invalid"}')
375
376    def testDecodeInvalidMessage(self):
377        encoded = """{
378        "a_nested_datetime": {
379          "nested_dt_value": "invalid"
380          }
381        }
382        """
383        self.assertRaises(messages.DecodeError, protojson.decode_message,
384                          MyMessage, encoded)
385
386    def testEncodeDateTime(self):
387        for datetime_string, datetime_vals in (
388                ('2012-09-30T15:31:50.262000',
389                 (2012, 9, 30, 15, 31, 50, 262000)),
390                ('2012-09-30T15:31:50.262123',
391                 (2012, 9, 30, 15, 31, 50, 262123)),
392                ('2012-09-30T15:31:50',
393                 (2012, 9, 30, 15, 31, 50, 0))):
394            decoded_message = protojson.encode_message(
395                MyMessage(a_datetime=datetime.datetime(*datetime_vals)))
396            expected_decoding = '{"a_datetime": "%s"}' % datetime_string
397            self.CompareEncoded(expected_decoding, decoded_message)
398
399    def testDecodeRepeatedDateTime(self):
400        message = protojson.decode_message(
401            MyMessage,
402            '{"a_repeated_datetime": ["2012-09-30T15:31:50.262", '
403            '"2010-01-21T09:52:00", "2000-01-01T01:00:59.999999"]}')
404        expected_message = MyMessage(
405            a_repeated_datetime=[
406                datetime.datetime(2012, 9, 30, 15, 31, 50, 262000),
407                datetime.datetime(2010, 1, 21, 9, 52),
408                datetime.datetime(2000, 1, 1, 1, 0, 59, 999999)])
409
410        self.assertEquals(expected_message, message)
411
412    def testDecodeCustom(self):
413        message = protojson.decode_message(MyMessage, '{"a_custom": 1}')
414        self.assertEquals(MyMessage(a_custom=1), message)
415
416    def testDecodeInvalidCustom(self):
417        self.assertRaises(messages.ValidationError, protojson.decode_message,
418                          MyMessage, '{"a_custom": "invalid"}')
419
420    def testEncodeCustom(self):
421        decoded_message = protojson.encode_message(MyMessage(a_custom=1))
422        self.CompareEncoded('{"a_custom": 1}', decoded_message)
423
424    def testDecodeRepeatedCustom(self):
425        message = protojson.decode_message(
426            MyMessage, '{"a_repeated_custom": [1, 2, 3]}')
427        self.assertEquals(MyMessage(a_repeated_custom=[1, 2, 3]), message)
428
429    def testDecodeRepeatedEmpty(self):
430        message = protojson.decode_message(
431            MyMessage, '{"a_repeated": []}')
432        self.assertEquals(MyMessage(a_repeated=[]), message)
433
434    def testDecodeNone(self):
435        message = protojson.decode_message(
436            MyMessage, '{"an_integer": []}')
437        self.assertEquals(MyMessage(an_integer=None), message)
438
439    def testDecodeBadBase64BytesField(self):
440        """Test decoding improperly encoded base64 bytes value."""
441        self.assertRaisesWithRegexpMatch(
442            messages.DecodeError,
443            'Base64 decoding error: Incorrect padding',
444            protojson.decode_message,
445            test_util.OptionalMessage,
446            '{"bytes_value": "abcdefghijklmnopq"}')
447
448
449class CustomProtoJson(protojson.ProtoJson):
450
451    def encode_field(self, field, value):
452        return '{encoded}' + value
453
454    def decode_field(self, field, value):
455        return '{decoded}' + value
456
457
458class CustomProtoJsonTest(test_util.TestCase):
459    """Tests for serialization overriding functionality."""
460
461    def setUp(self):
462        self.protojson = CustomProtoJson()
463
464    def testEncode(self):
465        self.assertEqual(
466            '{"a_string": "{encoded}xyz"}',
467            self.protojson.encode_message(MyMessage(a_string='xyz')))
468
469    def testDecode(self):
470        self.assertEqual(
471            MyMessage(a_string='{decoded}xyz'),
472            self.protojson.decode_message(MyMessage, '{"a_string": "xyz"}'))
473
474    def testDecodeEmptyMessage(self):
475        self.assertEqual(
476            MyMessage(a_string='{decoded}'),
477            self.protojson.decode_message(MyMessage, '{"a_string": ""}'))
478
479    def testDefault(self):
480        self.assertTrue(protojson.ProtoJson.get_default(),
481                        protojson.ProtoJson.get_default())
482
483        instance = CustomProtoJson()
484        protojson.ProtoJson.set_default(instance)
485        self.assertTrue(instance is protojson.ProtoJson.get_default())
486
487
488if __name__ == '__main__':
489    unittest.main()
490