notes testing done

This commit is contained in:
Peter D. Gray 2024-01-24 12:06:58 -05:00
parent b2afc88b4c
commit 9826e6a174
No known key found for this signature in database
GPG Key ID: A2DCD558C2BE5D7C
3 changed files with 450 additions and 19 deletions

View File

@ -207,6 +207,7 @@ def enter_pin(enter_number, need_keypress, cap_screen, is_q1):
@pytest.fixture(scope='module')
def do_keypresses(need_keypress):
# do a series of keypresses, any kind
def doit(value):
for ch in value:
need_keypress(ch)
@ -216,15 +217,20 @@ def do_keypresses(need_keypress):
@pytest.fixture(scope='module')
def enter_text(need_keypress, is_q1):
# enter a text value, might be a number or string ... on Q can be multiline
def doit(value, multiline=False):
if not multiline:
assert '\r' not in value
assert KEY_ENTER not in value
if not is_q1:
assert value.isdigit(), f'bad value: {value}'
assert not multiline
for ch in value:
need_keypress(ch)
if is_q1:
need_keypress('\r' if not multiline else '\b')
time.sleep(0.010)
need_keypress(KEY_ENTER if not multiline else KEY_CANCEL)
else:
need_keypress('y')
@ -411,6 +417,22 @@ def cap_screen(sim_exec):
return doit
@pytest.fixture(scope='module')
def cap_text_box(cap_screen):
# provides text inside a lined box on the screen right now - Q1 only
def doit():
# capture text shown; 4-10 lines or so?
lines = cap_screen().split('\n')
rv = []
for ln in lines:
ll = ln.find('\x03') # left-side vertical line
rr = ln.find('\x07') # right-side vertical line (dashed)
if ll >=0 and rr >= ll:
rv.append(ln[ll+1:rr])
return rv
return doit
@pytest.fixture(scope='module')
def cap_story(sim_exec):
# returns (title, body) of whatever story is being actively shown
@ -675,7 +697,7 @@ def pick_menu_item(cap_menu, need_keypress, has_qwerty, cap_screen):
raise KeyError(text, "%r not in menu: %r" % (text, m))
# double check we're looking at this menu, not stale data
assert m[0] in cap_screen(), 'not in menu mode'
assert m[0][0:33] in cap_screen(), 'not in menu mode'
m_pos = m.index(text)
@ -928,8 +950,8 @@ def settings_set(sim_exec):
@pytest.fixture()
def settings_get(sim_exec):
def doit(key):
cmd = f"RV.write(repr(settings.get('{key}')))"
def doit(key, def_val=None):
cmd = f"RV.write(repr(settings.get('{key}', {def_val!r})))"
resp = sim_exec(cmd)
assert 'Traceback' not in resp, resp
return eval(resp)
@ -1558,6 +1580,8 @@ def nfc_write(request, needs_nfc):
@pytest.fixture()
def scan_a_qr(sim_exec, is_q1):
# simulate a QR being scanned
# XXX limitation: our USB protocol can't send a v40 QR, limit is more like 30 or so
if not is_q1:
raise pytest.xfail('needs scanner')

View File

