コード例 #1
0
    def __init__(self, org_uid, provider_config, redirect_url):
        """
        Prepares the org for linking.

        Args:
            org_uid(str): org identifier
            provider_config(ProviderConfig): ndb model holding the provider config for the org
            redirect_url(str): the url to which the linker should send the user to after saving zuora tokens
        """

        org = Org.get_by_id(org_uid) or Org(
            id=org_uid, provider='zuora', provider_config=provider_config.key)

        # If this is a `relink`, check the org has a provider_config set
        if org.provider_config is None:
            org.provider_config = provider_config.key

        msg = "setting org status to linking (status {}) and saving redirect_url ({})'"
        logging.info(msg.format(LINKING, redirect_url))

        org.status = LINKING
        org.redirect_url = redirect_url
        org.put()

        self.org = org
コード例 #2
0
def mark_as_connected(org_uid, also_linked=False):
    """
    Flags an org as connected. The org will get included in update cycles from this point.

    Args:
        org_uid(str): org identifier
    """
    logging.info(
        "marking the org as connected (status value {})".format(CONNECTED))
    org = Org.get_by_id(org_uid)
    org.status = CONNECTED

    if also_linked:
        org.linked_at = datetime.utcnow()

    org.connected_at = datetime.utcnow()
    org.put()

    if also_linked:
        publish_status(org_uid, LINK_STATUS_TYPE, LINK_STATUS_LINKED)

    publish_status(org_uid, CONNECT_STATUS_TYPE, CONNECT_STATUS_CONNECTED)

    if is_changeset_in_progress(org):
        logging.info(
            "publishing syncing changeset status for changeset {}:{}".format(
                org_uid, org.changeset))
        publish_changeset_status(org_uid, org.changeset,
                                 CHANGESET_STATUS_SYNCING)
コード例 #3
0
    def test_init_update_existing_org(self, publish_mock):
        """
        Tests how new changeset is initialised an existing org (previously synced).

        Args:
            publish_mock(Mock): mock of the changeset publish function
        """
        some_date = datetime.utcnow()
        Org(id='test',
            changeset=10,
            changeset_started_at=some_date,
            changeset_completed_at=some_date,
            last_update_cycle_completed_at=some_date -
            timedelta(hours=1)).put()

        sync_utils.init_update('test')
        org = Org.get_by_id('test')

        # changeset has been incremented
        self.assertEqual(org.changeset, 11)

        # and changeset timestamps are set
        self.assertIsNotNone(org.changeset_started_at)
        self.assertIsNone(org.changeset_completed_at)

        # and the update task has been created
        task_count = len(self.taskqueue.get_filtered_tasks())
        self.assertEqual(task_count, 1)

        # and changeset status is published
        publish_mock.assert_called_once_with('test', 11, 'syncing')
コード例 #4
0
def mark_as_disconnected(org_uid, deactivate_update_cycle):
    """
    Flags an org as disconnected by changing its status to DISCONNECTED and completing current changeset. This is useful
    if the sync gives up because of authentication issues with the provider for example. This does not forcibly
    disconnect the org by deleting the auth keys.

    Publishes an error status for changeset currently being ingested.

    Args:
        org_uid(str): org identifier
        deactivate_update_cycle(bool): indicates if the update_cycle_active flag should be set to false
    """
    logging.info("marking the org as disconnected (status value {})".format(
        DISCONNECTED))
    org = Org.get_by_id(org_uid)
    org.status = DISCONNECTED

    if deactivate_update_cycle:
        org.update_cycle_active = False

    org.put()
    publish_status(org_uid, CONNECT_STATUS_TYPE, CONNECT_STATUS_DISCONNECTED)

    if is_changeset_in_progress(org):
        logging.info(
            "publishing error changeset status for changeset {}:{}".format(
                org_uid, org.changeset))
        publish_changeset_status(org_uid, org.changeset,
                                 CHANGESET_STATUS_ERROR)
