From 0f2dd2a69359ec75d55583598c55084e082984eb Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Mon, 1 Aug 2022 21:56:27 -0700 Subject: [PATCH] convert tests to be data driven, use pathlib, fix creating metadata update print order to print common fields first per order in list.. if ident doesn't change it (no args), print out current identity.. --- ui/Makefile | 2 +- ui/medashare/cli.py | 214 +++++++++++++++++++++++--------------------- 2 files changed, 115 insertions(+), 101 deletions(-) diff --git a/ui/Makefile b/ui/Makefile index 27e12f6..cfebcd6 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -3,7 +3,7 @@ MODULES=medashare test: fixtures/sample.data.pasn1 fixtures/sample.persona.pasn1 fixtures/sample.mtree (. ./p/bin/activate && \ - (find $(MODULES) -type f | entr sh -c 'python3 -m coverage run -m unittest --failfast $(MODULES).tests && coverage report -m --omit=p/\*')) + ((find fixtures -type f; find $(MODULES) -type f) | entr sh -c 'python3 -m coverage run -m unittest --failfast $(MODULES).tests && coverage report -m --omit=p/\*')) env: $(VIRTUALENV) p diff --git a/ui/medashare/cli.py b/ui/medashare/cli.py index ee39778..60bd115 100644 --- a/ui/medashare/cli.py +++ b/ui/medashare/cli.py @@ -7,14 +7,19 @@ from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey, \ from cryptography.hazmat.primitives.serialization import Encoding, \ PrivateFormat, PublicFormat, NoEncryption +from unittest import mock + import base58 import copy import datetime import functools import hashlib -from unittest import mock +import io +import json import os.path +import pathlib import pasn1 +import re import shutil import string import sys @@ -29,12 +34,18 @@ _defaulthash = 'sha512' _validhashes = set([ 'sha256', 'sha512' ]) _hashlengths = { len(getattr(hashlib, x)().hexdigest()): x for x in _validhashes } -def _iterdictlist(obj): - for k, v in sorted(obj.items()): +def _keyordering(x): + k, v = x + try: + return (MDBase._common_names_list.index(k), k, v) + except ValueError: + return (2**32, k, v) + +def _iterdictlist(obj, **kwargs): + l = list(sorted(obj.items(**kwargs), key=_keyordering)) + for k, v in l: if isinstance(v, list): - v = v[:] - v.sort() - for i in v: + for i in sorted(v): yield k, i else: yield k, v @@ -65,11 +76,13 @@ class MDBase(object): _instance_properties = { 'uuid': _makeuuid, 'created_by_ref': _makeuuid, + #'parent_refs': lambda x: [ _makeuuid(y) for y in x ], } _common_properties = [ 'type', 'created_by_ref' ] # XXX - add lang? _common_optional = set(('parent_refs', 'sig')) _common_names = set(_common_properties + list(_generated_properties.keys())) + _common_names_list = _common_properties + list(_generated_properties.keys()) def __init__(self, obj={}, **kwargs): obj = copy.deepcopy(obj) @@ -573,9 +586,14 @@ def cmd_ident(options): persona = Persona.load(identfname) - persona.new_version(*(x.split('=', 1) for x in options.tagvalue)) + if options.tagvalue: + persona.new_version(*(x.split('=', 1) for x in options.tagvalue)) - persona.store(identfname) + persona.store(identfname) + else: + ident = persona.get_identity() + for k, v in _iterdictlist(ident, skipcommon=False): + print('%s:\t%s' % (k, v)) def cmd_pubkey(options): identfname = os.path.expanduser('~/.medashare_identity.pasn1') @@ -589,12 +607,12 @@ def cmd_modify(options): props = [[ x[0] ] + x[1:].split('=', 1) for x in options.modtagvalues] if any(x[0] not in ('+', '-') for x in props): - print('ERROR: tag needs to start w/ a "+" (add) or a "-" (remove).', file=sys.stderr) + print('ERROR: tag needs to start with a "+" (add) or a "-" (remove).', file=sys.stderr) sys.exit(1) badtags = list(x[1] for x in props if x[1] in (MDBase._common_names | MDBase._common_optional)) if any(badtags): - print('ERROR: invalid tag(s): %s.' % repr(badtags), file=sys.stderr) + print('ERROR: invalid tag%s: %s.' % ( 's' if len(badtags) > 1 else '', repr(badtags)), file=sys.stderr) sys.exit(1) adds = [ x[1:] for x in props if x[0] == '+' ] @@ -606,11 +624,13 @@ def cmd_modify(options): dels = [ x[1:] for x in props if x[0] == '-' ] for i in options.files: + # Get MetaData try: objs = objstr.by_file(i) except KeyError: - fobj = objstr - objs = [ persona.by_file(i) ] + fobj = persona.by_file(i) + objstr.loadobj(fobj) + objs = [ persona.MetaData(hashes=fobj.hashes) ] for j in objs: # make into key/values @@ -646,6 +666,7 @@ def cmd_list(options): for k, v in _iterdictlist(j): print('%s:\t%s' % (k, v)) except (KeyError, FileNotFoundError): + # XXX - tell the difference? print('ERROR: file not found: %s' % repr(i), file=sys.stderr) sys.exit(1) @@ -665,7 +686,7 @@ def main(): parser_gi.set_defaults(func=cmd_genident) parser_i = subparsers.add_parser('ident', help='update identity') - parser_i.add_argument('tagvalue', nargs='+', + parser_i.add_argument('tagvalue', nargs='*', help='add the arg as metadata for the identity, tag=[value]') parser_i.set_defaults(func=cmd_ident) @@ -717,20 +738,25 @@ class _TestCononicalCoder(unittest.TestCase): class _TestCases(unittest.TestCase): def setUp(self): - d = os.path.realpath(tempfile.mkdtemp()) + self.fixtures = pathlib.Path('fixtures').resolve() + + d = pathlib.Path(tempfile.mkdtemp()).resolve() self.basetempdir = d - self.tempdir = os.path.join(d, 'subdir') + self.tempdir = d / 'subdir' persona = Persona.load(os.path.join('fixtures', 'sample.persona.pasn1')) self.created_by_ref = persona.get_identity().uuid - shutil.copytree(os.path.join('fixtures', 'testfiles'), - self.tempdir) + shutil.copytree(self.fixtures / 'testfiles', self.tempdir) + + self.oldcwd = os.getcwd() def tearDown(self): shutil.rmtree(self.basetempdir) self.tempdir = None + os.chdir(self.oldcwd) + def test_mdbase(self): self.assertRaises(ValueError, MDBase, created_by_ref='') self.assertRaises(ValueError, MDBase.create_obj, { 'type': 'unknosldkfj' }) @@ -827,7 +853,7 @@ class _TestCases(unittest.TestCase): oldid = ftest.id self.assertEqual(ftest.filename, fname) - self.assertEqual(ftest.dir, self.tempdir) + self.assertEqual(ftest.dir, str(self.tempdir)) # XXX - do we add host information? self.assertEqual(ftest.id, uuid.uuid5(_NAMESPACE_MEDASHARE_PATH, '/'.join(os.path.split(self.tempdir) + @@ -983,6 +1009,10 @@ class _TestCases(unittest.TestCase): # and that it can be verified persona.verify(mdobj) + # that when round tripped through pasn1. + a = mdobj.encode() + b = MDBase.decode(a) + def test_objectstore(self): objst = ObjectStore.load(os.path.join('fixtures', 'sample.data.pasn1')) @@ -1036,6 +1066,70 @@ class _TestCases(unittest.TestCase): # Tests to add: # Non-duplicates when same metadata is located by multiple hashes. + def run_command_file(self, f): + with open(f) as fp: + cmds = json.load(fp) + + # setup object store + storefname = self.tempdir / 'storefname' + identfname = self.tempdir / 'identfname' + + # setup path mapping + def expandusermock(arg): + if arg == '~/.medashare_store.pasn1': + return storefname + elif arg == '~/.medashare_identity.pasn1': + return identfname + + # setup test fname + testfname = os.path.join(self.tempdir, 'test.txt') + newtestfname = os.path.join(self.tempdir, 'newfile.txt') + + for cmd in cmds: + try: + special = cmd['special'] + except KeyError: + pass + else: + if special == 'copy newfile.txt to test.txt': + shutil.copy(newtestfname, testfname) + elif special == 'change newfile.txt': + with open(newtestfname, 'w') as fp: + fp.write('some new contents') + + continue + + with self.subTest(file=f, title=cmd['title']), \ + mock.patch('os.path.expanduser', + side_effect=expandusermock) as eu, \ + mock.patch('sys.stdout', io.StringIO()) as stdout, \ + mock.patch('sys.stderr', io.StringIO()) as stderr, \ + mock.patch('sys.argv', [ 'progname', ] + + cmd['cmd']) as argv: + with self.assertRaises(SystemExit) as cm: + main() + + # XXX - Minor hack till other tests fixed + sys.exit(0) + + # with the correct output + self.maxDiff = None + outre = cmd.get('stdout_re') + if outre: + self.assertRegex(stdout.getvalue(), outre) + else: + self.assertEqual(stdout.getvalue(), cmd.get('stdout', '')) + self.assertEqual(stderr.getvalue(), cmd.get('stderr', '')) + + self.assertEqual(cm.exception.code, cmd.get('exit', 0)) + + def test_cmds(self): + cmds = self.fixtures.glob('cmd.*.json') + + for i in cmds: + os.chdir(self.tempdir) + self.run_command_file(i) + def test_main(self): # Test the main runner, this is only testing things that are # specific to running the program, like where the store is @@ -1044,6 +1138,8 @@ class _TestCases(unittest.TestCase): # setup object store storefname = os.path.join(self.tempdir, 'storefname') identfname = os.path.join(self.tempdir, 'identfname') + + # XXX part of the problem shutil.copy(os.path.join('fixtures', 'sample.data.pasn1'), storefname) # setup path mapping @@ -1057,7 +1153,6 @@ class _TestCases(unittest.TestCase): testfname = os.path.join(self.tempdir, 'test.txt') newtestfname = os.path.join(self.tempdir, 'newfile.txt') - import io import itertools real_stderr = sys.stderr @@ -1139,87 +1234,6 @@ class _TestCases(unittest.TestCase): # and that the old Persona can verify the new one self.assertTrue(persona.verify(nident)) - # that when asked to print the public key - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'pubkey' ]) as argv: - main() - - # the correct key is printed - self.assertEqual(stdout.getvalue(), - '%s\n' % persona.get_pubkey().decode('ascii')) - - # and looked up the correct file - eu.assert_called_with('~/.medashare_identity.pasn1') - - # that when a new file is printed - with mock.patch('sys.stderr', io.StringIO()) as stderr, mock.patch('sys.argv', [ 'progname', 'list', newtestfname ]) as argv: - # that it exits - with self.assertRaises(SystemExit) as cm: - main() - - # with error code 1 - self.assertEqual(cm.exception.code, 1) - - # and outputs an error message - self.assertEqual(stderr.getvalue(), - 'ERROR: file not found: %s\n' % repr(newtestfname)) - - # that when a tag is incomplete - with mock.patch('sys.stderr', io.StringIO()) as stderr, mock.patch('sys.argv', [ 'progname', 'modify', '+tag', newtestfname ]) as argv: - # that it exits - with self.assertRaises(SystemExit) as cm: - main() - - # with error code 1 - self.assertEqual(cm.exception.code, 1) - - # and outputs an error message - self.assertEqual(stderr.getvalue(), - 'ERROR: invalid tag, needs an "=".\n') - - # that when a new file has a tag added - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'modify', '+tag=', newtestfname ]) as argv: - main() - - # nothing is printed - self.assertEqual(stdout.getvalue(), ''); - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'list', testfname ]) as argv: - main() - self.assertEqual(stdout.getvalue(), - 'dc:creator:\tJohn-Mark Gurney\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n') - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'modify', '+dc:creator=Another user', '+foo=bar=baz', testfname ]) as argv: - main() - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'list', testfname ]) as argv: - main() - self.assertEqual(stdout.getvalue(), - 'dc:creator:\tAnother user\ndc:creator:\tJohn-Mark Gurney\nfoo:\tbar=baz\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n') - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'modify', '-dc:creator', testfname ]) as argv: - main() - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'list', testfname ]) as argv: - main() - self.assertEqual(stdout.getvalue(), - 'foo:\tbar=baz\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n') - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'modify', '+foo=bleh', testfname ]) as argv: - main() - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'list', testfname ]) as argv: - main() - self.assertEqual(stdout.getvalue(), - 'foo:\tbar=baz\nfoo:\tbleh\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n') - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'modify', '-foo=bar=baz', testfname ]) as argv: - main() - - with mock.patch('sys.stdout', io.StringIO()) as stdout, mock.patch('sys.argv', [ 'progname', 'list', testfname ]) as argv: - main() - self.assertEqual(stdout.getvalue(), - 'foo:\tbleh\nhashes:\tsha256:91751cee0a1ab8414400238a761411daa29643ab4b8243e9a91649e25be53ada\nhashes:\tsha512:7d5768d47b6bc27dc4fa7e9732cfa2de506ca262a2749cb108923e5dddffde842bbfee6cb8d692fb43aca0f12946c521cce2633887914ca1f96898478d10ad3f\nlang:\ten\n') - orig_open = open with mock.patch('os.path.expanduser', side_effect=expandusermock) \ as eu, mock.patch('medashare.cli.open') as op: