Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(438)

Side by Side Diff: appengine/config_service/validation_test.py

Issue 1221643020: config services: services.cfg and validation (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « appengine/config_service/validation.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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()
OLDNEW
« no previous file with comments | « appengine/config_service/validation.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698