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'])
def __init__(self, response): self.conf = ConfigDB() self.eventstore = EventStore() r = self.validate(response) self.process(r)
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]
def usage(num=100): """Returns 'events': List of credits log entries.""" es = EventStore() events = es.get_events(num) return {'events': events}
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