コード例 #5
0
    def __init__(self, org_uid, callback_args):
        """
        Third step of the Oauth1 flow. Processing the callback from Xero and
        using the callback params for fetching the access token.

        Args:
            org_uid(str): org identifier
            callback_args(dict): request parameters send by Xero
        """

        self.org_uid = org_uid
        self.callback_args = callback_args
        self.org = Org.get_by_id(org_uid)
        self.provider = self.org.provider_config.get()
        rsa_key, rsa_method = _get_partner_session_attrs(self.provider)
        request_token = OrgCredentials.get_by_id(self.org_uid, parent=self.org.key).token

        super(XeroTokenSession, self).__init__(
            self.provider.client_id,
            client_secret=self.provider.client_secret,
            resource_owner_key=callback_args['oauth_token'],
            resource_owner_secret=request_token.get('oauth_token_secret'),
            verifier=callback_args['oauth_verifier'],
            rsa_key=rsa_key,
            signature_method=rsa_method
        )
コード例 #6
0
def reset_endpoints_task(org_uid):
    """
    Processes org reset task from the task queue (clears endpoint state to cause the next sync to fetch all the data,
    and creates a task on the update queue to kick of the sync cycle for the org).
    """
    org = Org.get_by_id(org_uid)

    if (org.changeset_started_at
            and not org.changeset_completed_at) or org.update_cycle_active:
        logging.info("org syncing at the moment, will try again later")
        return '', 423

    endpoint_indexes = request.form.getlist('endpoint_index')
    logging.info("resetting markers for org {} and endpoints {}".format(
        org_uid, endpoint_indexes))

    # TODO: this is a hack, this should be delegated to a qbo class, instantiated via a factory from the org provider
    sync_data = QboSyncData.get_by_id(org_uid)

    if not sync_data:
        logging.warning("could not find sync data")
        return '', 204

    for endpoint_index in [int(_index) for _index in endpoint_indexes]:
        sync_data.markers[endpoint_index] = START_OF_TIME

    sync_data.put()
    sync_utils.init_update(org_uid)

    return '', 204
コード例 #7
0
    def test_init_update_new_org(self, publish_mock):
        """
        Tests how new changeset is initialised a new org (never synced).

        Args:
            publish_status_mock(Mock): pubsub publish function mock
        """
        Org(id='test', changeset_started_at=None,
            changeset_completed_at=None).put()

        sync_utils.init_update('test')
        org = Org.get_by_id('test')

        # changeset has been incremented
        self.assertEqual(org.changeset, 0)

        # and changeset timestamps are set
        self.assertIsNotNone(org.changeset_started_at)
        self.assertIsNone(org.changeset_completed_at)

        # and the update task has been created
        task_count = len(self.taskqueue.get_filtered_tasks())
        self.assertEqual(task_count, 1)

        # and changeset status is published
        publish_mock.assert_called_once_with('test', 0, 'syncing')
コード例 #8
0
    def __init__(self, org_uid, provider_config, redirect_url):
        """
        Prepares the org for linking.

        Args:
            org_uid(str): org identifier
            provider_config(ProviderConfig): ndb model holding the provider config for the org
            redirect_url(str): the url to which the linker should send the user to after saving qbo tokens
        """
        org = Org.get_by_id(org_uid) or Org(id=org_uid, provider='qbo', provider_config=provider_config.key)

        # If this is a `relink`, check the org has a provider_config set
        if org.provider_config is None:
            org.provider_config = provider_config.key

        msg = "setting org status to linking (status {}) and saving redirect_url ({})'"
        logging.info(msg.format(LINKING, redirect_url))

        org.status = LINKING
        org.redirect_url = redirect_url
        org.put()

        callback_uri = client_utils.get_redirect_uri_for(org.provider)
        super(QboAuthorizationSession, self).__init__(
            provider_config.client_id,
            redirect_uri=callback_uri,
            scope=SCOPES,
            state=org_uid
        )
コード例 #9
0
    def test_init_update_inactive_update_cycle(self, publish_mock):
        """
        Verifies that a new changeset is not created for an org with a sync in progress with an active update cycle (ie.
        has a task on adapter-update).

        Args:
            publish_mock(Mock): mock of the changeset publish function
        """
        some_date = datetime.utcnow()
        Org(id='test',
            changeset=10,
            changeset_started_at=some_date,
            changeset_completed_at=None,
            update_cycle_active=False).put()

        sync_utils.init_update('test')
        org = Org.get_by_id('test')

        # changeset has not been changed
        self.assertEqual(org.changeset, 10)

        # and changeset timestamps have not been changed
        self.assertIsNotNone(org.changeset_started_at)
        self.assertIsNone(org.changeset_completed_at)

        # and a new update task has been created because the update_cycle_active was false
        task_count = len(self.taskqueue.get_filtered_tasks())
        self.assertEqual(task_count, 1)

        # and changeset status is published
        publish_mock.assert_called_once_with('test', 10, 'syncing')
