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