refstack/refstack/tests/api/test_results.py
Lukáš Piwowarski d18f8ad221 Make refstack jobs work again
There were three main issues in the code from refstack repository that
were preventing refstack jobs from working:

1) Improper handling of sessions in the database API.
2) PEP8 job was failing because of outdated version of flake8.
3) The new version of cryptography library doesn't support signer() and
   verifier() functions.

Issue #1 was solved by using the get_session() function as a context
manager instead of using session.begin() as a context manager. Using
session.begin() as a context manager does not ensure that the session
will be closed at the end of the context (see "Opening and Closing
a Session" and "Framing out a begin / commit / rollback block"
here [1]).

Issue #2 was solved by updating the libraries in
test-requirements.txt file. This change also forces flake8 to ignore
some pep8 errors (similar to the ones ignored in tempest project).

Issue #3 was solved by using the sign() and verify() functions instead
of verifier() and signer() functions [2].

Related Taiga issues:
 - https://tree.taiga.io/project/openstack-interop-working-group/issue/77
 - https://tree.taiga.io/project/openstack-interop-working-group/issue/79

[1] https://docs.sqlalchemy.org/en/14/orm/session_basics.html
[2] e71c0df301

Change-Id: If98670475b371d1ece7c877a0eea3158f6c1b3f5
2023-02-01 15:29:36 +01:00

509 lines
19 KiB
Python

# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import binascii
import json
from unittest import mock
import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from oslo_config import fixture as config_fixture
import webtest.app
from refstack.api import constants as api_const
from refstack.api import validators
from refstack import db
from refstack.tests import api
FAKE_TESTS_RESULT = {
'cpid': 'foo',
'duration_seconds': 10,
'results': [
{'name': 'tempest.foo.bar'},
{'name': 'tempest.buzz',
'uid': '42'}
]
}
FAKE_JSON_WITH_EMPTY_RESULTS = {
'cpid': 'foo',
'duration_seconds': 20,
'results': [
]
}
class TestResultsEndpoint(api.FunctionalTest):
"""Test case for the 'results' API endpoint."""
URL = '/v1/results/'
def setUp(self):
super(TestResultsEndpoint, self).setUp()
self.config_fixture = config_fixture.Config()
self.CONF = self.useFixture(self.config_fixture).conf
def test_post(self):
"""Test results endpoint with post request."""
results = json.dumps(FAKE_TESTS_RESULT)
actual_response = self.post_json(self.URL, params=results)
self.assertIn('test_id', actual_response)
try:
uuid.UUID(actual_response.get('test_id'), version=4)
except ValueError:
self.fail("actual_response doesn't contain test_id")
def test_post_with_empty_result(self):
"""Test results endpoint with empty test results request."""
results = json.dumps(FAKE_JSON_WITH_EMPTY_RESULTS)
self.assertRaises(webtest.app.AppError,
self.post_json,
self.URL,
params=results)
def test_post_with_invalid_schema(self):
"""Test post request with invalid schema."""
results = json.dumps({
'foo': 'bar',
'duration_seconds': 999,
})
self.assertRaises(webtest.app.AppError,
self.post_json,
self.URL,
params=results)
@mock.patch('refstack.api.utils.check_owner')
@mock.patch('refstack.api.utils.check_user_is_foundation_admin')
@mock.patch('refstack.api.utils.get_user_id', return_value='test-open-id')
def test_put(self, mock_user, mock_check_foundation, mock_check_owner):
"""Test results endpoint with put request."""
results = json.dumps(FAKE_TESTS_RESULT)
test_response = self.post_json(self.URL, params=results)
test_id = test_response.get('test_id')
url = self.URL + test_id
user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(user_info)
fake_product = {
'name': 'product name',
'description': 'product description',
'product_type': api_const.CLOUD,
}
# Create a product
product_response = self.post_json('/v1/products/',
params=json.dumps(fake_product))
# Create a product version
version_url = '/v1/products/' + product_response['id'] + '/versions/'
version_response = self.post_json(version_url,
params=json.dumps({'version': '1'}))
# Test Foundation admin can put.
mock_check_foundation.return_value = True
body = {'product_version_id': version_response['id']}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertEqual(version_response['id'],
get_response['product_version']['id'])
# Test when product_version_id is None.
body = {'product_version_id': None}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertIsNone(get_response['product_version'])
# Test when test verification preconditions are not met.
body = {'verification_status': api_const.TEST_VERIFIED}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Share the test run.
db.save_test_result_meta_item(test_id, api_const.SHARED_TEST_RUN, True)
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Now associate guideline and target program. Now we should be
# able to mark a test verified.
db.save_test_result_meta_item(test_id, 'target', 'platform')
db.save_test_result_meta_item(test_id, 'guideline', '2016.01.json')
put_response = self.put_json(url, params=json.dumps(body))
self.assertEqual(api_const.TEST_VERIFIED,
put_response['verification_status'])
# Unshare the test, and check that we can mark it not verified.
db.delete_test_result_meta_item(test_id, api_const.SHARED_TEST_RUN)
body = {'verification_status': api_const.TEST_NOT_VERIFIED}
put_response = self.put_json(url, params=json.dumps(body))
self.assertEqual(api_const.TEST_NOT_VERIFIED,
put_response['verification_status'])
# Test when verification_status value is invalid.
body = {'verification_status': 111}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(400, put_response.status_code)
# Check test owner can put.
mock_check_foundation.return_value = False
mock_check_owner.return_value = True
body = {'product_version_id': version_response['id']}
self.put_json(url, params=json.dumps(body))
get_response = self.get_json(url)
self.assertEqual(version_response['id'],
get_response['product_version']['id'])
# Test non-Foundation user can't change verification_status.
body = {'verification_status': 1}
put_response = self.put_json(url, expect_errors=True,
params=json.dumps(body))
self.assertEqual(403, put_response.status_code)
# Test unauthorized put.
mock_check_foundation.return_value = False
mock_check_owner.return_value = False
self.assertRaises(webtest.app.AppError,
self.put_json,
url,
params=json.dumps(body))
def test_get_one(self):
"""Test get request."""
results = json.dumps(FAKE_TESTS_RESULT)
post_response = self.post_json(self.URL, params=results)
get_response = self.get_json(self.URL + post_response.get('test_id'))
# CPID is only exposed to the owner.
self.assertNotIn('cpid', get_response)
self.assertEqual(FAKE_TESTS_RESULT['duration_seconds'],
get_response['duration_seconds'])
for test in FAKE_TESTS_RESULT['results']:
self.assertIn(test['name'], get_response['results'])
def test_get_one_with_nonexistent_uuid(self):
"""Test get request with nonexistent uuid."""
self.assertRaises(webtest.app.AppError,
self.get_json,
self.URL + str(uuid.uuid4()))
def test_get_one_schema(self):
"""Test get request for getting JSON schema."""
validator = validators.TestResultValidator()
expected_schema = validator.schema
actual_schema = self.get_json(self.URL + 'schema')
self.assertEqual(actual_schema, expected_schema)
def test_get_one_invalid_url(self):
"""Test get request with invalid url."""
self.assertRaises(webtest.app.AppError,
self.get_json,
self.URL + 'fake_url')
def test_get_pagination(self):
self.CONF.set_override('results_per_page',
2,
'api')
responses = []
for i in range(3):
fake_results = {
'cpid': str(i),
'duration_seconds': i,
'results': [
{'name': 'tempest.foo.bar'},
{'name': 'tempest.buzz'}
]
}
actual_response = self.post_json(self.URL,
params=json.dumps(fake_results))
responses.append(actual_response)
page_one = self.get_json(self.URL)
page_two = self.get_json('/v1/results?page=2')
self.assertEqual(len(page_one['results']), 2)
self.assertEqual(len(page_two['results']), 1)
self.assertNotIn(page_two['results'][0], page_one)
self.assertEqual(page_one['pagination']['current_page'], 1)
self.assertEqual(page_one['pagination']['total_pages'], 2)
self.assertEqual(page_two['pagination']['current_page'], 2)
self.assertEqual(page_two['pagination']['total_pages'], 2)
def test_get_with_not_existing_page(self):
self.assertRaises(webtest.app.AppError,
self.get_json,
'/v1/results?page=2')
def test_get_with_empty_database(self):
results = self.get_json(self.URL)
self.assertEqual([], results['results'])
def test_get_with_cpid_filter(self):
self.CONF.set_override('results_per_page',
2,
'api')
responses = []
for i in range(2):
fake_results = {
'cpid': '12345',
'duration_seconds': i,
'results': [
{'name': 'tempest.foo'},
{'name': 'tempest.bar'}
]
}
json_result = json.dumps(fake_results)
actual_response = self.post_json(self.URL,
params=json_result)
responses.append(actual_response)
for i in range(3):
fake_results = {
'cpid': '54321',
'duration_seconds': i,
'results': [
{'name': 'tempest.foo'},
{'name': 'tempest.bar'}
]
}
results = self.get_json('/v1/results?page=1&cpid=12345')
self.assertEqual(len(results), 2)
response_test_ids = [test['test_id'] for test in responses[0:2]]
for r in results['results']:
self.assertIn(r['id'], response_test_ids)
def test_get_with_date_filters(self):
self.CONF.set_override('results_per_page',
10,
'api')
responses = []
for i in range(5):
fake_results = {
'cpid': '12345',
'duration_seconds': i,
'results': [
{'name': 'tempest.foo'},
{'name': 'tempest.bar'}
]
}
json_result = json.dumps(fake_results)
actual_response = self.post_json(self.URL,
params=json_result)
responses.append(actual_response)
all_results = self.get_json(self.URL)
slice_results = all_results['results'][1:4]
url = '/v1/results?start_date=%(start)s&end_date=%(end)s' % {
'start': slice_results[2]['created_at'],
'end': slice_results[0]['created_at']
}
filtering_results = self.get_json(url)
for r in slice_results:
self.assertIn(r, filtering_results['results'])
url = '/v1/results?end_date=1000-01-01 12:00:00'
filtering_results = self.get_json(url)
self.assertEqual([], filtering_results['results'])
@mock.patch('refstack.api.utils.get_user_id')
def test_get_with_product_id(self, mock_get_user):
user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(user_info)
mock_get_user.return_value = 'test-open-id'
fake_product = {
'name': 'product name',
'description': 'product description',
'product_type': api_const.CLOUD,
}
product = json.dumps(fake_product)
response = self.post_json('/v1/products/', params=product)
product_id = response['id']
# Create a version.
version_url = '/v1/products/' + product_id + '/versions'
version = {'cpid': '123', 'version': '6.0'}
post_response = self.post_json(version_url, params=json.dumps(version))
version_id = post_response['id']
# Create a test and associate it to the product version and user.
results = json.dumps(FAKE_TESTS_RESULT)
post_response = self.post_json('/v1/results', params=results)
test_id = post_response['test_id']
test_info = {'id': test_id, 'product_version_id': version_id}
db.update_test_result(test_info)
db.save_test_result_meta_item(test_id, api_const.USER, 'test-open-id')
url = self.URL + '?page=1&product_id=' + product_id
# Test GET.
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
# Test unauthorized.
mock_get_user.return_value = 'test-foo-id'
response = self.get_json(url, expect_errors=True)
self.assertEqual(403, response.status_code)
# Make product public.
product_info = {'id': product_id, 'public': 1}
db.update_product(product_info)
# Test result is not shared yet, so no tests should return.
response = self.get_json(url)
self.assertFalse(response['results'])
# Share the test run.
db.save_test_result_meta_item(test_id, api_const.SHARED_TEST_RUN, 1)
response = self.get_json(url)
self.assertEqual(1, len(response['results']))
self.assertEqual(test_id, response['results'][0]['id'])
@mock.patch('refstack.api.utils.check_owner')
def test_delete(self, mock_check_owner):
results = json.dumps(FAKE_TESTS_RESULT)
test_response = self.post_json(self.URL, params=results)
test_id = test_response.get('test_id')
url = self.URL + test_id
mock_check_owner.return_value = True
# Test can't delete verified test run.
db.update_test_result({'id': test_id, 'verification_status': 1})
resp = self.delete(url, expect_errors=True)
self.assertEqual(403, resp.status_code)
# Test can delete verified test run.
db.update_test_result({'id': test_id, 'verification_status': 0})
resp = self.delete(url, expect_errors=True)
self.assertEqual(204, resp.status_code)
class TestResultsEndpointNoAnonymous(api.FunctionalTest):
URL = '/v1/results/'
def _generate_keypair_(self):
return rsa.generate_private_key(
public_exponent=65537,
key_size=1024,
backend=default_backend()
)
def _sign_body_(self, keypair, body):
return keypair.sign(body, padding.PKCS1v15(), hashes.SHA256())
def _get_public_key_(self, keypair):
pubkey = keypair.public_key().public_bytes(
serialization.Encoding.OpenSSH,
serialization.PublicFormat.OpenSSH
)
return pubkey
def setUp(self):
super(TestResultsEndpointNoAnonymous, self).setUp()
self.config_fixture = config_fixture.Config()
self.CONF = self.useFixture(self.config_fixture).conf
self.CONF.api.enable_anonymous_upload = False
self.user_info = {
'openid': 'test-open-id',
'email': 'foo@bar.com',
'fullname': 'Foo Bar'
}
db.user_save(self.user_info)
good_key = self._generate_keypair_()
self.body = json.dumps(FAKE_TESTS_RESULT).encode()
signature = self._sign_body_(good_key, self.body)
pubkey = self._get_public_key_(good_key)
x_signature = binascii.b2a_hex(signature)
self.good_headers = {
'X-Signature': x_signature,
'X-Public-Key': pubkey
}
self.pubkey_info = {
'openid': 'test-open-id',
'format': 'ssh-rsa',
'pubkey': pubkey.split()[1],
'comment': 'comment'
}
db.store_pubkey(self.pubkey_info)
bad_key = self._generate_keypair_()
bad_signature = self._sign_body_(bad_key, self.body)
bad_pubkey = self._get_public_key_(bad_key)
x_bad_signature = binascii.b2a_hex(bad_signature)
self.bad_headers = {
'X-Signature': x_bad_signature,
'X-Public-Key': bad_pubkey
}
def test_post_with_no_token(self):
"""Test results endpoint with post request."""
results = json.dumps(FAKE_TESTS_RESULT)
actual_response = self.post_json(self.URL, expect_errors=True,
params=results)
self.assertEqual(actual_response.status_code, 401)
def test_post_with_valid_token(self):
"""Test results endpoint with post request."""
results = json.dumps(FAKE_TESTS_RESULT)
actual_response = self.post_json(self.URL,
headers=self.good_headers,
params=results)
self.assertIn('test_id', actual_response)
try:
uuid.UUID(actual_response.get('test_id'), version=4)
except ValueError:
self.fail("actual_response doesn't contain test_id")
def test_post_with_invalid_token(self):
results = json.dumps(FAKE_TESTS_RESULT)
actual_response = self.post_json(self.URL,
headers=self.bad_headers,
expect_errors=True,
params=results)
self.assertEqual(actual_response.status_code, 401)