| 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 access: "a@a.com" |
| 83 access: "user:a@a.com" |
| 84 access: "group:abc" |
| 85 } |
| 86 services { |
| 87 owners: "not an email" |
| 88 config_location { |
| 89 storage_type: GITILES |
| 90 url: "../some" |
| 91 } |
| 92 metadata_url: "not an url" |
| 93 access: "**&" |
| 94 access: "group:**&" |
| 95 access: "a:b" |
| 96 } |
| 97 services { |
| 98 id: "b" |
| 99 config_location { |
| 100 storage_type: GITILES |
| 101 url: "https://gitiles.host.com/project" |
| 102 } |
| 103 } |
| 104 services { |
| 105 id: "a-unsorted" |
| 106 } |
| 107 ''' |
| 108 result = validation.validate_config( |
| 109 config.self_config_set(), 'services.cfg', cfg) |
| 110 |
| 111 self.assertEqual( |
| 112 [m.text for m in result.messages], |
| 113 [ |
| 114 'Service #2: id is not specified', |
| 115 ('Service #2: config_location: ' |
| 116 'storage_type must not be set if relative url is used'), |
| 117 'Service #2: invalid email: "not an email"', |
| 118 'Service #2: metadata_url: hostname not specified', |
| 119 'Service #2: metadata_url: scheme must be "https"', |
| 120 'Service #2: access #1: invalid email: "**&"', |
| 121 'Service #2: access #2: invalid group: **&', |
| 122 'Service #2: access #3: Identity has invalid format: b', |
| 123 'Services are not sorted by id. First offending id: a-unsorted', |
| 124 ] |
| 125 ) |
| 126 |
| 127 |
| 128 def test_validate_service_dynamic_metadata_blob(self): |
| 129 def expect_errors(blob, expected_messages): |
| 130 ctx = config.validation.Context() |
| 131 validation.validate_service_dynamic_metadata_blob(blob, ctx) |
| 132 self.assertEqual( |
| 133 [m.text for m in ctx.result().messages], expected_messages) |
| 134 |
| 135 expect_errors([], ['Service dynamic metadata must be an object']) |
| 136 expect_errors({}, []) |
| 137 expect_errors({'validation': 'bad'}, ['validation: must be an object']) |
| 138 expect_errors( |
| 139 { |
| 140 'validation': { |
| 141 'patterns': 'bad', |
| 142 } |
| 143 }, |
| 144 [ |
| 145 'validation: url: not specified', |
| 146 'validation: patterns must be a list', |
| 147 ]) |
| 148 expect_errors( |
| 149 { |
| 150 'validation': { |
| 151 'url': 'bad url', |
| 152 'patterns': [ |
| 153 'bad', |
| 154 { |
| 155 }, |
| 156 { |
| 157 'config_set': 'a:b', |
| 158 'path': '/foo', |
| 159 }, |
| 160 { |
| 161 'config_set': 'regex:)(', |
| 162 'path': '../b', |
| 163 }, |
| 164 { |
| 165 'config_set': 'projects/foo', |
| 166 'path': 'bar.cfg', |
| 167 }, |
| 168 ] |
| 169 } |
| 170 }, |
| 171 [ |
| 172 'validation: url: hostname not specified', |
| 173 'validation: url: scheme must be "https"', |
| 174 'validation: pattern #1: must be an object', |
| 175 'validation: pattern #2: config_set: Pattern must be a string', |
| 176 'validation: pattern #2: path: Pattern must be a string', |
| 177 'validation: pattern #3: config_set: Invalid pattern kind: a', |
| 178 'validation: pattern #3: path: must not be absolute: /foo', |
| 179 'validation: pattern #4: config_set: unbalanced parenthesis', |
| 180 ('validation: pattern #4: path: ' |
| 181 'must not contain ".." or "." components: ../b'), |
| 182 ] |
| 183 ) |
| 184 |
| 126 def test_validate_schemas(self): | 185 def test_validate_schemas(self): |
| 127 cfg = ''' | 186 cfg = ''' |
| 128 schemas { | 187 schemas { |
| 129 name: "services/config:foo" | 188 name: "services/config:foo" |
| 130 url: "https://foo" | 189 url: "https://foo" |
| 131 } | 190 } |
| 132 schemas { | 191 schemas { |
| 133 name: "projects:foo" | 192 name: "projects:foo" |
| 134 url: "https://foo" | 193 url: "https://foo" |
| 135 } | 194 } |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 200 self.assertEqual( | 259 self.assertEqual( |
| 201 [m.text for m in result.messages], | 260 [m.text for m in result.messages], |
| 202 [ | 261 [ |
| 203 'Ref #2: name is not specified', | 262 'Ref #2: name is not specified', |
| 204 'Ref #3: duplicate ref: refs/heads/master', | 263 'Ref #3: duplicate ref: refs/heads/master', |
| 205 'Ref #4: name does not start with "refs/": does_not_start_with_ref', | 264 'Ref #4: name does not start with "refs/": does_not_start_with_ref', |
| 206 'Ref #4: must not contain ".." or "." components: ../bad/path' | 265 'Ref #4: must not contain ".." or "." components: ../bad/path' |
| 207 ], | 266 ], |
| 208 ) | 267 ) |
| 209 | 268 |
| 210 def test_endpoint_validate_async(self): | 269 def test_validation_by_service_async(self): |
| 211 cfg = '# a config' | 270 cfg = '# a config' |
| 212 cfg_b64 = base64.b64encode(cfg) | 271 cfg_b64 = base64.b64encode(cfg) |
| 213 | 272 |
| 214 self.mock(storage, 'get_self_config_async', mock.Mock()) | 273 self.services = [ |
| 215 storage.get_self_config_async.return_value = future( | 274 service_config_pb2.Service(id='a'), |
| 216 service_config_pb2.ValidationCfg( | 275 service_config_pb2.Service(id='b'), |
| 217 rules=[ | 276 service_config_pb2.Service(id='c'), |
| 218 service_config_pb2.ValidationCfg.Rule( | 277 ] |
| 219 config_set='services/foo', | 278 |
| 220 path='bar.cfg', | 279 @ndb.tasklet |
| 280 def get_metadata_async(service_id): |
| 281 if service_id == 'a': |
| 282 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( |
| 283 validation=service_config_pb2.Validator( |
| 284 patterns=[service_config_pb2.ConfigPattern( |
| 285 config_set='services/foo', |
| 286 path='bar.cfg', |
| 287 )], |
| 221 url='https://bar.verifier', | 288 url='https://bar.verifier', |
| 222 ), | 289 ) |
| 223 service_config_pb2.ValidationCfg.Rule( | 290 )) |
| 224 config_set='regex:projects/[^/]+', | 291 if service_id == 'b': |
| 225 path='regex:.+.\cfg', | 292 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( |
| 293 validation=service_config_pb2.Validator( |
| 294 patterns=[service_config_pb2.ConfigPattern( |
| 295 config_set=r'regex:projects/[^/]+', |
| 296 path=r'regex:.+\.cfg', |
| 297 )], |
| 226 url='https://bar2.verifier', | 298 url='https://bar2.verifier', |
| 227 ), | 299 ))) |
| 228 service_config_pb2.ValidationCfg.Rule( | 300 if service_id == 'c': |
| 229 config_set='regex:.+', | 301 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( |
| 230 path='regex:.+', | 302 validation=service_config_pb2.Validator( |
| 303 patterns=[service_config_pb2.ConfigPattern( |
| 304 config_set=r'regex:.+', |
| 305 path=r'regex:.+', |
| 306 )], |
| 231 url='https://ultimate.verifier', | 307 url='https://ultimate.verifier', |
| 232 ), | 308 ))) |
| 233 ] | 309 return None |
| 234 )) | 310 self.mock(services, 'get_metadata_async', mock.Mock()) |
| 311 services.get_metadata_async.side_effect = get_metadata_async |
| 235 | 312 |
| 236 @ndb.tasklet | 313 @ndb.tasklet |
| 237 def json_request_async(url, **kwargs): | 314 def json_request_async(url, **kwargs): |
| 238 raise ndb.Return({ | 315 raise ndb.Return({ |
| 239 'messages': [{ | 316 'messages': [{ |
| 240 'text': 'OK from %s' % url, | 317 'text': 'OK from %s' % url, |
| 241 # default severity | 318 # default severity |
| 242 }], | 319 }], |
| 243 }) | 320 }) |
| 244 | 321 |
| (...skipping 12 matching lines...) Expand all Loading... |
| 257 text='OK from https://ultimate.verifier', severity=logging.INFO) | 334 text='OK from https://ultimate.verifier', severity=logging.INFO) |
| 258 ]) | 335 ]) |
| 259 net.json_request_async.assert_any_call( | 336 net.json_request_async.assert_any_call( |
| 260 'https://bar.verifier', | 337 'https://bar.verifier', |
| 261 method='POST', | 338 method='POST', |
| 262 payload={ | 339 payload={ |
| 263 'config_set': 'services/foo', | 340 'config_set': 'services/foo', |
| 264 'path': 'bar.cfg', | 341 'path': 'bar.cfg', |
| 265 'content': cfg_b64, | 342 'content': cfg_b64, |
| 266 }, | 343 }, |
| 267 scope='https://www.googleapis.com/auth/userinfo.email', | 344 scope=net.EMAIL_SCOPE, |
| 268 ) | 345 ) |
| 269 net.json_request_async.assert_any_call( | 346 net.json_request_async.assert_any_call( |
| 270 'https://ultimate.verifier', | 347 'https://ultimate.verifier', |
| 271 method='POST', | 348 method='POST', |
| 272 payload={ | 349 payload={ |
| 273 'config_set': 'services/foo', | 350 'config_set': 'services/foo', |
| 274 'path': 'bar.cfg', | 351 'path': 'bar.cfg', |
| 275 'content': cfg_b64, | 352 'content': cfg_b64, |
| 276 }, | 353 }, |
| 277 scope='https://www.googleapis.com/auth/userinfo.email', | 354 scope=net.EMAIL_SCOPE, |
| 278 ) | 355 ) |
| 279 | 356 |
| 280 ############################################################################ | 357 ############################################################################ |
| 281 | 358 |
| 282 result = validation.validate_config('projects/foo', 'bar.cfg', cfg) | 359 result = validation.validate_config('projects/foo', 'bar.cfg', cfg) |
| 283 self.assertEqual( | 360 self.assertEqual( |
| 284 result.messages, | 361 result.messages, |
| 285 [ | 362 [ |
| 286 validation_context.Message( | 363 validation_context.Message( |
| 287 text='OK from https://bar2.verifier', severity=logging.INFO), | 364 text='OK from https://bar2.verifier', severity=logging.INFO), |
| 288 validation_context.Message( | 365 validation_context.Message( |
| 289 text='OK from https://ultimate.verifier', severity=logging.INFO) | 366 text='OK from https://ultimate.verifier', severity=logging.INFO) |
| 290 ]) | 367 ]) |
| 291 net.json_request_async.assert_any_call( | 368 net.json_request_async.assert_any_call( |
| 292 'https://bar2.verifier', | 369 'https://bar2.verifier', |
| 293 method='POST', | 370 method='POST', |
| 294 payload={ | 371 payload={ |
| 295 'config_set': 'projects/foo', | 372 'config_set': 'projects/foo', |
| 296 'path': 'bar.cfg', | 373 'path': 'bar.cfg', |
| 297 'content': cfg_b64, | 374 'content': cfg_b64, |
| 298 }, | 375 }, |
| 299 scope='https://www.googleapis.com/auth/userinfo.email', | 376 scope=net.EMAIL_SCOPE, |
| 300 ) | 377 ) |
| 301 net.json_request_async.assert_any_call( | 378 net.json_request_async.assert_any_call( |
| 302 'https://ultimate.verifier', | 379 'https://ultimate.verifier', |
| 303 method='POST', | 380 method='POST', |
| 304 payload={ | 381 payload={ |
| 305 'config_set': 'projects/foo', | 382 'config_set': 'projects/foo', |
| 306 'path': 'bar.cfg', | 383 'path': 'bar.cfg', |
| 307 'content': cfg_b64, | 384 'content': cfg_b64, |
| 308 }, | 385 }, |
| 309 scope='https://www.googleapis.com/auth/userinfo.email', | 386 scope=net.EMAIL_SCOPE, |
| 310 ) | 387 ) |
| 311 | 388 |
| 312 ############################################################################ | 389 ############################################################################ |
| 313 # Error found | 390 # Error found |
| 314 | 391 |
| 315 net.json_request_async.side_effect = None | 392 net.json_request_async.side_effect = None |
| 316 net.json_request_async.return_value = ndb.Future() | 393 net.json_request_async.return_value = ndb.Future() |
| 317 net.json_request_async.return_value.set_result({ | 394 net.json_request_async.return_value.set_result({ |
| 318 'messages': [{ | 395 'messages': [{ |
| 319 'text': 'error', | 396 'text': 'error', |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 365 'config_set: projects/baz/refs/x\n' | 442 'config_set: projects/baz/refs/x\n' |
| 366 'path: qux.cfg\n' | 443 'path: qux.cfg\n' |
| 367 'response: %r' % res), | 444 'response: %r' % res), |
| 368 ), | 445 ), |
| 369 ], | 446 ], |
| 370 ) | 447 ) |
| 371 | 448 |
| 372 | 449 |
| 373 if __name__ == '__main__': | 450 if __name__ == '__main__': |
| 374 test_env.main() | 451 test_env.main() |
| OLD | NEW |