コード例 #10
0
    def __init__(self, org_uid, callback_args):
        """
        Extracts QBO file details and access tokens from the QBO callback.

        Args:
            org_uid(str): org identifier
            redirect_uri(str): uri to which qbo sends tokens
            callback_args(dict): request parameters send by qbo
        """
        self.org_uid = org_uid
        self.callback_args = callback_args
        self.org = Org.get_by_id(org_uid)

        if callback_args.get('error') == 'access_denied':
            raise AuthCancelled(self.org)

        entity_id = callback_args.get('realmId')

        if self.org.entity_id and self.org.entity_id != entity_id:
            raise MismatchingFileConnectionAttempt(self.org)

        logging.info("saving entity_id '{}' for org '{}'".format(entity_id, org_uid))
        self.org.entity_id = entity_id
        self.org.put()

        super(QboTokenSession, self).__init__(
            redirect_uri=client_utils.get_redirect_uri_for(self.org.provider),
        )
コード例 #11
0
def basic_auth(provider, org_uid, username, password):
    """
    Handles basic username/password auth flow.
    Users credentials (username/password) are stored in the UserCredentials kind
    TODO: This should be temporary! and only implemented in DEV until vault is integrated

    Args:
        provider(str): The provider
        org_uid(str): The org ID
        username(str): The username
        password(str): The password

    Returns:
        (str): Response text
    """

    # If authenticating for Zuora, get a session cookie and store in OrgCredentials
    if provider == 'zuora':

        # Multi-entity may be enabled, we need to specify it as a header when authenticating
        # TODO: Fix this to work with multiple entities once its figured out how it works.
        entity_id = None
        session = client_factory.get_token_session(provider, org_uid, username,
                                                   password)

    try:
        session.get_and_save_token()
    except UnauthorizedApiCallException:
        logging.info("got an error - Invalid Credentials".format(provider))
        _abort_link(org_uid)
        return _respond(Org.get_by_id(org_uid),
                        {'error_code': 'invalid_credentials'}, 'not okidoki')

    mark_as_connected(org_uid=org_uid, also_linked=True)

    try:
        data_source_name = client_factory.get_api_session(
            provider, org_uid).get_company_name()
    except FailedToGetCompanyName:
        # TODO: this should be sent to the client as an error code rather than an empty name
        data_source_name = None

    init_update(org_uid)
    return _respond(Org.get_by_id(org_uid),
                    {'data_source_name': data_source_name}, 'okidoki')
コード例 #12
0
    def test_basic_auth_creds_provided_by_apigee(self, init_update_mock,
                                                 save_token_mock,
                                                 connected_mock, publish_mock):
        """
        Verifies that linking works correctly when user credentials are supplied from apigee via
        the connect endpoint
        """
        org = Org(provider='zuora',
                  id='test',
                  redirect_url="http://app",
                  provider_config=self.provider_configs['zuora']).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'access_token': 'blah'
                       }).put()

        response = self.app.post(
            '/linker/zuora/test/connect?redirect_url=http://app&app_family=local_host_family',
            json={
                'username': '******',
                'password': '******'
            },
            content_type='application/json')
        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location,
                         'http://app?data_source_name=ACUIT')

        # org is linking and status is published on pubsub
        self.assertEqual(Org.get_by_id('test').status, LINKING)

        # app redirect url is saved
        self.assertEqual(Org.get_by_id('test').redirect_url, "http://app")

        # token is saved
        save_token_mock.assert_called_once()

        # and then org is connected (this publishes status as connected as well)
        connected_mock.assert_called_once()

        # and the initial sync has been kicked off
        init_update_mock.assert_called_once()
