def setUp(self):
     self.test_app = TestApp(core.federer.app.wsgifunc())
     self.fixtures_path = 'core/tests/fixtures/'
     self.event_store = EventStore()
 def setUp(self):
     self.test_app = TestApp(core.federer.app.wsgifunc())
     self.endpoint = '/smscdr'
     self.event_store = EventStore()
class CallCDRTestCase(unittest.TestCase):
    """Handling call CDRs."""
    @classmethod
    def setUpClass(cls):
        """Load up some pricing data into the config db."""
        price_data = [{
            'directionality': 'off_network_send',
            'prefix': '789',
            'country_name': 'Ocenaia',
            'country_code': 'OC',
            'cost_to_subscriber_per_sms': 300,
            'cost_to_subscriber_per_min': 200,
        }, {
            'directionality': 'off_network_receive',
            'cost_to_subscriber_per_sms': 400,
            'cost_to_subscriber_per_min': 100,
        }, {
            'directionality': 'on_network_send',
            'cost_to_subscriber_per_sms': 40,
            'cost_to_subscriber_per_min': 10,
        }, {
            'directionality': 'on_network_receive',
            'cost_to_subscriber_per_sms': 30,
            'cost_to_subscriber_per_min': 40,
        }]
        # Create a simplified checkin response with just price data.
        checkin_response = {'config': {'prices': price_data}}
        # Mock the checkin handler object such that validation just returns the
        # object to-be-validated (without checking JWT).
        mock_checkin_handler = CheckinHandler
        mock_checkin_handler.validate = lambda self, data: data
        mock_checkin_handler(checkin_response)

        # mock subscriber so we dont actually execute DB queries
        mock_subscriber = mocks.MockSubscriber()
        cls.original_subscriber = core.federer_handlers.cdr.subscriber
        core.federer_handlers.cdr.subscriber = mock_subscriber

    @classmethod
    def tearDownClass(cls):
        core.federer_handlers.cdr.subscriber = cls.original_subscriber

    def setUp(self):
        self.test_app = TestApp(core.federer.app.wsgifunc())
        self.fixtures_path = 'core/tests/fixtures/'
        self.event_store = EventStore()

    def tearDown(self):
        # Reset the EventStore.
        self.event_store.drop_table()

    def test_get_raises_404(self):
        """Cannot GET to this endpoint."""
        response = self.test_app.get('/cdr', expect_errors=True)
        self.assertEqual(404, response.status)

    def test_post_without_cdr_raises_400(self):
        """Must send CDR data."""
        data = {}
        response = self.test_app.post('/cdr', params=data, expect_errors=True)
        self.assertEqual(400, response.status)

    def test_post_cdr_with_origination_does_not_generate_event(self):
        """CDRs are not processed if they contain an <origination> tag."""
        cdr_path = self.fixtures_path + 'cdr-with-origination.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        response = self.test_app.post('/cdr', params=data)
        # We expect to get a 200 OK, but no event will be in the EventStore.
        self.assertEqual(200, response.status)
        self.assertEqual(0, len(self.event_store.get_events()))

    def test_post_cdr_sans_origination_generates_event_with_duration(self):
        """CDRs can be posted to the server and turned into events."""
        cdr_path = self.fixtures_path + 'outside-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        # We should be able to successfully send the data to the server.
        response = self.test_app.post('/cdr', params=data)
        self.assertEqual(200, response.status)
        # And the server should have added data to the DB, so we should be able
        # to query for it.  The most recently added event should have a
        # call_duration.
        events = self.event_store.get_events()
        event = events[-1]
        # Get the expected call duration from <call_duration> tag in the CDR.
        expected_call_duration = 67
        self.assertEqual(expected_call_duration, event['call_duration'])
        # Get the expected billsec from <billsec> tag in the CDR.
        expected_billsec = 42
        self.assertEqual(expected_billsec, event['billsec'])

    def test_parse_outside_call_cdr(self):
        """We can extract info from outside_call CDRs."""
        cdr_path = self.fixtures_path + 'outside-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        # We can send this info to the server and it should have added data to
        # the DB.
        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual('IMSI510555550000071', event['from_imsi'])
        self.assertEqual('6285574719949', event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual('7892395268385', event['to_number'])
        self.assertEqual('outside_call', event['kind'])
        self.assertEqual(200, event['tariff'])

    def test_parse_local_call_cdr(self):
        """We can extract info from local_call CDRs."""
        cdr_path = self.fixtures_path + 'local-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        caller_event = events[-2]
        callee_event = events[-1]
        self.assertEqual('IMSI510555550000071', caller_event['from_imsi'])
        self.assertEqual('6285574719949', caller_event['from_number'])
        self.assertEqual('IMSI510555550000081', caller_event['to_imsi'])
        self.assertEqual('6285574719944', caller_event['to_number'])
        self.assertEqual('local_call', caller_event['kind'])
        self.assertEqual(10, caller_event['tariff'])

        self.assertEqual('IMSI510555550000071', callee_event['from_imsi'])
        self.assertEqual('6285574719949', callee_event['from_number'])
        self.assertEqual('IMSI510555550000081', callee_event['to_imsi'])
        self.assertEqual('6285574719944', callee_event['to_number'])
        self.assertEqual('local_recv_call', callee_event['kind'])
        self.assertEqual(40, callee_event['tariff'])

    def test_parse_local_call_msisdn_cdr(self):
        """
        We can extract info from local_call_msisdn CDR which have
        MSISDN as the callee_id_number instead of IMSI.
        """
        cdr_path = self.fixtures_path + 'local-call-msisdn-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}

        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        caller_event = events[-2]
        callee_event = events[-1]
        self.assertEqual('IMSI123451234512342', caller_event['from_imsi'])
        self.assertEqual('639360100757', caller_event['from_number'])
        # Check the IMSI value from MockSubscriber's get_imsi_from_number
        self.assertEqual('IMSI000227', caller_event['to_imsi'])
        self.assertEqual('639360100755', caller_event['to_number'])
        self.assertEqual('local_call', caller_event['kind'])
        self.assertEqual(10, caller_event['tariff'])

        self.assertEqual('IMSI123451234512342', callee_event['from_imsi'])
        self.assertEqual('639360100757', callee_event['from_number'])
        # Check the IMSI value from MockSubscriber's get_imsi_from_number
        self.assertEqual('IMSI000227', callee_event['to_imsi'])
        self.assertEqual('639360100755', callee_event['to_number'])
        self.assertEqual('local_recv_call', callee_event['kind'])
        self.assertEqual(40, callee_event['tariff'])

    def test_parse_free_call_cdr(self):
        """We can extract info from free_call CDRs."""
        cdr_path = self.fixtures_path + 'free-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual('IMSI510555550000996', event['from_imsi'])
        self.assertEqual(None, event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual('888', event['to_number'])
        self.assertEqual(0, event['tariff'])

    def test_parse_error_call_cdr(self):
        """We can extract info from error_call CDRs."""
        cdr_path = self.fixtures_path + 'error-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual('IMSI510555550000087', event['from_imsi'])
        self.assertEqual(None, event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual('6281248174025', event['to_number'])
        self.assertEqual(0, event['tariff'])

    def test_parse_incoming_call_cdr(self):
        """ We can extract info from well-formed incoming call CDRs """
        cdr_path = self.fixtures_path + 'incoming-call-cdr.xml'
        with open(cdr_path) as cdr_file:
            data = {'cdr': cdr_file.read()}
        self.test_app.post('/cdr', params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual(None, event['from_imsi'])
        self.assertEqual('18657194461', event['from_number'])
        self.assertEqual('IMSI901550000000072', event['to_imsi'])
        self.assertEqual('12529178659', event['to_number'])
        self.assertEqual(100, event['tariff'])
class SMSCDRTestCase(unittest.TestCase):
    """Handling SMS CDRs."""
    @classmethod
    def setUpClass(cls):
        """Load up some pricing data into the config db."""
        price_data = [{
            'directionality': 'off_network_send',
            'prefix': '789',
            'country_name': 'Ocenaia',
            'country_code': 'OC',
            'cost_to_subscriber_per_sms': 300,
            'cost_to_subscriber_per_min': 200,
        }, {
            'directionality': 'off_network_receive',
            'cost_to_subscriber_per_sms': 400,
            'cost_to_subscriber_per_min': 100,
        }, {
            'directionality': 'on_network_send',
            'cost_to_subscriber_per_sms': 40,
            'cost_to_subscriber_per_min': 10,
        }, {
            'directionality': 'on_network_receive',
            'cost_to_subscriber_per_sms': 30,
            'cost_to_subscriber_per_min': 40,
        }]
        # Create a simplified checkin response with just price data.
        checkin_response = {'config': {'prices': price_data}}
        # Mock the checkin handler object such that validation just returns the
        # object to-be-validated (without checking JWT).
        mock_checkin_handler = CheckinHandler
        mock_checkin_handler.validate = lambda self, data: data
        mock_checkin_handler(checkin_response)

        # mock subscriber so we dont actually execute DB queries
        mock_subscriber = mocks.MockSubscriber()
        cls.original_subscriber = core.federer_handlers.sms_cdr.subscriber
        core.federer_handlers.sms_cdr.subscriber = mock_subscriber

    @classmethod
    def tearDownClass(cls):
        core.federer_handlers.sms_cdr.subscriber = cls.original_subscriber

    def setUp(self):
        self.test_app = TestApp(core.federer.app.wsgifunc())
        self.endpoint = '/smscdr'
        self.event_store = EventStore()

    def tearDown(self):
        # Reset the EventStore.
        self.event_store.drop_table()

    def test_get(self):
        """Cannot GET to this endpoint."""
        response = self.test_app.get(self.endpoint, expect_errors=True)
        self.assertEqual(405, response.status)

    def test_post_without_data_raises_400(self):
        """Must send some data."""
        data = {}
        response = self.test_app.post(self.endpoint,
                                      params=data,
                                      expect_errors=True)
        self.assertEqual(400, response.status)

    def test_post_with_data_generates_event(self):
        data = {
            'from_name': 'IMSI901550000000084',
            'from_number': '12345',
            'service_type': 'local_sms',
            'destination': '5551234',
        }
        response = self.test_app.post(self.endpoint, params=data)
        self.assertEqual(200, response.status)
        # Local SMS will actually generate two events -- one for the sender and
        # one for the recipient.
        self.assertEqual(2, len(self.event_store.get_events()))

    def test_local_sms(self):
        """We should set event info when sending local_sms."""
        data = {
            'from_name': 'IMSI000234',
            'from_number': '12345',
            'service_type': 'local_sms',
            'destination': '5552345',
        }
        self.test_app.post(self.endpoint, params=data)
        events = self.event_store.get_events()
        event = events[-1]
        # TODO(matt): do we really not have the from_number?
        # XXX(omar): apparently it is required else tests fail
        self.assertEqual(data['from_name'], event['from_imsi'])
        self.assertEqual(data['from_number'], event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual(data['destination'], event['to_number'])
        self.assertEqual(30, event['tariff'])
        # TODO(matt): check that the recipient was also billed.

    def test_outside_sms(self):
        """We should set event info when sending outside_sms."""
        data = {
            'from_name': 'IMSI000345',
            'from_number': '12345',
            'service_type': 'outside_sms',
            'destination': '7895551234',
        }
        self.test_app.post(self.endpoint, params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual(data['from_name'], event['from_imsi'])
        self.assertEqual(data['from_number'], event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual(data['destination'], event['to_number'])
        self.assertEqual(300, event['tariff'])

    def test_free_sms(self):
        """We should set event info when sending free_sms."""
        data = {
            'from_name': 'IMSI000111',
            'from_number': '12345',
            'service_type': 'free_sms',
            'destination': '5552888',
        }
        self.test_app.post(self.endpoint, params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual(data['from_name'], event['from_imsi'])
        self.assertEqual(data['from_number'], event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual(data['destination'], event['to_number'])
        self.assertEqual(0, event['tariff'])

    def test_incoming_sms(self):
        """We should set event info when sending incoming_sms.

        TODO(matt): I think this test is misleading because we do not post
                    incoming_sms events to /smscdr in the real app.  These
                    messages go to federer_handlers.sms.endaga_sms.
        """
        data = {
            'from_name': 'IMSI000333',
            'from_number': '12345',
            'service_type': 'incoming_sms',
            'destination': '5554433',
        }
        self.test_app.post(self.endpoint, params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual(data['from_name'], event['from_imsi'])
        self.assertEqual(data['from_number'], event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual(data['destination'], event['to_number'])
        self.assertEqual(400, event['tariff'])

    def test_error_sms(self):
        """We should set event info when sending error_sms."""
        data = {
            'from_name': 'IMSI000889',
            'from_number': '12345',
            'service_type': 'error_sms',
            'destination': '5556411',
        }
        self.test_app.post(self.endpoint, params=data)
        events = self.event_store.get_events()
        event = events[-1]
        self.assertEqual(data['from_name'], event['from_imsi'])
        self.assertEqual(data['from_number'], event['from_number'])
        self.assertEqual(None, event['to_imsi'])
        self.assertEqual(data['destination'], event['to_number'])
        self.assertEqual(0, event['tariff'])
Ejemplo n.º 5
0
 def __init__(self, response):
     self.conf = ConfigDB()
     self.eventstore = EventStore()
     r = self.validate(response)
     self.process(r)
Ejemplo n.º 6
0
class CheckinHandler(object):

    CONFIG_SECTION = "config"
    EVENTS_SECTION = "events"
    SUBSCRIBERS_SECTION = "subscribers"

    # NOTE: Keys in section_ctx dictionary below must match the keys of
    # optimized checkin sections: "config", "events", "subscribers", etc.
    section_ctx = {
        CONFIG_SECTION: delta.DeltaProtocolCtx(),
        # Note: EVENTS_SECTION is not optimized
        SUBSCRIBERS_SECTION: delta.DeltaProtocolCtx(),
    }

    def __init__(self, response):
        self.conf = ConfigDB()
        self.eventstore = EventStore()
        r = self.validate(response)
        self.process(r)

    def process(self, resp_dict):
        """Process sections of a checkin response.

        Right now we have three sections: config, events, and subscribers.
        """
        if 'status' in resp_dict and resp_dict['status'] == 'deregistered':
            reset_registration()
        for section in resp_dict:
            if section == CheckinHandler.CONFIG_SECTION:
                self.process_config(resp_dict[section])
            elif section == CheckinHandler.EVENTS_SECTION:
                self.process_events(resp_dict[section])
            elif section == CheckinHandler.SUBSCRIBERS_SECTION:
                self.process_subscribers(resp_dict[section])
            elif section != 'status':
                logger.error("Unexpected checkin section: %s" % section)

    def validate(self, response):
        """Validates a response.

        Args:
          response: decoded json response from the server as a python
                    dictionary.

        Returns: a python dictionary containing the checkin response, otherwise
                 throws errors.
        """
        r = json.loads(response)
        return r['response']

    @delta.DeltaCapable(section_ctx['config'], True)
    def process_config(self, config_dict):
        for section in config_dict:
            if section == "endaga":
                self.conf.process_config_update(config_dict[section])
            # TODO cloud should use generic key names not openbts specific
            elif section == "openbts":
                bts.process_bts_settings(config_dict[section])
            elif section == "prices":
                process_prices(config_dict['prices'], self.conf)
            elif section == "autoupgrade":
                self.process_autoupgrade(config_dict['autoupgrade'])

    # wrap the subscriber method in order to keep delta context encapsulated
    @delta.DeltaCapable(section_ctx['subscribers'], True)
    def process_subscribers(self, data_dict):
        subscriber.process_update(data_dict)

    def process_events(self, data_dict):
        """Process information about events.

        Right now, there should only be one value here: seqno, which denotes
        the highest seqno for this BTS for which the server has ack'd.
        """
        if "seqno" in data_dict:
            seqno = int(data_dict['seqno'])
            self.eventstore.ack(seqno)

    def process_autoupgrade(self, data):
        """Process information about autoupgrade preferences.

        Args:
          data: a dict of the form {
            'enabled': True,
            'channel': 'dev',
            'in_window': True,  # whether to upgrade in a window or not.  If
                                # not, this means we should upgrade as soon as
                                # new packages are available.
            'window_start': '02:45:00'
            'latest_stable_version': '1.2.3',
            'latest_beta_version': '5.6.7',
          }

        The configdb keys are prefixed with "autoupgrade." (e.g.
        autoupgrade.enabled).
        """
        for key in ('enabled', 'channel', 'in_window', 'window_start',
                    'latest_stable_version', 'latest_beta_version'):
            configdb_key = 'autoupgrade.%s' % key
            # Set the value if it's not already in the config db or if it's
            # changed.
            existing_value = self.conf.get(configdb_key, None)
            if existing_value != data[key]:
                self.conf[configdb_key] = data[key]
Ejemplo n.º 7
0
def usage(num=100):
    """Returns 'events': List of credits log entries."""
    es = EventStore()
    events = es.get_events(num)
    return {'events': events}
Ejemplo n.º 8
0
def _create_event(imsi,
                  old_credit,
                  new_credit,
                  reason,
                  kind=None,
                  call_duration=None,
                  billsec=None,
                  from_imsi=None,
                  from_number=None,
                  to_imsi=None,
                  to_number=None,
                  tariff=None,
                  up_bytes=None,
                  down_bytes=None,
                  timespan=None,
                  write=True):
    """Logs a generic UsageEvent in the EventStore.

    Also writes this action to logger.

    Args:
      imsi: the IMSI connected to this event
      old_credit: the account's balance before this action
      new_credit: the account's balance after this action
      reason: a string describing this event
      kind: the type of event.  If None, we will attempt to lookup the type
            based on the reason.
      call_duration: duration, including connect, if it was a call (seconds)
      billsec: billable duration of the event if it was a call (seconds)
      from_imsi: sender IMSI
      from_number: sender number
      to_imsi: destination IMSI
      to_number: destination number
      tariff: the cost per unit applied during this transaction
      up_bytes: integer amount of data uploaded during the timespan
      down_bytes: integer amount of data downloaded during the timespan
      timsespan: number of seconds over which this measurement was taken
      write: write event to the eventstore (default: True; only for tests)

    Returns:
        A dictionary representing the event
    """
    template = ('new event: user: %s, old_credit: %d, new_credit: %d,'
                ' change: %d, reason: %s\n')
    message = template % (imsi, old_credit, new_credit,
                          new_credit - old_credit, reason)
    logger.info(message)
    # Add this event to the DB.  This is the canonical definition of a
    # UsageEvent.
    # Version 5: added up_bytes, down_bytes and timespan
    # Version 4: added billsec, call_duration (removed underscore)
    # Version 3: ~~a mystery~~
    # Version 2: added 'call duration', from_imsi, to_imsi, from_number,
    #            to_number
    # Version 1: date, imsi, oldamt, newamt, change, reason, kind
    if not kind:
        kind = kind_from_reason(reason)
    data = {
        'date': time.strftime('%Y-%m-%d %H:%M:%S'),
        'imsi': imsi,
        'oldamt': old_credit,
        'newamt': new_credit,
        'change': new_credit - old_credit,
        'reason': reason,
        'kind': kind,
        'call_duration': call_duration,
        'billsec': billsec,
        'from_imsi': from_imsi,
        'to_imsi': to_imsi,
        'from_number': from_number,
        'to_number': to_number,
        'tariff': tariff,
        'up_bytes': up_bytes,
        'down_bytes': down_bytes,
        'timespan': timespan,
        'version': 5,
    }

    # TODO(shasan): find a way to remove this and mock out in testing instead
    if write:
        event_store = EventStore()
        event_store.add(data)
    return data