Merge pull request #394 from kdmukai/settings_qr

SettingsQR: Final changes before official release
This commit is contained in:
Nick Klockenga
2023-08-02 23:48:21 -04:00
committed by GitHub
9 changed files with 278 additions and 182 deletions
@@ -183,6 +183,7 @@ class SettingsUpdatedScreen(ButtonListScreen):
def __post_init__(self):
# Customize defaults
self.button_data = ["Home"]
self.show_back_button = False
super().__post_init__()
+31 -107
View File
@@ -9,14 +9,12 @@ from embit import psbt, bip39
from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
from urtypes.crypto import Account, Output
from urtypes.bytes import Bytes
from seedsigner.helpers.ur2.ur_decoder import URDecoder
from seedsigner.models.psbt_parser import PSBTParser
from . import QRType, Seed
from .settings import SettingsConstants
from seedsigner.models import QRType, Seed
from seedsigner.models.settings import SettingsConstants
logger = logging.getLogger(__name__)
@@ -175,12 +173,7 @@ class DecodeQR:
def get_settings_data(self):
if self.is_settings:
return self.decoder.settings
def get_settings_config_name(self):
if self.is_settings:
return self.decoder.config_name
return self.decoder.data
def get_address(self):
@@ -364,7 +357,7 @@ class DecodeQR:
return QRType.BITCOIN_ADDRESS
# config data
if "type=settings" in s:
if s.startswith("settings::"):
return QRType.SETTINGS
# Seed
@@ -578,6 +571,8 @@ class DecodeQR:
return descriptor
class BaseQrDecoder:
def __init__(self):
self.total_segments = None
@@ -673,6 +668,8 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder):
def parse_segment(self, segment) -> str:
return segment.split(" ")[-1].strip()
class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder):
"""
Decodes single frame base64 encoded qr image.
@@ -826,106 +823,33 @@ class SeedQrDecoder(BaseSingleFrameQrDecoder):
# TODO: Refactor this to work with the new SettingsDefinition
class SettingsQrDecoder(BaseSingleFrameQrDecoder):
"""
Decodes settings data from the SettingsQR Generator.
"""
def __init__(self):
super().__init__()
self.settings = {}
self.config_name = None
self.data = None
def add(self, segment, qr_type=QRType.SETTINGS):
# print(f"SettingsQR:\n{segment}")
try:
self.settings = {}
"""
* Ignores unrecognized settings options.
* Raises an Exception if a settings value is invalid.
# QR Settings format is space-separated key/value pairs, but should also
# parse \n-separated keys.
for entry in segment.split():
key = entry.split("=")[0].strip()
value = entry.split("=")[1].strip()
self.settings[key] = value
See `Settings.update()` for info on settings validation, especially for
missing settings.
"""
if not segment.startswith("settings::"):
raise Exception("Invalid SettingsQR data")
# Leave any other parsing or validation up to the Settings class itself.
# SettingsQR are just ascii data to hand it over as-is.
self.data = segment
# Remove values only needed for import
self.settings.pop("type", None)
version = self.settings.pop("version", None)
if not version or int(version) != 1:
raise Exception(f"Settings QR version {version} not supported")
self.config_name = self.settings.pop("name", None)
if self.config_name:
self.config_name = self.config_name.replace("_", " ")
# Have to translate the abbreviated settings into the human-readable values
# used in the normal Settings.
map_abbreviated_enable = {
"0": SettingsConstants.OPTION__DISABLED,
"1": SettingsConstants.OPTION__ENABLED,
"2": SettingsConstants.OPTION__PROMPT,
}
map_abbreviated_sig_types = {
"s": SettingsConstants.SINGLE_SIG,
"m": SettingsConstants.MULTISIG,
}
map_abbreviated_scripts = {
"na": SettingsConstants.NATIVE_SEGWIT,
"ne": SettingsConstants.NESTED_SEGWIT,
"tr": SettingsConstants.TAPROOT,
"cu": SettingsConstants.CUSTOM_DERIVATION,
}
map_abbreviated_coordinators = {
"bw": SettingsConstants.COORDINATOR__BLUE_WALLET,
"sw": SettingsConstants.COORDINATOR__SPARROW,
"sd": SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
}
def convert_abbreviated_value(category, key, abbreviation_map, is_list=False, new_key_name=None):
try:
if key not in self.settings:
print(f"'{key}' not found in settings")
return
value = self.settings[key]
if not is_list:
new_value = abbreviation_map.get(value)
if not new_value:
logger.error(f"No abbreviation map value for \"{value}\" for setting {key}")
return
else:
# `value` is a comma-separated list; yields list of map matches
values = value.split(",")
new_value = []
for v in values:
mapped_value = abbreviation_map.get(v)
if not mapped_value:
logger.error(f"No abbreviation map value for \"{v}\" for setting {key}")
return
new_value.append(mapped_value)
del self.settings[key]
if new_key_name:
key = new_key_name
if category not in self.settings:
self.settings[category] = {}
self.settings[category][key] = new_value
except Exception as e:
logger.exception(e)
return
convert_abbreviated_value("wallet", "coord", map_abbreviated_coordinators, is_list=True, new_key_name="coordinators")
convert_abbreviated_value("features", "xpub", map_abbreviated_enable, new_key_name="xpub_export")
convert_abbreviated_value("features", "sigs", map_abbreviated_sig_types, is_list=True, new_key_name="sig_types")
convert_abbreviated_value("features", "scripts", map_abbreviated_scripts, is_list=True, new_key_name="script_types")
convert_abbreviated_value("features", "xp_det", map_abbreviated_enable, new_key_name="show_xpub_details")
convert_abbreviated_value("features", "passphrase", map_abbreviated_enable)
convert_abbreviated_value("features", "priv_warn", map_abbreviated_enable, new_key_name="show_privacy_warnings")
convert_abbreviated_value("features", "dire_warn", map_abbreviated_enable, new_key_name="show_dire_warnings")
self.complete = True
self.collected_segments = 1
return DecodeQRStatus.COMPLETE
except Exception as e:
logger.exception(e)
return DecodeQRStatus.INVALID
self.complete = True
self.collected_segments = 1
return DecodeQRStatus.COMPLETE
@@ -1091,11 +1015,11 @@ class GenericWalletQrDecoder(BaseSingleFrameQrDecoder):
def get_wallet_descriptor(self):
return self.descriptor
class MultiSigConfigFileQRDecoder(GenericWalletQrDecoder):
def add(self, segment, qr_type=QRType.WALLET__CONFIGFILE):
descriptor = DecodeQR.multisig_setup_file_to_descriptor(segment)
return super().add(descriptor,qr_type=QRType.WALLET__CONFIGFILE)
+74 -25
View File
@@ -2,13 +2,17 @@ import json
import os
import platform
from typing import Any, List
from typing import List
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from .singleton import Singleton
class InvalidSettingsQRData(Exception):
pass
class Settings(Singleton):
HOSTNAME = platform.uname()[1]
@@ -28,11 +32,69 @@ class Settings(Singleton):
# Read persistent settings file, if it exists
if os.path.exists(Settings.SETTINGS_FILENAME):
with open(Settings.SETTINGS_FILENAME) as settings_file:
settings.update(json.load(settings_file), disable_missing_entries=False)
settings.update(json.load(settings_file))
return cls._instance
@classmethod
def parse_settingsqr(cls, data: str) -> tuple[str, dict]:
"""
Parses SettingsQR data and returns a tuple of (config_name, settings_dict).
The resulting settings config can be applied by calling `Settings.update(settings_dict)`.
"""
if not data.startswith("settings::"):
raise InvalidSettingsQRData()
version = data.split()[0].split("::")[1]
if version != "v1":
raise InvalidSettingsQRData(f"Unsupported SettingsQR version: {version}")
# Start parsing key/value settings at the nth split() index
split_index = 1
# handle optional "name" attr
config_name = None
if "name=" in data.split()[1]:
config_name = data.split("name=")[1].split()[0].replace("_", " ")
split_index += 1
updated_settings = {}
for entry in data.split()[split_index:]:
abbreviated_name, value = entry.split("=")
# Parse multi-value settings; integer-ize where needed
if "," in value:
values_updated = []
for v in value.split(","):
if v.isdigit():
v = int(v)
values_updated.append(v)
value = values_updated
elif value.isdigit():
value = int(value)
# Replace abbreviated name with full attr_name
settings_entry = SettingsDefinition.get_settings_entry_by_abbreviated_name(abbreviated_name)
if not settings_entry:
print(f"Ignoring unrecognized attribute: {abbreviated_name}")
continue
# Validate value(s) against SettingsDefinition's valid options
if type(value) is not list:
values = [value]
else:
values = value
for v in values:
if v not in [opt[0] for opt in settings_entry.selection_options]:
raise InvalidSettingsQRData(f"""{abbreviated_name} = '{v}' is not valid""")
updated_settings[settings_entry.attr_name] = value
return (config_name, updated_settings)
def __str__(self):
return json.dumps(self._data, indent=4)
@@ -47,32 +109,22 @@ class Settings(Singleton):
os.fsync(settings_file.fileno())
def update(self, new_settings: dict, disable_missing_entries: bool = True):
def update(self, new_settings: dict):
"""
* disable_missing_entries: The SettingsQR Generator omits any multiselect
fields with zero selections or disabled Enabled/Disabled toggles. So if a
field is missing, interpret it as such. But if this is set to False, keep
the existing value for the field; most likely this is a new setting that
the user may not have a value for when loading their persistent settings,
in which case this would preserve the new field's default value.
Replaces the current settings with the incoming dict.
If a setting is missing from `new_settings`:
* Hidden settings that have a value remain as-is.
* All other missing settings are set to their default value.
"""
for entry in SettingsDefinition.settings_entries:
if entry.attr_name not in new_settings:
if not disable_missing_entries:
if entry.visibility == SettingsConstants.VISIBILITY__HIDDEN and entry.attr_name in self._data:
# Preserve existing hidden values
new_settings[entry.attr_name] = self._data[entry.attr_name]
else:
# Setting is missing; insert default
new_settings[entry.attr_name] = entry.default_value
elif entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
# Missing hidden values always get their default
new_settings[entry.attr_name] = entry.default_value
elif entry.type == SettingsConstants.TYPE__MULTISELECT:
# Clear out the multiselect
new_settings[entry.attr_name] = []
elif entry.type in SettingsConstants.ALL_ENABLED_DISABLED_TYPES:
# Set DISABLED for this missing setting
new_settings[entry.attr_name] = SettingsConstants.OPTION__DISABLED
else:
# Clean the incoming data, if necessary
@@ -80,9 +132,6 @@ class Settings(Singleton):
if type(new_settings[entry.attr_name]) == str:
# Break comma-separated SettingsQR input into List
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
# TODO: If value is not in entry.selection_options...
# Can't just merge the _data dict; have to replace keys they have in common
# (otherwise list values will be merged instead of replaced).
+20 -13
View File
@@ -213,7 +213,6 @@ class SettingsEntry:
category: str
attr_name: str
display_name: str
verbose_name: str = None
abbreviated_name: str = None
visibility: str = SettingsConstants.VISIBILITY__GENERAL
type: str = SettingsConstants.TYPE__ENABLED_DISABLED
@@ -229,7 +228,7 @@ class SettingsEntry:
self.selection_options = SettingsConstants.OPTIONS__ENABLED_DISABLED_PROMPT
elif self.type == SettingsConstants.TYPE__ENABLED_DISABLED_PROMPT_REQUIRED:
self.selection_options = [SettingsConstants.ALL_OPTIONS]
self.selection_options = SettingsConstants.ALL_OPTIONS
# Account for List[tuple] and tuple formats as default_value
if type(self.default_value) == list and type(self.default_value[0]) == tuple:
@@ -248,18 +247,12 @@ class SettingsEntry:
def get_selection_option_value(self, i: int):
""" Returns the value of the selection option at index `i` """
value = self.selection_options[i]
if type(value) == tuple:
value = value[0]
return value
def get_selection_option_display_name(self, i: int) -> str:
value = self.selection_options[i]
if type(value) == tuple:
value = value[1]
return value
def get_selection_option_display_name_by_value(self, value) -> str:
for option in self.selection_options:
@@ -305,9 +298,8 @@ class SettingsEntry:
return {
"category": self.category,
"attr_name": self.attr_name,
"display_name": self.display_name,
"verbose_name": self.verbose_name,
"abbreviated_name": self.abbreviated_name,
"display_name": self.display_name,
"visibility": self.visibility,
"type": self.type,
"help_text": self.help_text,
@@ -340,6 +332,7 @@ class SettingsDefinition:
# TODO: Full babel multilanguage support! Until then, type == HIDDEN
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__LANGUAGE,
abbreviated_name="lang",
display_name="Language",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -349,6 +342,7 @@ class SettingsDefinition:
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
abbreviated_name="wordlist_lang",
display_name="Mnemonic language",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -357,12 +351,14 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
abbreviated_name="persistent",
display_name="Persistent settings",
help_text="Store Settings on SD card.",
default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
attr_name=SettingsConstants.SETTING__COORDINATORS,
abbreviated_name="coords",
display_name="Coordinator software",
type=SettingsConstants.TYPE__MULTISELECT,
selection_options=SettingsConstants.ALL_COORDINATORS,
@@ -375,6 +371,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
abbreviated_name="denom",
display_name="Denomination display",
type=SettingsConstants.TYPE__SELECT_1,
selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS,
@@ -406,6 +403,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__SIG_TYPES,
abbreviated_name="sigs",
display_name="Sig types",
type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -414,6 +412,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__SCRIPT_TYPES,
abbreviated_name="scripts",
display_name="Script types",
type=SettingsConstants.TYPE__MULTISELECT,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -436,6 +435,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__CAMERA_ROTATION,
abbreviated_name="camera",
display_name="Camera rotation",
type=SettingsConstants.TYPE__SELECT_1,
visibility=SettingsConstants.VISIBILITY__ADVANCED,
@@ -450,18 +450,21 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS,
abbreviated_name="bip85",
display_name="BIP-85 child seeds",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
abbreviated_name="priv_warn",
display_name="Show privacy warnings",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED),
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__DIRE_WARNINGS,
abbreviated_name="dire_warn",
display_name="Show dire warnings",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED),
@@ -474,6 +477,7 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__PARTNER_LOGOS,
abbreviated_name="partners",
display_name="Show partner logos",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__ENABLED),
@@ -489,6 +493,7 @@ class SettingsDefinition:
# "Hidden" settings with no UI interaction
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS,
abbreviated_name="qr_brightness",
display_name="QR background color",
type=SettingsConstants.TYPE__FREE_ENTRY,
visibility=SettingsConstants.VISIBILITY__HIDDEN,
@@ -513,8 +518,10 @@ class SettingsDefinition:
@classmethod
def parse_abbreviated_ini(cls, abbreviated_ini: str) -> dict:
raise Exception("Not implemented, maybe not needed")
def get_settings_entry_by_abbreviated_name(cls, abbreviated_name: str) -> SettingsEntry:
for entry in cls.settings_entries:
if abbreviated_name in [entry.abbreviated_name, entry.attr_name]:
return entry
@classmethod
+4 -32
View File
@@ -1,14 +1,12 @@
import json
import re
from embit.descriptor import Descriptor
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.gui.screens import scan_screens
from seedsigner.models import DecodeQR, Seed
from seedsigner.models.settings import SettingsConstants
from .view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
from seedsigner.views.view import MainMenuView, NotYetImplementedView, View, Destination
@@ -55,13 +53,8 @@ class ScanView(View):
return Destination(PSBTSelectSeedView, skip_current_view=True)
elif self.decoder.is_settings:
from seedsigner.models.settings import Settings
settings = self.decoder.get_settings_data()
Settings.get_instance().update(new_settings=settings)
print(json.dumps(Settings.get_instance()._data, indent=4))
return Destination(SettingsUpdatedView, {"config_name": self.decoder.get_settings_config_name()})
data = self.decoder.get_settings_data()
return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data))
elif self.decoder.is_wallet_descriptor:
from seedsigner.views.seed_views import MultisigWalletDescriptorView
@@ -113,24 +106,3 @@ class ScanView(View):
raise Exception("QRCode not recognized or not yet supported.")
return Destination(MainMenuView)
class SettingsUpdatedView(View):
def __init__(self, config_name: str):
super().__init__()
self.config_name = config_name
def run(self):
from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen
screen = SettingsUpdatedScreen(config_name=self.config_name)
selected_menu_num = screen.display()
if selected_menu_num == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)
# Only one exit point
return Destination(MainMenuView)
+26 -1
View File
@@ -1,9 +1,12 @@
import logging
from seedsigner.gui.components import SeedSignerCustomIconConstants
from .view import View, Destination, MainMenuView
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
from seedsigner.models.settings import SettingsConstants, SettingsDefinition
from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
logger = logging.getLogger(__name__)
@@ -185,6 +188,28 @@ class SettingsEntryUpdateSelectionView(View):
class SettingsIngestSettingsQRView(View):
def __init__(self, data: str):
super().__init__()
# May raise an Exception which will bubble up to the Controller to display to the
# user.
self.config_name, settings_update_dict = Settings.parse_settingsqr(data)
self.settings.update(settings_update_dict)
def run(self):
from seedsigner.gui.screens.scan_screens import SettingsUpdatedScreen
self.run_screen(
SettingsUpdatedScreen,
config_name=self.config_name
)
# Only one exit point
return Destination(MainMenuView)
"""****************************************************************************
Misc
****************************************************************************"""
+2 -2
View File
@@ -1,6 +1,6 @@
# Must import test base before the Controller
from base import BaseTest, FlowTest, FlowStep
from base import FlowTestUnexpectedViewException, FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
import pytest
from seedsigner.models.settings import SettingsConstants
@@ -251,7 +251,7 @@ class TestSeedFlows(FlowTest):
SettingsConstants.SETTING__SIG_TYPES: SettingsConstants.MULTISIG,
SettingsConstants.SETTING__SCRIPT_TYPES: SettingsConstants.NESTED_SEGWIT,
SettingsConstants.SETTING__COORDINATORS: SettingsConstants.COORDINATOR__SPECTER_DESKTOP,
}, disable_missing_entries=False)
})
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
+91 -2
View File
@@ -1,10 +1,16 @@
import pytest
from base import BaseTest
from seedsigner.models.settings import Settings
from seedsigner.models.settings import InvalidSettingsQRData, Settings
from seedsigner.models.settings_definition import SettingsConstants
class TestSettings(BaseTest):
@classmethod
def setup_class(cls):
cls.settings = Settings.get_instance()
def test_reset_settings(self):
""" BaseTest.reset_settings() should wipe out any previous Settings changes """
settings = Settings.get_instance()
@@ -14,3 +20,86 @@ class TestSettings(BaseTest):
BaseTest.reset_settings()
settings = Settings.get_instance()
assert settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
def test_parse_settingsqr_data(self):
"""
SettingsQR parser should successfully parse a valid settingsqr input string and
return the resulting config_name and formatted settings_update_dict.
"""
settings_name = "Test SettingsQR"
settingsqr_data = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"""
# First explicitly set settings that differ from the settingsqr_data
self.settings.set_value(SettingsConstants.SETTING__COMPACT_SEEDQR, SettingsConstants.OPTION__DISABLED)
self.settings.set_value(SettingsConstants.SETTING__DIRE_WARNINGS, SettingsConstants.OPTION__DISABLED)
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [SettingsConstants.COORDINATOR__BLUE_WALLET, SettingsConstants.COORDINATOR__SPARROW])
# Now parse the settingsqr_data
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert(config_name == settings_name)
self.settings.update(new_settings=settings_update_dict)
# Now verify that the settings were updated correctly
assert(self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) == SettingsConstants.OPTION__ENABLED)
assert(self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__ENABLED)
coordinators = self.settings.get_value(SettingsConstants.SETTING__COORDINATORS)
assert(SettingsConstants.COORDINATOR__BLUE_WALLET not in coordinators)
assert(SettingsConstants.COORDINATOR__SPARROW in coordinators)
assert(SettingsConstants.COORDINATOR__SPECTER_DESKTOP in coordinators)
def test_settingsqr_version(self):
""" SettingsQR parser should accept SettingsQR v1 and reject any others """
settingsqr_data = "settings::v1 name=Foo"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
settingsqr_data = "settings::v2 name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
assert("Unsupported SettingsQR version" in str(e.value))
# Should also fail if version omitted
settingsqr_data = "settings name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
# And if "settings" is omitted entirely
settingsqr_data = "name=Foo"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
def test_settingsqr_ignores_unrecognized_setting(self):
""" SettingsQR parser should ignore unrecognized settings """
settingsqr_data = "settings::v1 name=Foo favorite_food=bacon xpub_export=D"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert("favorite_food" not in settings_update_dict)
assert("xpub_export" in settings_update_dict)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
def test_settingsqr_fails_unrecognized_option(self):
""" SettingsQR parser should fail if a settings has an unrecognized option """
settingsqr_data = "settings::v1 name=Foo xpub_export=Yep"
with pytest.raises(InvalidSettingsQRData) as e:
Settings.parse_settingsqr(settingsqr_data)
assert("xpub_export" in str(e.value))
def test_settingsqr_parses_line_break_separators(self):
""" SettingsQR parser should read line breaks as acceptable separators """
settingsqr_data = "settings::v1\nname=Foo\nsigs=ss,ms\nscripts=nat,nes,tr\nxpub_export=E\n"
config_name, settings_update_dict = Settings.parse_settingsqr(settingsqr_data)
assert(len(settings_update_dict.keys()) == 3)
# Accepts update with no Exceptions
self.settings.update(new_settings=settings_update_dict)
+29
View File
@@ -0,0 +1,29 @@
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
class TestSettingsQRDecoder:
def test_decode_settingsqr(self):
"""
Assume the QR reader decodes the SettingsQR content correctly and begin this test
with parsing the result.
"""
settings_name = "Test SettingsQR"
settings_qr_str = f"""settings::v1 name={ settings_name.replace(" ", "_") } persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss,ms scripts=nat,nes,tr xpub_details=E passphrase=E camera=180 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"""
# Now parse the settings_qr_str
decoder = DecodeQR()
status = decoder.add_data(settings_qr_str)
assert(decoder.is_settings)
assert(status == DecodeQRStatus.COMPLETE)
data = decoder.get_settings_data()
assert(data == settings_qr_str)
def test_settingsqr_version(self):
""" Should fail if the "settings" header is missing """
settings_qr_str = "name=Foo"
decoder = DecodeQR()
status = decoder.add_data(settings_qr_str)
assert(status == DecodeQRStatus.INVALID)