From 36ae8cd0cd119c9731c139f1a6279a4bd36eefb4 Mon Sep 17 00:00:00 2001 From: Lon Hohberger Date: Fri, 19 Jan 2024 17:29:27 -0500 Subject: [PATCH] tests: Add tests for JIRA input translation & update TODO --- TODO | 6 ++ jirate/jira_input.py | 16 ++- jirate/tests/__init__.py | 212 ++++++++++++++++++++++++++----------- jirate/tests/test_input.py | 138 ++++++++++++++++++++++++ 4 files changed, 311 insertions(+), 61 deletions(-) create mode 100644 jirate/tests/test_input.py diff --git a/TODO b/TODO index fd90e63..2aa317b 100644 --- a/TODO +++ b/TODO @@ -12,6 +12,12 @@ JIRA: - searching/listing with component(s)? - sorting by column in ls/search output - Editing components / other component fields +- Date/Datetime processing on input: right now, it's just a string. + e.g. 23-Oct-2023, 2023-Oct-23, 2023-10-23 should all work for dates, + for example +- All table outputs should have configurable field widths +- All table outputs should be able to avoid truncating to the width + of the terminal Attachments - add File attachments diff --git a/jirate/jira_input.py b/jirate/jira_input.py index 41e1425..24932a9 100644 --- a/jirate/jira_input.py +++ b/jirate/jira_input.py @@ -39,7 +39,10 @@ def in_owc(value): 'priority': in_name, 'version': in_name, 'securitylevel': in_name, - 'option-with-child': in_owc + 'option-with-child': in_owc, + 'user': in_string # When setting array, you specify 'name': name + # When setting assignee, you just give the name + # as a string } @@ -132,6 +135,17 @@ def transmogrify_value(value, field_info): # Channelling ... Calvin def transmogrify_input(field_definitions, **args): + """ Translate text input into something Jira understands natively when + pasting to the API. + + Parameters: + field_definitions: Create/Update metadata or /field dictionary + args: User-provided key,value pairs (strings) + + Returns: + Updated dictionary with JIRA field IDs and values corresponding to + metadata in field_definitions + """ drop_fields = ['attachment', 'reporter', 'issuelinks'] # These are not set during create/update simple_fields = ['project', 'issuetype'] # Don't process these fields at all output = {} diff --git a/jirate/tests/__init__.py b/jirate/tests/__init__.py index e624761..7e70d88 100644 --- a/jirate/tests/__init__.py +++ b/jirate/tests/__init__.py @@ -8,6 +8,53 @@ fake_fields = [ + {'clauseNames': ['priority', 'Priority'], + 'custom': False, + 'id': 'priority', + 'name': 'Priority', + 'schema': {'system': 'priority', 'type': 'priority'}, + 'allowedValues': [ + {'id': 101, + 'name': 'Blocker'}, + {'id': 102, + 'name': 'Critical'}, + {'id': 103, + 'name': 'Major'}, + {'id': 104, + 'name': 'Normal'}, + {'id': 105, + 'name': 'Minor'}, + {'id': 106, + 'name': 'Undefined'}], + 'defaultValue': {'id': 104, 'name': 'Normal'}, + 'hasDefaultValue': True, + 'operations': ['set'] + }, + {'clauseNames': ['description', 'Description'], + 'custom': False, + 'name': 'Description', + 'id': 'description', + 'required': True, + 'operations': ['set'], + 'schema': {'type': 'string'}, + }, + {'clauseNames': ['components', 'Component/s'], + 'custom': False, + 'id': 'components', + 'name': 'Component/s', + 'schema': {'system': 'items', 'type': 'array', 'items': 'component'}, + 'allowedValues': [ + {'id': 1001, + 'name': 'python'}, + {'id': 1002, + 'name': 'kernel'}, + {'id': 1003, + 'name': 'glibc'}, + {'id': 1004, + 'name': 'porkchop'}], + 'operations': ['add', 'set', 'remove'], + 'required': True + }, {'clauseNames': ['cf[1234567]', 'Fixed in Build'], 'custom': True, 'id': 'customfield_1234567', @@ -17,6 +64,7 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:textfield', 'customId': 1234567, 'type': 'string'}, + 'operations': ['set'], 'searchable': True}, {'clauseNames': ['cf[1234568]', 'Score'], 'custom': True, @@ -27,6 +75,7 @@ 'schema': {'custom': 'org.jboss.labs.jira.plugin.jboss-custom-field-types-plugin:jbonlynumber', 'customId': 1234568, 'type': 'number'}, + 'operations': ['set'], 'searchable': True}, {'clauseNames': ['cf[1234569]', 'Array of Options'], 'custom': True, @@ -38,6 +87,15 @@ 'customId': 1234569, 'items': 'option', 'type': 'array'}, + 'allowedValues': [ + {'value': 'One', + 'id': '1'}, + {'value': 'Two', + 'id': '2'}, + {'value': 'Three', + 'id': '3'} + ], + 'operations': ['add', 'set', 'remove'], 'searchable': True}, {'clauseNames': ['cf[1234570]', 'Array of Versions'], 'custom': True, @@ -49,6 +107,13 @@ 'customId': 1234570, 'items': 'version', 'type': 'array'}, + 'allowedValues': [ + {'name': '1.0.1', + 'id': '11'}, + {'name': '1.0.2', + 'id': '12'} + ], + 'operations': ['add', 'set', 'remove'], 'searchable': True}, {'clauseNames': ['cf[1234571]', 'Array of Users'], 'custom': True, @@ -60,6 +125,7 @@ 'customId': 1234571, 'items': 'user', 'type': 'array'}, + 'operations': ['add', 'set', 'remove'], 'searchable': True}, {'clauseNames': ['cf[1234572]', 'Array of Strings'], 'custom': True, @@ -71,6 +137,7 @@ 'customId': 1234572, 'items': 'string', 'type': 'array'}, + 'operations': ['add', 'set', 'remove'], 'searchable': True}, {'clauseNames': ['cf[1234573]', 'Array of Groups'], 'custom': True, @@ -92,6 +159,7 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234574, 'type': 'any'}, + 'operations': ['add', 'set', 'remove'], 'searchable': True}, {'clauseNames': ['cf[1234575]', 'Date Value'], 'custom': True, @@ -102,6 +170,7 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234575, 'type': 'date'}, + 'operations': ['set'], 'searchable': True}, {'clauseNames': ['cf[1234576]', 'Datetime Value'], 'custom': True, @@ -112,6 +181,7 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234576, 'type': 'datetime'}, + 'operations': ['set'], 'searchable': True}, {'clauseNames': ['cf[1234577]', 'Related Issue'], 'custom': True, @@ -122,6 +192,7 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234577, 'type': 'issuelinks'}, + 'operations': ['set'], 'searchable': True}, {'clauseNames': ['cf[1234578]', 'Option Value'], 'custom': True, @@ -129,6 +200,15 @@ 'name': 'Option Value', 'navigable': True, 'orderable': True, + 'operations': ['set'], + 'allowedValues': [ + {'value': 'One', + 'id': '1'}, + {'value': 'Two', + 'id': '2'}, + {'value': 'Three', + 'id': '3'} + ], 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234578, 'type': 'option'}, @@ -142,6 +222,14 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234579, 'type': 'option-with-child'}, + 'allowedValues': [ + {'value': 'One', + 'id': '1'}, + {'value': 'Two', + 'id': '2'}, + {'value': 'Three', + 'id': '3'} + ], 'searchable': True}, {'clauseNames': ['cf[1234580]', 'User Value'], 'custom': True, @@ -162,11 +250,20 @@ 'schema': {'custom': 'com.atlassian.jira.plugin.system.customfieldtypes:multiselect', 'customId': 1234581, 'type': 'version'}, + 'allowedValues': [ + {'name': '1.0.1', + 'id': '11'}, + {'name': '1.0.2', + 'id': '12'} + ], 'searchable': True} ] +fake_metadata = {val['id']: val for val in fake_fields} + + fake_issues = { 'TEST-1': {'expand': 'renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations', 'fields': {'aggregateprogress': {'progress': 0, 'total': 0}, @@ -199,9 +296,7 @@ 'subtask': False}, 'labels': ['label1', 'label2'], 'lastViewed': '2023-11-22T15:51:50.281+0000', - 'priority': {'id': '10200', - 'name': 'Normal', - 'self': 'https://domain.com/rest/api/2/priority/10200'}, + 'priority': {'id': '104', 'name': 'Normal'}, 'progress': {'progress': 0, 'total': 0}, 'project': {'id': '1234', 'key': 'TEST', @@ -312,63 +407,60 @@ 'subtask': False}, 'labels': ['label2'], 'lastViewed': '2023-11-22T15:51:50.281+0000', - 'priority': { - 'id': '10200', - 'name': 'Normal', - 'self': 'https://domain.com/rest/api/2/priority/10200'}, - 'progress': {'progress': 0, 'total': 0}, - 'project': {'id': '1234', - 'key': 'TEST', - 'name': 'Test Project', - 'projectCategory': {'description': 'Test Projects', - 'id': '1073', - 'name': 'Projecto', - 'self': 'https://domain.com/rest/api/2/projectCategory/1073'}, - 'projectTypeKey': 'software', - 'self': 'https://domain.com/rest/api/2/project/1234'}, - 'reporter': {'active': True, - 'displayName': 'Pork Chop', - 'emailAddress': 'pchop@domai.com', - 'key': 'JIRAUSER1', - 'name': 'pchop@domain.com', - 'self': 'https://domain.com/rest/api/2/user?username=pchop%40domain.com', - 'timeZone': 'Asia/Kolkata'}, - 'resolution': None, - 'resolutiondate': None, - 'security': None, - 'status': {'description': 'Initial creation status. ', - 'id': '10', - 'name': 'New', - 'self': 'https://domain.com/rest/api/2/status/10016', - 'statusCategory': {'colorName': 'default', - 'id': 2, - 'key': 'new', - 'name': 'To Do', - 'self': 'https://domain.com/rest/api/2/statuscategory/2'}}, - 'subtasks': [], - 'summary': 'Test 1', - 'timeestimate': None, - 'timeoriginalestimate': None, - 'timespent': None, - 'timetracking': {}, - 'updated': '2023-11-30T15:06:39.875+0000', - 'versions': [{'archived': False, - 'description': '', - 'id': '6', - 'name': 'version-6', - 'released': False, - 'self': 'https://domain.com/rest/api/2/version/6'}], - 'votes': {'hasVoted': False, - 'self': 'https://domain.com/rest/api/2/issue/TEST-1/votes', - 'votes': 0}, - 'watches': {'isWatching': False, - 'self': 'https://domain.com/rest/api/2/issue/TEST-1/watchers', - 'watchCount': 2}, - 'worklog': {'maxResults': 20, - 'startAt': 0, - 'total': 0, - 'worklogs': []}, - 'workratio': -1}, + 'priority': {'id': 104, 'name': 'Normal'}, + 'progress': {'progress': 0, 'total': 0}, + 'project': {'id': '1234', + 'key': 'TEST', + 'name': 'Test Project', + 'projectCategory': {'description': 'Test Projects', + 'id': '1073', + 'name': 'Projecto', + 'self': 'https://domain.com/rest/api/2/projectCategory/1073'}, + 'projectTypeKey': 'software', + 'self': 'https://domain.com/rest/api/2/project/1234'}, + 'reporter': {'active': True, + 'displayName': 'Pork Chop', + 'emailAddress': 'pchop@domai.com', + 'key': 'JIRAUSER1', + 'name': 'pchop@domain.com', + 'self': 'https://domain.com/rest/api/2/user?username=pchop%40domain.com', + 'timeZone': 'Asia/Kolkata'}, + 'resolution': None, + 'resolutiondate': None, + 'security': None, + 'status': {'description': 'Initial creation status. ', + 'id': '10', + 'name': 'New', + 'self': 'https://domain.com/rest/api/2/status/10016', + 'statusCategory': {'colorName': 'default', + 'id': 2, + 'key': 'new', + 'name': 'To Do', + 'self': 'https://domain.com/rest/api/2/statuscategory/2'}}, + 'subtasks': [], + 'summary': 'Test 1', + 'timeestimate': None, + 'timeoriginalestimate': None, + 'timespent': None, + 'timetracking': {}, + 'updated': '2023-11-30T15:06:39.875+0000', + 'versions': [{'archived': False, + 'description': '', + 'id': '6', + 'name': 'version-6', + 'released': False, + 'self': 'https://domain.com/rest/api/2/version/6'}], + 'votes': {'hasVoted': False, + 'self': 'https://domain.com/rest/api/2/issue/TEST-1/votes', + 'votes': 0}, + 'watches': {'isWatching': False, + 'self': 'https://domain.com/rest/api/2/issue/TEST-1/watchers', + 'watchCount': 2}, + 'worklog': {'maxResults': 20, + 'startAt': 0, + 'total': 0, + 'worklogs': []}, + 'workratio': -1}, 'id': '1000002', 'key': 'TEST-2', 'self': 'https://domain.com/rest/api/2/issue/1000002'} diff --git a/jirate/tests/test_input.py b/jirate/tests/test_input.py new file mode 100644 index 0000000..b13fedd --- /dev/null +++ b/jirate/tests/test_input.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +from jirate.tests import fake_metadata +from jirate.jira_input import transmogrify_input + +import os +import time + +import pytest + +os.environ['TZ'] = 'America/New_York' +time.tzset() + + +def test_trans_string_reflective(): + # identity + inp = {'description': 'This is a description'} + out = {'description': 'This is a description'} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_string_nym(): + # We translate to JIRA field IDs + inp = {'Description': 'This is a description'} + out = {'description': 'This is a description'} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_priority(): + inp = {'Priority': 'blocker'} + out = {'priority': {'name': 'Blocker'}} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_custom_basic(): + inp = {'fixed_in_build': 'build123'} + out = {'customfield_1234567': 'build123'} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_number(): + inp = {'score': '1'} + out = {'customfield_1234568': 1.0} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_options(): + inp = {'array_of_options': 'One,Two'} + out = {'customfield_1234569': [{'value': 'One'}, {'value': 'Two'}]} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_missing_option(): + with pytest.raises(ValueError): + transmogrify_input(fake_metadata, **{'option_value': 'One,Two,Three'}) + + +def test_trans_array_versions(): + inp = {'array_of_versions': '1.0.1,1.0.2'} + out = {'customfield_1234570': [{'name': '1.0.1'}, {'name': '1.0.2'}]} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_users(): + inp = {'array_of_users': 'user1,user2'} + out = {'customfield_1234571': [{'name': 'user1'}, {'name': 'user2'}]} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_strings(): + inp = {'array_of_strings': '"string one","string 2"'} + out = {'customfield_1234572': ['string one', 'string 2']} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_groups(): + inp = {'array_of_groups': 'group1,group2'} + out = {'customfield_1234573': [{'name': 'group1'}, {'name': 'group2'}]} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_array_any(): + inp = {'any_value': 'one,2'} + out = {'customfield_1234574': 'one,2'} + + assert transmogrify_input(fake_metadata, **inp) == out + + +# TODO: Fixup date and datetime; these should take various input formats +# but are strings + + +def test_trans_option(): + inp = {'option_value': 'one'} + out = {'customfield_1234578': {'value': 'One'}} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_missing_option(): + with pytest.raises(ValueError): + transmogrify_input(fake_metadata, **{'option_value': 'Four'}) + + +def test_trans_missing_field(): + inp = {'not_a_real_field': 'one'} + out = {} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_user_value(): + inp = {'User Value': 'user1'} + out = {'customfield_1234580': 'user1'} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_version_value(): + inp = {'version_value': '1.0.1'} + out = {'customfield_1234581': {'name': '1.0.1'}} + + assert transmogrify_input(fake_metadata, **inp) == out + + +def test_trans_missing_version(): + with pytest.raises(ValueError): + transmogrify_input(fake_metadata, **{'version_value': '999'})