diff --git a/docs/examples/fields.rst b/docs/examples/fields.rst index dfb32324..204dc6c4 100644 --- a/docs/examples/fields.rst +++ b/docs/examples/fields.rst @@ -575,9 +575,10 @@ Cursor managing iteration and addition of comments HistoryField ------------ -Returns a readonly RevisionCursor object that abstracts out retrieval and pagination of record history +Returns a readonly RevisionCursor object that abstracts out retrieval of record history. -Performs additional request(s) to history API endpoint as accessed +Each item in the RevisionCursor is a RecordRevision object, which performs additional requests to history API endpoints +as accessed. See the "Resources" section of the documentation for more information about the RecordRevision object. .. code-block:: python diff --git a/docs/examples/resources.rst b/docs/examples/resources.rst index dfd1a985..430532a1 100644 --- a/docs/examples/resources.rst +++ b/docs/examples/resources.rst @@ -55,6 +55,7 @@ on the report. ('Reference (Single-Select)', 'equals', target_record) ) + Keyword-searches can be performed by providing a `keywords` list parameter. All records with fields matching the provided keywords will be returned @@ -67,6 +68,7 @@ provided keywords will be returned ] ) + Available operators are just strings as shown above, but are made available as constants in the :mod:`~swimlane.core.search` module @@ -79,6 +81,7 @@ Available operators are just strings as shown above, but are made available as c ('Number Field', GTE, 0), ) + .. warning:: Report results are retrieved during on-demand during iteration, requesting record data from the API before each loop @@ -99,6 +102,7 @@ Available operators are just strings as shown above, but are made available as c limit=0 ) + To operate on large search results as records are returned from API or retrieve only partial results :class:`~swimlane.core.resources.report.Report` should be used instead. @@ -159,6 +163,7 @@ Any records not passing validation will cause the entire operation to fail. ... ) + .. note:: .. versionchanged:: 2.17.0 @@ -181,6 +186,7 @@ Delete multiple records at once. app.records.bulk_delete(record1, record2, record3) + Delete multiple records at once by filters using filter format from search. .. code-block:: python @@ -191,6 +197,7 @@ Delete multiple records at once by filters using filter format from search. ('Field_2', 'equals', value2) ) + Bulk Record Modify ^^^^^^^^^^^^^^^^^^^ @@ -217,6 +224,7 @@ Invalid field values will cause entire operation to fail. } ) + Bulk modify records by filter tuples without record instances. .. code-block:: python @@ -234,6 +242,7 @@ Bulk modify records by filter tuples without record instances. } ) + Use bulk modify to append, remove, or clear list field values .. code-block:: python @@ -250,6 +259,24 @@ Use bulk modify to append, remove, or clear list field values } ) + +Retrieve App Revisions +^^^^^^^^^^^^^^^^^^^^^^ + +Retrieve historical revisions of the application. + +.. code-block:: python + + # get all revisions + app_revisions = app.revisions.get_all() + + # get by revision number + app_revision = app.revisions.get(2) + + # get the historical version of the app + historical_app_version = app_revision.version + + Record ------ @@ -451,6 +478,24 @@ Record restrictions can be modified using :meth:`~swimlane.core.resources.record record.save() +Retrieve Record Revisions +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Retrieve historical revisions of the record. + +.. code-block:: python + + # get all revisions + record_revisions = record.revisions.get_all() + + # get by revision number + record_revision = record.revisions.get(2) + + # get the historical version of the app + # automatically retrieves the corresponding app revision to create the Record object + historical_record_version = record_revision.version + + UserGroup --------- @@ -519,3 +564,44 @@ To iterate over individual users in a group, use group.users property group = swimlane.groups.get(name='Everyone') for user in group.users: assert isinstance(user, User) + + +Revisions +--------- + +Revisions represent historical versions of another resource. Currently, App and Record revisions are supported. For more +details on how to retrieve revisions, see the "Retrieve App Revisions" and "Retrieve Record Revisions" sections above. + +Get Information About the Revision +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + revision = app.revisions.get(1) + + revision.modified_date # The date this revision was created. + revision.revision_number # The revision number of this revision. + revision.status # Indicates whether this revision is the current revision or a historical revision. + revision.user # The user that saved this revision. + + app = revision.version # returns an App or Record object representing the revision depending on revision type. + + # additional functions + text = str(revision) # returns name of the revision and the revision number as a string + json = revision.for_json # returns a dict containing modifiedDate, revisionNumber, and user keys/attribute values + + +Record Revisions +^^^^^^^^^^^^^^^^ + +Record revisions additionally have attributes containing information about their app. + +.. code-block:: python + + revision = record.revisions.get(1) + + revision.app_revision_number # The app revision number this record revision was created using. + + app = revision.app_version # Returns an App corresponding to the app_revision_number of this record revision. + + diff --git a/swimlane/core/adapters/__init__.py b/swimlane/core/adapters/__init__.py index 6abd3ac7..7cd3b4a2 100644 --- a/swimlane/core/adapters/__init__.py +++ b/swimlane/core/adapters/__init__.py @@ -5,3 +5,5 @@ from .report import ReportAdapter from .usergroup import UserAdapter, GroupAdapter from .helper import HelperAdapter +from .app_revision import AppRevisionAdapter +from .record_revision import RecordRevisionAdapter diff --git a/swimlane/core/adapters/app_revision.py b/swimlane/core/adapters/app_revision.py new file mode 100644 index 00000000..0664515c --- /dev/null +++ b/swimlane/core/adapters/app_revision.py @@ -0,0 +1,41 @@ +from swimlane.core.cache import check_cache +from swimlane.core.resolver import AppResolver +from swimlane.core.resources.app_revision import AppRevision +from swimlane.utils import one_of_keyword_only + + +class AppRevisionAdapter(AppResolver): + """Handles retrieval of Swimlane App Revision resources""" + + def get_all(self): + """ + Gets all app revisions. + + Returns: + AppRevision[]: Returns all AppRevisions for this Adapter's app. + """ + raw_revisions = self._swimlane.request('get', 'app/{0}/history'.format(self._app.id)).json() + return [AppRevision(self._swimlane, raw) for raw in raw_revisions] + + def get(self, revision_number): + """ + Gets a specific app revision. + + Supports resource cache + + Keyword Args: + revision_number (float): App revision number + + Returns: + AppRevision: The AppRevision for the given revision number. + """ + key_value = AppRevision.get_unique_id(self._app.id, revision_number) + return self.__get(app_id_revision=key_value) + + @check_cache(AppRevision) + @one_of_keyword_only('app_id_revision') + def __get(self, key, value): + """Underlying get method supporting resource cache.""" + app_id, revision_number = AppRevision.parse_unique_id(value) + app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json() + return AppRevision(self._swimlane, app_revision_raw) diff --git a/swimlane/core/adapters/record_revision.py b/swimlane/core/adapters/record_revision.py new file mode 100644 index 00000000..ba3991f8 --- /dev/null +++ b/swimlane/core/adapters/record_revision.py @@ -0,0 +1,35 @@ +from swimlane.core.resolver import AppResolver +from swimlane.core.resources.record_revision import RecordRevision + + +class RecordRevisionAdapter(AppResolver): + """Handles retrieval of Swimlane Record Revision resources""" + + def __init__(self, app, record): + super(RecordRevisionAdapter, self).__init__(app) + self.record = record + + def get_all(self): + """Get all revisions for a single record. + + Returns: + RecordRevision[]: All record revisions for the given record ID. + """ + raw_revisions = self._swimlane.request('get', + 'app/{0}/record/{1}/history'.format(self._app.id, self.record.id)).json() + return [RecordRevision(self._app, raw) for raw in raw_revisions] + + def get(self, revision_number): + """Gets a specific record revision. + + Keyword Args: + revision_number (float): Record revision number + + Returns: + RecordRevision: The RecordRevision for the given revision number. + """ + record_revision_raw = self._swimlane.request('get', + 'app/{0}/record/{1}/history/{2}'.format(self._app.id, + self.record.id, + revision_number)).json() + return RecordRevision(self._app, record_revision_raw) diff --git a/swimlane/core/fields/history.py b/swimlane/core/fields/history.py index 50e9f686..d50a89a4 100644 --- a/swimlane/core/fields/history.py +++ b/swimlane/core/fields/history.py @@ -1,7 +1,3 @@ -import pendulum - -from swimlane.core.resources.base import APIResource -from swimlane.core.resources.usergroup import UserGroup from .base import CursorField, FieldCursor, ReadOnly @@ -21,50 +17,8 @@ def _evaluate(self): return super(RevisionCursor, self)._evaluate() def _retrieve_revisions(self): - """Retrieve and populate Revision instances from history API endpoint""" - response = self._swimlane.request( - 'get', - 'history', - params={ - 'type': 'Records', - 'id': self._record.id - } - ) - - raw_revisions = response.json() - - return [Revision(self._record, raw) for raw in raw_revisions] - - -class Revision(APIResource): - """Encapsulates a single revision returned from a History lookup""" - - def __init__(self, record, raw): - super(Revision, self).__init__(record._swimlane, raw) - - self.record = record - - self.modified_date = pendulum.parse(self._raw['modifiedDate']) - self.revision_number = int(self._raw['revisionNumber']) - self.status = self._raw['status'] - - # UserGroupSelection, can't set as User without additional lookup - self.user = UserGroup(self._swimlane, self._raw['userId']) - - # Avoid circular imports - from swimlane.core.resources.record import Record - self.version = Record(self.record.app, self._raw['version']) - - def __str__(self): - return '{} ({})'.format(self.version, self.revision_number) - - def for_json(self): - """Return revision metadata""" - return { - 'modifiedDate': self._raw['modifiedDate'], - 'revisionNumber': self.revision_number, - 'user': self.user.for_json() - } + """Populate RecordRevision instances.""" + return self._record.revisions.get_all() class HistoryField(ReadOnly, CursorField): diff --git a/swimlane/core/resources/app.py b/swimlane/core/resources/app.py index 1f3db0b6..c300c56a 100644 --- a/swimlane/core/resources/app.py +++ b/swimlane/core/resources/app.py @@ -44,9 +44,10 @@ def __init__(self, swimlane, raw): self._keys_to_field_names[key] = name # Avoid circular import - from swimlane.core.adapters import RecordAdapter, ReportAdapter + from swimlane.core.adapters import RecordAdapter, ReportAdapter, AppRevisionAdapter self.records = RecordAdapter(self) self.reports = ReportAdapter(self) + self.revisions = AppRevisionAdapter(self) def __str__(self): return '{self.name} ({self.acronym})'.format(self=self) diff --git a/swimlane/core/resources/app_revision.py b/swimlane/core/resources/app_revision.py new file mode 100644 index 00000000..fb0d8a16 --- /dev/null +++ b/swimlane/core/resources/app_revision.py @@ -0,0 +1,43 @@ +from swimlane.core.resources.app import App +from swimlane.core.resources.revision_base import RevisionBase + + +class AppRevision(RevisionBase): + """ + Encapsulates a single revision returned from a History lookup. + + Attributes: + Attributes: + modified_date: The date this app revision was created. + revision_number: The revision number of this app revision. + status: Indicates whether this revision is the current revision or a historical revision. + user: The user that saved this revision of the record. + version: The App corresponding to the data contained in this app revision. + """ + + # Separator for unique ids. Unlikely to be found in application ids. Although technically we do not currently + # validate app ids in the backend for specific characters so this sequence could be found. + SEPARATOR = ' --- ' + + @staticmethod + def get_unique_id(app_id, revision_number): + """Returns the unique identifier for the given AppRevision.""" + return '{0}{1}{2}'.format(app_id, AppRevision.SEPARATOR, revision_number) + + @staticmethod + def parse_unique_id(unique_id): + """Returns an array containing two items: the app_id and revision number parsed from the given unique_id.""" + return unique_id.split(AppRevision.SEPARATOR) + + @property + def version(self): + """Returns an App from the _raw_version info in this app revision. Lazy loaded. Overridden from base class.""" + if not self._version: + self._version = App(self._swimlane, self._raw_version) + return self._version + + def get_cache_index_keys(self): + """Returns cache index keys for this AppRevision.""" + return { + 'app_id_revision': self.get_unique_id(self.version.id, self.revision_number) + } diff --git a/swimlane/core/resources/record.py b/swimlane/core/resources/record.py index 14d2e23f..6910e99b 100644 --- a/swimlane/core/resources/record.py +++ b/swimlane/core/resources/record.py @@ -59,6 +59,10 @@ def __init__(self, app, raw): self._fields = {} self.__premap_fields() + # avoid circular reference + from swimlane.core.adapters import RecordRevisionAdapter + self.revisions = RecordRevisionAdapter(app, self) + @property def app(self): return self.__app diff --git a/swimlane/core/resources/record_revision.py b/swimlane/core/resources/record_revision.py new file mode 100644 index 00000000..95e8bc29 --- /dev/null +++ b/swimlane/core/resources/record_revision.py @@ -0,0 +1,36 @@ +from swimlane.core.resources.record import Record +from swimlane.core.resources.revision_base import RevisionBase + + +class RecordRevision(RevisionBase): + """ + Encapsulates a single revision returned from a History lookup. + + Attributes: + app_revision_number: The app revision number this record revision was created using. + + Properties: + app_version: Returns an App corresponding to the app_revision_number of this record revision. + version: Returns a Record corresponding to the app_version and data contained in this record revision. + """ + def __init__(self, app, raw): + super(RecordRevision, self).__init__(app._swimlane, raw) + + self.__app_version = None + self._app = app + + self.app_revision_number = self._raw_version['applicationRevision'] + + @property + def app_version(self): + """The app revision corresponding to this record revision. Lazy loaded""" + if not self.__app_version: + self.__app_version = self._app.revisions.get(self.app_revision_number).version + return self.__app_version + + @property + def version(self): + """The record contained in this record revision. Lazy loaded. Overridden from base class.""" + if not self._version: + self._version = Record(self.app_version, self._raw_version) + return self._version diff --git a/swimlane/core/resources/revision_base.py b/swimlane/core/resources/revision_base.py new file mode 100644 index 00000000..55700009 --- /dev/null +++ b/swimlane/core/resources/revision_base.py @@ -0,0 +1,45 @@ +import pendulum + +from swimlane.core.resources.base import APIResource +from swimlane.core.resources.usergroup import UserGroup + + +class RevisionBase(APIResource): + """ + The base class representing a single revision returned from a History lookup. + + Attributes: + Attributes: + modified_date: The date this revision was created. + revision_number: The revision number of this revision. + status: Indicates whether this revision is the current revision or a historical revision. + user: The user that saved this revision. + """ + + def __init__(self, swimlane, raw): + super(RevisionBase, self).__init__(swimlane, raw) + + self.modified_date = pendulum.parse(self._raw['modifiedDate']) + self.revision_number = self._raw['revisionNumber'] + self.status = self._raw['status'] + + # UserGroupSelection, can't set as User without additional lookup + self.user = UserGroup(self._swimlane, self._raw['userId']) + + self._raw_version = self._raw['version'] + self._version = None + + def __str__(self): + return '{} ({})'.format(self.version, self.revision_number) + + @property + def version(self): + raise NotImplementedError + + def for_json(self): + """Return revision metadata""" + return { + 'modifiedDate': self._raw['modifiedDate'], + 'revisionNumber': self.revision_number, + 'user': self.user.for_json() + } diff --git a/tests/adapters/test_app_revision_adapter.py b/tests/adapters/test_app_revision_adapter.py new file mode 100644 index 00000000..b29cfe01 --- /dev/null +++ b/tests/adapters/test_app_revision_adapter.py @@ -0,0 +1,36 @@ +import mock + +from swimlane.core.resources.app_revision import AppRevision + + +class TestAppRevisionAdapter(object): + def test_get_all(self, mock_swimlane, mock_revision_app, raw_app_revision_data): + mock_response = mock.MagicMock() + raw_revision = raw_app_revision_data + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revisions = mock_revision_app.revisions.get_all() + + mock_swimlane.request.assert_called_with('get', 'app/{0}/history'.format(mock_revision_app.id,)) + + for idx, revision in enumerate(revisions): + assert isinstance(revision, AppRevision) + assert revision.revision_number is raw_app_revision_data[idx]['revisionNumber'] + + def test_get(self, mock_swimlane, mock_revision_app, raw_app_revision_data): + mock_response = mock.MagicMock() + raw_revision = raw_app_revision_data[0] + mock_response.json.return_value = raw_revision + mock_response.status_code = 200 + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revision = mock_revision_app.revisions.get(raw_revision['revisionNumber']) + + mock_swimlane.request.assert_called_with('get', 'app/{0}/history/{1}'.format(mock_revision_app.id, + raw_revision['revisionNumber'])) + + assert isinstance(revision, AppRevision) + assert revision.revision_number is raw_revision['revisionNumber'] + diff --git a/tests/adapters/test_record_revision_adapter.py b/tests/adapters/test_record_revision_adapter.py new file mode 100644 index 00000000..2b330203 --- /dev/null +++ b/tests/adapters/test_record_revision_adapter.py @@ -0,0 +1,37 @@ +import mock + +from swimlane.core.resources.record_revision import RecordRevision + + +class TestRecordRevisionAdapter(object): + def test_get_all(self, mock_swimlane, mock_revision_record, raw_record_revision_data): + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revisions = mock_revision_record.revisions.get_all() + + mock_swimlane.request.assert_called_with('get', + 'app/{0}/record/{1}/history'.format(mock_revision_record.app.id, + mock_revision_record.id)) + + for idx, revision in enumerate(revisions): + assert isinstance(revision, RecordRevision) + assert revision.revision_number is raw_record_revision_data[idx]['revisionNumber'] + + def test_get(self, mock_swimlane, mock_revision_record, raw_record_revision_data): + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = raw_record_revision_data[0] + + with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): + revision = mock_revision_record.revisions.get(3) + + mock_swimlane.request.assert_called_with('get', 'app/{0}/record/{1}/history/{2}'.format( + mock_revision_record.app.id, + mock_revision_record.id, + 3)) + + assert isinstance(revision, RecordRevision) + assert revision.revision_number is raw_record_revision_data[0]['revisionNumber'] diff --git a/tests/conftest.py b/tests/conftest.py index db9f9e01..0217541b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3799,3 +3799,6 @@ def random_mock_user(mock_swimlane): 'passwordResetRequired': False, 'roles': [], 'userName': 'admin'}) + + +pytest_plugins = ['conftest_revisions'] diff --git a/tests/conftest_revisions.py b/tests/conftest_revisions.py new file mode 100644 index 00000000..9d191720 --- /dev/null +++ b/tests/conftest_revisions.py @@ -0,0 +1,709 @@ +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.app_revision import AppRevision +from swimlane.core.resources.record import Record +from swimlane.core.resources.record_revision import RecordRevision + + +@pytest.fixture +def mock_revision_app(mock_swimlane): + """An app with two fields: value selection and history.""" + return App(mock_swimlane, { + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + "5cd46fce433cf20015dd4727" + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "permissions":{ + "$type":"Core.Models.Security.PermissionMatrix, Core" + }, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + }) + + +@pytest.fixture +def mock_revision_record(mock_revision_app): + """A record from the mock_revision_app.""" + return Record(mock_revision_app, { + "$type":"Core.Models.Record.Record, Core", + "name":"PHT-1", + "allowed":[ + + ], + "trackingId":1.0, + "trackingFull":"PHT-1", + "applicationId":"a34xbNOoo2P3ivyjY", + "isNew":False, + "values":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01":{ + "$type":"Core.Models.Record.ValueSelection, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "value":"value1" + }, + "5cd46fce433cf20015dd46f7":"PHT-1" + }, + "repeatFieldCurrentValues":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision":3.0, + "locked":False, + "comments":{ + "$type":"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate":"2019-05-09T18:23:39.881Z", + "modifiedDate":"2019-05-09T18:24:20.027Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "sessionTimeSpent":0, + "totalTimeSpent":0, + "timeTrackingEnabled":True, + "isHangfireCreatedAndUnpersisted":False, + "infiniteLoopFlag":False, + "id":"aPkMtjrzV54YxyS59", + "disabled":False + }) + + +@pytest.fixture +def raw_record_revision_data(): + """Raw record revision data for mock_revision_record.""" + return [ + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 3.0, + "status": "current", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:24:20.027Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe3ca4746cda7e41a33", + "value": "value1" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "valuesDocument": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe3ca4746cda7e41a33", + "value": "value1" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 3.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:24:20.027Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + }, + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 2.0, + "status": "historical", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:23:48.539Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe379be68acd2039061", + "value": "value2" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 2.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:23:48.539Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + }, + { + "$type": "Core.Models.History.Revision, Core", + "revisionNumber": 1.0, + "status": "historical", + "userId": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedDate": "2019-05-09T18:23:39.881Z", + "version": { + "$type": "Core.Models.Record.Record, Core", + "name": "PHT-1", + "allowed": [ + + ], + "trackingId": 1.0, + "trackingFull": "PHT-1", + "applicationId": "a34xbNOoo2P3ivyjY", + "referencedRecordIds": [ + + ], + "referencedByIds": [ + + ], + "isNew": False, + "values": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib", + "agy01": { + "$type": "Core.Models.Record.ValueSelection, Core", + "id": "5cd46fe302c8ff905e042cc0", + "value": "value3" + }, + "5cd46fce433cf20015dd46f7": "PHT-1" + }, + "repeatFieldCurrentValues": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "actionsExecuted": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib" + }, + "visualizations": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "applicationRevision": 2.0, + "locked": False, + "comments": { + "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], System.Private.CoreLib]], System.Private.CoreLib" + }, + "createdDate": "2019-05-09T18:23:39.881Z", + "modifiedDate": "2019-05-09T18:23:39.881Z", + "createdByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "modifiedByUser": { + "$type": "Core.Models.Utilities.UserGroupSelection, Core", + "id": "aBZ3vZmPSsd6l4GLj", + "name": "admin" + }, + "sessionTimeSpent": 0, + "totalTimeSpent": 0, + "timeTrackingEnabled": True, + "isHangfireCreatedAndUnpersisted": False, + "infiniteLoopFlag": False, + "id": "aPkMtjrzV54YxyS59", + "disabled": False + } + } + ] + + +@pytest.fixture +def mock_record_revisions(mock_revision_app, raw_record_revision_data): + """All record revisions for mock_revision_record.""" + return [RecordRevision(mock_revision_app, raw) for raw in raw_record_revision_data] + + +@pytest.fixture +def raw_app_revision_data(): + """Raw app revision data for mock_revision_app""" + return [ + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":3.0, + "status":"current", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:24:02.642Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":2.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"agy01", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fdac741d5d54a2b5c32", + "row":1, + "col":1, + "sizex":2.0, + "sizey":0.0 + }, + { + "$type":"Core.Models.Layouts.FieldLayout, Core", + "fieldId":"axtys", + "helpTextType":"none", + "helpText":" ", + "layoutType":"field", + "id":"5cd46fddb36d66d467d4abe6", + "row":1, + "col":3, + "sizex":2.0, + "sizey":0.0 + } + ], + "fields":[ + { + "$type":"Core.Models.Fields.ValuesListField, Core", + "values":[ + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe3ca4746cda7e41a33", + "name":"value1", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe379be68acd2039061", + "name":"value2", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + }, + { + "$type":"Core.Models.Fields.ValuesList.ValuesListValues, Core", + "id":"5cd46fe302c8ff905e042cc0", + "name":"value3", + "selected":False, + "description":"", + "otherText":False, + "otherTextDescription":"", + "otherTextDefaultValue":"", + "otherTextRequired":"False" + } + ], + "controlType":"select", + "selectionType":"single", + "id":"agy01", + "name":"Selection", + "key":"selection", + "fieldType":"valuesList", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.History.HistoryField, Core", + "id":"axtys", + "name":"History", + "key":"history", + "fieldType":"history", + "required":False, + "readOnly":False, + "supportsMultipleOutputMappings":False + }, + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":1.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:23:08.766Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + }, + { + "$type":"Core.Models.History.Revision, Core", + "revisionNumber":1.0, + "status":"historical", + "userId":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "version":{ + "$type":"Core.Models.Application.Application, Core", + "acronym":"PHT", + "trackingFieldId":"5cd46fce433cf20015dd46f7", + "layout":[ + + ], + "fields":[ + { + "$type":"Core.Models.Fields.TrackingField, Core", + "prefix":"PHT-", + "id":"5cd46fce433cf20015dd46f7", + "name":"Tracking Id", + "key":"tracking-id", + "fieldType":"tracking", + "readOnly":True, + "supportsMultipleOutputMappings":False + } + ], + "maxTrackingId":0.0, + "workspaces":[ + + ], + "createWorkspace":False, + "createdDate":"2019-05-09T18:22:06.181Z", + "createdByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "modifiedDate":"2019-05-09T18:22:06.181Z", + "modifiedByUser":{ + "$type":"Core.Models.Utilities.UserGroupSelection, Core", + "id":"aBZ3vZmPSsd6l4GLj", + "name":"admin" + }, + "timeTrackingEnabled":False, + "id":"a34xbNOoo2P3ivyjY", + "name":"Pydriver History Test", + "disabled":False + } + } + ] + +@pytest.fixture +def mock_app_revisions(mock_swimlane, raw_app_revision_data): + """All record revisions for mock_revision_record.""" + return [AppRevision(mock_swimlane, raw) for raw in raw_app_revision_data] diff --git a/tests/fields/test_history.py b/tests/fields/test_history.py index c5cf1a48..ff6d6e14 100644 --- a/tests/fields/test_history.py +++ b/tests/fields/test_history.py @@ -1,488 +1,62 @@ -from datetime import datetime - import mock +import pytest +import pendulum -from swimlane.core.fields.history import RevisionCursor, Revision +from swimlane.core.fields.history import RevisionCursor +from swimlane.core.resources.record_revision import RecordRevision from swimlane.core.resources.usergroup import UserGroup +from swimlane.core.resources.app import App +from swimlane.core.resources.record import Record + + +@pytest.fixture +def mock_history_swimlane(mock_swimlane, raw_app_revision_data, raw_record_revision_data): + def request_mock(method, endpoint, params=None): + """Mocks swimlane.request function, returns the API responses for the history endpoints.""" + if 'record' in endpoint: + return_value = raw_record_revision_data + else: + revision = float(endpoint.split('/')[3]) + return_value = next(x for x in raw_app_revision_data if x['revisionNumber'] == revision) + + mock_response = mock.MagicMock() + mock_response.json.return_value = return_value + return mock_response -raw_revision_data = [ - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:17.065Z', - 'revisionNumber': 3.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6bee81f119baf40a659b9': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.984Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bb9ad6e4d34e8f557b2b': True, - '58e7bbba9bc82babdd655653': True, - '58e7bd86ad9ba1f93ef4afae': True, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'ac1oa': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 5, - 'dow': 3, - 'h': 23, - 'm': 42, - 'mm': 4, - 'q': 2, - 'w': 14, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.999Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a0k7v': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bae8c61082e377cc4d82', - 'value': 'Open'}, - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5v09': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bafd83f418637cf91c3e', - 'value': 'ESA Alert'}, - 'a5yg3': '3172', - 'a6if8': '[{"lifetime": "60", "rid": "4531291", "payload": "7808", "size": "11956", "service": "0", "eth.src": "00:0C:29:5B:98:5E", "feed.name": "Swimlane_Feed", "medium": "1", "ip.dst": "178.238.235.143", "org.dst": "Contabo GmbH", "sessionid": "4534669", "latdec.dst": "51.2993", "eth.type": "2048", "ip.src": "10.0.1.183", "domain.dst": "contabo.host", "eth.dst": "00:50:56:A7:4D:79", "did": "packdec1", "longdec.dst": "9.491", "packets": "122", "streams": "2", "country.dst": "Germany", "time": 1491593904, "ip.proto": "1"}]', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'aasnf': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bb144121bd73d8f53845', - 'value': 'High'}, - 'ac1oa': '2017-04-05T23:42:54Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}, - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:16.998Z', - 'revisionNumber': 2.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bb9ad6e4d34e8f557b2b': True, - '58e7bbba9bc82babdd655653': True, - '58e7bd86ad9ba1f93ef4afae': True, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'ac1oa': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 5, - 'dow': 3, - 'h': 23, - 'm': 42, - 'mm': 4, - 'q': 2, - 'w': 14, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.954Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a0k7v': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bae8c61082e377cc4d82', - 'value': 'Open'}, - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5v09': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bafd83f418637cf91c3e', - 'value': 'ESA Alert'}, - 'a5yg3': '3172', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'aasnf': {'$type': 'Core.Models.Record.ValueSelection, Core', - 'id': '58e7bb144121bd73d8f53845', - 'value': 'High'}, - 'ac1oa': '2017-04-05T23:42:54Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}, - {'$type': 'Core.Models.History.Revision, Core', - 'modifiedDate': '2017-04-10T16:26:16.953Z', - 'revisionNumber': 1.0, - 'status': 'historical', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'version': {'$type': 'Core.Models.Record.Record, Core', - 'actionsExecuted': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e557c8b81ce3e2c6515fd1': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.389Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6ac5c88412c4acda3d66c': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.396Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6aca5293267c35188ea37': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.401Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6acc099097a98678ccfcf': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.407Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c62f257f2c4994b84140': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.412Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c6536afac95743437254': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.419Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e6c83415177d0ad68ea961': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.426Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}, - '58e7bddc6ed1ceda2950b621': { - '$type': 'Core.Models.Workflow.History.ActionHistory, Core', - 'dateExecuted': '2017-04-10T16:26:16.437Z', - 'userId': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}}}, - 'allowed': [], - 'applicationId': '58e4bb4407637a0e4c4f9873', - 'applicationRevision': 72.0, - 'comments': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.Comments, Core]], mscorlib]], mscorlib'}, - 'createdByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'createdDate': '2017-04-10T16:26:16.486Z', - 'disabled': False, - 'id': '58ebb22807637a02d4a14bd6', - 'isNew': False, - 'linkedData': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - 'a99ut': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}, - 'aiir8': {'$type': 'Core.Models.Record.DateTimeParsed, Core', - 'd': 10, - 'dow': 1, - 'h': 16, - 'm': 26, - 'mm': 4, - 'q': 2, - 'w': 15, - 'y': 2017}}, - 'modifiedByUser': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'modifiedDate': '2017-04-10T16:26:16.486Z', - 'referencedByIds': [], - 'referencedRecordIds': [], - 'sessionTimeSpent': 0, - 'timeTrackingEnabled': True, - 'totalTimeSpent': 0, - 'trackingFull': 'RA-7', - 'trackingId': 7.0, - 'values': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib', - '58e4bb4407637a0e4c4f9875': 'RA-7', - 'a04rv': '10.0.1.184:56005:3172', - 'a0g40': 'Alert Action', - 'a18qr': 'User.Activity.Failed Logins', - 'a1r3v': 'Logon', - 'a3rvw': '3172', - 'a4l9p': 'logdec1', - 'a5yg3': '3172', - 'a7gm5': '<14>Apr 5 23:42:54 localhost CEF:0|RSA|Security Analytics ESA|10.4|Module_58e40ea9e4b070dfd2a59ad5_Alert|Alert Action|7|rt=2017-04-05T23:42Z id=890eb2b1-2d90-46a5-b1ed-6e58b408ed83 source=10.0.1.184:56005:3172 action=authentication failure;; client=sshd;; device_class=Unix;; device_ip=10.0.1.178;; device_type=crossbeamc;; did=logdec1;; ec_activity=Logon;; ec_outcome=Failure;; ec_subject=User;; ec_theme=Authentication;; esa_time=1491435774280;; event_cat_name=User.Activity.Failed Logins;; event_source_id=10.0.1.184:56005:3172;; header_id=0006;; host_src=178.238.235.143;; level=4;; medium=32;; msg_id=000103;; rid=3172;; sessionid=3172;; size=175;; time=1491435771;; user_dst=user;; username=0;;\x00', - 'a7ira': 'User', - 'a8vjm': 'Failure', - 'a990o': '7', - 'a99ut': '2017-04-10T16:26:17.457Z', - 'acqdq': '1491435774280', - 'af967': '890eb2b1-2d90-46a5-b1ed-6e58b408ed83', - 'afbbv': '', - 'afff3': {'$type': 'Core.Models.Utilities.UserGroupSelection, Core', - 'id': '58de1d1c07637a0264c0ca6a', - 'name': 'admin'}, - 'ahziu': '178.238.235.143', - 'ai1zz': '4', - 'aiir8': '2017-04-10T16:26:16.389Z', - 'aio1w': 'sshd', - 'aokp5': 'Unix', - 'apf9i': '10.0.1.178', - 'arnd6': 'authentication failure', - 'arok4': 'crossbeamc', - 'aw9gd': 'ip.src=10.0.1.178 || ip.dst=10.0.1.178', - 'awmtq': 'Authentication', - 'axdvr': '175'}, - 'visualizations': { - '$type': 'System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Collections.Generic.List`1[[Core.Models.Record.VisualizationData, Core]], mscorlib]], mscorlib'}}}] + with mock.patch.object(mock_swimlane, 'request', side_effect=request_mock): + yield mock_swimlane -def test_history_field(mock_record, mock_swimlane): - history = mock_record['History'] - assert isinstance(history, RevisionCursor) +@pytest.fixture +def mock_history_record(mock_history_swimlane, mock_revision_app, mock_revision_record): + app = App(mock_history_swimlane, mock_revision_app._raw) + return Record(app, mock_revision_record._raw) - mock_response = mock.MagicMock() - mock_response.json.return_value = raw_revision_data - with mock.patch.object(mock_swimlane, 'request', return_value=mock_response): +@pytest.fixture +def history(mock_history_record): + return mock_history_record['History'] + +class TestHistory(object): + def test_revision_cursor(self, history): + assert isinstance(history, RevisionCursor) + + def test_num_revisions(self, history): # Get number of revisions num_revisions = len(history) assert num_revisions == 3 - # Iterate backwards over revisions + def test_revision_str(self, history): + for revision in history: + assert str(revision) == 'PHT-1 ({0})'.format(revision.revision_number) + + def test_revisions(self, history, mock_history_record): for idx, revision in enumerate(history): - assert isinstance(revision, Revision) - assert str(revision) == 'RA-7 ({})'.format(revision.revision_number) - assert isinstance(revision.modified_date, datetime) + assert isinstance(revision, RecordRevision) + assert isinstance(revision.app_version, App) + assert isinstance(revision.modified_date, pendulum.datetime) assert isinstance(revision.user, UserGroup) - assert revision.version.id == mock_record.id - assert num_revisions - revision.revision_number == idx - + assert isinstance(revision.version, Record) + assert revision.version.id == mock_history_record.id + assert len(history) - revision.revision_number == idx diff --git a/tests/resources/test_app.py b/tests/resources/test_app.py index 64df6c8e..e4fbab63 100644 --- a/tests/resources/test_app.py +++ b/tests/resources/test_app.py @@ -1,75 +1,74 @@ import copy - import pytest +from swimlane.core.adapters import AppRevisionAdapter from swimlane.exceptions import UnknownField -def test_repr(mock_app): - assert repr(mock_app) == '' - - -def test_get_field_definitions(mock_app): - """Test retrieving field definitions by name, key, or id and UnknownField error with recommendation(s)""" - field_def = mock_app.get_field_definition_by_name('Numeric') - assert field_def['name'] == 'Numeric' - - assert field_def == mock_app.get_field_definition_by_id(field_def['id']) - - # Test UnknownField metadata - # Suggestions for potential typo - try: - mock_app.get_field_definition_by_name('Muneric') - except UnknownField as error: - assert error.app is mock_app - assert error.field_name == 'Muneric' - assert error.similar_field_names == ['Numeric'] - assert 'Numeric' in str(error) - else: - raise RuntimeError - - # Same behavior for get_by_id - try: - mock_app.get_field_definition_by_id('aqkf3') - except UnknownField as error: - assert error.app is mock_app - assert error.field_name == 'aqkf3' - assert error.similar_field_names == ['aqkg3', 'ayqk6', 'aqc6k'] - else: - raise RuntimeError - - -def test_resolve_field_name(mock_app): - """Test that fields keys + names resolve to field name or None if not found""" - # Field name - assert mock_app.resolve_field_name('Action') == 'Action' - # Key - assert mock_app.resolve_field_name('action-key') == 'Action' - # Missing - assert mock_app.resolve_field_name('unknown field name/key') is None - - -def test_get_field_definition_by_name_resolves_keys(mock_app): - """Test that field keys are auto-resolved to field names when getting definition by name""" - assert mock_app.get_field_definition_by_name('action-key') is mock_app.get_field_definition_by_name('Action') - - -def test_equality(mock_app): - """Test equality and inequality of Apps by name""" - app_clone = copy.copy(mock_app) - assert app_clone == mock_app - assert not app_clone != mock_app - - app_clone.name = mock_app.name + 'test' - assert app_clone != mock_app - assert not app_clone == mock_app - - -def test_comparison(mock_app, mock_record): - with pytest.raises(TypeError): - mock_app < mock_record - - app_clone = copy.copy(mock_app) - app_clone.name = mock_app.name + 'test' - assert mock_app < app_clone - assert app_clone > mock_app +class TestApp(object): + def test_repr(self, mock_app): + assert repr(mock_app) == '' + + def test_get_field_definitions(self, mock_app): + """Test retrieving field definitions by name, key, or id and UnknownField error with recommendation(s)""" + field_def = mock_app.get_field_definition_by_name('Numeric') + assert field_def['name'] == 'Numeric' + + assert field_def == mock_app.get_field_definition_by_id(field_def['id']) + + # Test UnknownField metadata + # Suggestions for potential typo + try: + mock_app.get_field_definition_by_name('Muneric') + except UnknownField as error: + assert error.app is mock_app + assert error.field_name == 'Muneric' + assert error.similar_field_names == ['Numeric'] + assert 'Numeric' in str(error) + else: + raise RuntimeError + + # Same behavior for get_by_id + try: + mock_app.get_field_definition_by_id('aqkf3') + except UnknownField as error: + assert error.app is mock_app + assert error.field_name == 'aqkf3' + assert error.similar_field_names == ['aqkg3', 'ayqk6', 'aqc6k'] + else: + raise RuntimeError + + def test_resolve_field_name(self, mock_app): + """Test that fields keys + names resolve to field name or None if not found""" + # Field name + assert mock_app.resolve_field_name('Action') == 'Action' + # Key + assert mock_app.resolve_field_name('action-key') == 'Action' + # Missing + assert mock_app.resolve_field_name('unknown field name/key') is None + + def test_get_field_definition_by_name_resolves_keys(self, mock_app): + """Test that field keys are auto-resolved to field names when getting definition by name""" + assert mock_app.get_field_definition_by_name('action-key') is mock_app.get_field_definition_by_name('Action') + + def test_equality(self, mock_app): + """Test equality and inequality of Apps by name""" + app_clone = copy.copy(mock_app) + assert app_clone == mock_app + assert not app_clone != mock_app + + app_clone.name = mock_app.name + 'test' + assert app_clone != mock_app + assert not app_clone == mock_app + + def test_comparison(self, mock_app, mock_record): + with pytest.raises(TypeError): + mock_app < mock_record + + app_clone = copy.copy(mock_app) + app_clone.name = mock_app.name + 'test' + assert mock_app < app_clone + assert app_clone > mock_app + + def test_revisions(self, mock_app): + assert isinstance(mock_app.revisions, AppRevisionAdapter) diff --git a/tests/resources/test_app_revision.py b/tests/resources/test_app_revision.py new file mode 100644 index 00000000..d3301e06 --- /dev/null +++ b/tests/resources/test_app_revision.py @@ -0,0 +1,49 @@ +import pendulum +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.app_revision import AppRevision +from swimlane.core.resources.usergroup import UserGroup + + +@pytest.fixture +def mock_ar_raw_app_revision(raw_app_revision_data): + return raw_app_revision_data[0] + + +@pytest.fixture +def mock_ar_app_revision(mock_swimlane, mock_ar_raw_app_revision): + return AppRevision(mock_swimlane, mock_ar_raw_app_revision) + + +class TestAppRevision(object): + def test_constructor(self, mock_swimlane, mock_ar_app_revision, mock_ar_raw_app_revision): + assert str(mock_ar_app_revision.modified_date) == str(pendulum.parse(mock_ar_raw_app_revision['modifiedDate'])) + assert mock_ar_app_revision.revision_number is mock_ar_raw_app_revision['revisionNumber'] + assert mock_ar_app_revision.status is mock_ar_raw_app_revision['status'] + assert str(mock_ar_app_revision.user) == str(UserGroup(mock_swimlane, mock_ar_raw_app_revision['userId'])) + + def test_version(self, mock_ar_app_revision): + version = mock_ar_app_revision.version + + assert isinstance(version, App) + assert version._raw is mock_ar_app_revision._raw['version'] + + def test_get_cache_index(self, mock_ar_app_revision): + keys = mock_ar_app_revision.get_cache_index_keys() + + assert len(keys) is 1 + assert 'app_id_revision' in keys + assert keys['app_id_revision'] == 'a34xbNOoo2P3ivyjY --- 3.0' + + def test_for_json(self, mock_ar_app_revision): + json = mock_ar_app_revision.for_json() + + assert 'modifiedDate' in json + assert 'revisionNumber' in json + assert 'user' in json + + def test_str(self, mock_ar_app_revision): + text = str(mock_ar_app_revision) + + assert text == 'Pydriver History Test (PHT) (3.0)' diff --git a/tests/resources/test_record.py b/tests/resources/test_record.py index 763345c9..aae486c4 100644 --- a/tests/resources/test_record.py +++ b/tests/resources/test_record.py @@ -4,6 +4,7 @@ import mock import pytest +from swimlane.core.adapters import RecordRevisionAdapter from swimlane.core.resources.record import Record, record_factory from swimlane.exceptions import UnknownField, ValidationError @@ -204,3 +205,6 @@ def test_for_json_dumps(self, mock_record): """Test .for_json() with all fields passes json.dumps() with no specific assertions beyond no exceptions""" result = mock_record.for_json() json.dumps(result) + + def test_revisions(self, mock_record): + assert isinstance(mock_record.revisions, RecordRevisionAdapter) diff --git a/tests/resources/test_record_revision.py b/tests/resources/test_record_revision.py new file mode 100644 index 00000000..f8836788 --- /dev/null +++ b/tests/resources/test_record_revision.py @@ -0,0 +1,73 @@ +import mock +import pendulum +import pytest + +from swimlane.core.resources.app import App +from swimlane.core.resources.record import Record +from swimlane.core.resources.record_revision import RecordRevision +from swimlane.core.resources.usergroup import UserGroup + + +@pytest.fixture +def mock_rr_raw_record_revision(raw_record_revision_data): + return raw_record_revision_data[0] + + +@pytest.fixture +def mock_rr_app_revision(mock_app_revisions, mock_rr_raw_record_revision): + app_revision_number = mock_rr_raw_record_revision['version']['applicationRevision'] + return next(x for x in mock_app_revisions if x.revision_number == app_revision_number) + + +@pytest.fixture +def mock_rr_app(mock_rr_app_revision, mock_revision_app): + mock_revisions = mock.MagicMock() + mock_revisions.get.return_value = mock_rr_app_revision + + mock_revisions_property = mock.PropertyMock(return_value=mock_revisions) + + # mocks App.revisions as a property so it doesn't need to be called like a method to get the mock_revisions + # sets it up so mock_rr_app.revisions.get() returns mock_rr_app_revision + with mock.patch.object(mock_revision_app, 'revisions', new_callable=mock_revisions_property): + yield mock_revision_app + + +@pytest.fixture +def mock_rr_record_revision(mock_rr_raw_record_revision, mock_rr_app): + return RecordRevision(mock_rr_app, mock_rr_raw_record_revision) + + +class TestRecordRevision(object): + def test_constructor(self, mock_swimlane, mock_rr_record_revision, mock_rr_raw_record_revision): + assert mock_rr_record_revision.app_revision_number is mock_rr_raw_record_revision['version'][ + 'applicationRevision'] + assert str(mock_rr_record_revision.modified_date) == str( + pendulum.parse(mock_rr_raw_record_revision['modifiedDate'])) + assert mock_rr_record_revision.revision_number is mock_rr_raw_record_revision['revisionNumber'] + assert mock_rr_record_revision.status is mock_rr_raw_record_revision['status'] + assert str(mock_rr_record_revision.user) == str(UserGroup(mock_swimlane, mock_rr_raw_record_revision['userId'])) + + def test_app_version(self, mock_rr_record_revision, mock_rr_app_revision, mock_rr_app): + app_version = mock_rr_record_revision.app_version + mock_rr_app.revisions.get.assert_called_with(mock_rr_record_revision.app_revision_number) + assert isinstance(app_version, App) + assert app_version is mock_rr_app_revision.version + + def test_version(self, mock_rr_app_revision, mock_rr_record_revision): + version = mock_rr_record_revision.version + + assert isinstance(version, Record) + assert version.app is mock_rr_app_revision.version + assert version._raw is mock_rr_record_revision._raw['version'] + + def test_for_json(self, mock_rr_record_revision): + json = mock_rr_record_revision.for_json() + + assert 'modifiedDate' in json + assert 'revisionNumber' in json + assert 'user' in json + + def test_str(self, mock_rr_record_revision): + text = str(mock_rr_record_revision) + + assert text == 'PHT-1 (3.0)'