Revision rollback API.
This commit implements the revision rollback API, allowing users to rollback to a previous revision, whereby a new revision is created. An exception (400 Bad Request) is raised if the revision being rolled back to is identical to the latest revision or if no changes exist between the latest revision and the one being rolled back to. Included in this commit: - API endpoint for revision rollback. - Back-end logic for rolling back to a previous revision. The associated documents are also re-recreated. The revision_id assigned to each document depends on whether it has changed between the latest revision and the one being rolled back to: if changed, the new revision_id is assigned, else the original one, to maintain the correct revision history. - Associated unit tests. - Unskip all associated functional tests. Change-Id: I5c120a92e106544f7f8a4266fc386fb60622d6b3
This commit is contained in:
parent
ef4f65037d
commit
81b3e42013
@ -24,6 +24,7 @@ from deckhand.control import revision_diffing
|
||||
from deckhand.control import revision_documents
|
||||
from deckhand.control import revision_tags
|
||||
from deckhand.control import revisions
|
||||
from deckhand.control import rollback
|
||||
from deckhand.control import versions
|
||||
from deckhand.db.sqlalchemy import api as db_api
|
||||
|
||||
@ -68,7 +69,8 @@ def start_api(state_manager=None):
|
||||
revision_documents.RevisionDocumentsResource()),
|
||||
('revisions/{revision_id}/tags', revision_tags.RevisionTagsResource()),
|
||||
('revisions/{revision_id}/tags/{tag}',
|
||||
revision_tags.RevisionTagsResource())
|
||||
revision_tags.RevisionTagsResource()),
|
||||
('rollback/{revision_id}', rollback.RollbackResource())
|
||||
]
|
||||
|
||||
for path, res in v1_0_routes:
|
||||
|
39
deckhand/control/rollback.py
Normal file
39
deckhand/control/rollback.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2017 AT&T Intellectual Property. All other 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 falcon
|
||||
|
||||
from deckhand.control import base as api_base
|
||||
from deckhand.control.views import revision as revision_view
|
||||
from deckhand.db.sqlalchemy import api as db_api
|
||||
from deckhand import errors
|
||||
|
||||
|
||||
class RollbackResource(api_base.BaseResource):
|
||||
"""API resource for realizing revision rollback."""
|
||||
|
||||
view_builder = revision_view.ViewBuilder()
|
||||
|
||||
def on_post(self, req, resp, revision_id):
|
||||
try:
|
||||
revision = db_api.revision_rollback(revision_id)
|
||||
except errors.RevisionNotFound as e:
|
||||
raise falcon.HTTPNotFound(description=e.format_message())
|
||||
except errors.InvalidRollback as e:
|
||||
raise falcon.HTTPBadRequest(description=e.format_message())
|
||||
|
||||
revision_resp = self.view_builder.show(revision)
|
||||
resp.status = falcon.HTTP_201
|
||||
resp.append_header('Content-Type', 'application/x-yaml')
|
||||
resp.body = self.to_yaml_body(revision_resp)
|
@ -144,8 +144,7 @@ def documents_create(bucket_name, documents, session=None):
|
||||
doc.save(session=session)
|
||||
doc.safe_delete(session=session)
|
||||
deleted_documents.append(doc)
|
||||
|
||||
resp.extend([d.to_dict() for d in deleted_documents])
|
||||
resp.append(doc.to_dict())
|
||||
|
||||
if documents_to_create:
|
||||
LOG.debug('Creating documents: %s.',
|
||||
@ -155,12 +154,11 @@ def documents_create(bucket_name, documents, session=None):
|
||||
doc['bucket_id'] = bucket['id']
|
||||
doc['revision_id'] = revision['id']
|
||||
doc.save(session=session)
|
||||
|
||||
# NOTE(fmontei): The orig_revision_id is not copied into the
|
||||
# revision_id for each created document, because the revision_id here
|
||||
# should reference the just-created revision. In case the user needs
|
||||
# the original revision_id, that is returned as well.
|
||||
resp.extend([d.to_dict() for d in documents_to_create])
|
||||
resp.append(doc.to_dict())
|
||||
# NOTE(fmontei): The orig_revision_id is not copied into the
|
||||
# revision_id for each created document, because the revision_id here
|
||||
# should reference the just-created revision. In case the user needs
|
||||
# the original revision_id, that is returned as well.
|
||||
|
||||
return resp
|
||||
|
||||
@ -180,6 +178,13 @@ def _documents_create(bucket_name, values_list, session=None):
|
||||
for values in values_list:
|
||||
values['_metadata'] = values.pop('metadata')
|
||||
values['name'] = values['_metadata']['name']
|
||||
|
||||
# Hash the combination of the document's metadata and data to later
|
||||
# efficiently check whether those data have changed.
|
||||
dict_to_hash = values['_metadata'].copy()
|
||||
dict_to_hash.update(values['data'])
|
||||
values['hash'] = utils.make_hash(dict_to_hash)
|
||||
|
||||
values['is_secret'] = 'secret' in values['data']
|
||||
# Hash the combination of the document's metadata and data to later
|
||||
# efficiently check whether those data have changed.
|
||||
@ -302,6 +307,7 @@ def revision_create(session=None):
|
||||
def revision_get(revision_id, session=None):
|
||||
"""Return the specified `revision_id`.
|
||||
|
||||
:param revision_id: The ID corresponding to the ``Revision`` object.
|
||||
:param session: Database session object.
|
||||
:returns: Dictionary representation of retrieved revision.
|
||||
:raises: RevisionNotFound if the revision was not found.
|
||||
@ -335,6 +341,17 @@ def require_revision_exists(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def _update_revision_history(documents):
|
||||
# Since documents that are unchanged across revisions need to be saved for
|
||||
# each revision, we need to ensure that the original revision is shown
|
||||
# for the document's `revision_id` to maintain the correct revision
|
||||
# history.
|
||||
for doc in documents:
|
||||
if doc['orig_revision_id']:
|
||||
doc['revision_id'] = doc['orig_revision_id']
|
||||
return documents
|
||||
|
||||
|
||||
def revision_get_all(session=None):
|
||||
"""Return list of all revisions.
|
||||
|
||||
@ -419,17 +436,6 @@ def revision_get_documents(revision_id=None, include_history=True,
|
||||
return filtered_documents
|
||||
|
||||
|
||||
def _update_revision_history(documents):
|
||||
# Since documents that are unchanged across revisions need to be saved for
|
||||
# each revision, we need to ensure that the original revision is shown
|
||||
# for the document's `revision_id` to maintain the correct revision
|
||||
# history.
|
||||
for doc in documents:
|
||||
if doc['orig_revision_id']:
|
||||
doc['revision_id'] = doc['orig_revision_id']
|
||||
return documents
|
||||
|
||||
|
||||
def _filter_revision_documents(documents, unique_only, **filters):
|
||||
"""Return the list of documents that match filters.
|
||||
|
||||
@ -725,3 +731,81 @@ def revision_tag_delete_all(revision_id, session=None):
|
||||
session.query(models.RevisionTag)\
|
||||
.filter_by(revision_id=revision_id)\
|
||||
.delete(synchronize_session=False)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
@require_revision_exists
|
||||
def revision_rollback(revision_id, session=None):
|
||||
"""Rollback the latest revision to revision specified by ``revision_id``.
|
||||
|
||||
Rolls back the latest revision to the revision specified by ``revision_id``
|
||||
thereby creating a new, carbon-copy revision.
|
||||
|
||||
:param revision_id: Revision ID to which to rollback.
|
||||
:returns: The newly created revision.
|
||||
"""
|
||||
session = session or get_session()
|
||||
|
||||
# We know that the last revision exists, since require_revision_exists
|
||||
# ensures revision_id exists, which at the very least is the last revision.
|
||||
latest_revision = session.query(models.Revision)\
|
||||
.order_by(models.Revision.created_at.desc())\
|
||||
.first()
|
||||
latest_revision_hashes = [d['hash'] for d in latest_revision['documents']]
|
||||
|
||||
# If the rollback revision is the same as the latest revision, then there's
|
||||
# no point in rolling back.
|
||||
if latest_revision['id'] == revision_id:
|
||||
raise errors.InvalidRollback(revision_id=revision_id)
|
||||
|
||||
orig_revision = revision_get(revision_id, session=session)
|
||||
|
||||
# A mechanism for determining whether a particular document has changed
|
||||
# between revisions. Keyed with the document_id, the value is True if
|
||||
# it has changed, else False.
|
||||
doc_diff = {}
|
||||
for orig_doc in orig_revision['documents']:
|
||||
if orig_doc['hash'] not in latest_revision_hashes:
|
||||
doc_diff[orig_doc['id']] = True
|
||||
else:
|
||||
doc_diff[orig_doc['id']] = False
|
||||
|
||||
# If no changges have been made between the target revision to rollback to
|
||||
# and the latest revision, raise an exception.
|
||||
if set(doc_diff.values()) == set([False]):
|
||||
raise errors.InvalidRollback(revision_id=revision_id)
|
||||
|
||||
# Create the new revision,
|
||||
new_revision = models.Revision()
|
||||
with session.begin():
|
||||
new_revision.save(session=session)
|
||||
|
||||
# Create the documents for the revision.
|
||||
for orig_document in orig_revision['documents']:
|
||||
orig_document['revision_id'] = new_revision['id']
|
||||
orig_document['_metadata'] = orig_document.pop('metadata')
|
||||
|
||||
new_document = models.Document()
|
||||
new_document.update({x: orig_document[x] for x in (
|
||||
'name', '_metadata', 'data', 'hash', 'schema', 'bucket_id')})
|
||||
|
||||
new_document['revision_id'] = new_revision['id']
|
||||
|
||||
# If the document has changed, then use the revision_id of the new
|
||||
# revision, otherwise use the original revision_id to preserve the
|
||||
# revision history.
|
||||
if doc_diff[orig_document['id']]:
|
||||
new_document['orig_revision_id'] = new_revision['id']
|
||||
else:
|
||||
new_document['orig_revision_id'] = orig_revision['id']
|
||||
|
||||
with session.begin():
|
||||
new_document.save(session=session)
|
||||
|
||||
new_revision = new_revision.to_dict()
|
||||
new_revision['documents'] = _update_revision_history(
|
||||
new_revision['documents'])
|
||||
|
||||
return new_revision
|
||||
|
@ -127,6 +127,12 @@ class RevisionTagBadFormat(DeckhandException):
|
||||
code = 400
|
||||
|
||||
|
||||
class InvalidRollback(DeckhandException):
|
||||
msg_fmt = ("The requested rollback for target revision %(revision)s is "
|
||||
"invalid as the latest revision matches the target revision.")
|
||||
code = 400
|
||||
|
||||
|
||||
class BarbicanException(DeckhandException):
|
||||
|
||||
def __init__(self, message, code):
|
||||
|
@ -23,34 +23,29 @@ tests:
|
||||
desc: Begin testing from known state.
|
||||
DELETE: /api/v1.0/revisions
|
||||
status: 204
|
||||
skip: Not implemented.
|
||||
|
||||
- name: initialize
|
||||
desc: Create initial documents
|
||||
PUT: /api/v1.0/bucket/mop/documents
|
||||
status: 200
|
||||
data: <@resources/design-doc-layering-sample.yaml
|
||||
skip: Not implemented.
|
||||
|
||||
- name: update_single_document
|
||||
desc: Update a single document, ignore other documents in the bucket
|
||||
PUT: /api/v1.0/bucket/mop/documents
|
||||
status: 200
|
||||
data: <@resources/design-doc-layering-sample-with-update.yaml
|
||||
skip: Not implemented.
|
||||
|
||||
- name: delete_document
|
||||
desc: Delete a single document
|
||||
PUT: /api/v1.0/bucket/mop/documents
|
||||
status: 200
|
||||
data: <@resources/design-doc-layering-sample-with-delete.yaml
|
||||
skip: Not implemented.
|
||||
|
||||
- name: rollback
|
||||
desc: Rollback to revision 1
|
||||
POST: /api/v1.0/rollback/$HISTORY.$RESPONSE['$.documents[0].status.revision']
|
||||
POST: /api/v1.0/rollback/$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']
|
||||
status: 201
|
||||
skip: Not implemented.
|
||||
|
||||
# Verify document history
|
||||
- name: verify_revision_1
|
||||
@ -59,22 +54,21 @@ tests:
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.[*].metadata.name:
|
||||
- global-1234
|
||||
- layering-policy
|
||||
- global-1234
|
||||
- region-1234
|
||||
- site-1234
|
||||
$.[*].status.revision:
|
||||
- "$RESPONSE['$.[0].status.revision']"
|
||||
- "$RESPONSE['$.[0].status.revision']"
|
||||
- "$RESPONSE['$.[0].status.revision']"
|
||||
- "$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
$.[*].status.bucket:
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
$.[3].data.b: 4
|
||||
skip: Not implemented.
|
||||
|
||||
- name: verify_revision_2
|
||||
desc: Verify updated document count and revisions
|
||||
@ -82,22 +76,21 @@ tests:
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.[*].metadata.name:
|
||||
- global-1234
|
||||
- layering-policy
|
||||
- global-1234
|
||||
- region-1234
|
||||
- site-1234
|
||||
$.[*].status.revision:
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['update_single_document'].$RESPONSE['$.[0].status.revision']"
|
||||
$.[*].status.bucket:
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
$.[3].data.b: 5
|
||||
skip: Not implemented.
|
||||
|
||||
- name: verify_revision_3
|
||||
desc: Verify document deletion
|
||||
@ -105,8 +98,8 @@ tests:
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.[*].metadata.name:
|
||||
- global-1234
|
||||
- layering-policy
|
||||
- global-1234
|
||||
- site-1234
|
||||
$.[*].status.revision:
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
@ -117,27 +110,25 @@ tests:
|
||||
- mop
|
||||
- mop
|
||||
$.[2].data.b: 5
|
||||
skip: Not implemented.
|
||||
|
||||
- name: verify_revision_4
|
||||
desc: Verify rollback revision
|
||||
GET: /api/v1.0/revisions/$HISTORY['rollback'].$RESPONSE['$.[0].status.revision']/documents
|
||||
GET: /api/v1.0/revisions/$HISTORY['rollback'].$RESPONSE['$.[0].id']/documents
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.[*].metadata.name:
|
||||
- global-1234
|
||||
- layering-policy
|
||||
- global-1234
|
||||
- region-1234
|
||||
- site-1234
|
||||
$.[*].status.revision:
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['initialize'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['rollback'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['rollback'].$RESPONSE['$.[0].status.revision']"
|
||||
- "$HISTORY['rollback'].$RESPONSE['$.[0].id']"
|
||||
- "$HISTORY['rollback'].$RESPONSE['$.[0].id']"
|
||||
$.[*].status.bucket:
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
- mop
|
||||
$.[3].data.b: 4
|
||||
skip: Not implemented.
|
||||
|
@ -21,6 +21,8 @@ from deckhand.control import revision_diffing
|
||||
from deckhand.control import revision_documents
|
||||
from deckhand.control import revision_tags
|
||||
from deckhand.control import revisions
|
||||
from deckhand.control import rollback
|
||||
from deckhand.control import versions
|
||||
from deckhand.tests.unit import base as test_base
|
||||
|
||||
|
||||
@ -29,7 +31,7 @@ class TestApi(test_base.DeckhandTestCase):
|
||||
def setUp(self):
|
||||
super(TestApi, self).setUp()
|
||||
for resource in (buckets, revision_diffing, revision_documents,
|
||||
revision_tags, revisions):
|
||||
revision_tags, revisions, rollback, versions):
|
||||
resource_name = resource.__name__.split('.')[-1]
|
||||
resource_obj = mock.patch.object(
|
||||
resource, '%sResource' % resource_name.title().replace(
|
||||
@ -63,7 +65,10 @@ class TestApi(test_base.DeckhandTestCase):
|
||||
mock.call('/api/v1.0/revisions/{revision_id}/tags',
|
||||
self.revision_tags_resource()),
|
||||
mock.call('/api/v1.0/revisions/{revision_id}/tags/{tag}',
|
||||
self.revision_tags_resource())
|
||||
self.revision_tags_resource()),
|
||||
mock.call('/api/v1.0/rollback/{revision_id}',
|
||||
self.rollback_resource()),
|
||||
mock.call('/versions', self.versions_resource())
|
||||
])
|
||||
|
||||
mock_db_api.drop_db.assert_called_once_with()
|
||||
|
@ -101,6 +101,9 @@ class TestDbBase(base.DeckhandWithDBTestCase):
|
||||
def list_revisions(self):
|
||||
return db_api.revision_get_all()
|
||||
|
||||
def rollback_revision(self, revision_id):
|
||||
return db_api.revision_rollback(revision_id)
|
||||
|
||||
def _validate_object(self, obj):
|
||||
for attr in BASE_EXPECTED_FIELDS:
|
||||
if attr.endswith('_at'):
|
||||
|
81
deckhand/tests/unit/db/test_revision_rollback.py
Normal file
81
deckhand/tests/unit/db/test_revision_rollback.py
Normal file
@ -0,0 +1,81 @@
|
||||
# Copyright 2017 AT&T Intellectual Property. All other 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.
|
||||
|
||||
from deckhand.tests import test_utils
|
||||
from deckhand.tests.unit.db import base
|
||||
|
||||
|
||||
class TestRevisionRollback(base.TestDbBase):
|
||||
|
||||
def test_create_update_rollback(self):
|
||||
# Revision 1: Create 4 documents.
|
||||
payload = base.DocumentFixture.get_minimal_multi_fixture(count=4)
|
||||
bucket_name = test_utils.rand_name('bucket')
|
||||
created_documents = self.create_documents(bucket_name, payload)
|
||||
orig_revision_id = created_documents[0]['revision_id']
|
||||
|
||||
# Revision 2: Update the last document.
|
||||
payload[-1]['data'] = {'foo': 'bar'}
|
||||
self.create_documents(bucket_name, payload)
|
||||
|
||||
# Revision 3: rollback to revision 1.
|
||||
rollback_revision = self.rollback_revision(orig_revision_id)
|
||||
|
||||
self.assertEqual(3, rollback_revision['id'])
|
||||
self.assertEqual(
|
||||
[1, 1, 1, 3],
|
||||
[d['revision_id'] for d in rollback_revision['documents']])
|
||||
self.assertEqual(
|
||||
[1, 1, 1, 3],
|
||||
[d['orig_revision_id'] for d in rollback_revision['documents']])
|
||||
|
||||
rollback_documents = self.list_revision_documents(
|
||||
rollback_revision['id'])
|
||||
self.assertEqual([1, 1, 1, 3],
|
||||
[d['revision_id'] for d in rollback_documents])
|
||||
self.assertEqual([1, 1, 1, 3],
|
||||
[d['orig_revision_id'] for d in rollback_documents])
|
||||
|
||||
def test_create_update_delete_rollback(self):
|
||||
# Revision 1: Create 4 documents.
|
||||
payload = base.DocumentFixture.get_minimal_multi_fixture(count=4)
|
||||
bucket_name = test_utils.rand_name('bucket')
|
||||
created_documents = self.create_documents(bucket_name, payload)
|
||||
orig_revision_id = created_documents[0]['revision_id']
|
||||
|
||||
# Revision 2: Update the last document.
|
||||
payload[-1]['data'] = {'foo': 'bar'}
|
||||
self.create_documents(bucket_name, payload)
|
||||
|
||||
# Revision 3: Delete the third document.
|
||||
payload.pop(2)
|
||||
self.create_documents(bucket_name, payload)
|
||||
|
||||
# Rollback 4: rollback to revision 1.
|
||||
rollback_revision = self.rollback_revision(orig_revision_id)
|
||||
|
||||
self.assertEqual(4, rollback_revision['id'])
|
||||
self.assertEqual(
|
||||
[1, 1, 4, 4],
|
||||
[d['revision_id'] for d in rollback_revision['documents']])
|
||||
self.assertEqual(
|
||||
[1, 1, 4, 4],
|
||||
[d['orig_revision_id'] for d in rollback_revision['documents']])
|
||||
|
||||
rollback_documents = self.list_revision_documents(
|
||||
rollback_revision['id'])
|
||||
self.assertEqual([1, 1, 4, 4],
|
||||
[d['revision_id'] for d in rollback_documents])
|
||||
self.assertEqual([1, 1, 4, 4],
|
||||
[d['orig_revision_id'] for d in rollback_documents])
|
47
deckhand/tests/unit/db/test_revision_rollback_negative.py
Normal file
47
deckhand/tests/unit/db/test_revision_rollback_negative.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Copyright 2017 AT&T Intellectual Property. All other 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.
|
||||
|
||||
from deckhand import errors
|
||||
from deckhand.tests import test_utils
|
||||
from deckhand.tests.unit.db import base
|
||||
|
||||
|
||||
class TestRevisionRollbackNegative(base.TestDbBase):
|
||||
|
||||
def test_rollback_same_revision_raises_error(self):
|
||||
# Revision 1: Create 4 documents.
|
||||
payload = base.DocumentFixture.get_minimal_multi_fixture(count=4)
|
||||
bucket_name = test_utils.rand_name('bucket')
|
||||
created_documents = self.create_documents(bucket_name, payload)
|
||||
orig_revision_id = created_documents[0]['revision_id']
|
||||
|
||||
# Attempt to rollback to the latest revision, which should result
|
||||
# in an error.
|
||||
self.assertRaises(
|
||||
errors.InvalidRollback, self.rollback_revision, orig_revision_id)
|
||||
|
||||
def test_rollback_unchanged_revision_history_raises_error(self):
|
||||
# Revision 1: Create 4 documents.
|
||||
payload = base.DocumentFixture.get_minimal_multi_fixture(count=4)
|
||||
bucket_name = test_utils.rand_name('bucket')
|
||||
created_documents = self.create_documents(bucket_name, payload)
|
||||
orig_revision_id = created_documents[0]['revision_id']
|
||||
|
||||
# Create a 2nd revision that is a carbon-copy of 1st.
|
||||
self.create_documents(bucket_name, payload)
|
||||
|
||||
# Attempt to rollback to the 1st revision, which should result in an
|
||||
# error, as it is identical to the latest revision.
|
||||
self.assertRaises(
|
||||
errors.InvalidRollback, self.rollback_revision, orig_revision_id)
|
Loading…
x
Reference in New Issue
Block a user