コード例 #13
0
    def test_connect(self, publish_mock):
        """
        Tests the first step of the oauth flow authorisation.
        """

        response = self.app.post(
            '/linker/qbo/test/connect?redirect_url=http://app&app_family=local_host_family'
        )
        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location, "http://qbo")

        # org is linking and status is published on pubsub
        self.assertEqual(Org.get_by_id('test').status, LINKING)

        # app redirect url is saved
        self.assertEqual(Org.get_by_id('test').redirect_url, "http://app")
コード例 #14
0
def perform_disconnect(org_uid):
    logging.info("disconnecting the org explicitly")

    org = Org.get_by_id(org_uid)

    if not org:
        logging.info("org {} not found".format(org_uid))
        raise NotFoundException("org {} not found".format(org_uid))

    publish_status(org_uid, LINK_STATUS_TYPE, LINK_STATUS_UNLINKED)
    mark_as_disconnected(org_uid=org_uid, deactivate_update_cycle=False)
コード例 #15
0
    def __init__(self, org_uid):
        """
        Initialises the class.

        Args:
            org_uid(str): org identifier
        """
        self.org_uid = org_uid
        self.sync_data = QboSyncData.get_by_id(org_uid)
        self.org = Org.get_by_id(org_uid)
        self.entity_id = self.org.entity_id
コード例 #16
0
    def __init__(self, org_uid):
        """
        Initialises the class.

        Args:
            org_uid(str): org identifier
        """
        self.org_uid = org_uid
        self.org = Org.get_by_id(org_uid)
        self.entity_id = self.org.entity_id
        self.api_url = "{}company/{}/query?minorversion={}".format(
            BASE_API_URI, self.entity_id, API_MINOR_VERSION)
コード例 #17
0
    def __init__(self, org_uid):
        """
        Initialises the class.

        Args:
            org_uid(str): org identifier
        """
        self.org_uid = org_uid
        self.org = Org.get_by_id(org_uid)
        self.entity_id = self.org.entity_id
        self.sync_data = ZuoraSyncData.get_by_id(org_uid) or ZuoraSyncData(
            id=org_uid, endpoint_index=0)
コード例 #18
0
def publish_status(org_uid, status_type, status_value):
    """
    Utility function for publishing org status events on pubsub.

    Args:
        org_uid(str): org identifier
        status_type(str): status group (link_status or connect_status)
        status_value(str): status (eg. unlinked|linked, disconnected|connected)
    """
    topic = get_client().topic(STATUS_TOPIC)
    org = Org.get_by_id(org_uid)

    payload = {
        "meta": {
            "version": "2.0.0",
            "data_source_id": org_uid,
            "timestamp": datetime.utcnow().replace(microsecond=0).isoformat()
        },
        "data": [{
            "type": status_type,
            "id": org_uid,
            "attributes": {
                "status": status_value
            }
        }]
    }

    attributes = payload['data'][0]['attributes']

    if status_type == LINK_STATUS_TYPE:
        if status_value == LINK_STATUS_LINKED:
            attributes['linked_at'] = org.linked_at.replace(
                microsecond=0).isoformat()
        else:
            attributes['linked_at'] = None

    if status_type == CONNECT_STATUS_TYPE:
        if status_value == CONNECT_STATUS_CONNECTED:
            attributes['connected_at'] = org.connected_at.replace(
                microsecond=0).isoformat()
            attributes['disconnected_at'] = None
            attributes['disconnect_type'] = None
        else:
            attributes['connected_at'] = None
            attributes['disconnected_at'] = org.disconnected_at.replace(
                microsecond=0).isoformat()
            attributes['disconnect_type'] = org.disconnect_type

    logging.info("publishing on status pubsub topic: {}".format(payload))

    topic.publish(json.dumps(payload))
