diff --git a/certbot-apache/certbot_apache/assertions.py b/certbot-apache/certbot_apache/assertions.py new file mode 100644 index 000000000..17b56073a --- /dev/null +++ b/certbot-apache/certbot_apache/assertions.py @@ -0,0 +1,138 @@ +from certbot_apache import interfaces + + +PASS = "CERTBOT_PASS_ASSERT" + + +def assertEqual(first, second): + """ Equality assertion """ + + if isinstance(first, interfaces.CommentNode): + assertEqualComment(first, second) + elif isinstance(first, interfaces.DirectiveNode): + assertEqualDirective(first, second) + elif isinstance(first, interfaces.BlockNode): + assertEqualBlock(first, second) + + # Skip tests if filepath includes the pass value. This is done + # because filepath is variable of the base ParserNode interface, and + # unless the implementation is actually done, we cannot assume getting + # correct results from boolean assertion for dirty + if not isPass(first.filepath, second.filepath): + assert first.dirty == second.dirty + # We might want to disable this later if testing with two separate + # (but identical) directory structures. + assert first.filepath == second.filepath + +def assertEqualComment(first, second): + """ Equality assertion for CommentNode """ + + assert isinstance(first, interfaces.CommentNode) + assert isinstance(second, interfaces.CommentNode) + + if not isPass(first.comment, second.comment): + assert first.comment == second.comment + + if not isPass(first.filepath, second.filepath): + assert first.filepath == second.filepath + +def _assertEqualDirectiveComponents(first, second): + """ Handles assertion for instance variables for DirectiveNode and BlockNode""" + + # Enabled value cannot be asserted, because Augeas implementation + # is unable to figure that out. + # assert first.enabled == second.enabled + if not isPass(first.name, second.name): + assert first.name == second.name + + if not isPass(first.filepath, second.filepath): + assert first.filepath == second.filepath + + if not isPass(first.parameters, second.parameters): + assert first.parameters == second.parameters + +def assertEqualDirective(first, second): + """ Equality assertion for DirectiveNode """ + + assert isinstance(first, interfaces.DirectiveNode) + assert isinstance(second, interfaces.DirectiveNode) + _assertEqualDirectiveComponents(first, second) + +def assertEqualBlock(first, second): + """ Equality assertion for BlockNode """ + + # first was checked in the assertEqual method + assert isinstance(first, interfaces.BlockNode) + assert isinstance(second, interfaces.BlockNode) + _assertEqualDirectiveComponents(first, second) + # Children cannot be asserted, because Augeas implementation will not + # prepopulate the sequence of children. + # assert len(first.children) == len(second.children) + +def isPass(first, second): + """ Checks if either first or second holds the assertion pass value """ + + if isinstance(first, tuple) or isinstance(first, list): + if PASS in first: + return True + if isinstance(second, tuple) or isinstance(second, list): + if PASS in second: + return True + if PASS in [first, second]: + return True + return False + +def isPassNodeList(nodelist): + """ Checks if a ParserNode in the nodelist should pass the assertion, + this function is used for results of find_* methods. Unimplemented find_* + methods should return a sequence containing a single ParserNode instance + with assertion pass string.""" + + try: + node = nodelist[0] + except IndexError: + node = None + + if not node: + # Empty result means that the method is implemented + return False + + if isinstance(node, interfaces.BlockNode): + return _isPassDirective(node) + if isinstance(node, interfaces.DirectiveNode): + return _isPassDirective(node) + return _isPassComment(node) + +def _isPassDirective(block): + """ Checks if BlockNode or DirectiveNode should pass the assertion """ + + if block.name == PASS: + return True + if PASS in block.parameters: + return True + if block.filepath == PASS: + return True + return False + +def _isPassComment(comment): + """ Checks if CommentNode should pass the assertion """ + + if comment.comment == PASS: + return True + if comment.filepath == PASS: + return True + return False + +def assertSimple(first, second): + """ Simple assertion """ + if not isPass(first, second): + assert first == second + +def assertSimpleList(first, second): + """ Simple assertion that lists contain the same objects. This needs to + be used when there's uncertainty about the ordering of the list. """ + + if not isPass(first, second): + if first: + for f in first: + assert f in second diff --git a/certbot-apache/certbot_apache/augeasparser.py b/certbot-apache/certbot_apache/augeasparser.py new file mode 100644 index 000000000..3d50001f1 --- /dev/null +++ b/certbot-apache/certbot_apache/augeasparser.py @@ -0,0 +1,127 @@ +""" Augeas implementation of the ParserNode interface """ +from certbot_apache import assertions +from certbot_apache import interfaces +from certbot_apache import parsernode_util as util + + +class AugeasParserNode(interfaces.ParserNode): + """ Augeas implementation of ParserNode interface """ + + def __init__(self, **kwargs): + ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) + self.ancestor = ancestor + # self.filepath = filepath + self.filepath = assertions.PASS + self.dirty = dirty + self.metadata = metadata + + def save(self, msg): # pragma: no cover + pass + + +class AugeasCommentNode(AugeasParserNode): + """ Augeas implementation of CommentNode interface """ + + def __init__(self, **kwargs): + comment, kwargs = util.commentnode_kwargs(kwargs) + super(AugeasCommentNode, self).__init__(**kwargs) + self.comment = comment + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.comment == other.comment and + self.filepath == other.filepath and + self.dirty == other.dirty and + self.ancestor == other.ancestor and + self.metadata == other.metadata) + + +class AugeasDirectiveNode(AugeasParserNode): + """ Augeas implementation of DirectiveNode interface """ + + def __init__(self, **kwargs): + name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs) + super(AugeasDirectiveNode, self).__init__(**kwargs) + self.name = name + self.parameters = parameters + self.enabled = enabled + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.name == other.name and + self.filepath == other.filepath and + self.parameters == other.parameters and + self.enabled == other.enabled and + self.dirty == other.dirty and + self.ancestor == other.ancestor and + self.metadata == other.metadata) + + def set_parameters(self, parameters): + self.parameters = parameters + + +class AugeasBlockNode(AugeasDirectiveNode): + """ Augeas implementation of BlockNode interface """ + + def __init__(self, **kwargs): + super(AugeasBlockNode, self).__init__(**kwargs) + self.children = () + + def __eq__(self, other): + if isinstance(other, self.__class__): + return (self.name == other.name and + self.filepath == other.filepath and + self.parameters == other.parameters and + self.children == other.children and + self.enabled == other.enabled and + self.dirty == other.dirty and + self.ancestor == other.ancestor and + self.metadata == other.metadata) + + + def add_child_block(self, name, parameters=None, position=None): + new_block = AugeasBlockNode(name=assertions.PASS, + ancestor=self, + filepath=assertions.PASS) + self.children += (new_block,) + return new_block + + def add_child_directive(self, name, parameters=None, position=None): + new_dir = AugeasDirectiveNode(name=assertions.PASS, + ancestor=self, + filepath=assertions.PASS) + self.children += (new_dir,) + return new_dir + + def add_child_comment(self, comment="", position=None): + new_comment = AugeasCommentNode(comment=assertions.PASS, + ancestor=self, + filepath=assertions.PASS) + self.children += (new_comment,) + return new_comment + + def find_blocks(self, name, exclude=True): + return [AugeasBlockNode(name=assertions.PASS, + ancestor=self, + filepath=assertions.PASS)] + + def find_directives(self, name, exclude=True): + return [AugeasDirectiveNode(name=assertions.PASS, + ancestor=self, + filepath=assertions.PASS)] + + def find_comments(self, comment, exact=False): + return [AugeasCommentNode(comment=assertions.PASS, + ancestor=self, + filepath=assertions.PASS)] + + def delete_child(self, child): # pragma: no cover + pass + + def unsaved_files(self): # pragma: no cover + return [assertions.PASS] + + +interfaces.CommentNode.register(AugeasCommentNode) +interfaces.DirectiveNode.register(AugeasDirectiveNode) +interfaces.BlockNode.register(AugeasBlockNode) diff --git a/certbot-apache/certbot_apache/dualparser.py b/certbot-apache/certbot_apache/dualparser.py new file mode 100644 index 000000000..f9186440e --- /dev/null +++ b/certbot-apache/certbot_apache/dualparser.py @@ -0,0 +1,318 @@ +""" Tests for ParserNode interface """ +from certbot_apache import assertions +from certbot_apache import augeasparser + + +class DualNodeBase(object): + """ Dual parser interface for in development testing. This is used as the + base class for dual parser interface classes. This class handles runtime + attribute value assertions.""" + + def save(self, msg): # pragma: no cover + """ Call save for both parsers """ + self.primary.save(msg) + self.secondary.save(msg) + + def __getattr__(self, aname): + """ Attribute value assertion """ + firstval = getattr(self.primary, aname) + secondval = getattr(self.secondary, aname) + if not assertions.isPass(firstval, secondval): + assertions.assertSimple(firstval, secondval) + return firstval + + +class DualCommentNode(DualNodeBase): + """ Dual parser implementation of CommentNode interface """ + + def __init__(self, **kwargs): + """ This initialization implementation allows ordinary initialization + of CommentNode objects as well as creating a DualCommentNode object + using precreated or fetched CommentNode objects if provided as optional + arguments primary and secondary. + + Parameters other than the following are from interfaces.CommentNode: + + :param CommentNode primary: Primary pre-created CommentNode, mainly + used when creating new DualParser nodes using add_* methods. + :param CommentNode secondary: Secondary pre-created CommentNode + """ + + kwargs.setdefault("primary", None) + kwargs.setdefault("secondary", None) + primary = kwargs.pop("primary") + secondary = kwargs.pop("secondary") + + if not primary: + self.primary = augeasparser.AugeasCommentNode(**kwargs) + else: + self.primary = primary + if not secondary: + self.secondary = augeasparser.AugeasCommentNode(**kwargs) + else: + self.secondary = secondary + + assertions.assertEqual(self.primary, self.secondary) + + +class DualDirectiveNode(DualNodeBase): + """ Dual parser implementation of DirectiveNode interface """ + + def __init__(self, **kwargs): + """ This initialization implementation allows ordinary initialization + of DirectiveNode objects as well as creating a DualDirectiveNode object + using precreated or fetched DirectiveNode objects if provided as optional + arguments primary and secondary. + + Parameters other than the following are from interfaces.DirectiveNode: + + :param DirectiveNode primary: Primary pre-created DirectiveNode, mainly + used when creating new DualParser nodes using add_* methods. + :param DirectiveNode secondary: Secondary pre-created DirectiveNode + + + """ + + kwargs.setdefault("primary", None) + kwargs.setdefault("secondary", None) + primary = kwargs.pop("primary") + secondary = kwargs.pop("secondary") + + if not primary: + self.primary = augeasparser.AugeasDirectiveNode(**kwargs) + else: + self.primary = primary + + if not secondary: + self.secondary = augeasparser.AugeasDirectiveNode(**kwargs) + else: + self.secondary = secondary + + assertions.assertEqual(self.primary, self.secondary) + + def set_parameters(self, parameters): + """ Sets parameters and asserts that both implementation successfully + set the parameter sequence """ + + self.primary.set_parameters(parameters) + self.secondary.set_parameters(parameters) + assertions.assertEqual(self.primary, self.secondary) + + +class DualBlockNode(DualDirectiveNode): + """ Dual parser implementation of BlockNode interface """ + + def __init__(self, **kwargs): + """ This initialization implementation allows ordinary initialization + of BlockNode objects as well as creating a DualBlockNode object + using precreated or fetched BlockNode objects if provided as optional + arguments primary and secondary. + + Parameters other than the following are from interfaces.BlockNode: + + :param BlockNode primary: Primary pre-created BlockNode, mainly + used when creating new DualParser nodes using add_* methods. + :param BlockNode secondary: Secondary pre-created BlockNode + """ + + kwargs.setdefault("primary", None) + kwargs.setdefault("secondary", None) + primary = kwargs.pop("primary") + secondary = kwargs.pop("secondary") + + if not primary: + self.primary = augeasparser.AugeasBlockNode(**kwargs) + else: + self.primary = primary + + if not secondary: + self.secondary = augeasparser.AugeasBlockNode(**kwargs) + else: + self.secondary = secondary + + assertions.assertEqual(self.primary, self.secondary) + + def add_child_block(self, name, parameters=None, position=None): + """ Creates a new child BlockNode, asserts that both implementations + did it in a similar way, and returns a newly created DualBlockNode object + encapsulating both of the newly created objects """ + + primary_new = self.primary.add_child_block(name, parameters, position) + secondary_new = self.secondary.add_child_block(name, parameters, position) + assertions.assertEqual(primary_new, secondary_new) + new_block = DualBlockNode(primary=primary_new, secondary=secondary_new) + return new_block + + def add_child_directive(self, name, parameters=None, position=None): + """ Creates a new child DirectiveNode, asserts that both implementations + did it in a similar way, and returns a newly created DualDirectiveNode + object encapsulating both of the newly created objects """ + + primary_new = self.primary.add_child_directive(name, parameters, position) + secondary_new = self.secondary.add_child_directive(name, parameters, position) + assertions.assertEqual(primary_new, secondary_new) + new_dir = DualDirectiveNode(primary=primary_new, secondary=secondary_new) + return new_dir + + def add_child_comment(self, comment="", position=None): + """ Creates a new child CommentNode, asserts that both implementations + did it in a similar way, and returns a newly created DualCommentNode + object encapsulating both of the newly created objects """ + + primary_new = self.primary.add_child_comment(comment, position) + secondary_new = self.secondary.add_child_comment(comment, position) + assertions.assertEqual(primary_new, secondary_new) + new_comment = DualCommentNode(primary=primary_new, secondary=secondary_new) + return new_comment + + def _create_matching_list(self, primary_list, secondary_list): # pragma: no cover + """ Matches the list of primary_list to a list of secondary_list and + returns a list of tuples. This is used to create results for find_ + methods. """ + + matched = list() + for p in primary_list: + match = None + for s in secondary_list: + try: + assertions.assertEqual(p, s) + match = s + except AssertionError: + continue + if match: + matched.append((p,s)) + else: + raise AssertionError("Could not find a matching node.") + return matched + + def find_blocks(self, name, exclude=True): # pragma: no cover + """ + Performs a search for BlockNodes using both implementations and does simple + checks for results. This is built upon the assumption that unimplemented + find_* methods return a list with a single assertion passing object. + After the assertion, it creates a list of newly created DualBlockNode + instances that encapsulate the pairs of returned BlockNode objects. + """ + + primary_blocks = self.primary.find_blocks(name, exclude) + secondary_blocks = self.secondary.find_blocks(name, exclude) + + # The order of search results for Augeas implementation cannot be + # assured. + + pass_primary = assertions.isPassNodeList(primary_blocks) + pass_secondary = assertions.isPassNodeList(secondary_blocks) + new_blocks = list() + + if pass_primary and pass_secondary: + # Both unimplemented + new_blocks.append(DualBlockNode(primary=primary_blocks[0], + secondary=secondary_blocks[0])) + elif pass_primary: + for c in secondary_blocks: + new_blocks.append(DualBlockNode(primary=primary_blocks[0], + secondary=c)) + elif pass_secondary: + for c in primary_blocks: + new_blocks.append(DualBlockNode(primary=c, + secondary=secondary_blocks[0])) + else: + assert len(primary_blocks) == len(secondary_blocks) + matches = self._create_matching_list(primary_blocks, secondary_blocks) + for p,s in matches: + new_blocks.append(DualBlockNode(primary=p, secondary=s)) + + return new_blocks + + def find_directives(self, name, exclude=True): # pragma: no cover + """ + Performs a search for DirectiveNodes using both implementations and + checks the results. This is built upon the assumption that unimplemented + find_* methods return a list with a single assertion passing object. + After the assertion, it creates a list of newly created DualDirectiveNode + instances that encapsulate the pairs of returned DirectiveNode objects. + """ + primary_dirs = self.primary.find_directives(name, exclude) + secondary_dirs = self.secondary.find_directives(name, exclude) + + # The order of search results for Augeas implementation cannot be + # assured. + + pass_primary = assertions.isPassNodeList(primary_dirs) + pass_secondary = assertions.isPassNodeList(secondary_dirs) + new_dirs = list() + + if pass_primary and pass_secondary: + # Both unimplemented + new_dirs.append(DualDirectiveNode(primary=primary_dirs[0], + secondary=secondary_dirs[0])) + elif pass_primary: + for c in secondary_dirs: + new_dirs.append(DualDirectiveNode(primary=primary_dirs[0], + secondary=c)) + elif pass_secondary: + for c in primary_dirs: + new_dirs.append(DualDirectiveNode(primary=c, + secondary=secondary_dirs[0])) + else: + assert len(primary_dirs) == len(secondary_dirs) + matches = self._create_matching_list(primary_dirs, secondary_dirs) + for p,s in matches: + new_dirs.append(DualDirectiveNode(primary=p, secondary=s)) + + return new_dirs + + def find_comments(self, comment, exact=False): # pragma: no cover + """ + Performs a search for CommentNodes using both implementations and + checks the results. This is built upon the assumption that unimplemented + find_* methods return a list with a single assertion passing object. + After the assertion, it creates a list of newly created DualCommentNode + instances that encapsulate the pairs of returned CommentNode objects. + """ + primary_com = self.primary.find_comments(comment, exact) + secondary_com = self.secondary.find_comments(comment, exact) + + # The order of search results for Augeas implementation cannot be + # assured. + + pass_primary = assertions.isPassNodeList(primary_com) + pass_secondary = assertions.isPassNodeList(secondary_com) + new_com = list() + + if pass_primary and pass_secondary: + # Both unimplemented + new_com.append(DualCommentNode(primary=primary_com[0], + secondary=secondary_com[0])) + elif pass_primary: + for c in secondary_com: + new_com.append(DualCommentNode(primary=primary_com[0], + secondary=c)) + elif pass_secondary: + for c in primary_com: + new_com.append(DualCommentNode(primary=c, + secondary=secondary_com[0])) + else: + assert len(primary_com) == len(secondary_com) + matches = self._create_matching_list(primary_com, secondary_com) + for p,s in matches: + new_com.append(DualCommentNode(primary=p, secondary=s)) + + return new_com + + def delete_child(self, child): # pragma: no cover + """Deletes a child from the ParserNode implementations. The actual + ParserNode implementations are used here directly in order to be able + to match a child to the list of children.""" + + self.primary.delete_child(child.primary) + self.secondary.delete_child(child.secondary) + + def unsaved_files(self): + """ Fetches the list of unsaved file paths and asserts that the lists + match """ + primary_files = self.primary.unsaved_files() + secondary_files = self.secondary.unsaved_files() + assertions.assertSimple(primary_files, secondary_files) + + return primary_files diff --git a/certbot-apache/certbot_apache/tests/dualnode_test.py b/certbot-apache/certbot_apache/tests/dualnode_test.py new file mode 100644 index 000000000..dc55aadcc --- /dev/null +++ b/certbot-apache/certbot_apache/tests/dualnode_test.py @@ -0,0 +1,167 @@ +"""Tests for DualParserNode implementation""" +import unittest + +import mock + +from certbot_apache import assertions +from certbot_apache import dualparser + + +class DualParserNodeTest(unittest.TestCase): + """DualParserNode tests""" + def setUp(self): # pylint: disable=arguments-differ + self.block = dualparser.DualBlockNode(name="block", + ancestor=None, + filepath="/tmp/something") + self.block_two = dualparser.DualBlockNode(name="block", + ancestor=self.block, + filepath="/tmp/something") + self.directive = dualparser.DualDirectiveNode(name="directive", + ancestor=self.block, + filepath="/tmp/something") + self.comment = dualparser.DualCommentNode(comment="comment", + ancestor=self.block, + filepath="/tmp/something") + + def test_create_with_primary(self): + cnode = dualparser.DualCommentNode(comment="comment", + ancestor=self.block, + filepath="/tmp/something", + primary=self.comment.secondary) + dnode = dualparser.DualDirectiveNode(name="directive", + ancestor=self.block, + filepath="/tmp/something", + primary=self.directive.secondary) + bnode = dualparser.DualBlockNode(name="block", + ancestor=self.block, + filepath="/tmp/something", + primary=self.block.secondary) + self.assertTrue(cnode.primary is self.comment.secondary) + self.assertTrue(dnode.primary is self.directive.secondary) + self.assertTrue(bnode.primary is self.block.secondary) + + def test_create_with_secondary(self): + cnode = dualparser.DualCommentNode(comment="comment", + ancestor=self.block, + filepath="/tmp/something", + secondary=self.comment.primary) + dnode = dualparser.DualDirectiveNode(name="directive", + ancestor=self.block, + filepath="/tmp/something", + secondary=self.directive.primary) + bnode = dualparser.DualBlockNode(name="block", + ancestor=self.block, + filepath="/tmp/something", + secondary=self.block.primary) + self.assertTrue(cnode.secondary is self.comment.primary) + self.assertTrue(dnode.secondary is self.directive.primary) + self.assertTrue(bnode.secondary is self.block.primary) + + def test_set_params(self): + params = ("first", "second") + self.directive.set_parameters(params) + self.assertEqual(self.directive.primary.parameters, params) + self.assertEqual(self.directive.secondary.parameters, params) + + def test_set_parameters(self): + pparams = mock.MagicMock() + sparams = mock.MagicMock() + pparams.parameters = ("a", "b") + sparams.parameters = ("a", "b") + self.directive.primary.set_parameters = pparams + self.directive.secondary.set_parameters = sparams + self.directive.set_parameters(("param","seq")) + self.assertTrue(pparams.called) + self.assertTrue(sparams.called) + + def test_delete_child(self): + pdel = mock.MagicMock() + sdel = mock.MagicMock() + self.block.primary.delete_child = pdel + self.block.secondary.delete_child = sdel + self.block.delete_child(self.comment) + self.assertTrue(pdel.called) + self.assertTrue(sdel.called) + + def test_unsaved_files(self): + puns = mock.MagicMock() + suns = mock.MagicMock() + puns.return_value = assertions.PASS + suns.return_value = assertions.PASS + self.block.primary.unsaved_files = puns + self.block.secondary.unsaved_files = suns + self.block.unsaved_files() + self.assertTrue(puns.called) + self.assertTrue(suns.called) + + def test_add_child_block(self): + p = mock.MagicMock() + p.return_value = self.block + s = mock.MagicMock() + s.return_value = self.block + self.block.primary.add_child_block = p + self.block.secondary.add_child_block = s + self.block.add_child_block("name") + self.assertTrue(p.called) + self.assertTrue(s.called) + + def test_add_child_directive(self): + p = mock.MagicMock() + p.return_value = self.directive + s = mock.MagicMock() + s.return_value = self.directive + self.block.primary.add_child_directive = p + self.block.secondary.add_child_directive = s + self.block.add_child_directive("name") + self.assertTrue(p.called) + self.assertTrue(s.called) + + def test_add_child_comment(self): + p = mock.MagicMock() + p.return_value = self.comment + s = mock.MagicMock() + s.return_value = self.comment + self.block.primary.add_child_comment = p + self.block.secondary.add_child_comment = s + self.block.add_child_comment("comment") + self.assertTrue(p.called) + self.assertTrue(s.called) + + def test_find_blocks(self): + dblks = self.block.find_blocks("block") + p_dblks = [d.primary for d in dblks] + s_dblks = [d.secondary for d in dblks] + p_blks = self.block.primary.find_blocks("block") + s_blks = self.block.secondary.find_blocks("block") + # Check that every block response is represented in the list of + # DualParserNode instances. + for p in p_dblks: + self.assertTrue(p in p_blks) + for s in s_dblks: + self.assertTrue(s in s_blks) + + def test_find_directives(self): + ddirs = self.block.find_directives("directive") + p_ddirs = [d.primary for d in ddirs] + s_ddirs = [d.secondary for d in ddirs] + p_dirs = self.block.primary.find_directives("directive") + s_dirs = self.block.secondary.find_directives("directive") + # Check that every directive response is represented in the list of + # DualParserNode instances. + for p in p_ddirs: + self.assertTrue(p in p_dirs) + for s in s_ddirs: + self.assertTrue(s in s_dirs) + + def test_find_comments(self): + dcoms = self.block.find_comments("comment") + p_dcoms = [d.primary for d in dcoms] + s_dcoms = [d.secondary for d in dcoms] + p_coms = self.block.primary.find_comments("comment") + s_coms = self.block.secondary.find_comments("comment") + # Check that every comment response is represented in the list of + # DualParserNode instances. + for p in p_dcoms: + self.assertTrue(p in p_coms) + for s in s_dcoms: + self.assertTrue(s in s_coms)