• 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    encoded_invalid_repeated_enum = '{"enum_value": ["VAL1", "undefined"]}'
157
158    def testConvertIntegerToFloat(self):
159        """Test that integers passed in to float fields are converted.
160
161        This is necessary because JSON outputs integers for numbers
162        with 0 decimals.
163
164        """
165        message = protojson.decode_message(MyMessage, '{"a_float": 10}')
166
167        self.assertTrue(isinstance(message.a_float, float))
168        self.assertEquals(10.0, message.a_float)
169
170    def testConvertStringToNumbers(self):
171        """Test that strings passed to integer fields are converted."""
172        message = protojson.decode_message(MyMessage,
173                                           """{"an_integer": "10",
174                                           "a_float": "3.5",
175                                           "a_repeated": ["1", "2"],
176                                           "a_repeated_float": ["1.5", "2", 10]
177                                           }""")
178
179        self.assertEquals(MyMessage(an_integer=10,
180                                    a_float=3.5,
181                                    a_repeated=[1, 2],
182                                    a_repeated_float=[1.5, 2.0, 10.0]),
183                          message)
184
185    def testWrongTypeAssignment(self):
186        """Test when wrong type is assigned to a field."""
187        self.assertRaises(messages.ValidationError,
188                          protojson.decode_message,
189                          MyMessage, '{"a_string": 10}')
190        self.assertRaises(messages.ValidationError,
191                          protojson.decode_message,
192                          MyMessage, '{"an_integer": 10.2}')
193        self.assertRaises(messages.ValidationError,
194                          protojson.decode_message,
195                          MyMessage, '{"an_integer": "10.2"}')
196
197    def testNumericEnumeration(self):
198        """Test that numbers work for enum values."""
199        message = protojson.decode_message(MyMessage, '{"an_enum": 2}')
200
201        expected_message = MyMessage()
202        expected_message.an_enum = MyMessage.Color.GREEN
203
204        self.assertEquals(expected_message, message)
205
206    def testNumericEnumerationNegativeTest(self):
207        """Test with an invalid number for the enum value."""
208        # The message should successfully decode.
209        message = protojson.decode_message(MyMessage,
210                                           '{"an_enum": 89}')
211
212        expected_message = MyMessage()
213
214        self.assertEquals(expected_message, message)
215        # The roundtrip should result in equivalent encoded
216        # message.
217        self.assertEquals('{"an_enum": 89}', protojson.encode_message(message))
218
219    def testAlphaEnumeration(self):
220        """Test that alpha enum values work."""
221        message = protojson.decode_message(MyMessage, '{"an_enum": "RED"}')
222
223        expected_message = MyMessage()
224        expected_message.an_enum = MyMessage.Color.RED
225
226        self.assertEquals(expected_message, message)
227
228    def testAlphaEnumerationNegativeTest(self):
229        """The alpha enum value is invalid."""
230        # The message should successfully decode.
231        message = protojson.decode_message(MyMessage,
232                                           '{"an_enum": "IAMINVALID"}')
233
234        expected_message = MyMessage()
235
236        self.assertEquals(expected_message, message)
237        # The roundtrip should result in equivalent encoded message.
238        self.assertEquals('{"an_enum": "IAMINVALID"}',
239                          protojson.encode_message(message))
240
241    def testEnumerationNegativeTestWithEmptyString(self):
242        """The enum value is an empty string."""
243        # The message should successfully decode.
244        message = protojson.decode_message(MyMessage, '{"an_enum": ""}')
245
246        expected_message = MyMessage()
247
248        self.assertEquals(expected_message, message)
249        # The roundtrip should result in equivalent encoded message.
250        self.assertEquals('{"an_enum": ""}', protojson.encode_message(message))
251
252    def testNullValues(self):
253        """Test that null values overwrite existing values."""
254        self.assertEquals(MyMessage(),
255                          protojson.decode_message(MyMessage,
256                                                   ('{"an_integer": null,'
257                                                    ' "a_nested": null,'
258                                                    ' "an_enum": null'
259                                                    '}')))
260
261    def testEmptyList(self):
262        """Test that empty lists are ignored."""
263        self.assertEquals(MyMessage(),
264                          protojson.decode_message(MyMessage,
265                                                   '{"a_repeated": []}'))
266
267    def testNotJSON(self):
268        """Test error when string is not valid JSON."""
269        self.assertRaises(
270            ValueError,
271            protojson.decode_message, MyMessage,
272            '{this is not json}')
273
274    def testDoNotEncodeStrangeObjects(self):
275        """Test trying to encode a strange object.
276
277        The main purpose of this test is to complete coverage. It
278        ensures that the default behavior of the JSON encoder is
279        preserved when someone tries to serialized an unexpected type.
280
281        """
282        class BogusObject(object):
283
284            def check_initialized(self):
285                pass
286
287        self.assertRaises(TypeError,
288                          protojson.encode_message,
289                          BogusObject())
290
291    def testMergeEmptyString(self):
292        """Test merging the empty or space only string."""
293        message = protojson.decode_message(test_util.OptionalMessage, '')
294        self.assertEquals(test_util.OptionalMessage(), message)
295
296        message = protojson.decode_message(test_util.OptionalMessage, ' ')
297        self.assertEquals(test_util.OptionalMessage(), message)
298
299    def testProtojsonUnrecognizedFieldName(self):
300        """Test that unrecognized fields are saved and can be accessed."""
301        decoded = protojson.decode_message(
302            MyMessage,
303            ('{"an_integer": 1, "unknown_val": 2}'))
304        self.assertEquals(decoded.an_integer, 1)
305        self.assertEquals(1, len(decoded.all_unrecognized_fields()))
306        self.assertEquals('unknown_val', decoded.all_unrecognized_fields()[0])
307        self.assertEquals((2, messages.Variant.INT64),
308                          decoded.get_unrecognized_field_info('unknown_val'))
309
310    def testProtojsonUnrecognizedFieldNumber(self):
311        """Test that unrecognized fields are saved and can be accessed."""
312        decoded = protojson.decode_message(
313            MyMessage,
314            '{"an_integer": 1, "1001": "unknown", "-123": "negative", '
315            '"456_mixed": 2}')
316        self.assertEquals(decoded.an_integer, 1)
317        self.assertEquals(3, len(decoded.all_unrecognized_fields()))
318        self.assertFalse(1001 in decoded.all_unrecognized_fields())
319        self.assertTrue('1001' in decoded.all_unrecognized_fields())
320        self.assertEquals(('unknown', messages.Variant.STRING),
321                          decoded.get_unrecognized_field_info('1001'))
322        self.assertTrue('-123' in decoded.all_unrecognized_fields())
323        self.assertEquals(('negative', messages.Variant.STRING),
324                          decoded.get_unrecognized_field_info('-123'))
325        self.assertTrue('456_mixed' in decoded.all_unrecognized_fields())
326        self.assertEquals((2, messages.Variant.INT64),
327                          decoded.get_unrecognized_field_info('456_mixed'))
328
329    def testProtojsonUnrecognizedNull(self):
330        """Test that unrecognized fields that are None are skipped."""
331        decoded = protojson.decode_message(
332            MyMessage,
333            '{"an_integer": 1, "unrecognized_null": null}')
334        self.assertEquals(decoded.an_integer, 1)
335        self.assertEquals(decoded.all_unrecognized_fields(), [])
336
337    def testUnrecognizedFieldVariants(self):
338        """Test that unrecognized fields are mapped to the right variants."""
339        for encoded, expected_variant in (
340                ('{"an_integer": 1, "unknown_val": 2}',
341                 messages.Variant.INT64),
342                ('{"an_integer": 1, "unknown_val": 2.0}',
343                 messages.Variant.DOUBLE),
344                ('{"an_integer": 1, "unknown_val": "string value"}',
345                 messages.Variant.STRING),
346                ('{"an_integer": 1, "unknown_val": [1, 2, 3]}',
347                 messages.Variant.INT64),
348                ('{"an_integer": 1, "unknown_val": [1, 2.0, 3]}',
349                 messages.Variant.DOUBLE),
350                ('{"an_integer": 1, "unknown_val": [1, "foo", 3]}',
351                 messages.Variant.STRING),
352                ('{"an_integer": 1, "unknown_val": true}',
353                 messages.Variant.BOOL)):
354            decoded = protojson.decode_message(MyMessage, encoded)
355            self.assertEquals(decoded.an_integer, 1)
356            self.assertEquals(1, len(decoded.all_unrecognized_fields()))
357            self.assertEquals(
358                'unknown_val', decoded.all_unrecognized_fields()[0])
359            _, decoded_variant = decoded.get_unrecognized_field_info(
360                'unknown_val')
361            self.assertEquals(expected_variant, decoded_variant)
362
363    def testDecodeDateTime(self):
364        for datetime_string, datetime_vals in (
365                ('2012-09-30T15:31:50.262', (2012, 9, 30, 15, 31, 50, 262000)),
366                ('2012-09-30T15:31:50', (2012, 9, 30, 15, 31, 50, 0))):
367            message = protojson.decode_message(
368                MyMessage, '{"a_datetime": "%s"}' % datetime_string)
369            expected_message = MyMessage(
370                a_datetime=datetime.datetime(*datetime_vals))
371
372            self.assertEquals(expected_message, message)
373
374    def testDecodeInvalidDateTime(self):
375        self.assertRaises(messages.DecodeError, protojson.decode_message,
376                          MyMessage, '{"a_datetime": "invalid"}')
377
378    def testDecodeInvalidMessage(self):
379        encoded = """{
380        "a_nested_datetime": {
381          "nested_dt_value": "invalid"
382          }
383        }
384        """
385        self.assertRaises(messages.DecodeError, protojson.decode_message,
386                          MyMessage, encoded)
387
388    def testEncodeDateTime(self):
389        for datetime_string, datetime_vals in (
390                ('2012-09-30T15:31:50.262000',
391                 (2012, 9, 30, 15, 31, 50, 262000)),
392                ('2012-09-30T15:31:50.262123',
393                 (2012, 9, 30, 15, 31, 50, 262123)),
394                ('2012-09-30T15:31:50',
395                 (2012, 9, 30, 15, 31, 50, 0))):
396            decoded_message = protojson.encode_message(
397                MyMessage(a_datetime=datetime.datetime(*datetime_vals)))
398            expected_decoding = '{"a_datetime": "%s"}' % datetime_string
399            self.CompareEncoded(expected_decoding, decoded_message)
400
401    def testDecodeRepeatedDateTime(self):
402        message = protojson.decode_message(
403            MyMessage,
404            '{"a_repeated_datetime": ["2012-09-30T15:31:50.262", '
405            '"2010-01-21T09:52:00", "2000-01-01T01:00:59.999999"]}')
406        expected_message = MyMessage(
407            a_repeated_datetime=[
408                datetime.datetime(2012, 9, 30, 15, 31, 50, 262000),
409                datetime.datetime(2010, 1, 21, 9, 52),
410                datetime.datetime(2000, 1, 1, 1, 0, 59, 999999)])
411
412        self.assertEquals(expected_message, message)
413
414    def testDecodeCustom(self):
415        message = protojson.decode_message(MyMessage, '{"a_custom": 1}')
416        self.assertEquals(MyMessage(a_custom=1), message)
417
418    def testDecodeInvalidCustom(self):
419        self.assertRaises(messages.ValidationError, protojson.decode_message,
420                          MyMessage, '{"a_custom": "invalid"}')
421
422    def testEncodeCustom(self):
423        decoded_message = protojson.encode_message(MyMessage(a_custom=1))
424        self.CompareEncoded('{"a_custom": 1}', decoded_message)
425
426    def testDecodeRepeatedCustom(self):
427        message = protojson.decode_message(
428            MyMessage, '{"a_repeated_custom": [1, 2, 3]}')
429        self.assertEquals(MyMessage(a_repeated_custom=[1, 2, 3]), message)
430
431    def testDecodeRepeatedEmpty(self):
432        message = protojson.decode_message(
433            MyMessage, '{"a_repeated": []}')
434        self.assertEquals(MyMessage(a_repeated=[]), message)
435
436    def testDecodeNone(self):
437        message = protojson.decode_message(
438            MyMessage, '{"an_integer": []}')
439        self.assertEquals(MyMessage(an_integer=None), message)
440
441    def testDecodeBadBase64BytesField(self):
442        """Test decoding improperly encoded base64 bytes value."""
443        self.assertRaisesWithRegexpMatch(
444            messages.DecodeError,
445            'Base64 decoding error',
446            protojson.decode_message,
447            test_util.OptionalMessage,
448            '{"bytes_value": "abcdefghijklmnopq"}')
449
450
451class CustomProtoJson(protojson.ProtoJson):
452
453    def encode_field(self, field, value):
454        return '{encoded}' + value
455
456    def decode_field(self, field, value):
457        return '{decoded}' + value
458
459
460class CustomProtoJsonTest(test_util.TestCase):
461    """Tests for serialization overriding functionality."""
462
463    def setUp(self):
464        self.protojson = CustomProtoJson()
465
466    def testEncode(self):
467        self.assertEqual(
468            '{"a_string": "{encoded}xyz"}',
469            self.protojson.encode_message(MyMessage(a_string='xyz')))
470
471    def testDecode(self):
472        self.assertEqual(
473            MyMessage(a_string='{decoded}xyz'),
474            self.protojson.decode_message(MyMessage, '{"a_string": "xyz"}'))
475
476    def testDecodeEmptyMessage(self):
477        self.assertEqual(
478            MyMessage(a_string='{decoded}'),
479            self.protojson.decode_message(MyMessage, '{"a_string": ""}'))
480
481    def testDefault(self):
482        self.assertTrue(protojson.ProtoJson.get_default(),
483                        protojson.ProtoJson.get_default())
484
485        instance = CustomProtoJson()
486        protojson.ProtoJson.set_default(instance)
487        self.assertTrue(instance is protojson.ProtoJson.get_default())
488
489
490if __name__ == '__main__':
491    unittest.main()
492