From 04e40c8e5de4ecc2839f3897b0d0ba97e595c3c6 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Sat, 11 Feb 2017 19:03:42 -0500 Subject: [PATCH 1/7] Lots of stuff to support aasheauth.models.AASHEUser.is_member --- membersuite_api_client/exceptions.py | 10 ++ membersuite_api_client/memberships/models.py | 2 +- .../memberships/services.py | 27 ++++- .../organizations/models.py | 5 +- membersuite_api_client/security/models.py | 56 ++++++++- membersuite_api_client/security/services.py | 53 +++++--- .../tests/test_memberships.py | 23 +++- membersuite_api_client/tests/test_security.py | 114 +++++++++++++++--- membersuite_api_client/tests/test_utils.py | 0 9 files changed, 252 insertions(+), 38 deletions(-) create mode 100644 membersuite_api_client/tests/test_utils.py diff --git a/membersuite_api_client/exceptions.py b/membersuite_api_client/exceptions.py index e8aa1a4..483a5ac 100644 --- a/membersuite_api_client/exceptions.py +++ b/membersuite_api_client/exceptions.py @@ -25,6 +25,16 @@ class LoginToPortalError(MemberSuiteAPIError): result_type = "LoginToPortalResult" +class LogoutError(MemberSuiteAPIError): + + result_type = "LogoutResult" + + class ExecuteMSQLError(MemberSuiteAPIError): result_type = "ExecuteMSQLResult" + + +class GetPrimaryMembershipError(MemberSuiteAPIError): + + result_type = "GetPrimaryMembershipResult" diff --git a/membersuite_api_client/memberships/models.py b/membersuite_api_client/memberships/models.py index ab26981..d4471fa 100644 --- a/membersuite_api_client/memberships/models.py +++ b/membersuite_api_client/memberships/models.py @@ -1,7 +1,7 @@ class Membership(object): def __init__(self, membership): - """ Create a Membership model from MemberSuite Member object + """ Create a Membership model from MemberSuite Membership object """ self.id = membership["ID"] self.owner = membership["Owner"] diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index 7faeec0..a0e5f10 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -4,10 +4,11 @@ http://api.docs.membersuite.com/#References/Objects/Membership.htm """ +from zeep.exceptions import TransportError from .models import Membership, MembershipProduct +from ..exceptions import GetPrimaryMembershipError from ..utils import convert_ms_object -from zeep.exceptions import TransportError class MembershipService(object): @@ -146,3 +147,27 @@ def package_membership_products(self, msql_result): product = MembershipProduct(sane_obj) product_list.append(product) return product_list + + +def get_primary_membership(organization_id, entity_id, client): + if not client.session_id: + client.request_session() + + concierge_request_header = client.construct_concierge_header( + url=("http://membersuite.com/contracts/IConciergeAPIService/" + "GetPrimaryMembership")) + + result = client.client.service.GetPrimaryMembership( + _soapheaders=[concierge_request_header], + membershipOrganizationID=organization_id, + entityID=entity_id) + + get_primary_membership_result = ( + result["body"]["GetPrimaryMembershipResult"]) + + if get_primary_membership_result["Success"]: + membership = get_primary_membership_result["ResultValue"]["Membership"] + + return Membership(membersuite_object_data=membership) + else: + raise GetPrimaryMembershipError(result=result) diff --git a/membersuite_api_client/organizations/models.py b/membersuite_api_client/organizations/models.py index 4a51904..04ddac4 100644 --- a/membersuite_api_client/organizations/models.py +++ b/membersuite_api_client/organizations/models.py @@ -4,8 +4,11 @@ def __init__(self, org): """Create an Organization model from MemberSuite Organization object """ self.account_num = org["ID"] + self.id = self.account_num self.membersuite_id = org["LocalID"] + self.local_id = self.membersuite_id self.org_name = org["Name"] + self.name = self.org_name self.picklist_name = org["SortName"] or '' address = org["Mailing_Address"] @@ -46,4 +49,4 @@ def __init__(self, org_type): '6faf90e4-01f3-c0f1-4593-0b3c3ca7ff6c': 'Deceased', '6faf90e4-01f3-c7ad-174c-0b3c52b7f497': 'Defunct', '6faf90e4-01f3-cd50-ffed-0b3c3ca7f4fd': 'Retired', -} \ No newline at end of file +} diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py index f99603e..dd723f3 100644 --- a/membersuite_api_client/security/models.py +++ b/membersuite_api_client/security/models.py @@ -4,6 +4,8 @@ from ..exceptions import ExecuteMSQLError from ..models import MemberSuiteObject +from ..memberships import services as membership_services +from ..organizations.models import Organization from ..utils import convert_ms_object @@ -36,6 +38,9 @@ def __str__(self): session_id=self.session_id)) def get_username(self): + """Return a username suitable for storing in auth.User.username. + + """ return "_membersuite_id_{}".format(self.id) def get_individual(self, client): @@ -55,11 +60,12 @@ def get_individual(self, client): if msql_result["Success"]: membersuite_object_data = (msql_result["ResultValue"] ["SingleObject"]) - return Individual(membersuite_object_data=membersuite_object_data, - portal_user=self) else: raise ExecuteMSQLError(result=result) + return Individual(membersuite_object_data=membersuite_object_data, + portal_user=self) + @python_2_unicode_compatible class Individual(MemberSuiteObject): @@ -76,6 +82,9 @@ def __init__(self, membersuite_object_data, portal_user=None): self.first_name = self.fields["FirstName"] self.last_name = self.fields["LastName"] + self.primary_organization__rtg = ( + self.fields["PrimaryOrganization__rtg"]) + self.portal_user = portal_user def __str__(self): @@ -85,3 +94,46 @@ def __str__(self): email_address=self.email_address, first_name=self.first_name, last_name=self.last_name)) + + def is_member(self, client): + """Is this Individual a member? + + """ + if not client.session_id: + client.request_session() + + primary_organization = self.get_primary_organization(client=client) + + membership = membership_services.get_primary_membership( + organization_id=primary_organization.id, + entity_id=self.id, + client=client) + + return membership.receives_member_benefits + + def get_primary_organization(self, client): + """Return the primary Organization for this Individual. + + Makes 1 or 2 MemberSuite API calls. + """ + if not client.session_id: + client.request_session() + + query = "SELECT OBJECT() FROM ORGANIZATION WHERE ID = '{}'".format( + self.primary_organization__rtg) + + result = client.runSQL(query) + + msql_result = result["body"]["ExecuteMSQLResult"] + + if msql_result["Success"]: + membersuite_object_data = (msql_result["ResultValue"] + ["SingleObject"]) + else: + raise ExecuteMSQLError(result=result) + + # Could omit this step if Organization inherits from MemberSuiteObject. + organization = convert_ms_object( + membersuite_object_data["Fields"]["KeyValueOfstringanyType"]) + + return Organization(org=organization) diff --git a/membersuite_api_client/security/services.py b/membersuite_api_client/security/services.py index b048ea4..8bbb1b8 100644 --- a/membersuite_api_client/security/services.py +++ b/membersuite_api_client/security/services.py @@ -1,14 +1,15 @@ from .models import PortalUser -from ..exceptions import LoginToPortalError +from ..exceptions import LoginToPortalError, LogoutError from ..utils import get_session_id -def login_to_portal(username, password, client): +def login_to_portal(username, password, client, retries=2): """Log `username` into the MemberSuite Portal. Returns a PortalUser object if successful, raises LoginToPortalError if not. + Will retry logging in if a GeneralException occurs, up to `retries`. """ if not client.session_id: client.request_session() @@ -17,19 +18,43 @@ def login_to_portal(username, password, client): url=("http://membersuite.com/contracts/IConciergeAPIService/" "LoginToPortal")) - result = client.client.service.LoginToPortal( - _soapheaders=[concierge_request_header], - portalUserName=username, - portalPassword=password) + attempts = 0 + while attempts < retries: + result = client.client.service.LoginToPortal( + _soapheaders=[concierge_request_header], + portalUserName=username, + portalPassword=password) - login_to_portal_result = result["body"]["LoginToPortalResult"] + login_to_portal_result = result["body"]["LoginToPortalResult"] - if login_to_portal_result["Success"]: - portal_user = login_to_portal_result["ResultValue"]["PortalUser"] + if login_to_portal_result["Success"]: + portal_user = login_to_portal_result["ResultValue"]["PortalUser"] - session_id = get_session_id(result=result) + session_id = get_session_id(result=result) - return PortalUser(membersuite_object_data=portal_user, - session_id=session_id) - else: - raise LoginToPortalError(result=result) + return PortalUser(membersuite_object_data=portal_user, + session_id=session_id) + else: + attempts += 1 + + raise LoginToPortalError(result=result) + + +def logout(client): + """Log out the currently logged-in user. + + """ + if not client.session_id: + client.request_session() + + concierge_request_header = client.construct_concierge_header( + url=("http://membersuite.com/contracts/IConciergeAPIService/" + "Logout")) + + logout_result = client.client.service.Logout( + _soapheaders=[concierge_request_header]) + + result = logout_result["body"]["LogoutResult"] + + if result["SessionID"]: # Will be None if logout succeeded. + raise LogoutError(result=result) diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index eaa3a18..44c8dca 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -1,5 +1,6 @@ from .base import BaseTestCase -from ..memberships.services import MembershipService +from ..exceptions import GetPrimaryMembershipError +from ..memberships.services import get_primary_membership, MembershipService from ..memberships.models import Membership, MembershipProduct @@ -43,3 +44,23 @@ def test_get_all_membership_products(self): self.assertTrue(len(membership_product_list) == 103) self.assertEqual(type(membership_product_list[0]), MembershipProduct) + + +class GetPrimaryMembershipTestCase(BaseTestCase): + + def test_get_primary_membership_returns_membership(self): + organization_id = "" # AASHE Test Campus + entity_id = "6faf90e4-0006-c59d-164a-0b3c5ba7d6d2" # Test User + result = get_primary_membership(organization_id=organization_id, + entity_id=entity_id, + client=self.client) + self.assertIsInstance(Membership, result) + + def test_get_primary_membership_fails(self): + """What happens when get_primary_membership() fails? + + """ + with self.assertRaises(GetPrimaryMembershipError): + get_primary_membership(organization_id="bogus", + entity_id="bogus", + client=self.client) diff --git a/membersuite_api_client/tests/test_security.py b/membersuite_api_client/tests/test_security.py index 07245ee..081d148 100644 --- a/membersuite_api_client/tests/test_security.py +++ b/membersuite_api_client/tests/test_security.py @@ -2,19 +2,37 @@ import unittest from ..client import ConciergeClient +from ..exceptions import LoginToPortalError, MemberSuiteAPIError from ..security import models -from ..security.services import (login_to_portal, - LoginToPortalError) +from ..security.services import login_to_portal, logout + + +def get_new_client(): + return ConciergeClient(access_key=os.environ["MS_ACCESS_KEY"], + secret_key=os.environ["MS_SECRET_KEY"], + association_id=os.environ["MS_ASSOCIATION_ID"]) + + +def get_portal_user(client, member=True): + if client.session_id is None: + client.request_session() + if member: + return login_to_portal( + username=os.environ["TEST_MS_PORTAL_USER_ID"], + password=os.environ["TEST_MS_PORTAL_USER_PASS"], + client=client) + else: + return login_to_portal( + username=os.environ["TEST_NON_MEMBER_MS_PORTAL_USER_ID"], + password=os.environ["TEST_NON_MEMBER_MS_PORTAL_USER_PASS"], + client=client) class SecurityServicesTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - cls.client = ConciergeClient( - access_key=os.environ["MS_ACCESS_KEY"], - secret_key=os.environ["MS_SECRET_KEY"], - association_id=os.environ["MS_ASSOCIATION_ID"]) + cls.client = get_new_client() def test_login_to_portal(self): """Can we log in to the portal?""" @@ -34,17 +52,77 @@ def test_login_to_portal_failure(self): class PortalUserTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.client = get_new_client() + + def setUp(self): + self.portal_user = get_portal_user(client=self.client) + + def test_get_username(self): + """Does get_username() work? + + """ + self.portal_user.id = "fake-membersuite-id" + self.assertEqual("_membersuite_id_fake-membersuite-id", + self.portal_user.get_username()) + def test_get_individual(self): - """Does get_individual() work?""" - client = ConciergeClient( - access_key=os.environ["MS_ACCESS_KEY"], - secret_key=os.environ["MS_SECRET_KEY"], - association_id=os.environ["MS_ASSOCIATION_ID"]) - portal_user = login_to_portal( - username=os.environ["TEST_MS_PORTAL_USER_ID"], - password=os.environ["TEST_MS_PORTAL_USER_PASS"], + """Does get_individual() work? + + """ + individual = self.portal_user.get_individual(client=self.client) + self.assertEqual(self.portal_user.first_name, individual.first_name) + self.assertEqual(self.portal_user.last_name, individual.last_name) + self.assertEqual(self.portal_user.owner, individual.id) + + +class IndividualTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.client = get_new_client() + + def setUp(self): + self.client.request_session() + member_portal_user = get_portal_user(client=self.client) + self.individual_member = member_portal_user.get_individual( + client=self.client) + + def test_is_member_for_member(self): + """Does is_member() work for members? + + """ + is_member = self.individual_member.is_member(client=self.client) + self.assertTrue(is_member) + + def test_is_member_for_nonmember(self): + """Does is_member() work for non-members? + + """ + # logout(client=self.client) + client = get_new_client() + client.request_session() + non_member_portal_user = get_portal_user(client=client, + member=False) + individual_non_member = non_member_portal_user.get_individual( client=client) - individual = portal_user.get_individual(client=client) - self.assertEqual(portal_user.first_name, individual.first_name) - self.assertEqual(portal_user.last_name, individual.last_name) - self.assertEqual(portal_user.owner, individual.id) + is_member = individual_non_member.is_member(client=client) + self.assertFalse(is_member) + + def test_get_primary_organization(self): + """Does get_primary_organization() work? + + """ + organization = self.individual_member.get_primary_organization( + client=self.client) + self.assertIsInstance(organization, models.Organization) + + def test_get_primary_organization_fails(self): + """What happens when get_primary_organization() fails? + + """ + with self.assertRaises(MemberSuiteAPIError): + self.individual_member.primary_organization__rtg = "bogus ID" + self.individual_member.get_primary_organization( + client=self.client) diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py new file mode 100644 index 0000000..e69de29 From 67bedd87b58fa86fbdd81351c0e79483b82c8eb6 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Sun, 12 Feb 2017 17:11:10 -0500 Subject: [PATCH 2/7] Remove memberships.services.get_primary_membership() Replaced by getting primary organization for an Individual, then the "first" membership for that organization, where "first" means the first one that comes back when we query MemberSuite for the list of all memberships for an organization (in memberships.services). --- membersuite_api_client/exceptions.py | 5 --- .../memberships/services.py | 25 -------------- membersuite_api_client/security/models.py | 11 +++--- membersuite_api_client/tests/test_client.py | 1 - .../tests/test_memberships.py | 34 +++++++------------ membersuite_api_client/tests/test_security.py | 10 ++++-- 6 files changed, 27 insertions(+), 59 deletions(-) diff --git a/membersuite_api_client/exceptions.py b/membersuite_api_client/exceptions.py index 483a5ac..a811c82 100644 --- a/membersuite_api_client/exceptions.py +++ b/membersuite_api_client/exceptions.py @@ -33,8 +33,3 @@ class LogoutError(MemberSuiteAPIError): class ExecuteMSQLError(MemberSuiteAPIError): result_type = "ExecuteMSQLResult" - - -class GetPrimaryMembershipError(MemberSuiteAPIError): - - result_type = "GetPrimaryMembershipResult" diff --git a/membersuite_api_client/memberships/services.py b/membersuite_api_client/memberships/services.py index a0e5f10..87eceb3 100644 --- a/membersuite_api_client/memberships/services.py +++ b/membersuite_api_client/memberships/services.py @@ -7,7 +7,6 @@ from zeep.exceptions import TransportError from .models import Membership, MembershipProduct -from ..exceptions import GetPrimaryMembershipError from ..utils import convert_ms_object @@ -147,27 +146,3 @@ def package_membership_products(self, msql_result): product = MembershipProduct(sane_obj) product_list.append(product) return product_list - - -def get_primary_membership(organization_id, entity_id, client): - if not client.session_id: - client.request_session() - - concierge_request_header = client.construct_concierge_header( - url=("http://membersuite.com/contracts/IConciergeAPIService/" - "GetPrimaryMembership")) - - result = client.client.service.GetPrimaryMembership( - _soapheaders=[concierge_request_header], - membershipOrganizationID=organization_id, - entityID=entity_id) - - get_primary_membership_result = ( - result["body"]["GetPrimaryMembershipResult"]) - - if get_primary_membership_result["Success"]: - membership = get_primary_membership_result["ResultValue"]["Membership"] - - return Membership(membersuite_object_data=membership) - else: - raise GetPrimaryMembershipError(result=result) diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py index dd723f3..8749d6f 100644 --- a/membersuite_api_client/security/models.py +++ b/membersuite_api_client/security/models.py @@ -104,17 +104,20 @@ def is_member(self, client): primary_organization = self.get_primary_organization(client=client) - membership = membership_services.get_primary_membership( - organization_id=primary_organization.id, - entity_id=self.id, + membership_service = membership_services.MembershipService( client=client) + try: + membership = membership_service.get_memberships_for_org( + account_num=primary_organization.id)[0] + except IndexError: + return False + return membership.receives_member_benefits def get_primary_organization(self, client): """Return the primary Organization for this Individual. - Makes 1 or 2 MemberSuite API calls. """ if not client.session_id: client.request_session() diff --git a/membersuite_api_client/tests/test_client.py b/membersuite_api_client/tests/test_client.py index 4decff2..410cae4 100644 --- a/membersuite_api_client/tests/test_client.py +++ b/membersuite_api_client/tests/test_client.py @@ -1,4 +1,3 @@ -import datetime import os import unittest diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 44c8dca..2871923 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -1,6 +1,7 @@ +import unittest + from .base import BaseTestCase -from ..exceptions import GetPrimaryMembershipError -from ..memberships.services import get_primary_membership, MembershipService +from ..memberships.services import MembershipService from ..memberships.models import Membership, MembershipProduct @@ -10,6 +11,7 @@ def setUp(self): super(MembershipServiceTestCase, self).setUp() self.service = MembershipService(self.client) + @unittest.skip("Need an Organization ID for a non-member org") def test_get_membership_for_org(self): """ Get membership info for a test org @@ -22,6 +24,14 @@ def test_get_membership_for_org(self): '6faf90e4-0074-cbb5-c1d2-0b3c539859ef') # Test org without a membership + + # Need an Organization ID for a non-member org. The one here + # is the same used above for testing with a member org. + # + # HERE! + # | + # | + # V test_org_id = "6faf90e4-0007-c9dc-98b7-0b3c53985743" membership_list = self.service.get_memberships_for_org(test_org_id) self.assertFalse(membership_list) @@ -44,23 +54,3 @@ def test_get_all_membership_products(self): self.assertTrue(len(membership_product_list) == 103) self.assertEqual(type(membership_product_list[0]), MembershipProduct) - - -class GetPrimaryMembershipTestCase(BaseTestCase): - - def test_get_primary_membership_returns_membership(self): - organization_id = "" # AASHE Test Campus - entity_id = "6faf90e4-0006-c59d-164a-0b3c5ba7d6d2" # Test User - result = get_primary_membership(organization_id=organization_id, - entity_id=entity_id, - client=self.client) - self.assertIsInstance(Membership, result) - - def test_get_primary_membership_fails(self): - """What happens when get_primary_membership() fails? - - """ - with self.assertRaises(GetPrimaryMembershipError): - get_primary_membership(organization_id="bogus", - entity_id="bogus", - client=self.client) diff --git a/membersuite_api_client/tests/test_security.py b/membersuite_api_client/tests/test_security.py index 081d148..dfe08f1 100644 --- a/membersuite_api_client/tests/test_security.py +++ b/membersuite_api_client/tests/test_security.py @@ -4,7 +4,7 @@ from ..client import ConciergeClient from ..exceptions import LoginToPortalError, MemberSuiteAPIError from ..security import models -from ..security.services import login_to_portal, logout +from ..security.services import login_to_portal def get_new_client(): @@ -96,11 +96,17 @@ def test_is_member_for_member(self): is_member = self.individual_member.is_member(client=self.client) self.assertTrue(is_member) + # test_is_member_for_nonmember() below can't succeed, because it + # doesn't know about any non-member to use. Once non-member data + # (at least a non-member Organization and a connected Individudal + # with Portal Access) is available, push it into the env in + # TEST_NON_MEMBER_MS_PORTAL_USER_ID and + # TEST_NON_MEMBER_MS_PORTAL_USER_PASS and unskip this test. + @unittest.skip("Because it can't succeed") def test_is_member_for_nonmember(self): """Does is_member() work for non-members? """ - # logout(client=self.client) client = get_new_client() client.request_session() non_member_portal_user = get_portal_user(client=client, From b0cd99d100befba3ea74497439d4bbec191a4147 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Sun, 12 Feb 2017 17:38:43 -0500 Subject: [PATCH 3/7] Exclude tests from coverage report --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 2fa1253..8bf72eb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,3 +5,4 @@ omit = */python?.?/* */site-packages/* */unittest2/* + */tests/* From b51239c6bcb670fe50cd222c9ea352ea057c232b Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Sun, 12 Feb 2017 17:39:39 -0500 Subject: [PATCH 4/7] Tell Travis not to test on Python 3.4 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c29eea1..ee17d2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - '2.7' - - '3.4' - '3.5' install: From 2b02e4c539ef943dcc3fb1a27cffc7da952e244c Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 16 Feb 2017 18:01:22 -0500 Subject: [PATCH 5/7] Apply misc fixes and small enhancements Test coverage up to 92%. --- membersuite_api_client/exceptions.py | 7 +- membersuite_api_client/security/models.py | 11 +++ membersuite_api_client/security/services.py | 8 +- .../tests/test_memberships.py | 8 -- membersuite_api_client/tests/test_security.py | 26 ++++-- membersuite_api_client/tests/test_utils.py | 85 +++++++++++++++++++ membersuite_api_client/utils.py | 57 +++++++++++++ setup.py | 3 +- 8 files changed, 185 insertions(+), 20 deletions(-) diff --git a/membersuite_api_client/exceptions.py b/membersuite_api_client/exceptions.py index a811c82..d46f550 100644 --- a/membersuite_api_client/exceptions.py +++ b/membersuite_api_client/exceptions.py @@ -16,8 +16,11 @@ def __str__(self): concierge_error=concierge_error) def get_concierge_error(self): - return (self.result["body"][self.result_type] - ["Errors"]["ConciergeError"]) + try: + return (self.result["body"][self.result_type] + ["Errors"]["ConciergeError"]) + except KeyError: + return (self.result["Errors"]) class LoginToPortalError(MemberSuiteAPIError): diff --git a/membersuite_api_client/security/models.py b/membersuite_api_client/security/models.py index 8749d6f..2cec28f 100644 --- a/membersuite_api_client/security/models.py +++ b/membersuite_api_client/security/models.py @@ -98,6 +98,14 @@ def __str__(self): def is_member(self, client): """Is this Individual a member? + Assumptions: + + - a "primary organization" in MemberSuite is the "current" + Organization for an Individual + + - get_memberships_for_org() returns Memberships ordered such + that the first one returned is the "current" one. + """ if not client.session_id: client.request_session() @@ -119,6 +127,9 @@ def get_primary_organization(self, client): """Return the primary Organization for this Individual. """ + if self.primary_organization__rtg is None: + return None + if not client.session_id: client.request_session() diff --git a/membersuite_api_client/security/services.py b/membersuite_api_client/security/services.py index 8bbb1b8..60feb45 100644 --- a/membersuite_api_client/security/services.py +++ b/membersuite_api_client/security/services.py @@ -43,6 +43,10 @@ def login_to_portal(username, password, client, retries=2): def logout(client): """Log out the currently logged-in user. + There's a really crappy side-effect here - the session_id + attribute of the `client` passed in will be reset to None if the + logout succeeds, which is going to be almost always, let's hope. + """ if not client.session_id: client.request_session() @@ -56,5 +60,7 @@ def logout(client): result = logout_result["body"]["LogoutResult"] - if result["SessionID"]: # Will be None if logout succeeded. + if result["SessionID"] is None: # Success! + client.session_id = None + else: # Failure . . . raise LogoutError(result=result) diff --git a/membersuite_api_client/tests/test_memberships.py b/membersuite_api_client/tests/test_memberships.py index 2871923..5a61554 100644 --- a/membersuite_api_client/tests/test_memberships.py +++ b/membersuite_api_client/tests/test_memberships.py @@ -24,14 +24,6 @@ def test_get_membership_for_org(self): '6faf90e4-0074-cbb5-c1d2-0b3c539859ef') # Test org without a membership - - # Need an Organization ID for a non-member org. The one here - # is the same used above for testing with a member org. - # - # HERE! - # | - # | - # V test_org_id = "6faf90e4-0007-c9dc-98b7-0b3c53985743" membership_list = self.service.get_memberships_for_org(test_org_id) self.assertFalse(membership_list) diff --git a/membersuite_api_client/tests/test_security.py b/membersuite_api_client/tests/test_security.py index dfe08f1..61078f6 100644 --- a/membersuite_api_client/tests/test_security.py +++ b/membersuite_api_client/tests/test_security.py @@ -1,16 +1,10 @@ import os import unittest -from ..client import ConciergeClient from ..exceptions import LoginToPortalError, MemberSuiteAPIError from ..security import models -from ..security.services import login_to_portal - - -def get_new_client(): - return ConciergeClient(access_key=os.environ["MS_ACCESS_KEY"], - secret_key=os.environ["MS_SECRET_KEY"], - association_id=os.environ["MS_ASSOCIATION_ID"]) +from ..security.services import login_to_portal, logout +from ..utils import get_new_client def get_portal_user(client, member=True): @@ -49,6 +43,22 @@ def test_login_to_portal_failure(self): password="wrong password", client=self.client) + def test_logout(self): + """Can we logout? + + This logs out from the API client session, not the MemberSuite + Portal. + + """ + self.client.session_id = None + + self.client.request_session() # A fresh session. Yum! + self.assertTrue(self.client.session_id) + + logout(self.client) + + self.assertIsNone(self.client.session_id) + class PortalUserTestCase(unittest.TestCase): diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py index e69de29..69949f2 100644 --- a/membersuite_api_client/tests/test_utils.py +++ b/membersuite_api_client/tests/test_utils.py @@ -0,0 +1,85 @@ +import unittest + +from ..exceptions import ExecuteMSQLError +from ..models import MemberSuiteObject +from ..security.models import Individual +from ..utils import (get_new_client, + submit_msql_query) + + +client = get_new_client() + + +class SubmitMSQLQueryTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + client.request_session() + + def test_class_familiar_to_factory(self): + individual = submit_msql_query( + query="SELECT OBJECT() FROM INDIVIDUAL", + client=client)[0] + self.assertIsInstance(individual, Individual) + + # test_class_not_familiar_to_factory below needs a MemberSuite + # object for which at least one instance is available, and which + # is not modeled in membersuite_api_client. Don't know of one + # of those that's reliably (or even now and then-ly) available, + # that's why this test gets skipped. + @unittest.skip("Needs data fixture") + def test_class_not_familiar_to_factory(self): + """Is a base MemberSuiteObject returned when the class is unfamiliar? + + Test will fail when MemberSuite Task is modeled (and added to + membersuite_object_factory.klasses). + """ + results = submit_msql_query(query="SELECT OBJECT() FROM ???", + client=client) + self.assertEqual(type(results[0]), MemberSuiteObject) + + def test_unpermitted_query(self): + with self.assertRaises(ExecuteMSQLError): + submit_msql_query(query="SELECT OBJECT() FROM TermsOfService", + client=client) + + def test_well_formed_but_invalid_msql(self): + with self.assertRaises(ExecuteMSQLError): + submit_msql_query(query="SELECT OBJECT() FROM BOB", + client=client) + + def test_query_with_no_results(self): + results = submit_msql_query( + query=("SELECT OBJECT() FROM INDIVIDUAL " + "WHERE LASTNAME = 'bo-o-o-ogus'"), + client=client) + self.assertEqual(0, len(results)) + + def test_query_with_multiple_results(self): + """Does submit_msql_query work with multiple results? + + NOTE: This test depends on multiple Individuals with LastName + of 'User' being available. + + """ + results = submit_msql_query( + query=("SELECT OBJECTS() FROM INDIVIDUAL " + "WHERE LASTNAME = 'User'"), + client=client) + self.assertTrue(len(results)) + + +class MemberSuiteAPIErrorTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + client.request_session() + + def test___str__(self): + try: + submit_msql_query(query="SELECT OBJECT() FROM BOB", + client=client) + except ExecuteMSQLError as exc: + pass + + self.assertTrue(str(exc)) diff --git a/membersuite_api_client/utils.py b/membersuite_api_client/utils.py index 590a1fd..f62a67b 100644 --- a/membersuite_api_client/utils.py +++ b/membersuite_api_client/utils.py @@ -1,3 +1,8 @@ +import os + +from .exceptions import ExecuteMSQLError + + def convert_ms_object(ms_object): """ Converts the list of dictionaries with keys "key" and "value" into @@ -34,3 +39,55 @@ def membersuite_object_factory(membersuite_object_data): membersuite_object_data=membersuite_object_data) else: return klass(membersuite_object_data=membersuite_object_data) + + +def get_new_client(): + """Return a new ConciergeClient, pulling secrets from the environment. + + """ + from .client import ConciergeClient + return ConciergeClient(access_key=os.environ["MS_ACCESS_KEY"], + secret_key=os.environ["MS_SECRET_KEY"], + association_id=os.environ["MS_ASSOCIATION_ID"]) + + +def submit_msql_query(query, client=None): + """Submit `query` to MemberSuite, returning .models.MemberSuiteObjects. + + So this is a converter from MSQL to .models.MemberSuiteObjects. + + Returns query results as a list of MemberSuiteObjects. + + """ + if client is None: + client = get_new_client() + if not client.session_id: + client.request_session() + + result = client.runSQL(query) + execute_msql_result = result["body"]["ExecuteMSQLResult"] + + membersuite_object_list = [] + + if execute_msql_result["Success"]: + result_value = execute_msql_result["ResultValue"] + if result_value["ObjectSearchResult"]["Objects"]: + # Multiple results. + membersuite_object_list = [] + for obj in (result_value["ObjectSearchResult"]["Objects"] + ["MemberSuiteObject"]): + membersuite_object = membersuite_object_factory(obj) + membersuite_object_list.append(membersuite_object) + elif result_value["SingleObject"]["ClassType"]: + # Only one result. + membersuite_object = membersuite_object_factory( + execute_msql_result["ResultValue"]["SingleObject"]) + membersuite_object_list.append(membersuite_object) + elif (result_value["ObjectSearchResult"]["Objects"] is None and + result_value["SingleObject"]["ClassType"] is None): + # No results, I guess. + pass + return membersuite_object_list + else: + # @TODO Fix - exposing only the first of possibly many errors here. + raise ExecuteMSQLError(result=execute_msql_result) diff --git a/setup.py b/setup.py index d25bd63..f9e8c85 100644 --- a/setup.py +++ b/setup.py @@ -26,5 +26,6 @@ def read(fname): 'Programming Language :: Python :: 3.4.3', ], include_package_data=True, - install_requires=["zeep>=0.26"] + install_requires=["future==0.16.0", + "zeep>=0.26"] ) From bed76540ef7ff5d6951256ee6e9950656f65ba37 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Thu, 16 Feb 2017 21:51:06 -0500 Subject: [PATCH 6/7] Patch test to run under Python3 --- membersuite_api_client/tests/test_utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py index 69949f2..031682f 100644 --- a/membersuite_api_client/tests/test_utils.py +++ b/membersuite_api_client/tests/test_utils.py @@ -76,10 +76,6 @@ def setUpClass(cls): client.request_session() def test___str__(self): - try: + with self.assertRaises(ExecuteMSQLError): submit_msql_query(query="SELECT OBJECT() FROM BOB", client=client) - except ExecuteMSQLError as exc: - pass - - self.assertTrue(str(exc)) From 7a1b551654cdbb704925cd8fd2faaa956387d730 Mon Sep 17 00:00:00 2001 From: Robert Erb Date: Fri, 17 Feb 2017 11:33:23 -0500 Subject: [PATCH 7/7] Add more tests --- .coveragerc | 1 + .../tests/test_exceptions.py | 27 +++++++++++++++++++ membersuite_api_client/tests/test_utils.py | 12 --------- 3 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 membersuite_api_client/tests/test_exceptions.py diff --git a/.coveragerc b/.coveragerc index 8bf72eb..741e45a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,3 +6,4 @@ omit = */site-packages/* */unittest2/* */tests/* + *_flymake.py diff --git a/membersuite_api_client/tests/test_exceptions.py b/membersuite_api_client/tests/test_exceptions.py new file mode 100644 index 0000000..b5b22f4 --- /dev/null +++ b/membersuite_api_client/tests/test_exceptions.py @@ -0,0 +1,27 @@ +import unittest + +from ..exceptions import ExecuteMSQLError +from ..utils import (get_new_client, + submit_msql_query) + + +client = get_new_client() + + +class MemberSuiteAPIErrorTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + client.request_session() + try: + submit_msql_query(query="SELECT OBJECT() FROM BOB", + client=client) + except ExecuteMSQLError as exc: + cls.exc = exc + + def test_get_concierge_error(self): + concierge_error = self.exc.get_concierge_error() + self.assertTrue(concierge_error is not None) + + def test___str__(self): + self.assertTrue(str(self.exc) > '') diff --git a/membersuite_api_client/tests/test_utils.py b/membersuite_api_client/tests/test_utils.py index 031682f..4d6ddcd 100644 --- a/membersuite_api_client/tests/test_utils.py +++ b/membersuite_api_client/tests/test_utils.py @@ -67,15 +67,3 @@ def test_query_with_multiple_results(self): "WHERE LASTNAME = 'User'"), client=client) self.assertTrue(len(results)) - - -class MemberSuiteAPIErrorTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - client.request_session() - - def test___str__(self): - with self.assertRaises(ExecuteMSQLError): - submit_msql_query(query="SELECT OBJECT() FROM BOB", - client=client)