コード例 #19
0
def handle_connect_search():
    """
    Handler for the org search/connect form. Ability to connect a new org provided here is to make local development
    easier (instead of hitting magic URLs in the browser).

    Returns:
        (str, int): search results page, or a redirect to provider for auth flow
    """

    provider = request.form.get("provider")

    # handle connect
    if request.form.get('connect'):
        org_uid = request.form.get('org_uid')

        if not org_uid:
            flash("Org UID is required")
            return redirect(prefix('/'))

        return redirect(
            "/linker/{}/{}/connect?redirect_url={}&app_family=local_host_family"
            .format(
                provider, org_uid,
                "{}admin/?connect_org_uid={}".format(request.url_root,
                                                     org_uid)))

    # handle search
    elif request.form.get('search'):
        org_uid = request.form.get('org_uid')
        if not org_uid:
            flash("Org UID is required")
            return redirect(prefix('/'))
        else:
            org = Org.get_by_id(org_uid)
            if not org:
                orgs = []
                message = 'Org {} not found'.format(org_uid)
            else:
                orgs = [org]
                message = None

        return render_template('org_list.html',
                               orgs=orgs,
                               next_cursor=None,
                               more=False,
                               changesets=_get_changesets(orgs),
                               message=message), 200

    return redirect(prefix('/'))
コード例 #20
0
def complete_changeset(org_uid):
    """
    Marks a changeset complete by setting changeset_completed_at for an org in the Org datastore kind.  This is called
    once the provider specific sync manager indicates that there is no more data to be pulled.

    This function also writes the changeset record to OrgChangeset kind. OrgChangeset kind keeps record of all
    changesets (as opposed to Org kind which only tracks ingestion status the current changeset for an org).
    OrgChangeset is used by the orchestrator service to coordinate publishing of the completed changesets.

    In case this is the initial sync (first changeset), we kick off a separate publish job immediately for it.

    Args:
        org_uid(str): org identifier
    """
    now = datetime.utcnow()

    org = Org.get_by_id(org_uid)
    org.changeset_completed_at = now
    org.update_cycle_active = False
    org.last_update_cycle_completed_at = now
    org.put()

    changeset = OrgChangeset(org_uid=org_uid,
                             provider='qbo',
                             changeset=org.changeset,
                             ingestion_started_at=org.changeset_started_at,
                             ingestion_completed_at=now,
                             publish_job_running=False,
                             publish_job_finished=False,
                             publish_job_count=0)

    changeset.put()

    if org.changeset == 0:
        taskqueue.add(queue_name='create-publish-job',
                      target='orchestrator',
                      url='/orchestrator/create_publish_job_task',
                      payload=json.dumps({
                          'job_params': {
                              'org_changeset_ids': [changeset.key.id()]
                          }
                      }),
                      transactional=True)
        logging.info(
            "requesting publish after initial sync for org {}".format(org_uid))

    logging.info("completed changeset {} for org {}".format(
        org.changeset, org_uid))
コード例 #21
0
    def test_mark_disconnected_deactivate(self, publish_status_mock):
        """
        Verifies that an org can be marked as disconnected with flagging of update cycle as inactive.

        Args:
            publish_status_mock(Mock): pubsub publish function mock
        """
        Org(id='test', status=CONNECTED, update_cycle_active=False).put()
        sync_utils.mark_as_disconnected(org_uid='test',
                                        deactivate_update_cycle=True)

        # status should be changed and new status broadcast on pubsub
        org = Org.get_by_id('test')
        self.assertEqual(org.status, DISCONNECTED)
        self.assertEqual(org.update_cycle_active, False)
        publish_status_mock.assert_called_with('test', 'connection_status',
                                               'disconnected')
コード例 #22
0
    def __init__(self, org_uid):
        """
        Initialises the class.

        Args:
            org_uid(str): org identifier
        """
        self.org_uid = org_uid
        self.org = Org.get_by_id(org_uid)
        self.entity_id = self.org.entity_id
        self.api_url = "{}company/{}/query?minorversion={}".format(
            BASE_API_URI, self.entity_id, API_MINOR_VERSION)
        self.sync_data = QboSyncData.get_by_id(org_uid) or QboSyncData(
            id=org_uid, endpoint_index=0, start_position=1)

        if self.sync_data.endpoint_index == 0:
            logging.info(
                "this is a start of a new changeset, starting to ingest all endpoints"
            )
