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: