Chromium Code Reviews| 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: None', | |
| 167 'validation: pattern #2: path: pattern must be a string: None', | |
| 168 'validation: pattern #3: config_set: unknown pattern kind: a', | |
| 169 'validation: pattern #3: path: must not be absolute: /foo', | |
| 170 ('validation: pattern #4: config_set: ' | |
| 171 'invalid regular expression ")(": unbalanced parenthesis'), | |
| 172 ('validation: pattern #4: path: ' | |
| 173 'must not contain ".." or "." components: ../b'), | |
| 174 ] | |
| 175 ) | |
| 176 | |
| 126 def test_validate_schemas(self): | 177 def test_validate_schemas(self): |
| 127 cfg = ''' | 178 cfg = ''' |
| 128 schemas { | 179 schemas { |
| 129 name: "services/config:foo" | 180 name: "services/config:foo" |
| 130 url: "https://foo" | 181 url: "https://foo" |
| 131 } | 182 } |
| 132 schemas { | 183 schemas { |
| 133 name: "projects:foo" | 184 name: "projects:foo" |
| 134 url: "https://foo" | 185 url: "https://foo" |
| 135 } | 186 } |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 200 self.assertEqual( | 251 self.assertEqual( |
| 201 [m.text for m in result.messages], | 252 [m.text for m in result.messages], |
| 202 [ | 253 [ |
| 203 'Ref #2: name is not specified', | 254 'Ref #2: name is not specified', |
| 204 'Ref #3: duplicate ref: refs/heads/master', | 255 'Ref #3: duplicate ref: refs/heads/master', |
| 205 'Ref #4: name does not start with "refs/": does_not_start_with_ref', | 256 'Ref #4: name does not start with "refs/": does_not_start_with_ref', |
| 206 'Ref #4: must not contain ".." or "." components: ../bad/path' | 257 'Ref #4: must not contain ".." or "." components: ../bad/path' |
| 207 ], | 258 ], |
| 208 ) | 259 ) |
| 209 | 260 |
| 210 def test_endpoint_validate_async(self): | 261 def test_validation_by_service_async(self): |
| 211 cfg = '# a config' | 262 cfg = '# a config' |
| 212 cfg_b64 = base64.b64encode(cfg) | 263 cfg_b64 = base64.b64encode(cfg) |
| 213 | 264 |
| 214 self.mock(storage, 'get_self_config_async', mock.Mock()) | 265 self.services = [ |
| 215 storage.get_self_config_async.return_value = future( | 266 service_config_pb2.Service(id='a'), |
| 216 service_config_pb2.ValidationCfg( | 267 service_config_pb2.Service(id='b'), |
| 217 rules=[ | 268 service_config_pb2.Service(id='c'), |
| 218 service_config_pb2.ValidationCfg.Rule( | 269 ] |
| 219 config_set='services/foo', | 270 |
| 220 path='bar.cfg', | 271 @ndb.tasklet |
| 272 def get_metadata_async(service_id): | |
| 273 if service_id == 'a': | |
| 274 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( | |
| 275 validation=service_config_pb2.Validator( | |
| 276 patterns=[service_config_pb2.ConfigPattern( | |
| 277 config_set='services/foo', | |
| 278 path='bar.cfg', | |
| 279 )], | |
| 221 url='https://bar.verifier', | 280 url='https://bar.verifier', |
| 222 ), | 281 ) |
| 223 service_config_pb2.ValidationCfg.Rule( | 282 )) |
| 224 config_set='regex:projects/[^/]+', | 283 if service_id == 'b': |
| 225 path='regex:.+.\cfg', | 284 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( |
| 285 validation=service_config_pb2.Validator( | |
| 286 patterns=[service_config_pb2.ConfigPattern( | |
| 287 config_set='regex:projects/[^/]+', | |
| 288 path='regex:.+.\cfg', | |
|
Sergiy Byelozyorov
2015/07/07 09:10:47
regex is invalid, please correct: r'regex:.+\.cfg'
| |
| 289 )], | |
| 226 url='https://bar2.verifier', | 290 url='https://bar2.verifier', |
| 227 ), | 291 ))) |
| 228 service_config_pb2.ValidationCfg.Rule( | 292 if service_id == 'c': |
| 229 config_set='regex:.+', | 293 raise ndb.Return(service_config_pb2.ServiceDynamicMetadata( |
| 230 path='regex:.+', | 294 validation=service_config_pb2.Validator( |
| 295 patterns=[service_config_pb2.ConfigPattern( | |
| 296 config_set='regex:.+', | |
| 297 path='regex:.+', | |
|
Sergiy Byelozyorov
2015/07/07 09:10:47
please also use raw string: r'regex:.+'
| |
| 298 )], | |
| 231 url='https://ultimate.verifier', | 299 url='https://ultimate.verifier', |
| 232 ), | 300 ))) |
| 233 ] | 301 return None |
| 234 )) | 302 self.mock(services, 'get_metadata_async', mock.Mock()) |
| 303 services.get_metadata_async.side_effect = get_metadata_async | |
| 235 | 304 |
| 236 @ndb.tasklet | 305 @ndb.tasklet |
| 237 def json_request_async(url, **kwargs): | 306 def json_request_async(url, **kwargs): |
| 238 raise ndb.Return({ | 307 raise ndb.Return({ |
| 239 'messages': [{ | 308 'messages': [{ |
| 240 'text': 'OK from %s' % url, | 309 'text': 'OK from %s' % url, |
| 241 # default severity | 310 # default severity |
| 242 }], | 311 }], |
| 243 }) | 312 }) |
| 244 | 313 |
| (...skipping 120 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 365 'config_set: projects/baz/refs/x\n' | 434 'config_set: projects/baz/refs/x\n' |
| 366 'path: qux.cfg\n' | 435 'path: qux.cfg\n' |
| 367 'response: %r' % res), | 436 'response: %r' % res), |
| 368 ), | 437 ), |
| 369 ], | 438 ], |
| 370 ) | 439 ) |
| 371 | 440 |
| 372 | 441 |
| 373 if __name__ == '__main__': | 442 if __name__ == '__main__': |
| 374 test_env.main() | 443 test_env.main() |
| OLD | NEW |