コード例 #23
0
    def get_company_name(self):
        """
        Makes an API call to QBO and extracts the company name.

        Returns:
            str: company name
        """
        entity_id = Org.get_by_id(self.org_uid).entity_id
        url_template = "{}company/{}/companyinfo/{}?minorversion={}"
        url = url_template.format(BASE_API_URI, entity_id, entity_id, API_MINOR_VERSION)

        try:
            urlfetch.set_default_fetch_deadline(10)
            data = self.get(url, headers={'Accept': 'application/json'})
        except Exception:
            # we don't want this to interrupt the linking flow
            logging.warning("failed to get company name for entity {}".format(entity_id), exc_info=True)
            raise FailedToGetCompanyName()

        return data.get('CompanyInfo', {}).get('CompanyName')
コード例 #24
0
    def __init__(self, org_uid, provider_config, redirect_url):
        """
        Prepares the org for linking.

        Args:
            org_uid(str): org identifier
            provider_config(ProviderConfig): ndb model holding the provider config for the org
            redirect_url(str): the url to which the linker should send the user to after saving xero tokens
        """

        org = Org.get_by_id(org_uid) or Org(id=org_uid, provider='xerov2', provider_config=provider_config.key)

        # If this is a `relink`, check the org has a provider_config set
        if org.provider_config is None:
            org.provider_config = provider_config.key

        logging.info(
            "Provider secret = {} provider id {}".format(
                provider_config.client_secret,
                provider_config.client_id
            )
        )

        msg = "setting org status to linking (status {}) and saving redirect_url ({})'"
        logging.info(msg.format(LINKING, redirect_url))

        org.status = LINKING
        org.redirect_url = redirect_url
        org.put()

        rsa_key, rsa_method = _get_partner_session_attrs(provider_config)
        callback_uri = client_utils.get_redirect_uri_for(org.provider, org_uid)
        self.org_uid = org_uid

        super(XeroAuthorizationSession, self).__init__(
            client_key=provider_config.client_id,
            client_secret=provider_config.client_secret,
            callback_uri=callback_uri,
            rsa_key=rsa_key,
            signature_method=rsa_method
        )
コード例 #25
0
    def test_account_balance_deduplication(self, get_mock):
        """
        Verifies that an account balance is fetched, but hasn't changed since the last fetch, doesn't get saved to Item.

        Args:
            get_mock(Mock): mock of the api get function
        """
        with open('tests/resources/trial_balance_report.json'
                  ) as report_contents:
            sample_report = json.load(report_contents)

        get_mock.return_value = sample_report

        # setup org and sync state
        self.create_org(status=CONNECTED, changeset=0)
        QboSyncData(id='test', account_balance_marker='2010-01-01').put()

        # run sync
        stage = AccountBalanceReportStage('test')
        stage.next(payload={})

        # there should be 30 Items saved
        self.assertEqual(self.count_items(), 30)

        # setup next changeset and reset the marker to the same day (simulate the same day fetching again, but on the
        # next day update cycle)
        org = Org.get_by_id('test')
        org.changeset = 1
        org.put()
        QboSyncData(id='test', account_balance_marker='2010-01-01').put()

        # patch response mock so that one balance has changed
        sample_report['Rows']['Row'][0]['ColData'][1]['value'] = 100
        get_mock.return_value = sample_report

        # run sync
        stage = AccountBalanceReportStage('test')
        stage.next(payload={})

        # there should be 31 Items saved (one new balance for changeset 1)
        self.assertEqual(self.count_items(), 31)
コード例 #26
0
    def test_mark_connected(self, publish_status_mock):
        """
        Verifies that an org can be marked as connected.

        Args:
            publish_status_mock(Mock): pubsub publish function mock
        """
        Org(id='test', status=DISCONNECTED).put()
        sync_utils.mark_as_connected('test')

        # status should be changed and new status broadcast on pubsub
        org = Org.get_by_id('test')
        self.assertEqual(org.status, CONNECTED)

        # connected_at is updated, but linked_at is not
        self.assertIsNotNone(org.connected_at)
        self.assertIsNone(org.linked_at)

        # status change is published on pubsub
        publish_status_mock.assert_called_with('test', 'connection_status',
                                               'connected')
