def test_store_user(user_db_manager): # Tests that users can be properly stored in the database # Store user should work as long as the user_pk is properly formatted and data is a dictionary user_id = "02" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) assert user_db_manager.store_user(user_id, user_info.to_dict()) is True
def test_add_appointment(watcher, generate_dummy_appointment): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) # The slot count should not have been reduced and only one copy is kept. assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) watcher.gatekeeper.registered_users[another_user_id] = UserInfo( available_slots=available_slots, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert response.get("available_slots") == available_slots - 1 assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_load_user(user_db_manager): # Tests that loading a user should work, as long as the user is there # Add the user first user_id = "02" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) user_db_manager.store_user(user_id, user_info.to_dict()) # Now load it assert user_db_manager.load_user(user_id) == user_info.to_dict()
def test_user_info_from_dict_wrong_data(): # If any of the dictionary fields is missing, building from dict will fail d1 = {"available_slots": "", "subscription_expiry": ""} d2 = {"available_slots": "", "appointments": ""} d3 = {"subscription_expiry": "", "appointments": ""} for d in [d1, d2, d3]: with pytest.raises( ValueError, match="Wrong appointment data, some fields are missing"): UserInfo.from_dict(d)
def test_store_user_wrong(user_db_manager): # Tests that trying to store wrong data will fail # Wrong pks should return False on adding user_id = "04" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) assert user_db_manager.store_user(user_id, user_info.to_dict()) is False # Same for wrong types assert user_db_manager.store_user(42, user_info.to_dict()) is False # And for wrong type user data assert user_db_manager.store_user(user_id, 42) is False
def test_add_appointment_in_cache(watcher): # Generate an appointment and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=10) appointment, dispute_tx = generate_dummy_appointment() dispute_txid = watcher.block_processor.decode_raw_transaction(dispute_tx).get("txid") watcher.locator_cache.cache[appointment.locator] = dispute_txid # Try to add the appointment response = watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk)) # The appointment is accepted but it's not in the Watcher assert ( response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk(Cryptographer.recover_pk(appointment.serialize(), response.get("signature"))) ) assert not watcher.locator_uuid_map.get(appointment.locator) # It went to the Responder straightaway assert appointment.locator in [tracker.get("locator") for tracker in watcher.responder.trackers.values()] # Trying to send it again should fail since it is already in the Responder with pytest.raises(AppointmentAlreadyTriggered): watcher.add_appointment(appointment, Cryptographer.sign(appointment.serialize(), user_sk))
def test_add_appointment_multiple_times_same_user(internal_api, client, appointment, block_processor, n=MULTIPLE_APPOINTMENTS): # Multiple appointments with the same locator should be valid and count as updates appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Simulate registering enough slots (end time does not matter here) internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=n, subscription_expiry=block_processor.get_block_count() + 1) for _ in range(n): r = add_appointment( client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert r.status_code == HTTP_OK assert r.json.get("available_slots") == n - 1 assert r.json.get("start_block") == block_processor.get_block_count() # Since all updates came from the same user, only the last one is stored assert len(internal_api.watcher.locator_uuid_map[appointment.locator]) == 1
def test_add_appointment_in_cache_invalid_transaction( watcher, generate_dummy_appointment): # Generate an appointment that cannot be decrypted and add the dispute txid to the cache user_sk, user_pk = generate_keypair() user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=watcher.block_processor.get_block_count() + 1) appointment, dispute_tx = generate_dummy_appointment() appointment.encrypted_blob = appointment.encrypted_blob[::-1] dispute_txid = watcher.block_processor.decode_raw_transaction( dispute_tx).get("txid") watcher.locator_cache.cache[appointment.locator] = dispute_txid # Try to add the appointment user_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, user_signature) appointment_receipt = receipts.create_appointment_receipt( user_signature, response.get("start_block")) # The appointment is accepted but dropped (same as an invalid appointment that gets triggered) assert (response and response.get("locator") == appointment.locator and Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature")))) assert not watcher.locator_uuid_map.get(appointment.locator) assert appointment.locator not in [ tracker.get("locator") for tracker in watcher.responder.trackers.values() ]
def test_delete_outdated_users(gatekeeper): # This tests the deletion of users whose subscription has outdated (subscription expires now) # Create some users with associated data and add them to the gatekeeper users = {} current_height = gatekeeper.block_processor.get_block_count() for _ in range(10): appointments = { get_random_value_hex(32): Appointment(get_random_value_hex(32), None, None) } user_id = get_random_value_hex(16) user_info = UserInfo(available_slots=100, subscription_expiry=current_height, appointments=appointments) users[user_id] = user_info gatekeeper.registered_users[user_id] = user_info # Get a list of the users that should be deleted at this block height (must match the newly generated ones) users_to_be_deleted = gatekeeper.get_outdated_user_ids( current_height + gatekeeper.expiry_delta) assert users_to_be_deleted == list(users.keys()) # Delete the users Cleaner.delete_outdated_users(users_to_be_deleted, gatekeeper.registered_users, gatekeeper.user_db) # Check that the users are not in the gatekeeper anymore for user_id in users_to_be_deleted: assert user_id not in gatekeeper.registered_users assert not gatekeeper.user_db.load_user(user_id)
def test_delete_gatekeeper_appointments(gatekeeper): # delete_gatekeeper_appointments should delete the appointments from user as long as both exist appointments_not_to_delete = {} appointments_to_delete = {} # Let's add some users and appointments to the Gatekeeper for _ in range(10): user_id = get_random_value_hex(16) # The UserInfo params do not matter much here gatekeeper.registered_users[user_id] = UserInfo(available_slots=100, subscription_expiry=0) for _ in range(random.randint(0, 10)): # Add some appointments uuid = get_random_value_hex(16) gatekeeper.registered_users[user_id].appointments[uuid] = 1 if random.randint(0, 1) % 2: appointments_to_delete[uuid] = user_id else: appointments_not_to_delete[uuid] = user_id # Now let's delete half of them Cleaner.delete_gatekeeper_appointments(gatekeeper, appointments_to_delete) all_appointments_gatekeeper = [] # Let's get all the appointments in the Gatekeeper for user_id, user in gatekeeper.registered_users.items(): all_appointments_gatekeeper.extend(user.appointments) # Check that the first half of the appointments are not in the Gatekeeper, but the second half is assert not set(appointments_to_delete).issubset( all_appointments_gatekeeper) assert set(appointments_not_to_delete).issubset( all_appointments_gatekeeper)
def test_filter_valid_breaches(watcher, generate_dummy_appointment_w_trigger, monkeypatch): # filter_breaches returns computes two collections, one with the valid breaches (breaches that properly decrypt # appointments) and one with invalid ones. Test it with a single valid breach. # Create a new appointment dummy_appointment, dispute_txid = generate_dummy_appointment_w_trigger() # Mock the interaction with the Gatekeeper. We'll the appointment to the Watcher instead of mocking all the # structures + db expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) watcher.add_appointment(dummy_appointment, dummy_appointment.user_signature) # Filter the data and check potential_breaches = {dummy_appointment.locator: dispute_txid} valid_breaches, invalid_breaches = watcher.filter_breaches( potential_breaches) # We have "triggered" a single breach and it was valid. assert len(invalid_breaches) == 0 and len(valid_breaches) == 1
def test_add_appointment_multiple_times_different_users( internal_api, client, appointment, block_processor, n=MULTIPLE_APPOINTMENTS): # If the same appointment comes from different users, all are kept # Create user keys and appointment signatures user_keys = [generate_keypair() for _ in range(n)] signatures = [ Cryptographer.sign(appointment.serialize(), key[0]) for key in user_keys ] tmp_user_ids = [Cryptographer.get_compressed_pk(pk) for _, pk in user_keys] # Add one slot per public key for pair in user_keys: user_id = Cryptographer.get_compressed_pk(pair[1]) internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) # Send the appointments for compressed_pk, signature in zip(tmp_user_ids, signatures): r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": signature }, compressed_pk) assert r.status_code == HTTP_OK assert r.json.get("available_slots") == 0 assert r.json.get("start_block") == block_processor.get_block_count() # Check that all the appointments have been added and that there are no duplicates assert len(set( internal_api.watcher.locator_uuid_map[appointment.locator])) == n
def test_add_too_many_appointments(watcher, generate_dummy_appointment, monkeypatch): # Adding appointment beyond the user limit should fail # Mock the user being registered expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) for i in range(user_info.available_slots): appointment = generate_dummy_appointment() response = watcher.add_appointment(appointment, appointment.user_signature) appointment_receipt = receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment_receipt, response.get("signature"))) with pytest.raises(AppointmentLimitReached): appointment = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_too_many_appointments(watcher): # Simulate the user is registered user_sk, user_pk = generate_keypair() available_slots = 100 user_id = Cryptographer.get_compressed_pk(user_pk) watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=available_slots, subscription_expiry=10) # Appointments on top of the limit should be rejected watcher.appointments = dict() for i in range(MAX_APPOINTMENTS): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk(watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk(appointment.serialize(), response.get("signature")) ) assert response.get("available_slots") == available_slots - (i + 1) with pytest.raises(AppointmentLimitReached): appointment, dispute_tx = generate_dummy_appointment() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) watcher.add_appointment(appointment, appointment_signature)
def test_add_too_many_appointment(internal_api, client, block_processor, generate_dummy_appointment): # Give slots to the user internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=200, subscription_expiry=block_processor.get_block_count() + 1) free_appointment_slots = MAX_APPOINTMENTS - len( internal_api.watcher.appointments) for i in range(free_appointment_slots + 1): appointment, dispute_tx = generate_dummy_appointment() locator_dispute_tx_map[appointment.locator] = dispute_tx appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment( client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) if i < free_appointment_slots: assert r.status_code == HTTP_OK and r.json.get( "start_block") == block_processor.get_block_count() else: assert r.status_code == HTTP_SERVICE_UNAVAILABLE
def test_add_appointment_update_smaller(internal_api, client, appointment, block_processor): # Update an appointment by one bigger, and check slots are freed internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=2, subscription_expiry=block_processor.get_block_count() + 1) # This should take 2 slots appointment.encrypted_blob = TWO_SLOTS_BLOTS appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count()) # Let's update with one just small enough appointment.encrypted_blob = "A" * (ENCRYPTED_BLOB_MAX_SIZE_HEX - 2) appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 1 and r.json.get("start_block") == block_processor.get_block_count())
def test_add_appointment_update_same_size(internal_api, client, appointment, block_processor): # Update an appointment by one of the same size and check that no additional slots are filled internal_api.watcher.gatekeeper.registered_users[user_id] = UserInfo( available_slots=1, subscription_expiry=block_processor.get_block_count() + 1) appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count()) # The user has no additional slots, but it should be able to update # Let's just reverse the encrypted blob for example appointment.encrypted_blob = appointment.encrypted_blob[::-1] appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) r = add_appointment(client, { "appointment": appointment.to_dict(), "signature": appointment_signature }, user_id) assert (r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == block_processor.get_block_count())
def test_add_appointment_in_cache_invalid_transaction(api, client): api.watcher.gatekeeper.registered_users[user_id] = UserInfo(available_slots=1, subscription_expiry=0) # We need to create the appointment manually dispute_tx = create_dummy_transaction() dispute_txid = dispute_tx.tx_id.hex() penalty_tx = create_dummy_transaction(dispute_txid) locator = compute_locator(dispute_txid) dummy_appointment_data = {"tx": penalty_tx.hex(), "tx_id": dispute_txid, "to_self_delay": 20} encrypted_blob = Cryptographer.encrypt(dummy_appointment_data.get("tx")[::-1], dummy_appointment_data.get("tx_id")) appointment_data = { "locator": locator, "to_self_delay": dummy_appointment_data.get("to_self_delay"), "encrypted_blob": encrypted_blob, "user_id": get_random_value_hex(16), } appointment = ExtendedAppointment.from_dict(appointment_data) api.watcher.locator_cache.cache[appointment.locator] = dispute_tx.tx_id.hex() appointment_signature = Cryptographer.sign(appointment.serialize(), user_sk) # Add the data to the cache api.watcher.locator_cache.cache[dispute_txid] = appointment.locator # The appointment should be accepted r = add_appointment(client, {"appointment": appointment.to_dict(), "signature": appointment_signature}, user_id) assert ( r.status_code == HTTP_OK and r.json.get("available_slots") == 0 and r.json.get("start_block") == api.watcher.last_known_block )
def test_get_user(internal_api, stub, monkeypatch): # it doesn't matter they are not valid user ids and user data object for this test mock_user_id = "02c73bad28b78dd7e3bcad609d330e0d60b97fa0e08ca1cf486cb6cab8dd6140ac" mock_available_slots = 100 mock_subscription_expiry = 1234 mock_user_info = UserInfo(mock_available_slots, mock_subscription_expiry) def mock_get_user_info(user_id): if user_id == mock_user_id: return mock_user_info else: raise RuntimeError(f"called with an unexpected user_id: {user_id}") monkeypatch.setattr(internal_api.watcher, "get_user_info", mock_get_user_info) response = stub.get_user(GetUserRequest(user_id=mock_user_id)) assert isinstance(response, GetUserResponse) # FIXME: numbers are currently returned as floats, even if they are integers assert json_format.MessageToDict(response.user) == { "appointments": [], "available_slots": float(mock_available_slots), "subscription_expiry": float(mock_subscription_expiry), }
def test_update_outdated_users_cache(gatekeeper): # update_outdated_users_cache is used to add new entries to the cache and prune old ones if it grows beyond the # limit. In normal conditions (no reorg) the method is called once per block height, meaning that the data won't be # in the cache when called and it will be afterwards current_block_height = gatekeeper.block_processor.get_block_count() appointments = {get_random_value_hex(32): 1 for _ in range(10)} user_info = UserInfo(available_slots=1, subscription_expiry=current_block_height + 42, appointments=appointments) user_id = get_random_value_hex(32) gatekeeper.registered_users[user_id] = user_info # Check that the entry is not in the cache target_height = current_block_height + gatekeeper.expiry_delta + 42 assert target_height not in gatekeeper.outdated_users_cache # Update the cache and check gatekeeper.update_outdated_users_cache(target_height) cache_entry = gatekeeper.outdated_users_cache.get(target_height) # Values is not meant to be used straightaway for checking, but we can flatten it to check it matches flattened_appointments = [ uuid for user_appointments in cache_entry.values() for uuid in user_appointments ] assert list(cache_entry.keys()) == [ user_id ] and flattened_appointments == list(appointments.keys())
def test_update_outdated_users_cache_remove_data(gatekeeper): # Tests how the oldest piece of data is removed after OUTDATED_USERS_CACHE_SIZE_BLOCKS current_block_height = gatekeeper.block_processor.get_block_count() # Add users that are expiring from the current block to OUTDATED_USERS_CACHE_SIZE_BLOCKS -1 and fill the cache with # them data = {} for i in range(OUTDATED_USERS_CACHE_SIZE_BLOCKS): appointments = {get_random_value_hex(32): 1 for _ in range(10)} user_info = UserInfo(available_slots=1, subscription_expiry=current_block_height + i, appointments=appointments) user_id = get_random_value_hex(32) gatekeeper.registered_users[user_id] = user_info target_block = current_block_height + gatekeeper.expiry_delta + i gatekeeper.update_outdated_users_cache(target_block) # Create a local version of the expected data to compare {block_id: {user_id: [appointment_uuids]}, ...} data[target_block] = {user_id: list(appointments.keys())} # Check that the cache is full and that each position matches assert len( gatekeeper.outdated_users_cache) == OUTDATED_USERS_CACHE_SIZE_BLOCKS assert gatekeeper.outdated_users_cache == data # Add more blocks and check what data gets kicked (data has an offset of OUTDATED_USERS_CACHE_SIZE_BLOCKS, so we can # check if the previous key is there easily) for i in range(OUTDATED_USERS_CACHE_SIZE_BLOCKS): target_block = current_block_height + gatekeeper.expiry_delta + OUTDATED_USERS_CACHE_SIZE_BLOCKS + i assert target_block - OUTDATED_USERS_CACHE_SIZE_BLOCKS in gatekeeper.outdated_users_cache gatekeeper.update_outdated_users_cache(target_block) assert target_block - OUTDATED_USERS_CACHE_SIZE_BLOCKS not in gatekeeper.outdated_users_cache
def test_get_outdated_appointments(gatekeeper): # get_outdated_appointments returns a list of appointment uuids being outdated a a given height appointment = {} # Let's simulate adding some users with dummy expiry times gatekeeper.registered_users = {} for i in range(100): # Add more than one user expiring at the same time to check it works for multiple users for _ in range(2): uuid = get_random_value_hex(16) user_appointments = { get_random_value_hex(16): 1 for _ in range(10) } # Add a single appointment to the user gatekeeper.registered_users[uuid] = UserInfo( 100, i, user_appointments) if i in appointment: appointment[i].update(user_appointments) else: appointment[i] = deepcopy(user_appointments) # Now let's check that the appointments are outdated a the proper time for i in range(100): assert gatekeeper.get_outdated_appointments( i + gatekeeper.expiry_delta) == list(appointment[i].keys())
def test_user_info_init(): available_slots = 42 subscription_expiry = 1234 # UserInfo objects can be created without appointments user_info = UserInfo(available_slots, subscription_expiry) assert user_info.available_slots == available_slots assert user_info.subscription_expiry == subscription_expiry assert user_info.appointments == {} # But also with, in case they are being restored from the database (the actual data does not matter here) appointments = {get_random_value_hex(32): 1 for _ in range(10)} user_info = UserInfo(available_slots, subscription_expiry, appointments) assert user_info.available_slots == available_slots assert user_info.subscription_expiry == subscription_expiry assert user_info.appointments == appointments
def test_update_outdated_users_cache(gatekeeper, monkeypatch): # update_outdated_users_cache is used to add new entries to the cache and prune old ones if it grows beyond the # limit. In normal conditions (no reorg) the method is called once per block height, meaning that the data won't be # in the cache when called and it will be afterwards # Mock the required BlockProcessor calls from the Gatekeeper init_height = 0 monkeypatch.setattr(gatekeeper.block_processor, "get_block_count", lambda: init_height) appointments = {get_random_value_hex(32): 1 for _ in range(10)} user_info = UserInfo(available_slots=1, subscription_expiry=init_height + 42, appointments=appointments) user_id = get_random_value_hex(32) monkeypatch.setitem(gatekeeper.registered_users, user_id, user_info) # Check that the entry is not in the cache target_height = init_height + gatekeeper.expiry_delta + 42 assert target_height not in gatekeeper.outdated_users_cache # Update the cache and check gatekeeper.update_outdated_users_cache(target_height) cache_entry = gatekeeper.outdated_users_cache.get(target_height) # Values is not meant to be used straightaway for checking, but we can flatten it to check it matches flattened_appointments = list(flatten(cache_entry.values())) assert list(cache_entry.keys()) == [ user_id ] and flattened_appointments == list(appointments.keys())
def test_get_subscription_info(watcher, generate_dummy_appointment, generate_dummy_tracker, monkeypatch): # Tests how get_subscription_info should return no data for empty subscriptions, and the info matching the # subscriptions otherwise. # Mock a registered user available_slots = MAX_APPOINTMENTS subscription_expiry = 100 user_info = UserInfo(available_slots, subscription_expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, subscription_expiry)) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) message = "get subscription info" signature = Cryptographer.sign(message.encode("utf-8"), user_sk) # Empty subscription sub_info, locators = watcher.get_subscription_info(signature) assert len(locators) == 0 assert sub_info.available_slots == available_slots assert sub_info.subscription_expiry == subscription_expiry # Generate some subscription data uuid = uuid4().hex uuid2 = uuid4().hex appointment = generate_dummy_appointment() tracker = generate_dummy_tracker() # Mock the Watcher, Responder and Gatekeeper's data structures monkeypatch.setattr(watcher, "appointments", {uuid: appointment.get_summary()}) monkeypatch.setattr(watcher.responder, "trackers", {uuid2: tracker.get_summary()}) user_info.appointments = {uuid: 1, uuid2: 1} # And the interaction with the Responder monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: True) monkeypatch.setattr(watcher.responder, "get_tracker", lambda x: tracker.get_summary()) sub_info, locators = watcher.get_subscription_info(signature) assert set(locators) == {appointment.locator, tracker.locator} assert sub_info.available_slots == available_slots assert sub_info.subscription_expiry == subscription_expiry
def test_add_appointment(watcher, generate_dummy_appointment, monkeypatch): # A registered user with no subscription issues should be able to add an appointment appointment = generate_dummy_appointment() # Mock a registered user expiry = 100 user_info = UserInfo(MAX_APPOINTMENTS, expiry) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: user_id) monkeypatch.setattr(watcher.gatekeeper, "has_subscription_expired", lambda x: (False, expiry)) monkeypatch.setattr(watcher.responder, "has_tracker", lambda x: False) monkeypatch.setattr(watcher.gatekeeper, "add_update_appointment", lambda x, y, z: MAX_APPOINTMENTS - 1) monkeypatch.setattr(watcher.gatekeeper, "get_user_info", lambda x: user_info) response = watcher.add_appointment(appointment, appointment.user_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")), response.get("signature"), )) # Check that we can also add an already added appointment (same locator) response = watcher.add_appointment(appointment, appointment.user_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment.user_signature, response.get("start_block")), response.get("signature"), )) # One one copy is kept since the appointments were the same # (the slot count should have not been reduced, but that's something to be tested in the Gatekeeper) assert len(watcher.locator_uuid_map[appointment.locator]) == 1 # If two appointments with the same locator come from different users, they are kept. another_user_sk, another_user_pk = generate_keypair() another_user_id = Cryptographer.get_compressed_pk(another_user_pk) monkeypatch.setattr(watcher.gatekeeper, "authenticate_user", lambda x, y: another_user_id) appointment_signature = Cryptographer.sign(appointment.serialize(), another_user_sk) response = watcher.add_appointment(appointment, appointment_signature) assert response.get("locator") == appointment.locator assert Cryptographer.get_compressed_pk( watcher.signing_key.public_key) == Cryptographer.get_compressed_pk( Cryptographer.recover_pk( receipts.create_appointment_receipt( appointment_signature, response.get("start_block")), response.get("signature"), )) assert len(watcher.locator_uuid_map[appointment.locator]) == 2
def test_get_outdated_users(gatekeeper): # Gets a list of users whose subscription gets outdated at a given height current_height = gatekeeper.block_processor.get_block_count() uuids = [get_random_value_hex(32) for _ in range(20)] outdated_users_next = { get_random_value_hex(32): UserInfo( available_slots=10, subscription_expiry=current_height - gatekeeper.expiry_delta + 1, appointments={uuids[i]: 1}, # uuid:1 slot ) for i in range(10) } outdated_users_next_next = { get_random_value_hex(32): UserInfo( available_slots=10, subscription_expiry=current_height - gatekeeper.expiry_delta + 2, appointments={uuids[i + 10]: 1}, # uuid:1 slot ) for i in range(10) } # Add users to the Gatekeeper gatekeeper.registered_users.update(outdated_users_next) gatekeeper.registered_users.update(outdated_users_next_next) # Check that outdated_users_cache are outdated at the current height outdated_users = gatekeeper.get_outdated_users(current_height + 1).keys() outdated_appointment_uuids = [ uuid for user_appointments in gatekeeper.get_outdated_users(current_height + 1).values() for uuid in user_appointments ] assert outdated_users == outdated_users_next.keys( ) and outdated_appointment_uuids == uuids[:10] outdated_users = gatekeeper.get_outdated_users(current_height + 2).keys() outdated_appointment_uuids = [ uuid for user_appointments in gatekeeper.get_outdated_users(current_height + 2).values() for uuid in user_appointments ] assert outdated_users == outdated_users_next_next.keys( ) and outdated_appointment_uuids == uuids[10:]
def test_delete_user(user_db_manager): # Tests that deleting existing users should work stored_users = {} # Add some users first for _ in range(10): user_id = "02" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) user_db_manager.store_user(user_id, user_info.to_dict()) stored_users[user_id] = user_info # Deleting existing users should work for user_id, user_data in stored_users.items(): assert user_db_manager.delete_user(user_id) is True # There should be no users anymore assert not user_db_manager.load_all_users()
def test_load_all_users(user_db_manager): # Tests loading all the users in the database stored_users = {} # There should be no users at the moment assert user_db_manager.load_all_users() == {} stored_users = {} # Adding some and checking we get them all for i in range(10): user_id = "02" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) user_db_manager.store_user(user_id, user_info.to_dict()) stored_users[user_id] = user_info.to_dict() all_users = user_db_manager.load_all_users() assert all_users == stored_users
def test_load_all_users(user_db_manager): # There should be no users at the moment assert user_db_manager.load_all_users() == {} stored_users = {} # Adding some and checking we get them all for i in range(10): user_id = "02" + get_random_value_hex(32) user_info = UserInfo(available_slots=42, subscription_expiry=100) user_db_manager.store_user(user_id, user_info.to_dict()) stored_users[user_id] = user_info.to_dict() all_users = user_db_manager.load_all_users() assert set(all_users.keys()) == set(stored_users.keys()) for k, v in all_users.items(): assert stored_users[k] == v