blob: 5b99dfb029078bdaff842b1096fd1c71d55b07eb [file] [log] [blame]
#!/usr/bin/env vpython
# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import base64
import cStringIO
import datetime
import gzip
import sys
import unittest
from test_support import test_env
test_env.setup_test_env()
import mock
from google.appengine.ext import ndb
from google.protobuf import field_mask_pb2
from components import auth
from components import net
from components.config import remote
from components.prpc import client
from components.prpc import codes
from test_support import test_case
from .proto import config_service_pb2
import test_config_pb2
class RemoteTestCase(test_case.TestCase):
def setUp(self):
super(RemoteTestCase, self).setUp()
self.mock(net, 'json_request_async', mock.Mock())
net.json_request_async.side_effect = self.json_request_async
self.provider = remote.Provider('luci-config.appspot.com')
provider_future = ndb.Future()
provider_future.set_result(self.provider)
self.mock(remote, 'get_provider_async', lambda: provider_future)
# Inject a prpc config v2 client.
self.v2_cient_mock = mock.Mock()
mock.patch.object(
self.provider,
'_config_v2_client').start().return_value = self.v2_cient_mock
@ndb.tasklet
def json_request_async(self, url, **kwargs):
assert kwargs['scopes']
URL_PREFIX = 'https://luci-config.appspot.com/_ah/api/config/v1/'
if url == URL_PREFIX + 'config_sets/services%2Ffoo/config/bar.cfg':
assert kwargs['params']['hash_only']
raise ndb.Return({
'content_hash': 'deadbeef',
'revision': 'aaaabbbb',
})
if url == URL_PREFIX + 'config_sets/services%2Ffoo/config/baz.cfg':
assert kwargs['params']['hash_only']
raise ndb.Return({
'content_hash': 'badcoffee',
'revision': 'aaaabbbb',
})
if url == URL_PREFIX + 'config/deadbeef':
raise ndb.Return({
'content': base64.b64encode('a config'),
})
if url == URL_PREFIX + 'config/badcoffee':
raise ndb.Return({
'content': base64.b64encode('param: "qux"'),
})
if url == URL_PREFIX + 'projects':
raise ndb.Return({
'projects': [
{
'id': 'chromium',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/chromium/src',
'name': 'Chromium browser'
},
{
'id': 'infra',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/infra/infra',
},
]
})
if url == URL_PREFIX + 'config_sets/services%2Ffoo/config/abc.cfg':
assert kwargs['params']['hash_only']
raise ndb.Return({
'content_hash': 'v1:abchash',
'revision': 'aaaabbbb',
})
if url == URL_PREFIX + 'config/v1:abchash':
raise ndb.Return({
'content': base64.b64encode('abc config'),
})
self.fail('Unexpected url: %s' % url)
def test_get_async_v1(self):
revision, content = self.provider.get_async('services/foo',
'abc.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'abc config')
# Memcache coverage
net.json_request_async.reset_mock()
revision, content = self.provider.get_async('services/foo',
'abc.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'abc config')
self.assertFalse(net.json_request_async.called)
def test_get_async_v2(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigAsync.side_effect = [
future(
config_service_pb2.Config(revision='aaaabbbb',
content_sha256='sha256hash')),
future(config_service_pb2.Config(raw_content=b'a config')),
]
revision, content = self.provider.get_async('services/foo',
'bar.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'a config')
self.v2_cient_mock.GetConfigAsync.assert_has_calls([
mock.call(
config_service_pb2.GetConfigRequest(
config_set='services/foo',
path='bar.cfg',
fields=field_mask_pb2.FieldMask(
paths=['revision', 'content_sha256'])),
credentials=mock.ANY,
),
mock.call(
config_service_pb2.GetConfigRequest(
config_set="services/foo",
content_sha256='sha256hash',
fields=field_mask_pb2.FieldMask(paths=['content']),
),
credentials=mock.ANY,
),
])
# Memcache coverage
self.v2_cient_mock.reset_mock()
revision, content = self.provider.get_async('services/foo',
'bar.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'a config')
self.assertFalse(self.v2_cient_mock.called)
def test_get_async_v2_content_in_signed_url(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigAsync.side_effect = [
future(
config_service_pb2.Config(revision='aaaabbbb',
content_sha256='sha256hash')),
future(config_service_pb2.Config(signed_url='signed_url')),
]
@ndb.tasklet
def mock_request_async(url, **kwargs):
assert kwargs.get('headers', {}).get('Accept-Encoding') == 'gzip'
assert isinstance(kwargs['response_headers'], dict)
kwargs['response_headers'].update({'Content-Encoding': 'gzip'})
return _gzip_compress('a large config')
self.mock(net, 'request_async', mock.Mock())
net.request_async.side_effect = mock_request_async
revision, content = self.provider.get_async('services/foo',
'bar.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'a large config')
self.v2_cient_mock.GetConfigAsync.assert_has_calls([
mock.call(
config_service_pb2.GetConfigRequest(
config_set='services/foo',
path='bar.cfg',
fields=field_mask_pb2.FieldMask(
paths=['revision', 'content_sha256'])),
credentials=mock.ANY,
),
mock.call(
config_service_pb2.GetConfigRequest(
config_set="services/foo",
content_sha256='sha256hash',
fields=field_mask_pb2.FieldMask(paths=['content']),
),
credentials=mock.ANY,
),
])
# Memcache coverage
self.v2_cient_mock.reset_mock()
revision, content = self.provider.get_async('services/foo',
'bar.cfg').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'a large config')
self.assertFalse(self.v2_cient_mock.called)
def test_get_async_v2_not_found(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigAsync.side_effect = client.RpcError(
'Config Not Found', codes.StatusCode.NOT_FOUND, {})
revision, content = self.provider.get_async('services/foo',
'bar.cfg').get_result()
self.assertEqual(revision, None)
self.assertEqual(content, None)
def test_get_async_v2_rpc_err(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigAsync.side_effect = client.RpcError(
'Internal Error', codes.StatusCode.INTERNAL, {})
with self.assertRaises(client.RpcError) as err:
self.provider.get_async('services/foo', 'bar.cfg').get_result()
self.assertEqual(err.exception.status_code, codes.StatusCode.INTERNAL)
def test_get_async_with_revision(self):
revision, content = self.provider.get_async(
'services/foo', 'abc.cfg', revision='aaaabbbb').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'abc config')
net.json_request_async.assert_any_call(
'https://luci-config.appspot.com/_ah/api/config/v1/'
'config_sets/services%2Ffoo/config/abc.cfg',
params={
'hash_only': True,
'revision': 'aaaabbbb'
},
scopes=net.EMAIL_SCOPE)
# Memcache coverage
net.json_request_async.reset_mock()
revision, content = self.provider.get_async(
'services/foo', 'abc.cfg', revision='aaaabbbb').get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(content, 'abc config')
self.assertFalse(net.json_request_async.called)
def test_last_good(self):
revision, content = self.provider.get_async(
'services/foo', 'bar.cfg', store_last_good=True).get_result()
self.assertIsNone(revision)
self.assertIsNone(content)
self.assertTrue(remote.LastGoodConfig.get_by_id('services/foo:bar.cfg'))
remote.LastGoodConfig(
id='services/foo:bar.cfg',
content='a config',
content_hash='deadbeef',
revision='aaaaaaaa').put()
revision, content = self.provider.get_async(
'services/foo', 'bar.cfg', store_last_good=True).get_result()
self.assertEqual(revision, 'aaaaaaaa')
self.assertEqual(content, 'a config')
self.assertFalse(net.json_request_async.called)
self.assertFalse(self.v2_cient_mock.called)
def test_get_projects_v1(self):
projects = self.provider.get_projects_async().get_result()
self.assertEqual(projects, [
{
'id': 'chromium',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/chromium/src',
'name': 'Chromium browser'
},
{
'id': 'infra',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/infra/infra',
},
])
def test_get_projects_v2(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.ListConfigSetsAsync.return_value = future(
config_service_pb2.ListConfigSetsResponse(config_sets=[
config_service_pb2.ConfigSet(
name='projects/chromium',
url='https://chromium.googlesource.com/chromium/src'),
config_service_pb2.ConfigSet(
name='projects/infra',
url='https://chromium.googlesource.com/infra/infra'),
]))
projects = self.provider.get_projects_async().get_result()
self.assertEqual(projects, [
{
'id': 'chromium',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/chromium/src',
'name': 'chromium'
},
{
'id': 'infra',
'repo_type': 'GITILES',
'repo_url': 'https://chromium.googlesource.com/infra/infra',
'name': 'infra'
},
])
self.v2_cient_mock.ListConfigSetsAsync.assert_called_once_with(
config_service_pb2.ListConfigSetsRequest(domain='PROJECT'),
credentials=mock.ANY,
)
def test_get_projects_v2_rpc_err(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.ListConfigSetsAsync.side_effect = client.RpcError(
'Internal Error', codes.StatusCode.INTERNAL, {})
with self.assertRaises(client.RpcError) as err:
self.provider.get_projects_async().get_result()
self.assertEqual(err.exception.status_code, codes.StatusCode.INTERNAL)
def test_get_project_configs_async_receives_404_v1(self):
net.json_request_async.side_effect = net.NotFoundError(
'Not found', 404, None)
with self.assertRaises(net.NotFoundError):
self.provider.get_project_configs_async('cfg').get_result()
def test_get_project_configs_async_v1(self):
self.mock(net, 'json_request_async', mock.Mock())
net.json_request_async.return_value = ndb.Future()
net.json_request_async.return_value.set_result({
'configs': [
{
'config_set': 'projects/chromium',
'content_hash': 'deadbeef',
'path': 'cfg',
'revision': 'aaaaaaaa',
}
]
})
self.mock(self.provider, 'get_config_by_hash_async', mock.Mock())
self.provider.get_config_by_hash_async.return_value = ndb.Future()
self.provider.get_config_by_hash_async.return_value.set_result('a config')
configs = self.provider.get_project_configs_async('cfg').get_result()
self.assertEqual(configs, {'projects/chromium': ('aaaaaaaa', 'a config')})
def test_get_project_configs_async_v2(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetProjectConfigsAsync.return_value = future(
config_service_pb2.GetProjectConfigsResponse(configs=[
config_service_pb2.Config(config_set='projects/chromium',
revision='aaaaaaaa',
content_sha256='deadbeef')
]))
self.v2_cient_mock.GetConfigAsync.return_value = future(
config_service_pb2.Config(raw_content=b'a config'))
configs = self.provider.get_project_configs_async('cfg').get_result()
self.assertEqual(configs, {'projects/chromium': ('aaaaaaaa', 'a config')})
self.v2_cient_mock.GetProjectConfigsAsync.assert_called_once_with(
config_service_pb2.GetProjectConfigsRequest(
path='cfg',
fields=field_mask_pb2.FieldMask(
paths=['config_set', 'revision', 'content_sha256'])),
credentials=mock.ANY,
)
self.v2_cient_mock.GetConfigAsync.assert_called_once_with(
config_service_pb2.GetConfigRequest(
config_set="projects/chromium",
content_sha256='deadbeef',
fields=field_mask_pb2.FieldMask(paths=['content'])),
credentials=mock.ANY,
)
# The 2nd call hits Memcache
self.v2_cient_mock.reset_mock()
self.assertEqual(configs, {'projects/chromium': ('aaaaaaaa', 'a config')})
self.assertFalse(self.v2_cient_mock.GetConfigAsync.called)
def test_get_project_configs_async_v2_not_found(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetProjectConfigsAsync.return_value = future(
config_service_pb2.GetProjectConfigsResponse())
configs = self.provider.get_project_configs_async('cfg').get_result()
self.assertEqual(configs, {})
def test_get_project_configs_async_v2_rpc_err(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetProjectConfigsAsync.side_effect = client.RpcError(
'Internal Error', codes.StatusCode.INTERNAL, {})
with self.assertRaises(client.RpcError) as err:
self.provider.get_project_configs_async('cfg').get_result()
self.assertEqual(err.exception.status_code, codes.StatusCode.INTERNAL)
def test_get_config_set_location_async_v1(self):
self.mock(net, 'json_request_async', mock.Mock())
net.json_request_async.return_value = ndb.Future()
net.json_request_async.return_value.set_result({
'mappings': [
{
'config_set': 'services/abc',
'location': 'http://example.com',
},
],
})
r = self.provider.get_config_set_location_async('services/abc').get_result()
self.assertEqual(r, 'http://example.com')
net.json_request_async.assert_called_once_with(
'https://luci-config.appspot.com/_ah/api/config/v1/mapping',
scopes=net.EMAIL_SCOPE,
params={'config_set': 'services/abc'})
def test_get_config_set_location_async_v2(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigSetAsync.return_value = future(
config_service_pb2.ConfigSet(url='http://example.com'))
r = self.provider.get_config_set_location_async('services/abc').get_result()
self.assertEqual(r, 'http://example.com')
self.v2_cient_mock.GetConfigSetAsync.assert_called_once_with(
config_service_pb2.GetConfigSetRequest(
config_set='services/abc',
fields=field_mask_pb2.FieldMask(paths=['url'])),
credentials=mock.ANY,
)
def test_get_config_set_location_async_v2_not_found(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigSetAsync.side_effect = client.RpcError(
'Not Found', codes.StatusCode.NOT_FOUND, {})
r = self.provider.get_config_set_location_async('services/abc').get_result()
self.assertIsNone(r)
def test_get_config_set_location_async_v2_rpc_err(self):
self.provider.service_hostname = 'luci-config-v2.com'
self.v2_cient_mock.GetConfigSetAsync.side_effect = client.RpcError(
'Internal Error', codes.StatusCode.INTERNAL, {})
with self.assertRaises(client.RpcError) as err:
self.provider.get_config_set_location_async('services/abc').get_result()
self.assertEqual(err.exception.status_code, codes.StatusCode.INTERNAL)
def test_cron_update_last_good_configs(self):
self.provider.get_async(
'services/foo', 'bar.cfg', store_last_good=True).get_result()
self.provider.get_async(
'services/foo', 'baz.cfg', dest_type=test_config_pb2.Config,
store_last_good=True).get_result()
# Will be removed.
old_cfg = remote.LastGoodConfig(
id='projects/old:foo.cfg',
content_hash='aaaa',
content='content',
last_access_ts=datetime.datetime(2010, 1, 1))
old_cfg.put()
remote.cron_update_last_good_configs()
revision, config = self.provider.get_async(
'services/foo', 'bar.cfg', store_last_good=True).get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(config, 'a config')
revision, config = self.provider.get_async(
'services/foo', 'baz.cfg', dest_type=test_config_pb2.Config,
store_last_good=True).get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(config.param, 'qux')
baz_cfg = remote.LastGoodConfig.get_by_id(id='services/foo:baz.cfg')
self.assertIsNotNone(baz_cfg)
self.assertEquals(baz_cfg.content_binary, config.SerializeToString())
self.assertIsNone(old_cfg.key.get())
def test_cron_update_last_good_configs_v1_v2_transition(self):
revision, config = self.provider.get_async(
'services/foo', 'abc.cfg', store_last_good=True).get_result()
remote.cron_update_last_good_configs()
revision, config = self.provider.get_async(
'services/foo', 'abc.cfg', store_last_good=True).get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(config, 'abc config')
abc_cfg = remote.LastGoodConfig.get_by_id(id='services/foo:abc.cfg')
self.assertEqual(abc_cfg.content_hash, 'v1:abchash')
self.assertEqual(abc_cfg.content, 'abc config')
# Switch from v1 to v2
self.provider.service_hostname = 'luci-config-v2.com'
self.mock(self.provider, 'get_config_hash_async', mock.Mock())
self.mock(self.provider, 'get_config_by_hash_async', mock.Mock())
self.provider.get_config_hash_async.return_value = future(
('aaaabbbb', 'abchash'))
self.provider.get_config_by_hash_async.return_value = future('abc config')
# Still be able to get the result before the cron job running next time.
revision, config = self.provider.get_async(
'services/foo', 'abc.cfg', store_last_good=True).get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(config, 'abc config')
# After the job ran
remote.cron_update_last_good_configs()
revision, config = self.provider.get_async(
'services/foo', 'abc.cfg', store_last_good=True).get_result()
self.assertEqual(revision, 'aaaabbbb')
self.assertEqual(config, 'abc config')
abc_cfg = remote.LastGoodConfig.get_by_id(id='services/foo:abc.cfg')
self.assertEqual(abc_cfg.content_hash, 'abchash')
self.assertEqual(abc_cfg.content, 'abc config')
if __name__ == '__main__':
if '-v' in sys.argv:
unittest.TestCase.maxDiff = None
unittest.main()
def future(result):
f = ndb.Future()
f.set_result(result)
return f
def _gzip_compress(blob):
out = cStringIO.StringIO()
with gzip.GzipFile(fileobj=out, mode='w') as f:
f.write(blob)
return out.getvalue()