@ -17,7 +17,8 @@ def THIS_FILE_requires_q1(is_q1):
raise pytest.skip('Q1 only')
@pytest.fixture
def readback_bbqr(need_keypress, cap_screen_qr, sim_exec):
def readback_bbqr_ll(need_keypress, cap_screen_qr, sim_exec):
# low level version
def doit():
num_parts = None
encoding, file_type = None, None
@ -73,6 +74,23 @@ def readback_bbqr(need_keypress, cap_screen_qr, sim_exec):
return num_parts, encoding, file_type, parts
return doit
@pytest.fixture
def readback_bbqr(readback_bbqr_ll):
# give back just the decoded data and file_type
def doit():
num_parts, encoding, file_type, parts = readback_bbqr_ll()
if num_parts == 0:
# not sent as BBQr .. assume Hex
rb = a2b_hex(parts)
file_type = 'P' if rb[0:4] == b'psbt' else 'T'
else:
_, rb = join_qrs(parts.values())
return file_type, rb
return doit
@pytest.fixture
def render_bbqr(need_keypress, cap_screen_qr, sim_exec, readback_bbqr):
@ -174,10 +192,9 @@ def test_bbqr_psbt(size, encoding, max_ver, partial,
goto_home()
need_keypress(KEY_QR)
actual_vers, parts = split_qrs(psbt, 'P', max_version=max_ver, encoding=encoding)
# def split_qrs(raw, type_code, encoding=None,
# min_split=1, max_split=1295, min_version=5, max_version=40
actual_vers, parts = split_qrs(psbt, 'P', max_version=max_ver, encoding=encoding)
random.shuffle(parts)
for p in parts:
@ -198,14 +215,8 @@ def test_bbqr_psbt(size, encoding, max_ver, partial,
time.sleep(.2)
# expect signed txn back
num_parts, encoding, file_type, parts = readback_bbqr()
if num_parts == 0:
# not sent as BBQr .. assume Hex
rb = a2b_hex(parts)
file_type = 'P' if rb[0:4] == b'psbt' else 'T'
else:
assert file_type in 'TP'
_, rb = join_qrs(parts.values())
file_type, rb = readback_bbqr()
assert file_type in 'TP'
if file_type == 'T':
assert not partial
@ -222,6 +233,5 @@ def test_bbqr_psbt(size, encoding, max_ver, partial,
assert len(decoded['vout']) == num_out
need_keypress('x') # back to menu
# EOF

View File

@ -42,6 +42,16 @@ def goto_notes(cap_story, cap_menu, need_keypress, goto_home, pick_menu_item):
return doit
@pytest.fixture
def need_some_notes(settings_get, settings_set):
# create a note or use what's there, provide as obj
def doit():
notes = settings_get('notes', [])
if not notes:
settings_set('notes', [dict(misc='Body', title='Title Here')])
return notes
return doit
@pytest.mark.parametrize('n_title', [ 'a', 'aaa', 'b'*32])
@pytest.mark.parametrize('n_body', [ 'short', 'very long '*30])
@ -127,7 +137,7 @@ def test_build_note(n_title, n_body, goto_notes, pick_menu_item, enter_text, cap
# back to top notes menu
need_keypress(KEY_ENTER)
m = cap_menu()
assert 'Export All' in m
assert ('Export All' in m) or ('Disable Feature' in m)
@pytest.mark.parametrize('size', [ 4000, 30000])
@pytest.mark.parametrize('encoding', '2Z' )
@ -136,7 +146,7 @@ def test_huge_notes(size, encoding, goto_notes, pick_menu_item, enter_text, cap_
# Since we don't limit note sizes, by request of NVK ... test them
n_body = ''.join(chr((i%95) + 32) for i in prandom(size))
n_title = f'Size {size}'
n_title = f'Size {size} {random.randint(100000, 999999)}'
# kill old things, enable feature
settings_set('notes', [])
@ -155,6 +165,8 @@ def test_huge_notes(size, encoding, goto_notes, pick_menu_item, enter_text, cap_
time.sleep(2.0 / len(parts)) # just so we can watch
time.sleep(.5) # decompression time in some cases
m = cap_menu()
assert m[-1] == 'Export'
notes = settings_get('notes')
assert len(notes) == 1
@ -164,4 +176,389 @@ def test_huge_notes(size, encoding, goto_notes, pick_menu_item, enter_text, cap_
settings_set('notes', [])
goto_notes() # redraw
@pytest.mark.parametrize('key', 'AB' + KEY_F1 + KEY_F2 + KEY_F3 + KEY_F4 + KEY_F5 + KEY_QR)
def test_build_password(key, goto_notes, pick_menu_item, enter_text, cap_menu, cap_story, need_keypress, cap_screen_qr, readback_bbqr, nfc_read_text, cap_text_box, settings_get, settings_set, scan_a_qr):
# Test password entry, including all the auto-generation capabilities
case = '0x%02x' % ord(key)
n_title = f'Title {case}'
n_user = f'Username {case}'
n_pw = None
n_site = f'Site {case}'
n_body = f'More Notes {case}'
# create
goto_notes('New Password')
enter_text(n_title)
enter_text(n_user)
if key == 'A':
n_pw = 'A' * 99
enter_text(n_pw)
elif key == 'B':
n_pw = 'B' * 3
enter_text(n_pw)
elif key == KEY_QR:
n_pw = 'QR rocks'
need_keypress(KEY_QR)
time.sleep(1.1)
scan_a_qr(n_pw)
time.sleep(1.1)
need_keypress(KEY_ENTER)
else:
# function keys: let it auto gen
need_keypress(key)
time.sleep(0.1)
if key == KEY_F5: # bip-85
enter_text('34')
time.sleep(0.1)
n_pw = ''.join(cap_text_box()).strip()
assert n_pw and len(n_pw) > 10
need_keypress(KEY_ENTER)
enter_text(n_site)
enter_text(n_body, multiline=True)
# view
time.sleep(0.1)
m = cap_menu()
assert m[0] == f'"{n_title}"'
assert n_user in m[1]
assert n_site in m[2]
assert 'Export' in m
# top 3 menu items do same thing: view details
for idx in range(3):
pick_menu_item(m[idx])
title, story = cap_story()
assert title == n_title
assert f'User: {n_user}' in story
assert f'Site: {n_site}' in story
assert 'Password: (' in story
assert 'Notes:' in story
assert story.endswith(n_body)
need_keypress(KEY_CANCEL)
# view pw as text and QR
pick_menu_item('View Password')
title, story = cap_story()
assert title == n_title
assert story == n_pw
need_keypress(KEY_QR)
qr_rb = cap_screen_qr().decode('utf-8')
assert qr_rb == n_pw
need_keypress(KEY_CANCEL)
# change stuff
pick_menu_item('Edit Metadata')
mod = ' CHG%04d' % random.randint(1000, 9999)
enter_text(mod)
enter_text(mod)
enter_text(mod)
enter_text(KEY_CLEAR + n_body + mod, multiline=True)
# approve change
time.sleep(0.1)
title, story = cap_story()
assert 'SURE' in title
assert 'Site Name' in story
assert 'Title' in story
need_keypress(KEY_ENTER)
pick_menu_item('Change Password')
enter_text(KEY_CLEAR + 'default')
# confirm
time.sleep(0.1)
title, story = cap_story()
assert 'Confirm' in title
assert 'New Password' in story
assert 'default' in story
assert 'Old Password' in story
assert n_pw in story
need_keypress(KEY_ENTER)
# test changes at low-level
time.sleep(0.1)
notes = settings_get('notes')
note = [n for n in notes if n['title'] == n_title+mod][0]
assert note['site'] == n_site + mod
assert note['user'] == n_user + mod
assert note['misc'] == n_body + mod
assert note['password'] == 'default'
# wipe & redraw
settings_set('notes', notes[0:-3])
goto_notes()
def test_top_export(goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, readback_bbqr, need_some_notes):
notes = need_some_notes()
notes = settings_get('notes', [])
assert len(notes) >= 1
goto_notes()
pick_menu_item('Export All')
title, story = cap_story()
assert 'Export' in title
assert 'to SD Card' in story
assert 'to show QR' in story
assert 'WARNING' in story
assert 'will be cleartext' in story
need_keypress(KEY_QR)
file_type, data = readback_bbqr()
assert file_type == 'J'
obj = json.loads(data)
assert obj.keys() == {'coldcard_notes'}
assert obj['coldcard_notes'] == notes
need_keypress(KEY_ENTER)
def test_top_import(goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr, need_some_notes):
# make some
notes = need_some_notes()
# wipe them
settings_set('notes', [])
goto_notes('Import')
title, story = cap_story()
assert 'Import' in title
assert 'from SD Card' in story
assert 'to scan QR' in story
assert 'WARNING' not in story
jj = json.dumps(dict(coldcard_notes=notes))
need_keypress(KEY_QR)
_, parts = split_qrs(jj, 'J', max_version=20)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(.5) # decompression time in some cases
m = cap_menu()
assert notes[0]['title'] in m[0]
assert settings_get('notes', 'MISSING') == notes
goto_notes()
@pytest.mark.parametrize('key', 'AB' + KEY_F1 + KEY_F2 + KEY_F3 + KEY_F4 + KEY_F5 + KEY_QR)
def test_build_password(key, goto_notes, pick_menu_item, enter_text, cap_menu, cap_story, need_keypress, cap_screen_qr, readback_bbqr, nfc_read_text, cap_text_box, settings_get, settings_set, scan_a_qr):
# Test password entry, including all the auto-generation capabilities
case = '0x%02x' % ord(key)
n_title = f'Title {case}'
n_user = f'Username {case}'
n_pw = None
n_site = f'Site {case}'
n_body = f'More Notes {case}'
# create
goto_notes('New Password')
enter_text(n_title)
enter_text(n_user)
if key == 'A':
n_pw = 'A' * 99
enter_text(n_pw)
elif key == 'B':
n_pw = 'B' * 3
enter_text(n_pw)
elif key == KEY_QR:
n_pw = 'QR rocks'
need_keypress(KEY_QR)
time.sleep(1.1)
scan_a_qr(n_pw)
time.sleep(1.1)
need_keypress(KEY_ENTER)
else:
# function keys: let it auto gen
need_keypress(key)
time.sleep(0.1)
if key == KEY_F5: # bip-85
enter_text('34')
time.sleep(0.1)
n_pw = ''.join(cap_text_box()).strip()
assert n_pw and len(n_pw) > 10
need_keypress(KEY_ENTER)
enter_text(n_site)
enter_text(n_body, multiline=True)
# view
time.sleep(0.1)
m = cap_menu()
assert m[0] == f'"{n_title}"'
assert n_user in m[1]
assert n_site in m[2]
assert 'Export' in m
# top 3 menu items do same thing: view details
for idx in range(3):
pick_menu_item(m[idx])
title, story = cap_story()
assert title == n_title
assert f'User: {n_user}' in story
assert f'Site: {n_site}' in story
assert 'Password: (' in story
assert 'Notes:' in story
assert story.endswith(n_body)
need_keypress(KEY_CANCEL)
# view pw as text and QR
pick_menu_item('View Password')
title, story = cap_story()
assert title == n_title
assert story == n_pw
need_keypress(KEY_QR)
qr_rb = cap_screen_qr().decode('utf-8')
assert qr_rb == n_pw
need_keypress(KEY_CANCEL)
# change stuff
pick_menu_item('Edit Metadata')
mod = ' CHG%04d' % random.randint(1000, 9999)
enter_text(mod)
enter_text(mod)
enter_text(mod)
enter_text(KEY_CLEAR + n_body + mod, multiline=True)
# approve change
time.sleep(0.1)
title, story = cap_story()
assert 'SURE' in title
assert 'Site Name' in story
assert 'Title' in story
need_keypress(KEY_ENTER)
pick_menu_item('Change Password')
enter_text(KEY_CLEAR + 'default')
# confirm
time.sleep(0.1)
title, story = cap_story()
assert 'Confirm' in title
assert 'New Password' in story
assert 'default' in story
assert 'Old Password' in story
assert n_pw in story
need_keypress(KEY_ENTER)
# test changes at low-level
time.sleep(0.1)
notes = settings_get('notes')
note = [n for n in notes if n['title'] == n_title+mod][0]
assert note['site'] == n_site + mod
assert note['user'] == n_user + mod
assert note['misc'] == n_body + mod
assert note['password'] == 'default'
# wipe & redraw
settings_set('notes', notes[0:-3])
goto_notes()
def test_top_export(goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, readback_bbqr, need_some_notes):
notes = need_some_notes()
notes = settings_get('notes', [])
assert len(notes) >= 1
goto_notes()
pick_menu_item('Export All')
title, story = cap_story()
assert 'Export' in title
assert 'to SD Card' in story
assert 'to show QR' in story
assert 'WARNING' in story
assert 'will be cleartext' in story
need_keypress(KEY_QR)
file_type, data = readback_bbqr()
assert file_type == 'J'
obj = json.loads(data)
assert obj.keys() == {'coldcard_notes'}
assert obj['coldcard_notes'] == notes
need_keypress(KEY_ENTER)
def test_top_import(goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr, need_some_notes):
# make some
notes = need_some_notes()
# wipe them
settings_set('notes', [])
goto_notes('Import')
title, story = cap_story()
assert 'Import' in title
assert 'from SD Card' in story
assert 'to scan QR' in story
assert 'WARNING' not in story
jj = json.dumps(dict(coldcard_notes=notes))
need_keypress(KEY_QR)
_, parts = split_qrs(jj, 'J', max_version=20)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(.5) # decompression time in some cases
m = cap_menu()
assert notes[0]['title'] in m[0]
assert settings_get('notes', 'MISSING') == notes
goto_notes()
@pytest.mark.parametrize('qr,title', [
('otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30', 'ACME Co:john.doe@email.com'),
('otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi',
'pi@raspberrypi'),
('otpauth-migration://offline?data=CiAKCghCEIa1rWta1rUSDEV4YW1wbGUgRGF0YSABKAEwAhAB',
'Google Auth'),
])
def test_top_qr(qr, title, goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set, scan_a_qr):
# import some fun QR codes (will be notes) from top-level, undocumented
goto_notes()
need_keypress(KEY_QR)
scan_a_qr(qr)
time.sleep(.5)
# lazy readback
notes = settings_get('notes', [])
assert notes[-1]['title'] == title
assert notes[-1]['misc'] == qr
#pick_menu_item('Delete')
#need_keypress(KEY_ENTER)
def test_top_disable(goto_notes, pick_menu_item, cap_menu, cap_story, need_keypress, settings_get, settings_set):
# Keep last - disables, deletes notes
settings_set('notes', [])
goto_notes()
m = cap_menu()
assert 'Disable Feature' in m
pick_menu_item('Disable Feature')
m = cap_menu()
assert 'Ready To Sign' in m
assert settings_get('notes', 'MISSING') == 'MISSING'
# EOF