diff --git a/pytests/common_test_server.py b/pytests/common_test_server.py new file mode 100644 index 0000000..c3e0648 --- /dev/null +++ b/pytests/common_test_server.py @@ -0,0 +1,38 @@ + +importedOk=True + +# Allow for tests that can't don't import this to use this file still +try: + import v1pysdk +except ImportError: + importedOk=False + + +class PublicTestServerConnection(): + username = 'admin' + password = 'admin' + address = 'www14.v1host.com' + instance = 'v1sdktesting' + scheme = 'https' + # must match scheme + "://" + address + "/" + instance + instance_url = 'https://www14.v1host.com/v1sdktesting' + token = '1.VdeWXQVNdY0yVpYexTtznCxcWTQ=' + + def __init__(): + pass + + @staticmethod + def getV1Meta(): + """Creates a V1Meta object from the default configuration and returns it + """ + # If we couldn't import the v1pysdk, we can't create the object + if not importedOk: + return None + else: + return v1pysdk.V1Meta( + address = PublicTestServerConnection.address, + instance = PublicTestServerConnection.instance, + scheme = 'https', + username = PublicTestServerConnection.username, + password = PublicTestServerConnection.password, + ) diff --git a/pytests/conftest.py b/pytests/conftest.py new file mode 100644 index 0000000..d56b6c1 --- /dev/null +++ b/pytests/conftest.py @@ -0,0 +1,6 @@ +import pytest +import common_test_server + +@pytest.fixture +def v1(): + return common_test_server.PublicTestServerConnection.getV1Meta() diff --git a/pytests/test_common_setup.py b/pytests/test_common_setup.py new file mode 100644 index 0000000..6aa4479 --- /dev/null +++ b/pytests/test_common_setup.py @@ -0,0 +1,54 @@ +import math + +import pytest + +import v1pysdk + + +class TestV1CommonSetup(): + def test_initial_create_story(self, v1): + """Creates a very simple story and returns the object""" + #v1StoryName = self.getUniqueString() + v1StoryName = "tests.query_tests.TestV1Query.test_find_query-1" + defaultEstimate = 1.0 + reference = "http://test.com" + + scope = v1.Scope.select('Name').page(size=1) + defaultScope = None + if len(scope) > 0: + defaultScope = scope.first() + + epic = v1.Epic.select('Name').page(size=1) + defaultSuper = None + if len(epic) > 0: + defaultSuper = epic.first() + + # build a filter string that exactly matches what we've set above + baseFilterStr = "Reference='" + reference + "'&DetailEstimate='" + str(defaultEstimate) + "'&" + if defaultScope: + baseFilterStr += "Scope.Name='" + defaultScope.Name + "'&" + if defaultSuper: + baseFilterStr += "Super.Name='" + defaultSuper.Name + "'&" + baseFilterStr += "Name='" + v1StoryName + "'" + + newStory = None + try: + newStory = v1.Story.create( + Name = v1StoryName, + Scope = defaultScope, + Super = defaultSuper, + DetailEstimate = defaultEstimate, + Reference = reference, + ) + except Exception as e: + pytest.fail("Error creating new story: {0}".format(str(e))) + + #Perform a readback using the constructed filter to make sure the item's on the server + + createdItems = v1.Story.select('Name').filter(baseFilterStr) + for t in createdItems: # run query, but don't throw an exception if nothing is returned + pass + + assert(len(createdItems) > 0, "Created item can't be queried") + + return newStory diff --git a/pytests/test_connects.py b/pytests/test_connects.py new file mode 100755 index 0000000..7d3d85e --- /dev/null +++ b/pytests/test_connects.py @@ -0,0 +1,214 @@ +import sys +if sys.version_info >= (3,0): + from urllib.error import HTTPError +else: + from urllib2 import HTTPError + +# try the old version, then fallback to the new one +try: + from xml.etree import ElementTree + from xml.etree.ElementTree import parse, fromstring, Element +except ImportError: + from elementtree import ElementTree + from elementtree.ElementTree import parse, fromstring, Element + +from v1pysdk.client import * +from v1pysdk import V1Meta +from common_test_server import PublicTestServerConnection + +import pytest + +class TestV1Connection(): + def test_connect(self): + username = PublicTestServerConnection.username + password = PublicTestServerConnection.password + address = PublicTestServerConnection.address + instance = PublicTestServerConnection.instance + + server = V1Server(address=address, username=username, password=password,instance=instance) + # The story names, but limit to only the first result so we don't get inundated with results + code, body = server.fetch('/rest-1.v1/Data/Story?sel=Name&page=1,0') + + elem = fromstring(body) + assert(elem.tag == 'Assets') + + def test_meta_connect_instance_url(self): + v1 = None + try: + v1 = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + username = PublicTestServerConnection.username, + password = PublicTestServerConnection.password, + ) + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + + try: + items = v1.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from connection: {0}".format(str(e))) + + def test_meta_connect_instance_and_address(self): + v1 = None + try: + v1 = V1Meta( + address = PublicTestServerConnection.address, + instance = PublicTestServerConnection.instance, + username = PublicTestServerConnection.username, + password = PublicTestServerConnection.password, + ) + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + try: + items = v1.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from connection: {0}".format(str(e))) + + def test_meta_connect_instance_url_overrides_separate(self): + v1 = None + address = '' + instance = None + + try: + v1 = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + address = address, + instance = instance, + username = PublicTestServerConnection.username, + password = PublicTestServerConnection.password, + ) + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + + try: + items = v1.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + + def test_meta_connect_oauth(self): + v1 = None + try: + v1 = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + #no username + password = PublicTestServerConnection.token, + use_password_as_token=True, + ) + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + + try: + items = v1.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from connection: {0}".format(str(e))) + + def test_meta_connect_oauth_ignores_username(self): + v1 = None + username = '' + + try: + v1 = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + username = username, + password = PublicTestServerConnection.token, + use_password_as_token=True, + ) + except Exception as e: + pytest.fail("Error trying to create connection: {0}".format(str(e))) + + try: + items = v1.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from connection: {0}".format(str(e))) + + def test_connect_fails_when_invalid(self): + v1bad = None + username = '' + password = '' + + try: + v1bad = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + username = username, + password = password, + use_password_as_token=False, + ) + # we have to try to use it to get it to connect and fail + items = v1bad.Story.select('Name').page(size=1) + items.first() #run the query + except HTTPError as e: + assert(str(e.code) == '401', "Connection failed for reasons other than authorization") + else: + pytest.fail("Connection succeeded with bad credentials.") + + def test_reconnect_succeeds_after_invalid(self): + v1bad = None + username = '' + password = '' + + try: + v1bad = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + username = username, + password = password, + use_password_as_token=False, + ) + items = v1bad.Story.select('Name').page(size=1) + items.first() #run the query + except HTTPError as e: + assert(str(e.code) == '401', "Connection failed for reasons other than authorization") + else: + pytest.fail("First connection succeeded with bad credentials, cannot continue test") + + v1good = None + + # Connect correctly first + try: + v1good = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + password = PublicTestServerConnection.token, + use_password_as_token=True, + ) + items = v1good.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from good connection: {0}".format(str(e))) + + def test_reconnect_fails_when_invalid(self): + v1good = None + + # Connect correctly first + try: + v1good = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + password = PublicTestServerConnection.token, + use_password_as_token=True, + ) + items = v1good.Story.select('Name').page(size=1) + items.first() #run the query + except Exception as e: + pytest.fail("Error running query from good connection: {0}".format(str(e))) + + v1bad = None + username = '' + + password = '' + + try: + v1bad = V1Meta( + instance_url = PublicTestServerConnection.instance_url, + username = username, + password = password, + use_password_as_token=False, + ) + items = v1bad.Story.select('Name').page(size=1) + items.first() #run the query + except HTTPError as e: + assert(str(e.code) == "401", "Connection failed for reasons other than authorization") + else: + assert(str(e.code) == "401", "Second connection failed for reasons other than authorization") diff --git a/pytests/test_creations.py b/pytests/test_creations.py new file mode 100644 index 0000000..6886779 --- /dev/null +++ b/pytests/test_creations.py @@ -0,0 +1,10 @@ +import v1pysdk +import common_test_server +from test_common_setup import TestV1CommonSetup + +class TestV1Create(TestV1CommonSetup): + def test_create_story(self, v1): + """Creates a very simple story""" + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + # common setup already does this and tests the creation, it just needs a V1 instance to work on + self.test_initial_create_story(v1) diff --git a/pytests/test_operations.py b/pytests/test_operations.py new file mode 100644 index 0000000..d0d52b4 --- /dev/null +++ b/pytests/test_operations.py @@ -0,0 +1,52 @@ +""" +from testtools import TestCase +from testtools.assertions import assert_that +from testtools.content import text_content +from testtools.matchers import Equals +""" + +import v1pysdk +from common_test_server import PublicTestServerConnection +import common_test_server +import test_common_setup +import pytest + + +class TestV1Operations(test_common_setup.TestV1CommonSetup): + def test_quick_close_and_reopen(self): + """Creates a story, quick closes it, then reopens it, then quick closes again""" + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + newStory = None + try: + newStory = self.test_initial_create_story(v1) + except Exception as e: + pytest.fail("Unable to setup by creating initial test story: {0}".format(str(e))) + + assert(newStory.IsClosed == False, "New story created already closed, cannot test") + + try: + newStory.QuickClose() + except Exception as e: + pytest.fail("Error while quick closing story: {0}".format(str(e))) + + try: + v1.commit() + except Exception as e: + pytest.fail("Error while syncing commits after close {0}".format(str(e))) + + assert(newStory.IsClosed == True, "Story didn't close when QuickClose() was called") + + try: + newStory.Reactivate() + except Exception as e: + pytest.fail("Error while reactivating story {0}".format(str(e))) + + try: + v1.commit() + except Exception as e: + pytest.fail("Error while syncing commits after reactivation") + assert(newStory.IsClosed == 'false', "Story didn't re-open when Reactivate() was called") + + # "cleanup" by closing the story + newStory.QuickClose() + v1.commit() diff --git a/pytests/test_queries.py b/pytests/test_queries.py new file mode 100644 index 0000000..e8b16d6 --- /dev/null +++ b/pytests/test_queries.py @@ -0,0 +1,184 @@ +import v1pysdk +import common_test_server +import test_common_setup +import pytest + +class TestV1Query(test_common_setup.TestV1CommonSetup): + def test_select_story_as_generic_asset(self): + """Queries up to 5 story assets""" + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + querySucceeded=True + size=0 + item=None + try: + items = v1.AssetType.select('Name').where(Name='Story').page(size=5) + item = items.first() #triggers actual query to happen + size = len(items) + except: + querySucceeded=False + + # test assumes there is at least 1 Story on the test server + assert(querySucceeded == True) + assert((0 < size < 6) == True) + + def test_select_story(self): + """Queries up to 5 stories""" + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + querySucceeded=True + size=0 + item=None + try: + items = v1.Story.select('Name').page(size=5) + item = items.first() #triggers actual query to happen + size = len(items) + except: + querySucceeded=False + + # test assumes there is at least 1 Story on the test server + assert(querySucceeded == True) + assert((0 < size < 6) == True) + + def test_select_epic(self): + """Queries up to 5 Epics, called Portfolios in the GUI. + In order to create a new Story, we must be able to query for an Epic we want to put it under, + and pass that returned Epic object as the Super of the new Story. This confirms the Epic query + part always works. + """ + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + querySucceeded=True + size=0 + item=None + try: + items = v1.Epic.select('Name').page(size=5) + item = items.first() #triggers actual query to happen + size = len(items) + except: + querySucceeded=False + + # test assumes there is at least 1 Portfolio Item on the test server + assert(querySucceeded == True) + assert((0 < size < 6) == True) + + + def test_select_scope(self): + """Queries up to 5 Scopes, called Projects in the GUI. + In order to create a new Story, we must be able to query for a Scope we want to put it under, + and pass that returned Scope object as the Scope of the new Story. This confirms the Scope query + part always works. + """ + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + querySucceeded=True + size=0 + item=None + try: + items = v1.Scope.select('Name').page(size=5) + item = items.first() #triggers actual query to happen + size = len(items) + except: + querySucceeded=False + + # test assumes there is at least 1 Project on the test server + assert(querySucceeded == True) + assert((0 < size < 6) == True) + + def test_select_task(self): + """Queries up to 5 Tasks. + """ + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + querySucceeded=True + size=0 + item=None + try: + items = v1.Task.select('Name').page(size=5) + item = items.first() #triggers actual query to happen + size = len(items) + except: + querySucceeded=False + + # test assumes there is at least 1 Task on the test server + assert(querySucceeded == True) + assert((0 < size < 6) == True) + + def test_non_default_query(self): + """Queries an attribute that's not retrieved by default from a Story so it will requery for the specific + attribute that was requested. + """ + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + failedFetchCreateDate = False + failedFetchName = False + s = v1.Story.select('Name').page(size=1) + try: + junk = s.CreateDate # fetched on demand, not default + except: + failedFetchCreateDate = True + + assert(failedFetchCreateDate == False) + + try: + junk = s.Name # fetched by default on initial query + except: + failedFetchName = True + + assert(failedFetchName == False) + + + def test_sum_query(self): + """Queries for a summation of values of a numeric field (Actuals.Value) across a set of assests (Tasks). + """ + with common_test_server.PublicTestServerConnection.getV1Meta() as v1: + foundActuals=False + exceptionReached=False + try: + tasks = v1.Task.select('Name','Actuals.Value.@Sum').page(size=30) + tasks.first() #perform the actual query + if len(tasks) <= 0: + pytest.skip("Test server contains no Tasks") + return + else: + for t in tasks: + if 'Actuals.Value.@Sum' in t.data: + foundActuals=True + break + except: + exceptionReached=True + else: + if not foundActuals: + pytest.skip("Test server Tasks contained no Actuals.Value's") + return + + assert(exceptionReached == False) + + def test_find_query(self): + """Creates a story, then does a find to see if it can be located by a partial name from a separate + connection instance. + """ + searchName="" + exceptionReached=False + with common_test_server.PublicTestServerConnection.getV1Meta() as v1create: + createdStory = self.test_initial_create_story(v1create) + + # make a search term that's just one character shorter + searchName = createdStory.Name[:-1] + + with common_test_server.PublicTestServerConnection.getV1Meta() as v1find: + findItems = None + findItem = None + size = 0 + firstName = "" + try: + findItems = v1find.Story.select('Name').find(text=searchName, field='Name') + findItem = findItems.first() #actually run the query + size = len(findItems) + firstName = findItem.Name + except Exception as e: + raise e + #exceptions here are almost always because the query failed to work right + exceptionReached=True + else: + # at the very least we should have found the one we based the search string on + assert(size > 0) + # results need to contain the string we searched for + assert((searchName in firstName) == True) + + assert(exceptionReached == False) + diff --git a/pytests/test_string_utils.py b/pytests/test_string_utils.py new file mode 100755 index 0000000..fed578f --- /dev/null +++ b/pytests/test_string_utils.py @@ -0,0 +1,32 @@ +from v1pysdk.string_utils import split_attribute + +import pytest + +class TestStringUtils(): + + def test_split_attribute(self): + assert((['[testing]]'] == split_attribute('[testing]]'))) + assert(['[[testing]'] == split_attribute('[[testing]')) + assert(['testing','a','sentence','is','difficult'] == split_attribute('testing.a.sentence.is.difficult')) + assert(['testing','[a.sentence]','is','difficult'] == split_attribute('testing.[a.sentence].is.difficult')) + assert(['testing[.a.sentence]','is', 'difficult'] == split_attribute('testing[.a.sentence].is.difficult')) + assert(['testing','a[.sentence.]is','difficult'] == split_attribute('testing.a[.sentence.]is.difficult')) + assert(['testing','a','sentence','is','difficult]'] == split_attribute('testing.a.sentence.is.difficult]')) + assert(['testing', 'a','sentence','is',']difficult'] == split_attribute('testing.a.sentence.is.]difficult')) + assert(['[testing.a.sentence.is]','difficult'] == split_attribute('[testing.a.sentence.is].difficult')) + assert(['[testing.][a.sentence.is.difficult]'] == split_attribute('[testing.][a.sentence.is.difficult]')) + assert(['[testing]','[a]','[sentence]','[is]','[difficult]'] == + split_attribute('[testing].[a].[sentence].[is].[difficult]')) + assert(['testing','[[a.sentence.]is]','difficult'] == + split_attribute('testing.[[a.sentence.]is].difficult')) + assert(["History[Status.Name='Done']"] == split_attribute("History[Status.Name='Done']")) + assert(["ParentMeAndUp[Scope.Workitems.@Count='2']"] == + split_attribute("ParentMeAndUp[Scope.Workitems.@Count='2']") ) + assert(["Owners","OwnedWorkitems[ChildrenMeAndDown=$]","@DistinctCount"] == + split_attribute("Owners.OwnedWorkitems[ChildrenMeAndDown=$].@DistinctCount") ) + assert(["Workitems[ParentAndUp[Scope=$].@Count='1']"] == + split_attribute("Workitems[ParentAndUp[Scope=$].@Count='1']") ) + assert(["RegressionPlan","RegressionSuites[AssetState!='Dead']","TestSets[AssetState!='Dead']","Environment", "@DistinctCount"] + == split_attribute("RegressionPlan.RegressionSuites[AssetState!='Dead'].TestSets[AssetState!='Dead'].Environment.@DistinctCount") ) + assert(["Scope","ChildrenMeAndDown","Workitems:Story[ChildrenMeAndDown.ToDo.@Sum!='0.0']","Estimate","@Sum"] + == split_attribute("Scope.ChildrenMeAndDown.Workitems:Story[ChildrenMeAndDown.ToDo.@Sum!='0.0'].Estimate.@Sum") )