diff --git a/.gitignore b/.gitignore index 2b65a13..30fa2fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc dist openpaygo.egg-info -.DS_store \ No newline at end of file +.DS_store +__pycache__ +venv diff --git a/openpaygo/metrics_shared.py b/openpaygo/metrics_shared.py index 586ab2e..938857d 100644 --- a/openpaygo/metrics_shared.py +++ b/openpaygo/metrics_shared.py @@ -146,10 +146,27 @@ def generate_hash_string(cls, input_string, secret_key): @classmethod def load_secret_key_from_hex(cls, secret_key): + if isinstance(secret_key, (bytes, bytearray)): + secret_key_bytes = bytes(secret_key) + if len(secret_key_bytes) != 16: + raise ValueError( + "The secret key provided is not correctly formatted, it should be 16 " + "bytes. " + ) + return secret_key_bytes + try: - return codecs.decode(secret_key, "hex") + decoded = codecs.decode(secret_key, "hex") except Exception: raise ValueError( "The secret key provided is not correctly formatted, it should be 32 " "hexadecimal characters. " ) + + if len(decoded) != 16: + raise ValueError( + "The secret key provided is not correctly formatted, it should be 32 " + "hexadecimal characters. " + ) + + return decoded diff --git a/openpaygo/simulators/__init__.py b/openpaygo/simulators/__init__.py new file mode 100644 index 0000000..c01c5aa --- /dev/null +++ b/openpaygo/simulators/__init__.py @@ -0,0 +1,4 @@ +from .device_simulator import DeviceSimulator as DeviceSimulator +from .server_simulator import SingleDeviceServerSimulator as SingleDeviceServerSimulator + +__all__ = ["DeviceSimulator", "SingleDeviceServerSimulator"] \ No newline at end of file diff --git a/openpaygo/simulators/device_simulator.py b/openpaygo/simulators/device_simulator.py new file mode 100644 index 0000000..8737f66 --- /dev/null +++ b/openpaygo/simulators/device_simulator.py @@ -0,0 +1,159 @@ +from datetime import datetime, timedelta + +from openpaygo.token_decode import OpenPAYGOTokenDecoder, TokenType +from openpaygo.token_shared import OpenPAYGOTokenShared + + +class DeviceSimulator(object): + + def __init__( + self, + starting_code, + key, + starting_count=1, + restricted_digit_set=False, + waiting_period_enabled=True, + time_divider=1, + ): + self.starting_code = starting_code + self.key = key + self.time_divider = time_divider + self.restricted_digit_set = restricted_digit_set + self.waiting_period_enabled = ( + waiting_period_enabled # Should always be true except for testing + ) + + self.payg_enabled = True + self.count = starting_count + self.expiration_date = datetime.now() + self.invalid_token_count = 0 + self.token_entry_blocked_until = datetime.now() + self.used_counts = [] + + def print_status(self): + print('-------------------------') + print('Expiration Date: '+ str(self.expiration_date)) + print('Current count: '+str(self.count)) + print('PAYG Enabled: '+str(self.payg_enabled)) + print('Active: '+str(self.is_active())) + print('-------------------------') + + def is_active(self): + return self.expiration_date > datetime.now() + + def enter_token(self, token, show_result=True): + if len(token) == 9: + token_int = int(token) + return self._update_device_status_from_token(token_int, show_result) + else: + token_int = int(token) + return self._update_device_status_from_extended_token(token_int, show_result) + + def get_days_remaining(self): + if self.payg_enabled: + td = self.expiration_date - datetime.now() + days, hours, minutes = td.days, td.seconds//3600, (td.seconds//60)%60 + days = days + (hours + minutes/60)/24 + return round(days) + else: + return 'infinite' + + def _update_device_status_from_token(self, token, show_result=True): + if ( + self.token_entry_blocked_until > datetime.now() + and self.waiting_period_enabled + ): + if show_result: + print('TOKEN_ENTRY_BLOCKED') + return False + + token_value, token_type, token_count, updated_counts = ( + OpenPAYGOTokenDecoder.get_activation_value_count_and_type_from_token( + token=token, + starting_code=self.starting_code, + key=self.key, + last_count=self.count, + restricted_digit_set=self.restricted_digit_set, + used_counts=self.used_counts, + ) + ) + if token_value is None: + if token_type == TokenType.ALREADY_USED: + if show_result: + print('OLD_TOKEN') + return -2 + if show_result: + print('TOKEN_INVALID') + self.invalid_token_count += 1 + self.token_entry_blocked_until = datetime.now() + timedelta( + minutes=2**self.invalid_token_count + ) + return -1 + elif token_value == -2: + if show_result: + print('OLD_TOKEN') + return -2 + else: + if show_result: + print('TOKEN_VALID', ' | Value:', token_value) + if ( + token_count > self.count + or token_value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE + ): + self.count = token_count + self.used_counts = updated_counts + self.invalid_token_count = 0 + self._update_device_status_from_token_value(token_value, token_type) + return 1 + + def _update_device_status_from_extended_token(self, token, show_result=True): + if ( + self.token_entry_blocked_until > datetime.now() + and self.waiting_period_enabled + ): + if show_result: + print('TOKEN_ENTRY_BLOCKED') + + token_value, token_count = ( + OpenPAYGOTokenDecoder.get_activation_value_count_from_extended_token( + token=token, + starting_code=self.starting_code, + key=self.key, + last_count=self.count, + restricted_digit_set=self.restricted_digit_set, + ) + ) + if token_value is None: + if show_result: + print('TOKEN_INVALID') + self.invalid_token_count += 1 + self.token_entry_blocked_until = datetime.now() + timedelta( + minutes=2**self.invalid_token_count + ) + else: + if show_result: + print('Special token entered, value: '+str(token_value)) + + def _update_device_status_from_token_value(self, token_value, token_type): + if token_value <= OpenPAYGOTokenShared.MAX_ACTIVATION_VALUE: + if not self.payg_enabled and token_type == TokenType.SET_TIME: + self.payg_enabled = True + if self.payg_enabled: + self._update_expiration_date_from_value(token_value, token_type) + elif token_value == OpenPAYGOTokenShared.PAYG_DISABLE_VALUE: + self.payg_enabled = False + elif token_value != OpenPAYGOTokenShared.COUNTER_SYNC_VALUE: + # We do nothing if its the sync counter value, the counter has been synced already + print('COUNTER_SYNCED') + else: + # If it's another value we also do nothing, as they are not defined + print('UNKNOWN_COMMAND') + + def _update_expiration_date_from_value(self, toke_value, token_type): + number_of_days = toke_value/self.time_divider + if token_type == TokenType.SET_TIME: + self.expiration_date = datetime.now() + timedelta(days=number_of_days) + else: + if self.expiration_date < datetime.now(): + self.expiration_date = datetime.now() + self.expiration_date = self.expiration_date + timedelta(days=number_of_days) diff --git a/openpaygo/simulators/server_simulator.py b/openpaygo/simulators/server_simulator.py new file mode 100644 index 0000000..aeb2f9d --- /dev/null +++ b/openpaygo/simulators/server_simulator.py @@ -0,0 +1,110 @@ +from datetime import datetime + +from openpaygo.token_encode import OpenPAYGOTokenEncoder +from openpaygo.token_shared import OpenPAYGOTokenShared, TokenType + + +class SingleDeviceServerSimulator(object): + + def __init__( + self, + starting_code, + key, + starting_count=1, + restricted_digit_set=False, + time_divider=1, + ): + self.starting_code = starting_code + self.key = key + self.count = starting_count + self.expiration_date = datetime.now() + self.furthest_expiration_date = datetime.now() + self.payg_enabled = True + self.time_divider = time_divider + self.restricted_digit_set = restricted_digit_set + + def print_status(self): + print('Expiration Date: '+ str(self.expiration_date)) + print('Current count: '+str(self.count)) + print('PAYG Enabled: '+str(self.payg_enabled)) + + def generate_payg_disable_token(self): + self.count, token = OpenPAYGOTokenEncoder.generate_token( + starting_code=self.starting_code, + secret_key=self.key, + value=0, + count=self.count, + token_type=TokenType.DISABLE_PAYG, + restricted_digit_set=self.restricted_digit_set + ) + return SingleDeviceServerSimulator._format_token(token) + + def generate_counter_sync_token(self): + self.count, token = OpenPAYGOTokenEncoder.generate_token( + starting_code=self.starting_code, + secret_key=self.key, + value=0, + count=self.count, + token_type=TokenType.COUNTER_SYNC, + restricted_digit_set=self.restricted_digit_set + ) + return SingleDeviceServerSimulator._format_token(token) + + def generate_token_from_date(self, new_expiration_date, force=False): + furthest_expiration_date = self.furthest_expiration_date + if new_expiration_date > self.furthest_expiration_date: + self.furthest_expiration_date = new_expiration_date + + if new_expiration_date > furthest_expiration_date: + # If the date is strictly above the furthest date activated, use ADD + value = self._get_value_to_activate( + new_expiration_date, self.expiration_date, force + ) + self.expiration_date = new_expiration_date + return self._generate_token_from_value(value, mode=TokenType.ADD_TIME) + else: + # If the date is below or equal to the furthest date activated, use SET + value = self._get_value_to_activate( + new_expiration_date, datetime.now(), force + ) + self.expiration_date = new_expiration_date + return self._generate_token_from_value(value, mode=TokenType.SET_TIME) + + def _generate_token_from_value(self, value, mode): + self.count, token = OpenPAYGOTokenEncoder.generate_token( + starting_code=self.starting_code, + secret_key=self.key, + value=value, + count=self.count, + token_type=mode, + restricted_digit_set=self.restricted_digit_set + ) + return SingleDeviceServerSimulator._format_token(token) + + def _generate_extended_value_token(self, value): + pass + + def _get_value_to_activate(self, new_time, reference_time, force_maximum=False): + if new_time <= reference_time: + return 0 + else: + days = self._timedelta_to_days(new_time - reference_time) + value = int(round(days*self.time_divider, 0)) + if value > OpenPAYGOTokenShared.MAX_ACTIVATION_VALUE: + if not force_maximum: + raise Exception('TOO_MANY_DAYS_TO_ACTIVATE') + else: + # Will need to be activated again after those days + return OpenPAYGOTokenShared.MAX_ACTIVATION_VALUE + return value + + @staticmethod + def _timedelta_to_days(this_timedelta): + return this_timedelta.days + (this_timedelta.seconds / 3600 / 24) + + @staticmethod + def _format_token(token): + token = str(token) + if len(token) < 9: + token = '0' * (9 - len(token)) + token + return token diff --git a/openpaygo/token_decode.py b/openpaygo/token_decode.py index 5a389bb..b813fa0 100644 --- a/openpaygo/token_decode.py +++ b/openpaygo/token_decode.py @@ -134,6 +134,8 @@ def get_activation_value_count_and_type_from_token( @classmethod def _count_is_valid(cls, count, last_count, value, type, used_counts): + if used_counts is None: + used_counts = [] if value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE: if count > (last_count - cls.MAX_TOKEN_JUMP): return True @@ -147,7 +149,7 @@ def _count_is_valid(cls, count, last_count, value, type, used_counts): @classmethod def update_used_counts(cls, past_used_counts, value, new_count, type): - if not past_used_counts: + if past_used_counts is None: return None highest_count = max(past_used_counts) if past_used_counts else 0 if new_count > highest_count: diff --git a/openpaygo/token_shared.py b/openpaygo/token_shared.py index 2529b91..d7df940 100644 --- a/openpaygo/token_shared.py +++ b/openpaygo/token_shared.py @@ -61,14 +61,31 @@ def generate_starting_code(cls, key): @classmethod def load_secret_key_from_hex(cls, secret_key): + if isinstance(secret_key, (bytes, bytearray)): + secret_key_bytes = bytes(secret_key) + if len(secret_key_bytes) != 16: + raise ValueError( + "The secret key provided is not correctly formatted, it should be 16 " + "bytes. " + ) + return secret_key_bytes + try: - return codecs.decode(secret_key, "hex") + decoded = codecs.decode(secret_key, "hex") except Exception: raise ValueError( "The secret key provided is not correctly formatted, it should be 32 " "hexadecimal characters. " ) + if len(decoded) != 16: + raise ValueError( + "The secret key provided is not correctly formatted, it should be 32 " + "hexadecimal characters. " + ) + + return decoded + @classmethod def _convert_to_29_5_bits(cls, source): mask = ((1 << (32 - 2 + 1)) - 1) << 2 diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..7005540 --- /dev/null +++ b/test/README.md @@ -0,0 +1,17 @@ +# OpenPAYGO-Token Test + +To run the test, first, create and activate a virtual environment. Then install the module and run the test: + +``` +$ cd tests +$ python -m venv env +$ source env/bin/activate +$ python -m pip install -e .. +$ python simple_scenario_test.py +``` + +You can do it for full test procedure as well: + +``` +$ python full_test_procedure.py +``` diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/full_test_procedure.py b/test/full_test_procedure.py new file mode 100644 index 0000000..5cbcc7b --- /dev/null +++ b/test/full_test_procedure.py @@ -0,0 +1,250 @@ +import codecs + +from helpers import ( + ADD_TIME, + DISABLE_VALUE, + SET_TIME, + generate_from_device_data, + test_accepted_validator, + test_how_many_days_validator, + test_name, +) + +from openpaygo.simulators.device_simulator import DeviceSimulator + +# This tests the device simulator against the full test procedure + + +def run_core_token_tests(device_data, device_simulator): + test = "We enter an invalid token" + token_g1 = "123 456 789" + test_accepted_validator(device_simulator, "G1", token_g1, -1, description=test) + + test = "We enter a valid token for setting one day" + device_data, token_g2 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=1 + ) + test_how_many_days_validator(device_simulator, "G2", token_g2, 1, description=test) + + test = "We enter a valid token for adding one day" + device_data, token_g3 = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days_validator(device_simulator, "G3", token_g3, 2, description=test) + + test = ( + "We enter the same Add Time token for 1 day, the days should not be " + "added and the device should signal that the token was already used" + ) + test_how_many_days_validator( + device_simulator, + "G4A", + token_g3, + 2, + description=test, + accepted_but_used=True, + ) + test = ( + "We enter the older Set Time token for 1 day, the days should not " + "change and the device should signal that the token was already used" + ) + test_how_many_days_validator( + device_simulator, + "G4B", + token_g2, + 2, + description=test, + accepted_but_used=True, + ) + + test = ( + "We enter a valid token for setting 30 days and ensures it sets and does " + "not add to the existing" + ) + device_data, token_g5 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=30 + ) + test_how_many_days_validator(device_simulator, "G5", token_g5, 30, description=test) + + test = ( + "We enter a valid token for setting 0 days and ensures the device is " + "inactive with the outputs disabled immediately" + ) + device_data, token_g6 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days_validator(device_simulator, "G6", token_g6, 0, description=test) + + test = ( + "We enter 3 consecutive Add Time tokens with the maximum amount of days and " + "ensure that they cumulate properly" + ) + for i in range(1, 3 + 1): + device_data, token_g7 = generate_from_device_data( + device_data, token_type=ADD_TIME, value_raw=995 + ) + test_how_many_days_validator( + device_simulator, + test_name("G7", i), + token_g7, + value_raw=995 * i, + device_data=device_data, + description=test, + ) + test = "" + + test = ( + "We enter 21 consecutive Set Time tokens for 1, 2, 3, … 21 days " + "each with a count 30 higher than the other. The validation of the " + "token should not take more than 5 seconds" + ) + for i in range(1, 21 + 1): + device_data, token_g8 = generate_from_device_data( + device_data, + token_type=SET_TIME, + value_days=i, + token_count=device_data["token_count"] + 29, + ) + test_how_many_days_validator( + device_simulator, + test_name("G8", i), + token_g8, + value_days=i, + device_data=device_data, + description=test, + ) + test = "" + + test = "We enter a PAYG Disable token into the device" + device_data, token_g9 = generate_from_device_data( + device_data, token_type=SET_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days_validator( + device_simulator, "G9", token_g9, None, description=test + ) + + test = "We enter a Set Time token for 0 day, it should relock the device" + device_data, token_g10 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days_validator( + device_simulator, "G10", token_g10, 0, description=test + ) + + test = ( + "We enter a PAYG Disable token to relock the device, then enter a Add " + "Time token with 0 day, it should NOT relock the device (Optional)" + ) + device_data, token_g11a = generate_from_device_data( + device_data, token_type=SET_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days_validator( + device_simulator, "G11A", token_g11a, None, description=test + ) + device_data, token_g11b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=0 + ) + test_how_many_days_validator(device_simulator, "G11B", token_g11b, None) + + test = ( + "We deactivate the device with a Set Time of 0 days. We then wait " + "48 hours before entering a Add Time of 1 day and ensuring that the " + "days late are not considered in the activation time" + ) + device_data, token_g12a = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days_validator( + device_simulator, "G12A", token_g12a, 0, description=test + ) + device_data, token_g12b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days_validator(device_simulator, "G12B", token_g12b, 1) + + return device_data + + +def run_unordered_entry_tests(device_data, device_simulator): + test = ( + "We generate 3 Add Time tokens, then enter the 3rd, then first, then " + "second and ensure the days are added properly" + ) + device_data, token_u1a = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=60 + ) + test_how_many_days_validator( + device_simulator, "U1A", token_u1a, 60, description=test + ) + device_data, token_u1b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=10 + ) + device_data, token_u1c = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=5 + ) + device_data, token_u1d = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days_validator(device_simulator, "U1B", token_u1d, 61) + test_how_many_days_validator(device_simulator, "U1C", token_u1b, 71) + test_how_many_days_validator(device_simulator, "U1D", token_u1c, 76) + + test = ( + "We generate an Add Time, a Set Time and another Add Time token. We " + "enter the set time and ensure that the older Add Time does not work " + "but the newer does" + ) + device_data, token_u2a = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=5 + ) + device_data, token_u2b = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=10 + ) + device_data, token_u2c = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=3 + ) + test_how_many_days_validator( + device_simulator, "U2A", token_u2b, 10, description=test + ) + test_how_many_days_validator( + device_simulator, "U2B", token_u2a, 10, accepted_but_used=True + ) + test_how_many_days_validator(device_simulator, "U2C", token_u2c, 13) + + test = ( + "We generate an Add Time token and a Disable PAYG token, we enter the " + "Disable PAYG token and then the Add Time token should be refused" + ) + device_data, token_u3a = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + device_data, token_u3b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days_validator( + device_simulator, "U3A", token_u3b, None, description=test + ) + test_how_many_days_validator( + device_simulator, "U3B", token_u3a, None, accepted_but_used=True + ) + + return device_data + + +if __name__ == "__main__": + device_data = { + "serial_number": "XXX", + "starting_code": 516959010, + "key": "bc41ec9530f6dac86b1a29ab82edc5fb", + "restricted_digit_set": False, + "time_divider": 1, + "token_count": 1, + } + device_simulator = DeviceSimulator( + device_data["starting_code"], + codecs.decode(device_data["key"], "hex"), + restricted_digit_set=device_data["restricted_digit_set"], + waiting_period_enabled=False, + ) + device_data = run_core_token_tests(device_data, device_simulator) + device_data = run_unordered_entry_tests(device_data, device_simulator) diff --git a/test/helpers.py b/test/helpers.py new file mode 100644 index 0000000..d558ea2 --- /dev/null +++ b/test/helpers.py @@ -0,0 +1,98 @@ +import codecs + +import openpaygo +from openpaygo.token_shared import TokenType + +SET_TIME = TokenType.SET_TIME +ADD_TIME = TokenType.ADD_TIME +DISABLE_VALUE = 998 + +START_RED = "\033[91m" +END_RED = "\033[0m" + + +def generate_from_device_data( + device_data, token_type, value_raw=None, value_days=None, token_count=None +): + assert (value_days is not None) or (value_raw is not None) + if value_raw is None: + value_raw = value_days * device_data["time_divider"] + device_data["token_count"], token = ( + openpaygo.token_encode.OpenPAYGOTokenEncoder.generate_standard_token( + starting_code=device_data["starting_code"], + key=codecs.decode(device_data["key"], "hex"), + value=value_raw, + count=token_count or device_data["token_count"], + restricted_digit_set=device_data["restricted_digit_set"], + mode=token_type, + ) + ) + token = str(token).rjust(9, "0") + token = " ".join([token[i : i + 3] for i in range(0, len(token), 3)]) + return device_data, token + + +def test_how_many_days( + test_name, token, value_days=None, value_raw=None, device_data=None, description="" +): + if value_days is None: + if value_raw is not None: + value_days = value_raw / device_data["time_divider"] + else: + value_days = "infinite" + print( + test_name + + "," + + token + + "," + + str(value_days) + + " days active" + + ',"' + + description + + '"' + ) + + +def test_accepted(test_name, token, expected, description=""): + expected_string = "Token Accepted" if expected else "Token Refused" + print(test_name + "," + token + "," + expected_string + ',"' + description + '"') + + +def test_how_many_days_validator( + device_simulator, + test_name, + token, + value_days=None, + value_raw=None, + device_data=None, + description="", + accepted_but_used=False, +): + if value_days is None: + if value_raw is not None: + value_days = value_raw / device_data["time_divider"] + else: + value_days = "infinite" + result = device_simulator.enter_token(token.replace(" ", ""), show_result=False) + if result == 1 or (accepted_but_used and result == -2): + if device_simulator.get_days_remaining() == value_days: + print(test_name + ": Passed") + return + print(START_RED + test_name + ": Failed" + END_RED) + + +def test_accepted_validator( + device_simulator, test_name, token, expected, description="" +): + if ( + device_simulator.enter_token(token.replace(" ", ""), show_result=False) + == expected + ): + print(test_name + ": Passed") + return + else: + print(START_RED + test_name + ": Failed" + END_RED) + + +def test_name(test_base_name, test_number): + return test_base_name + chr(test_number + 64) diff --git a/test/simple_scenario_test.py b/test/simple_scenario_test.py new file mode 100644 index 0000000..1b73341 --- /dev/null +++ b/test/simple_scenario_test.py @@ -0,0 +1,259 @@ +from datetime import datetime, timedelta + +from openpaygo.simulators import DeviceSimulator, SingleDeviceServerSimulator +from openpaygo.token_shared import TokenType + + +def assert_time_equals(time1, time2): + assert time1.replace(second=0, microsecond=0) == time2.replace( + second=0, microsecond=0 + ) + + +if __name__ == "__main__": + # ------ IMPORTANT WARNING -------- + # DO NOT USE THIS KEY IN PRODUCTION + # IT IS JUST AN EXAMPLE + # --------------------------------- + device_key = b"\xa2\x9a\xb8.\xdc_\xbb\xc4\x1e\xc9S\x0fm\xac\x86\xb1" + device_starting_code = 123456789 # Generated by fair dice roll + restricted_digit_set = False + + print("Device: We initiate the device simulator with our device") + device_simulator = DeviceSimulator( + device_starting_code, + device_key, + restricted_digit_set=restricted_digit_set, + waiting_period_enabled=False, + ) + print("Server: We initiate the server simulator with our device") + server_simulator = SingleDeviceServerSimulator( + device_starting_code, device_key, restricted_digit_set=restricted_digit_set + ) + + print("\n") + print("Device: We try entering an invalid token into the device: 123456789") + device_simulator.enter_token("123456789") + print("Device: We check the device status (should be still inactive)") + device_simulator.print_status() + + print("\n") + print("Server: We add 1 days of activation for the device") + this_token = server_simulator.generate_token_from_date( + datetime.now() + timedelta(days=1) + ) + print("Token: " + this_token) + print("Device: We enter the generated token into the device") + device_simulator.enter_token(this_token) + print("Device: We check the device status (should be active with 1 day)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=1) + ) + + print("\n") + print( + "Device: We enter the token a second time to make sure it doesnt add the " + "days again" + ) + device_simulator.enter_token(this_token) + print("Device: We check the device status (should be active with 1 day)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=1) + ) + + print("\n") + print("Server: We set it to expire in 30 days") + this_token = server_simulator.generate_token_from_date( + datetime.now() + timedelta(days=30) + ) + print("Token: " + this_token) + print("Device: We enter the generated token into the device") + device_simulator.enter_token(this_token) + print("Device: We check the device status (should be active with 30 days)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=30) + ) + + print("\n") + print("Server: We set it to expire in 7 days (removing 23 days)") + this_token = server_simulator.generate_token_from_date( + datetime.now() + timedelta(days=7) + ) + print("Token: " + this_token) + print("Device: We enter the generated token into the device") + device_simulator.enter_token(this_token) + print("Device: We check the device status (should be active with 7 days)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=7) + ) + + print("\n") + print("Server: We generate a token for putting the device in PAYG-OFF mode") + this_payg_off_code = server_simulator.generate_payg_disable_token() + this_payg_off_code = str(this_payg_off_code) + print("Token: " + this_payg_off_code) + print("Device: We enter the generated token into the device") + device_simulator.enter_token(this_payg_off_code) + print("Device: We check the device status (should be active forver)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert device_simulator.payg_enabled is False + + print("\n") + print( + "Server: We generate a token for putting the device back PAYG-ON mode " + "with 0 days" + ) + this_token = server_simulator.generate_token_from_date( + datetime.now() + timedelta(days=0) + ) + this_token = str(this_token) + print("Token: " + this_token) + print("Device: We enter the generated token into the device") + device_simulator.enter_token(this_token) + print("Device: We check the device status (should not be active)") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert device_simulator.payg_enabled is True + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=0) + ) + + print("\n") + print( + "Server: We generate a bunch of 1 day tokens, but only enter the latest " "one" + ) + server_simulator.generate_token_from_date(datetime.now() + timedelta(days=1)) + server_simulator.generate_token_from_date(datetime.now() + timedelta(days=1)) + server_simulator.generate_token_from_date(datetime.now() + timedelta(days=1)) + server_simulator.generate_token_from_date(datetime.now() + timedelta(days=1)) + server_simulator.generate_token_from_date(datetime.now() + timedelta(days=1)) + this_token = server_simulator.generate_token_from_date( + datetime.now() + timedelta(days=1) + ) + this_token = str(this_token) + print("Token: " + this_token) + print("Device: We enter the latest generated token into the device") + device_simulator.enter_token(this_token) + print( + "Device: We check the device status (should be active with 1 day and " + "the count synchronised with the server)" + ) + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=1) + ) + + print("\n") + print("Server: We add generate 9 tokens each add-time of 1 day") + token_1 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_2 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_3 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_4 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_5 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_6 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_7 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_8 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_9 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + print( + "Tokens: ", + token_1, + token_2, + token_3, + token_4, + token_5, + token_6, + token_7, + token_8, + token_9, + ) + print("Device: We enter the 9th token into the device") + device_simulator.enter_token(token_9) + print( + "Device: We check the device status (should be active with +1 day (2 " + "days total)" + ) + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=2) + ) + print("Device: We enter the 1st token into the device") + device_simulator.enter_token(token_1) + print( + "Device: We check the device status , it should not have changed, " + "because its more than 5 add times before" + ) + device_simulator.print_status() + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=2) + ) + print("Device: We enter the tokens 5, 4, 3 and 2 into the device") + device_simulator.enter_token(token_5) + device_simulator.enter_token(token_4) + device_simulator.enter_token(token_3) + device_simulator.enter_token(token_2) + print( + "Device: We check the device status , it should have +4 days (6 days " "total)" + ) + device_simulator.print_status() + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=6) + ) + + print("\n") + print("Server: We add generate 2 tokens, first add-time and then set-time") + token_1 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + token_2 = server_simulator._generate_token_from_value(0, TokenType.SET_TIME) + print("Tokens: ", token_1, token_2) + print("Device: We enter the 2nd token") + device_simulator.enter_token(token_2) + print("Device: We check the device status should be active in 0 days.") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=0) + ) + print("Device: We enter the 1st token into the device") + device_simulator.enter_token(token_1) + print( + "Device: We check the device status , it should not have changed, " + "because you cannot use an add-time token older than a set-time" + ) + device_simulator.print_status() + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=0) + ) + + print("\n") + print("Server: We add generate 2 tokens, first add-time and then set-time") + token_1 = server_simulator._generate_token_from_value(1, TokenType.SET_TIME) + token_2 = server_simulator._generate_token_from_value(1, TokenType.ADD_TIME) + print("Tokens: ", token_1, token_2) + print("Device: We enter the 2nd token") + device_simulator.enter_token(token_2) + print("Device: We check the device status should be active in 0 days.") + device_simulator.print_status() + assert device_simulator.count == server_simulator.count + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=1) + ) + print("Device: We enter the 1st token into the device") + device_simulator.enter_token(token_1) + print( + "Device: We check the device status , it should not have changed, " + "because you cannot use an older set-time token" + ) + device_simulator.print_status() + assert_time_equals( + device_simulator.expiration_date, datetime.now() + timedelta(days=1) + ) diff --git a/test/spreadsheet_generator_test.py b/test/spreadsheet_generator_test.py new file mode 100755 index 0000000..9fc45e8 --- /dev/null +++ b/test/spreadsheet_generator_test.py @@ -0,0 +1,207 @@ +__test__ = False + +from test.helpers import ( + ADD_TIME, + DISABLE_VALUE, + SET_TIME, + generate_from_device_data, + test_accepted, + test_how_many_days, + test_name, +) + + +def run_core_token_tests(device_data): + test = "We enter an invalid token" + token_g1 = "123 456 789" + test_accepted("G1", token_g1, False, description=test) + + test = "We enter a valid token for setting one day" + device_data, token_g2 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=1 + ) + test_how_many_days("G2", token_g2, 1, description=test) + + test = "We enter a valid token for adding one day" + device_data, token_g3 = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days("G3", token_g3, 2, description=test) + + test = ( + "We enter the same Add Time token for 1 day, the days should not be " + "added and the device should signal that the token was already used" + ) + test_how_many_days("G4A", token_g3, 2, description=test) + test = ( + "We enter the older Set Time token for 1 day, the days should not " + "change and the device should signal that the token was already used" + ) + test_how_many_days("G4B", token_g2, 2, description=test) + + test = ( + "We enter a valid token for setting 30 days and ensures it sets and does " + "not add to the existing" + ) + device_data, token_g5 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=30 + ) + test_how_many_days("G5", token_g5, 30, description=test) + + test = ( + "We enter a valid token for setting 0 days and ensures the device is " + "inactive with the outputs disabled immediately" + ) + device_data, token_g6 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days("G6", token_g6, 0, description=test) + + test = ( + "We enter 3 consecutive Add Time tokens with the maximum amount of days " + "and ensure that they cumulate properly" + ) + for i in range(1, 3 + 1): + device_data, token_g7 = generate_from_device_data( + device_data, token_type=ADD_TIME, value_raw=995 + ) + test_how_many_days( + test_name("G7", i), + token_g7, + value_raw=995 * i, + device_data=device_data, + description=test, + ) + test = "" + + test = ( + "We enter 21 consecutive Set Time tokens for 1, 2, 3, … 21 days each " + "with a count 30 higher than the other. The validation of the token " + "should not take more than 5 seconds" + ) + for i in range(1, 21 + 1): + device_data, token_g8 = generate_from_device_data( + device_data, + token_type=SET_TIME, + value_days=i, + token_count=device_data["token_count"] + 29, + ) + test_how_many_days( + test_name("G8", i), + token_g8, + value_days=i, + device_data=device_data, + description=test, + ) + test = "" + + test = "We enter a PAYG Disable token into the device" + device_data, token_g9 = generate_from_device_data( + device_data, token_type=SET_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days("G9", token_g9, None, description=test) + + test = "We enter a Set Time token for 0 day, it should relock the device" + device_data, token_g10 = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days("G10", token_g10, 0, description=test) + + test = ( + "We enter a PAYG Disable token to relock the device, then enter a Add " + "Time token with 0 day, it should NOT relock the device (Optional)" + ) + device_data, token_g11a = generate_from_device_data( + device_data, token_type=SET_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days("G11A", token_g11a, None, description=test) + device_data, token_g11b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=0 + ) + test_how_many_days("G11B", token_g11b, None) + + test = ( + "We deactivate the device with a Set Time of 0 days. We then wait 48 " + "hours before entering a Add Time of 1 day and ensuring that the days " + "late are not considered in the activation time" + ) + device_data, token_g12a = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=0 + ) + test_how_many_days("G12A", token_g12a, 0, description=test) + device_data, token_g12b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days("G12B", token_g12b, 1) + + return device_data + + +def run_unordered_entry_tests(device_data): + test = ( + "We generate 3 Add Time tokens, then enter the 3rd, then first, then " + "second and ensure the days are added properly" + ) + device_data, token_u1a = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=60 + ) + test_how_many_days("U1A", token_u1a, 60, description=test) + device_data, token_u1b = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=10 + ) + device_data, token_u1c = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=5 + ) + device_data, token_u1d = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + test_how_many_days("U1B", token_u1d, 61) + test_how_many_days("U1C", token_u1b, 71) + test_how_many_days("U1D", token_u1c, 76) + + test = ( + "We generate an Add Time, a Set Time and another Add Time token. We " + "enter the set time and ensure that the older Add Time does not work " + "but the newer does" + ) + device_data, token_u2a = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=5 + ) + device_data, token_u2b = generate_from_device_data( + device_data, token_type=SET_TIME, value_days=10 + ) + device_data, token_u2c = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=3 + ) + test_how_many_days("U2A", token_u2b, 10, description=test) + test_accepted("U2B", token_u2a, False) + test_how_many_days("U2C", token_u2c, 13) + + test = ( + "We generate an Add Time token and a Disable PAYG token, we enter the " + "Disable PAYG token and then the Add Time token should be refused" + ) + device_data, token_u3a = generate_from_device_data( + device_data, token_type=ADD_TIME, value_days=1 + ) + device_data, token_u3b = generate_from_device_data( + device_data, token_type=SET_TIME, value_raw=DISABLE_VALUE + ) + test_how_many_days("U3A", token_u3b, None, description=test) + test_accepted("U3B", token_u3a, False) + + return device_data + + +if __name__ == "__main__": + device_data = { + "serial_number": "changeme", + "starting_code": 123456789, + "key": "a29ab82edc5fbbc41ec9530f6dac86b1", + "restricted_digit_set": False, + "time_divider": 1, + "token_count": 1, + } + print("Test Name,Token Used,Expected Result,Test Details,Result") + device_data = run_core_token_tests(device_data) + device_data = run_unordered_entry_tests(device_data)