コード例 #27
0
    def test_disconnect(self, publish_mock):
        """
        Tests the explicit disconnection of an org when connected or already disconnected.
        """
        Org(id='test', status=CONNECTED).put()

        response = self.app.post('/linker/qbo/test/disconnect')
        self.assertEqual(response.status_code, 204)

        # org is disconnected
        self.assertEqual(Org.get_by_id('test').status, DISCONNECTED)

        # linked and connected statuses have been published on pubsub
        publish_mock.assert_has_calls([
            call('test', 'link_status', 'unlinked'),
            call('test', 'connection_status', 'disconnected')
        ])

        response = self.app.post('/linker/qbo/test/disconnect')

        self.assertEqual(response.status_code, 204)
コード例 #28
0
def reconnect(org_uid):
    """
    Endpoint to facilitate long term org re-connection loop.

    Normal update process will mark an org as disconnected after getting 401s from the gl api after about 15 minutes,
    but sometimes a gl just returns 401 for a while. This long-term re-connect loop will make an api call to the gl
    every few hours, and update the status of the org to connected if the api call is successful. from that point on
    normal update cycle will resume for this org.
    """
    org = Org.get_by_id(org_uid)

    # the user could have connected the org manually by now
    if org.status == CONNECTED:
        logging.info("org is connected, nothing to do, resolving this task")
        return '', 204

    # 42 attempts is about a week with the current queue config (4 hours between attempts)
    exec_count = int(request.headers.get('X-AppEngine-TaskExecutionCount'))
    if exec_count > 42:
        logging.info("reached maximum number of reconnect attempts, giving up")
        return '', 204

    logging.info(
        "checking connection status (check number {})".format(exec_count))

    try:
        if client_factory.get_api_session(org.provider,
                                          org_uid).is_authenticated():
            logging.info(
                "made a successful api call, marking the org as connected")
            sync_utils.mark_as_connected(org_uid)
            sync_utils.init_update(org_uid)
            return '', 204
    except DisconnectException as e:
        logging.exception("failed reconnecting to client.", e)

    logging.info(
        "could not make a successful api call, leaving org as disconnected, will try again"
    )
    return '', 423
コード例 #29
0
    def test_country_extraction(self, get_mock):
        """
        Verifies that the file country is saved from the company info endpoint.

        Args:
            get_mock(Mock): mock of the api get function
        """
        get_mock.return_value = self.get_mock_api_response(
            COMPANY_INFO_ENDPOINT_NAME)
        self.create_org(status=CONNECTED)

        # set sync state so that the next pull will be company info
        QboSyncData(id='test',
                    stage_index=LIST_API_STAGE,
                    endpoint_index=COMPANY_INFO_ENDPOINT_INDEX).put()

        # run the sync
        stage = ListApiStage('test')
        stage.next(payload={})

        # the country should be saved
        self.assertEqual(Org.get_by_id('test').country, 'AU')
コード例 #30
0
    def test_complete_first_changeset(self):
        """
        Verifies that Org and OrgChangeset get updated to indicate that a changeset is complete.
        """
        started_at = datetime.now()

        org = Org(id='test', changeset=0,
                  changeset_started_at=started_at).put()
        sync_utils.complete_changeset('test')

        # Org flags/timestamps are updated
        org = Org.get_by_id('test')
        self.assertEqual(org.changeset_completed_at, datetime(2010, 1, 1))
        self.assertEqual(org.last_update_cycle_completed_at,
                         datetime(2010, 1, 1))
        self.assertFalse(org.update_cycle_active)

        # OrgChangeset record is added
        org_changeset = OrgChangeset.query().get()
        self.assertEqual(org_changeset.org_uid, 'test')
        self.assertEqual(org_changeset.changeset, 0)
        self.assertEqual(org_changeset.ingestion_started_at, started_at)
        self.assertEqual(org_changeset.ingestion_completed_at,
                         datetime(2010, 1, 1))
        self.assertFalse(org_changeset.publish_job_running)
        self.assertFalse(org_changeset.publish_job_finished)
        self.assertEqual(org_changeset.publish_job_count, 0)

        # Publish task is queued for the first changeset
        self.assertEqual(len(self.taskqueue.get_filtered_tasks()), 1)
        self.assertEqual(
            self.taskqueue.get_filtered_tasks()[0].payload,
            json.dumps({
                "job_params": {
                    "org_changeset_ids": [org_changeset.key.id()]
                }
            }))