# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # from __future__ import absolute_import, division, print_function __metaclass__ = type import unittest from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import ( KEEP_EXISTING_VALUES, VyosConf, ) class TestListElements(unittest.TestCase): def test_add(self): conf = VyosConf() conf.set_entry(["a", "b"], "c") self.assertEqual(conf.config, {"a": {"b": {"c": {}}}}) conf.set_entry(["a", "b"], "d") self.assertEqual(conf.config, {"a": {"b": {"c": {}, "d": {}}}}) conf.set_entry(["a", "c"], "b") self.assertEqual( conf.config, {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {}}}}, ) conf.set_entry(["a", "c", "b"], "d") self.assertEqual( conf.config, {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {"d": {}}}}}, ) def test_del(self): conf = VyosConf() conf.set_entry(["a", "b"], "c") conf.set_entry(["a", "c", "b"], "d") conf.set_entry(["a", "b"], "d") self.assertEqual( conf.config, {"a": {"b": {"c": {}, "d": {}}, "c": {"b": {"d": {}}}}}, ) conf.del_entry(["a", "c", "b"], "d") self.assertEqual(conf.config, {"a": {"b": {"c": {}, "d": {}}}}) conf.set_entry(["a", "b", "c"], "d") conf.del_entry(["a", "b", "c"], "d") self.assertEqual(conf.config, {"a": {"b": {"d": {}}}}) def test_del_missing_leaf_is_noop(self): """ Deleting a leaf that was never set must leave the config unchanged. Regression test: del_entry() used to raise KeyError when the leaf's parent had siblings, and could delete an unrelated ancestor subtree (or the entire config) when the parent path had no siblings. """ # parent has siblings: previously raised KeyError conf = VyosConf() conf.set_entry(["a", "b"], "c") conf.set_entry(["a", "b"], "d") conf.del_entry(["a", "b"], "nonexistent") self.assertEqual(conf.config, {"a": {"b": {"c": {}, "d": {}}}}) # parent path is an unbranched chain: previously deleted the # entire config instead of no-op'ing conf = VyosConf() conf.set_entry(["a", "b"], "d") conf.del_entry(["a", "b"], "c") self.assertEqual(conf.config, {"a": {"b": {"d": {}}}}) # missing intermediate path element already behaved correctly; # confirm it still does conf = VyosConf() conf.set_entry(["a", "b"], "c") conf.del_entry(["a", "x"], "c") self.assertEqual(conf.config, {"a": {"b": {"c": {}}}}) def test_parse(self): conf = VyosConf() self.assertListEqual( conf.parse_line("set a b c"), ["set", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line('set a b "c"'), ["set", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line("set a b 'c d'"), ["set", ["a", "b"], "c d"], ) self.assertListEqual( conf.parse_line("set a b 'c'"), ["set", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line("delete a b 'c'"), ["delete", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line("del a b 'c'"), ["del", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line("set a b '\"c'"), ["set", ["a", "b"], '"c'], ) self.assertListEqual( conf.parse_line("set a b 'c' #this is a comment"), ["set", ["a", "b"], "c"], ) self.assertListEqual( conf.parse_line("set a b '#c'"), ["set", ["a", "b"], "#c"], ) def test_run_commands(self): self.assertEqual( VyosConf(["set a b 'c'", "set a c 'b'"]).config, {"a": {"b": {"c": {}}, "c": {"b": {}}}}, ) self.assertEqual( VyosConf(["set a b c 'd'", "set a c 'b'", "del a b c d"]).config, {"a": {"c": {"b": {}}}}, ) def test_build_commands(self): self.assertEqual( sorted( VyosConf( [ "set a b 'c a'", "set a c a", "set a c b", "delete a c a", ], ).build_commands(), ), sorted(["set a b 'c a'", "set a c b"]), ) self.assertEqual( sorted( VyosConf( [ "set a b 10.0.0.1/24", "set a c ABCabc123+/=", "set a d $6$ABC.abc.123.+./=..", ], ).build_commands(), ), sorted( [ "set a b 10.0.0.1/24", "set a c 'ABCabc123+/='", "set a d '$6$ABC.abc.123.+./=..'", ], ), ) def test_check_commands(self): conf = VyosConf(["set a b 'c a'", "set a c b"]) self.assertListEqual( conf.check_commands( ["set a b 'c a'", "del a c b", "set a b 'c'", "del a a a"], ), [True, False, False, True], ) def test_diff_commands_to(self): conf = VyosConf(["set a b 'c a'", "set a c b"]) self.assertListEqual( conf.diff_commands_to(VyosConf(["set a c b"])), ["delete a b"], ) self.assertListEqual( conf.diff_commands_to(VyosConf(["set a b 'c a'", "set a c b"])), [], ) # KEEP_EXISTING_VALUES is no longer reachable via 'set'/'delete' # command text (see #6): a literal "..." leaf is now an ordinary # value, not a sentinel, so nothing is suppressed here. self.assertListEqual( conf.diff_commands_to(VyosConf(["set a b ..."])), ["delete a b 'c a'", "delete a c", "set a b ..."], ) def test_diff_commands_to_keep_existing_values_sentinel(self): # KEEP_EXISTING_VALUES is only reachable via the Python API now. # Build the candidate tree directly to prove diff_to() still # honours it when used that way. conf = VyosConf(["set a b 'c a'", "set a c b"]) candidate = VyosConf() candidate.config = {"a": {"b": {KEEP_EXISTING_VALUES: {}}}} self.assertListEqual( conf.diff_commands_to(candidate), ["delete a c"], ) if __name__ == "__main__": unittest.main()