| OLD | NEW | 
|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python | 
| 2 # Copyright 2015 The Swarming Authors. All rights reserved. | 2 # Copyright 2015 The Swarming Authors. All rights reserved. | 
| 3 # Use of this source code is governed by the Apache v2.0 license that can be | 3 # Use of this source code is governed by the Apache v2.0 license that can be | 
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. | 
| 5 | 5 | 
| 6 import base64 | 6 import base64 | 
| 7 import logging | 7 import logging | 
| 8 | 8 | 
| 9 from test_env import future | 9 from test_env import future | 
| 10 import test_env | 10 import test_env | 
| 11 test_env.setup_test_env() | 11 test_env.setup_test_env() | 
| 12 | 12 | 
| 13 from google.appengine.ext import ndb | 13 from google.appengine.ext import ndb | 
| 14 | 14 | 
| 15 from test_support import test_case | 15 from test_support import test_case | 
| 16 import mock | 16 import mock | 
| 17 | 17 | 
| 18 from components import config | 18 from components import config | 
| 19 from components import net | 19 from components import net | 
| 20 from components.config import validation_context | 20 from components.config import validation_context | 
| 21 | 21 | 
| 22 from proto import project_config_pb2 | 22 from proto import project_config_pb2 | 
| 23 from proto import service_config_pb2 | 23 from proto import service_config_pb2 | 
|  | 24 import services | 
| 24 import storage | 25 import storage | 
| 25 import validation | 26 import validation | 
| 26 | 27 | 
| 27 | 28 | 
| 28 class ValidationTestCase(test_case.TestCase): | 29 class ValidationTestCase(test_case.TestCase): | 
| 29   def test_validate_validation_cfg(self): | 30   def setUp(self): | 
| 30     cfg = ''' | 31     super(ValidationTestCase, self).setUp() | 
| 31       rules { | 32     self.services = [] | 
| 32         config_set: "projects/foo" | 33     self.mock(services, 'get_services_async', lambda: future(self.services)) | 
| 33         path: "bar.cfg" |  | 
| 34         url: "https://foo.com/validate_config" |  | 
| 35       } |  | 
| 36       rules { |  | 
| 37         config_set: "regex:projects\/foo" |  | 
| 38         path: "regex:.+" |  | 
| 39         url: "https://foo.com/validate_config" |  | 
| 40       } |  | 
| 41       rules { |  | 
| 42         config_set: "bad config set name" |  | 
| 43         path: "regex:))bad regex" |  | 
| 44         # no url |  | 
| 45       } |  | 
| 46       rules { |  | 
| 47         config_set: "regex:)(" |  | 
| 48         path: "/bar.cfg" |  | 
| 49         url: "http://not-https.com" |  | 
| 50       } |  | 
| 51       rules { |  | 
| 52         config_set: "projects/foo" |  | 
| 53         path: "a/../b.cfg" |  | 
| 54         url: "https://foo.com/validate_config" |  | 
| 55       } |  | 
| 56       rules { |  | 
| 57         config_set: "projects/foo" |  | 
| 58         path: "a/./b.cfg" |  | 
| 59         url: "/no/hostname" |  | 
| 60       } |  | 
| 61     ''' |  | 
| 62     result = validation.validate_config( |  | 
| 63         config.self_config_set(), 'validation.cfg', cfg) |  | 
| 64 |  | 
| 65     self.assertEqual( |  | 
| 66         [m.text for m in result.messages], |  | 
| 67         [ |  | 
| 68           'Rule #3: config_set: invalid config set: bad config set name', |  | 
| 69           ('Rule #3: path: invalid regular expression "))bad regex": ' |  | 
| 70            'unbalanced parenthesis'), |  | 
| 71           'Rule #3: url: not specified', |  | 
| 72           ('Rule #4: config_set: invalid regular expression ")(": ' |  | 
| 73             'unbalanced parenthesis'), |  | 
| 74           'Rule #4: path: must not be absolute: /bar.cfg', |  | 
| 75           'Rule #4: url: scheme must be "https"', |  | 
| 76           'Rule #5: path: must not contain ".." or "." components: a/../b.cfg', |  | 
| 77           'Rule #6: path: must not contain ".." or "." components: a/./b.cfg', |  | 
| 78           'Rule #6: url: hostname not specified', |  | 
| 79           'Rule #6: url: scheme must be "https"', |  | 
| 80         ] |  | 
| 81     ) |  | 
| 82 | 34 | 
| 83   def test_validate_project_registry(self): | 35   def test_validate_project_registry(self): | 
| 84     cfg = ''' | 36     cfg = ''' | 
| 85       projects { | 37       projects { | 
| 86         id: "a" | 38         id: "a" | 
| 87         config_location { | 39         config_location { | 
| 88           storage_type: GITILES | 40           storage_type: GITILES | 
| 89           url: "https://a.googlesource.com/project" | 41           url: "https://a.googlesource.com/project" | 
| 90         } | 42         } | 
| 91       } | 43       } | 
| (...skipping 20 matching lines...) Expand all  Loading... | 
| 112     self.assertEqual( | 64     self.assertEqual( | 
| 113         [m.text for m in result.messages], | 65         [m.text for m in result.messages], | 
| 114         [ | 66         [ | 
| 115           'Project b: config_location: storage_type is not set', | 67           'Project b: config_location: storage_type is not set', | 
| 116           'Project a: id is not unique', | 68           'Project a: id is not unique', | 
| 117           ('Project a: config_location: Invalid Gitiles repo url: ' | 69           ('Project a: config_location: Invalid Gitiles repo url: ' | 
| 118            'https://no-project.googlesource.com'), | 70            'https://no-project.googlesource.com'), | 
| 119           'Project #4: id is not specified', | 71           'Project #4: id is not specified', | 
| 120           ('Project #4: config_location: Invalid Gitiles repo url: ' | 72           ('Project #4: config_location: Invalid Gitiles repo url: ' | 
| 121            'https://no-project.googlesource.com/bad_plus/+'), | 73            'https://no-project.googlesource.com/bad_plus/+'), | 
| 122           'Project list is not sorted by id. First offending id: a', | 74           'Projects are not sorted by id. First offending id: a', | 
| 123         ] | 75         ] | 
| 124     ) | 76     ) | 
| 125 | 77 | 
|  | 78   def test_validate_services_registry(self): | 
|  | 79     cfg = ''' | 
|  | 80       services { | 
|  | 81         id: "a" | 
|  | 82       } | 
|  | 83       services { | 
|  | 84         owners: "not an email" | 
|  | 85         config_location { | 
|  | 86           storage_type: GITILES | 
|  | 87           url: "../some" | 
|  | 88         } | 
|  | 89         metadata_url: "not an url" | 
|  | 90       } | 
|  | 91       services { | 
|  | 92         id: "b" | 
|  | 93         config_location { | 
|  | 94           storage_type: GITILES | 
|  | 95           url: "https://gitiles.host.com/project" | 
|  | 96         } | 
|  | 97       } | 
|  | 98       services { | 
|  | 99         id: "a-unsorted" | 
|  | 100       } | 
|  | 101     ''' | 
|  | 102     result = validation.validate_config( | 
|  | 103         config.self_config_set(), 'services.cfg', cfg) | 
|  | 104 | 
|  | 105     self.assertEqual( | 
|  | 106         [m.text for m in result.messages], | 
|  | 107         [ | 
|  | 108           'Service #2: id is not specified', | 
|  | 109           ('Service #2: config_location: ' | 
|  | 110            'storage_type must not be set if relative url is used'), | 
|  | 111           'Service #2: invalid email: "not an email"', | 
|  | 112           'Service #2: metadata_url: hostname not specified', | 
|  | 113           'Service #2: metadata_url: scheme must be "https"', | 
|  | 114           'Services are not sorted by id. First offending id: a-unsorted', | 
|  | 115         ] | 
|  | 116     ) | 
|  | 117 | 
|  | 118 | 
|  | 119   def test_validate_service_dynamic_metadata_blob(self): | 
|  | 120     def expect_errors(blob, expected_messages): | 
|  | 121       ctx = config.validation.Context() | 
|  | 122       validation.validate_service_dynamic_metadata_blob(blob, ctx) | 
|  | 123       self.assertEqual( | 
|  | 124           [m.text for m in ctx.result().messages], expected_messages) | 
|  | 125 | 
|  | 126     expect_errors([], ['Service dynamic metadata must be an object']) | 
|  | 127     expect_errors({}, []) | 
|  | 128     expect_errors({'validation': 'bad'}, ['validation: must be an object']) | 
|  | 129     expect_errors( | 
|  | 130         { | 
|  | 131           'validation': { | 
|  | 132             'patterns': 'bad', | 
|  | 133           } | 
|  | 134         }, | 
|  | 135         [ | 
|  | 136           'validation: url: not specified', | 
|  | 137           'validation: patterns must be a list', | 
|  | 138         ]) | 
|  | 139     expect_errors( | 
|  | 140       { | 
|  | 141         'validation': { | 
|  | 142           'url': 'bad url', | 
|  | 143           'patterns': [ | 
|  | 144             'bad', | 
|  | 145             { | 
|  | 146             }, | 
|  | 147             { | 
|  | 148               'config_set': 'a:b', | 
|  | 149               'path': '/foo', | 
|  | 150             }, | 
|  | 151             { | 
|  | 152               'config_set': 'regex:)(', | 
|  | 153               'path': '../b', | 
|  | 154             }, | 
|  | 155             { | 
|  | 156               'config_set': 'projects/foo', | 
|  | 157               'path': 'bar.cfg', | 
|  | 158             }, | 
|  | 159           ] | 
|  | 160         } | 
|  | 161       }, | 
|  | 162       [ | 
|  | 163         'validation: url: hostname not specified', | 
|  | 164         'validation: url: scheme must be "https"', | 
|  | 165         'validation: pattern #1: must be an object', | 
|  | 166         'validation: pattern #2: config_set: Pattern must be a string', | 
|  | 167         'validation: pattern #2: path: Pattern must be a string', | 
|  | 168         'validation: pattern #3: config_set: Invalid pattern kind: a', | 
|  | 169         'validation: pattern #3: path: must not be absolute: /foo', | 
|  | 170         'validation: pattern #4: config_set: unbalanced parenthesis', | 
|  | 171         ('validation: pattern #4: path: ' | 
|  | 172          'must not contain ".." or "." components: ../b'), | 
|  | 173       ] | 
|  | 174     ) | 
|  | 175 | 
| 126   def test_validate_schemas(self): | 176   def test_validate_schemas(self): | 
| 127     cfg = ''' | 177     cfg = ''' | 
| 128       schemas { | 178       schemas { | 
| 129         name: "services/config:foo" | 179         name: "services/config:foo" | 
| 130         url: "https://foo" | 180         url: "https://foo" | 
| 131       } | 181       } | 
| 132       schemas { | 182       schemas { | 
| 133         name: "projects:foo" | 183         name: "projects:foo" | 
| 134         url: "https://foo" | 184         url: "https://foo" | 
| 135       } | 185       } | 
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 200     self.assertEqual( | 250     self.assertEqual( | 
| 201         [m.text for m in result.messages], | 251         [m.text for m in result.messages], | 
| 202         [ | 252         [ | 
| 203           'Ref #2: name is not specified', | 253           'Ref #2: name is not specified', | 
| 204           'Ref #3: duplicate ref: refs/heads/master', | 254           'Ref #3: duplicate ref: refs/heads/master', | 
| 205           'Ref #4: name does not start with "refs/": does_not_start_with_ref', | 255           'Ref #4: name does not start with "refs/": does_not_start_with_ref', | 
| 206           'Ref #4: must not contain ".." or "." components: ../bad/path' | 256           'Ref #4: must not contain ".." or "." components: ../bad/path' | 
| 207         ], | 257         ], | 
| 208     ) | 258     ) | 
| 209 | 259 | 
| 210   def test_endpoint_validate_async(self): | 260   def test_validation_by_service_async(self): | 
| 211     cfg = '# a config' | 261     cfg = '# a config' | 
| 212     cfg_b64 = base64.b64encode(cfg) | 262     cfg_b64 = base64.b64encode(cfg) | 
| 213 | 263 | 
| 214     self.mock(storage, 'get_self_config_async', mock.Mock()) | 264     self.services = [ | 
| 215     storage.get_self_config_async.return_value = future( | 265       service_config_pb2.Service(id='a'), | 
| 216         service_config_pb2.ValidationCfg( | 266       service_config_pb2.Service(id='b'), | 
| 217             rules=[ | 267       service_config_pb2.Service(id='c'), | 
| 218               service_config_pb2.ValidationCfg.Rule( | 268     ] | 
| 219                 config_set='services/foo', | 269 | 
| 220                 path='bar.cfg', | 270     @ndb.tasklet | 
|  | 271     def get_metadata_async(service_id): | 
|  | 272       if service_id == 'a': | 
|  | 273         raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( | 
|  | 274             validation=service_config_pb2.Validator( | 
|  | 275                 patterns=[service_config_pb2.ConfigPattern( | 
|  | 276                     config_set='services/foo', | 
|  | 277                     path='bar.cfg', | 
|  | 278                 )], | 
| 221                 url='https://bar.verifier', | 279                 url='https://bar.verifier', | 
| 222               ), | 280             ) | 
| 223               service_config_pb2.ValidationCfg.Rule( | 281         )) | 
| 224                 config_set='regex:projects/[^/]+', | 282       if service_id == 'b': | 
| 225                 path='regex:.+.\cfg', | 283         raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( | 
|  | 284             validation=service_config_pb2.Validator( | 
|  | 285                 patterns=[service_config_pb2.ConfigPattern( | 
|  | 286                     config_set=r'regex:projects/[^/]+', | 
|  | 287                     path=r'regex:.+\.cfg', | 
|  | 288                 )], | 
| 226                 url='https://bar2.verifier', | 289                 url='https://bar2.verifier', | 
| 227               ), | 290               ))) | 
| 228               service_config_pb2.ValidationCfg.Rule( | 291       if service_id == 'c': | 
| 229                 config_set='regex:.+', | 292         raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( | 
| 230                 path='regex:.+', | 293             validation=service_config_pb2.Validator( | 
|  | 294                 patterns=[service_config_pb2.ConfigPattern( | 
|  | 295                     config_set=r'regex:.+', | 
|  | 296                     path=r'regex:.+', | 
|  | 297                 )], | 
| 231                 url='https://ultimate.verifier', | 298                 url='https://ultimate.verifier', | 
| 232               ), | 299               ))) | 
| 233             ] | 300       return None | 
| 234           )) | 301     self.mock(services, 'get_metadata_async', mock.Mock()) | 
|  | 302     services.get_metadata_async.side_effect = get_metadata_async | 
| 235 | 303 | 
| 236     @ndb.tasklet | 304     @ndb.tasklet | 
| 237     def json_request_async(url, **kwargs): | 305     def json_request_async(url, **kwargs): | 
| 238       raise ndb.Return({ | 306       raise ndb.Return({ | 
| 239         'messages': [{ | 307         'messages': [{ | 
| 240           'text': 'OK from %s' % url, | 308           'text': 'OK from %s' % url, | 
| 241           # default severity | 309           # default severity | 
| 242         }], | 310         }], | 
| 243       }) | 311       }) | 
| 244 | 312 | 
| (...skipping 12 matching lines...) Expand all  Loading... | 
| 257               text='OK from https://ultimate.verifier', severity=logging.INFO) | 325               text='OK from https://ultimate.verifier', severity=logging.INFO) | 
| 258         ]) | 326         ]) | 
| 259     net.json_request_async.assert_any_call( | 327     net.json_request_async.assert_any_call( | 
| 260       'https://bar.verifier', | 328       'https://bar.verifier', | 
| 261       method='POST', | 329       method='POST', | 
| 262       payload={ | 330       payload={ | 
| 263         'config_set': 'services/foo', | 331         'config_set': 'services/foo', | 
| 264         'path': 'bar.cfg', | 332         'path': 'bar.cfg', | 
| 265         'content': cfg_b64, | 333         'content': cfg_b64, | 
| 266       }, | 334       }, | 
| 267       scope='https://www.googleapis.com/auth/userinfo.email', | 335       scope=net.EMAIL_SCOPE, | 
| 268     ) | 336     ) | 
| 269     net.json_request_async.assert_any_call( | 337     net.json_request_async.assert_any_call( | 
| 270       'https://ultimate.verifier', | 338       'https://ultimate.verifier', | 
| 271       method='POST', | 339       method='POST', | 
| 272       payload={ | 340       payload={ | 
| 273         'config_set': 'services/foo', | 341         'config_set': 'services/foo', | 
| 274         'path': 'bar.cfg', | 342         'path': 'bar.cfg', | 
| 275         'content': cfg_b64, | 343         'content': cfg_b64, | 
| 276       }, | 344       }, | 
| 277       scope='https://www.googleapis.com/auth/userinfo.email', | 345       scope=net.EMAIL_SCOPE, | 
| 278     ) | 346     ) | 
| 279 | 347 | 
| 280     ############################################################################ | 348     ############################################################################ | 
| 281 | 349 | 
| 282     result = validation.validate_config('projects/foo', 'bar.cfg', cfg) | 350     result = validation.validate_config('projects/foo', 'bar.cfg', cfg) | 
| 283     self.assertEqual( | 351     self.assertEqual( | 
| 284         result.messages, | 352         result.messages, | 
| 285         [ | 353         [ | 
| 286           validation_context.Message( | 354           validation_context.Message( | 
| 287               text='OK from https://bar2.verifier', severity=logging.INFO), | 355               text='OK from https://bar2.verifier', severity=logging.INFO), | 
| 288           validation_context.Message( | 356           validation_context.Message( | 
| 289               text='OK from https://ultimate.verifier', severity=logging.INFO) | 357               text='OK from https://ultimate.verifier', severity=logging.INFO) | 
| 290         ]) | 358         ]) | 
| 291     net.json_request_async.assert_any_call( | 359     net.json_request_async.assert_any_call( | 
| 292       'https://bar2.verifier', | 360       'https://bar2.verifier', | 
| 293       method='POST', | 361       method='POST', | 
| 294       payload={ | 362       payload={ | 
| 295         'config_set': 'projects/foo', | 363         'config_set': 'projects/foo', | 
| 296         'path': 'bar.cfg', | 364         'path': 'bar.cfg', | 
| 297         'content': cfg_b64, | 365         'content': cfg_b64, | 
| 298       }, | 366       }, | 
| 299       scope='https://www.googleapis.com/auth/userinfo.email', | 367       scope=net.EMAIL_SCOPE, | 
| 300     ) | 368     ) | 
| 301     net.json_request_async.assert_any_call( | 369     net.json_request_async.assert_any_call( | 
| 302       'https://ultimate.verifier', | 370       'https://ultimate.verifier', | 
| 303       method='POST', | 371       method='POST', | 
| 304       payload={ | 372       payload={ | 
| 305         'config_set': 'projects/foo', | 373         'config_set': 'projects/foo', | 
| 306         'path': 'bar.cfg', | 374         'path': 'bar.cfg', | 
| 307         'content': cfg_b64, | 375         'content': cfg_b64, | 
| 308       }, | 376       }, | 
| 309       scope='https://www.googleapis.com/auth/userinfo.email', | 377       scope=net.EMAIL_SCOPE, | 
| 310     ) | 378     ) | 
| 311 | 379 | 
| 312     ############################################################################ | 380     ############################################################################ | 
| 313     # Error found | 381     # Error found | 
| 314 | 382 | 
| 315     net.json_request_async.side_effect = None | 383     net.json_request_async.side_effect = None | 
| 316     net.json_request_async.return_value = ndb.Future() | 384     net.json_request_async.return_value = ndb.Future() | 
| 317     net.json_request_async.return_value.set_result({ | 385     net.json_request_async.return_value.set_result({ | 
| 318       'messages': [{ | 386       'messages': [{ | 
| 319         'text': 'error', | 387         'text': 'error', | 
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 365                   'config_set: projects/baz/refs/x\n' | 433                   'config_set: projects/baz/refs/x\n' | 
| 366                   'path: qux.cfg\n' | 434                   'path: qux.cfg\n' | 
| 367                   'response: %r' % res), | 435                   'response: %r' % res), | 
| 368               ), | 436               ), | 
| 369         ], | 437         ], | 
| 370     ) | 438     ) | 
| 371 | 439 | 
| 372 | 440 | 
| 373 if __name__ == '__main__': | 441 if __name__ == '__main__': | 
| 374   test_env.main() | 442   test_env.main() | 
| OLD | NEW | 
|---|