1# Copyright 2018 The gRPC Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Tests of grpc_status."""
15
16# NOTE(lidiz) This module only exists in Bazel BUILD file, for more details
17# please refer to comments in the "bazel_namespace_package_hack" module.
18try:
19    from tests import bazel_namespace_package_hack
20    bazel_namespace_package_hack.sys_path_to_site_dir_hack()
21except ImportError:
22    pass
23
24import unittest
25
26import logging
27import traceback
28
29import grpc
30from grpc_status import rpc_status
31
32from tests.unit import test_common
33
34from google.protobuf import any_pb2
35from google.rpc import code_pb2, status_pb2, error_details_pb2
36
37_STATUS_OK = '/test/StatusOK'
38_STATUS_NOT_OK = '/test/StatusNotOk'
39_ERROR_DETAILS = '/test/ErrorDetails'
40_INCONSISTENT = '/test/Inconsistent'
41_INVALID_CODE = '/test/InvalidCode'
42
43_REQUEST = b'\x00\x00\x00'
44_RESPONSE = b'\x01\x01\x01'
45
46_GRPC_DETAILS_METADATA_KEY = 'grpc-status-details-bin'
47
48_STATUS_DETAILS = 'This is an error detail'
49_STATUS_DETAILS_ANOTHER = 'This is another error detail'
50
51
52def _ok_unary_unary(request, servicer_context):
53    return _RESPONSE
54
55
56def _not_ok_unary_unary(request, servicer_context):
57    servicer_context.abort(grpc.StatusCode.INTERNAL, _STATUS_DETAILS)
58
59
60def _error_details_unary_unary(request, servicer_context):
61    details = any_pb2.Any()
62    details.Pack(
63        error_details_pb2.DebugInfo(stack_entries=traceback.format_stack(),
64                                    detail='Intentionally invoked'))
65    rich_status = status_pb2.Status(
66        code=code_pb2.INTERNAL,
67        message=_STATUS_DETAILS,
68        details=[details],
69    )
70    servicer_context.abort_with_status(rpc_status.to_status(rich_status))
71
72
73def _inconsistent_unary_unary(request, servicer_context):
74    rich_status = status_pb2.Status(
75        code=code_pb2.INTERNAL,
76        message=_STATUS_DETAILS,
77    )
78    servicer_context.set_code(grpc.StatusCode.NOT_FOUND)
79    servicer_context.set_details(_STATUS_DETAILS_ANOTHER)
80    # User put inconsistent status information in trailing metadata
81    servicer_context.set_trailing_metadata(
82        ((_GRPC_DETAILS_METADATA_KEY, rich_status.SerializeToString()),))
83
84
85def _invalid_code_unary_unary(request, servicer_context):
86    rich_status = status_pb2.Status(
87        code=42,
88        message='Invalid code',
89    )
90    servicer_context.abort_with_status(rpc_status.to_status(rich_status))
91
92
93class _GenericHandler(grpc.GenericRpcHandler):
94
95    def service(self, handler_call_details):
96        if handler_call_details.method == _STATUS_OK:
97            return grpc.unary_unary_rpc_method_handler(_ok_unary_unary)
98        elif handler_call_details.method == _STATUS_NOT_OK:
99            return grpc.unary_unary_rpc_method_handler(_not_ok_unary_unary)
100        elif handler_call_details.method == _ERROR_DETAILS:
101            return grpc.unary_unary_rpc_method_handler(
102                _error_details_unary_unary)
103        elif handler_call_details.method == _INCONSISTENT:
104            return grpc.unary_unary_rpc_method_handler(
105                _inconsistent_unary_unary)
106        elif handler_call_details.method == _INVALID_CODE:
107            return grpc.unary_unary_rpc_method_handler(
108                _invalid_code_unary_unary)
109        else:
110            return None
111
112
113class StatusTest(unittest.TestCase):
114
115    def setUp(self):
116        self._server = test_common.test_server()
117        self._server.add_generic_rpc_handlers((_GenericHandler(),))
118        port = self._server.add_insecure_port('[::]:0')
119        self._server.start()
120
121        self._channel = grpc.insecure_channel('localhost:%d' % port)
122
123    def tearDown(self):
124        self._server.stop(None)
125        self._channel.close()
126
127    def test_status_ok(self):
128        _, call = self._channel.unary_unary(_STATUS_OK).with_call(_REQUEST)
129
130        # Succeed RPC doesn't have status
131        status = rpc_status.from_call(call)
132        self.assertIs(status, None)
133
134    def test_status_not_ok(self):
135        with self.assertRaises(grpc.RpcError) as exception_context:
136            self._channel.unary_unary(_STATUS_NOT_OK).with_call(_REQUEST)
137        rpc_error = exception_context.exception
138
139        self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
140        # Failed RPC doesn't automatically generate status
141        status = rpc_status.from_call(rpc_error)
142        self.assertIs(status, None)
143
144    def test_error_details(self):
145        with self.assertRaises(grpc.RpcError) as exception_context:
146            self._channel.unary_unary(_ERROR_DETAILS).with_call(_REQUEST)
147        rpc_error = exception_context.exception
148
149        status = rpc_status.from_call(rpc_error)
150        self.assertEqual(rpc_error.code(), grpc.StatusCode.INTERNAL)
151        self.assertEqual(status.code, code_pb2.Code.Value('INTERNAL'))
152
153        # Check if the underlying proto message is intact
154        self.assertEqual(
155            status.details[0].Is(error_details_pb2.DebugInfo.DESCRIPTOR), True)
156        info = error_details_pb2.DebugInfo()
157        status.details[0].Unpack(info)
158        self.assertIn('_error_details_unary_unary', info.stack_entries[-1])
159
160    def test_code_message_validation(self):
161        with self.assertRaises(grpc.RpcError) as exception_context:
162            self._channel.unary_unary(_INCONSISTENT).with_call(_REQUEST)
163        rpc_error = exception_context.exception
164        self.assertEqual(rpc_error.code(), grpc.StatusCode.NOT_FOUND)
165
166        # Code/Message validation failed
167        self.assertRaises(ValueError, rpc_status.from_call, rpc_error)
168
169    def test_invalid_code(self):
170        with self.assertRaises(grpc.RpcError) as exception_context:
171            self._channel.unary_unary(_INVALID_CODE).with_call(_REQUEST)
172        rpc_error = exception_context.exception
173        self.assertEqual(rpc_error.code(), grpc.StatusCode.UNKNOWN)
174        # Invalid status code exception raised during coversion
175        self.assertIn('Invalid status code', rpc_error.details())
176
177
178if __name__ == '__main__':
179    logging.basicConfig()
180    unittest.main(verbosity=2)
181