Merge "Simplify template wrappers in NC client"
diff --git a/src/datastore_access.hpp b/src/datastore_access.hpp
index 05c1296..0856d41 100644
--- a/src/datastore_access.hpp
+++ b/src/datastore_access.hpp
@@ -46,12 +46,8 @@
     virtual ~DatastoreAccess() = 0;
     virtual Tree getItems(const std::string& path) = 0;
     virtual void setLeaf(const std::string& path, leaf_data_ value) = 0;
-    virtual void createPresenceContainer(const std::string& path) = 0;
-    virtual void deletePresenceContainer(const std::string& path) = 0;
-    virtual void createListInstance(const std::string& path) = 0;
-    virtual void deleteListInstance(const std::string& path) = 0;
-    virtual void createLeafListInstance(const std::string& path) = 0;
-    virtual void deleteLeafListInstance(const std::string& path) = 0;
+    virtual void createItem(const std::string& path) = 0;
+    virtual void deleteItem(const std::string& path) = 0;
     virtual void moveItem(const std::string& path, std::variant<yang::move::Absolute, yang::move::Relative> move) = 0;
     virtual Tree executeRpc(const std::string& path, const Tree& input) = 0;
 
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 9f0baf2..3cc74f2 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -57,7 +57,7 @@
     create_::name >> space_separator > (presenceContainerPath | listInstancePath | leafListElementPath);
 
 auto const delete_rule_def =
-    delete_::name >> space_separator > (presenceContainerPath | listInstancePath | leafListElementPath);
+    delete_::name >> space_separator > (presenceContainerPath | listInstancePath | leafListElementPath | writableLeafPath);
 
 auto const get_def =
     get_::name >> -(space_separator >> ((dataPathListEnd | dataPath) | (module >> "*")));
diff --git a/src/interpreter.cpp b/src/interpreter.cpp
index 3543e93..8ee2b55 100644
--- a/src/interpreter.cpp
+++ b/src/interpreter.cpp
@@ -87,22 +87,12 @@
 
 void Interpreter::operator()(const create_& create) const
 {
-    if (std::holds_alternative<listElement_>(create.m_path.m_nodes.back().m_suffix))
-        m_datastore.createListInstance(pathToString(toCanonicalPath(create.m_path)));
-    else if (std::holds_alternative<leafListElement_>(create.m_path.m_nodes.back().m_suffix))
-        m_datastore.createLeafListInstance(pathToString(toCanonicalPath(create.m_path)));
-    else
-        m_datastore.createPresenceContainer(pathToString(toCanonicalPath(create.m_path)));
+    m_datastore.createItem(pathToString(toCanonicalPath(create.m_path)));
 }
 
 void Interpreter::operator()(const delete_& delet) const
 {
-    if (std::holds_alternative<container_>(delet.m_path.m_nodes.back().m_suffix))
-        m_datastore.deletePresenceContainer(pathToString(toCanonicalPath(delet.m_path)));
-    else if (std::holds_alternative<leafListElement_>(delet.m_path.m_nodes.back().m_suffix))
-        m_datastore.deleteLeafListInstance(pathToString(toCanonicalPath(delet.m_path)));
-    else
-        m_datastore.deleteListInstance(pathToString(toCanonicalPath(delet.m_path)));
+    m_datastore.deleteItem(pathToString(toCanonicalPath(delet.m_path)));
 }
 
 void Interpreter::operator()(const ls_& ls) const
diff --git a/src/netconf_access.cpp b/src/netconf_access.cpp
index 2a2fa96..a7cd6b4 100644
--- a/src/netconf_access.cpp
+++ b/src/netconf_access.cpp
@@ -65,13 +65,13 @@
     doEditFromDataNode(node);
 }
 
-void NetconfAccess::createPresenceContainer(const std::string& path)
+void NetconfAccess::createItem(const std::string& path)
 {
     auto node = m_schema->dataNodeFromPath(path);
     doEditFromDataNode(node);
 }
 
-void NetconfAccess::deletePresenceContainer(const std::string& path)
+void NetconfAccess::deleteItem(const std::string& path)
 {
     auto node = m_schema->dataNodeFromPath(path);
     auto container = *(node->find_path(path.c_str())->data().begin());
@@ -79,33 +79,6 @@
     doEditFromDataNode(node);
 }
 
-void NetconfAccess::createListInstance(const std::string& path)
-{
-    auto node = m_schema->dataNodeFromPath(path);
-    doEditFromDataNode(node);
-}
-
-void NetconfAccess::deleteListInstance(const std::string& path)
-{
-    auto node = m_schema->dataNodeFromPath(path);
-    auto list = *(node->find_path(path.c_str())->data().begin());
-    list->insert_attr(m_schema->getYangModule("ietf-netconf"), "operation", "delete");
-    doEditFromDataNode(node);
-}
-
-void NetconfAccess::createLeafListInstance(const std::string& path)
-{
-    auto node = m_schema->dataNodeFromPath(path);
-    doEditFromDataNode(node);
-}
-void NetconfAccess::deleteLeafListInstance(const std::string& path)
-{
-    auto node = m_schema->dataNodeFromPath(path);
-    auto list = *(node->find_path(path.c_str())->data().begin());
-    list->insert_attr(m_schema->getYangModule("ietf-netconf"), "operation", "delete");
-    doEditFromDataNode(node);
-}
-
 struct impl_toYangInsert {
     std::string operator()(yang::move::Absolute& absolute)
     {
@@ -134,7 +107,7 @@
         if (m_schema->nodeType(source) == yang::NodeTypes::LeafList) {
             sourceNode->insert_attr(yangModule, "value", leafDataToString(relative.m_path.at(".")).c_str());
         } else {
-            sourceNode->insert_attr(yangModule, "key", instanceToString(node->node_module()->name(), relative.m_path).c_str());
+            sourceNode->insert_attr(yangModule, "key", instanceToString(relative.m_path, node->node_module()->name()).c_str());
         }
     }
     doEditFromDataNode(sourceNode);
diff --git a/src/netconf_access.hpp b/src/netconf_access.hpp
index 3875911..3f45a27 100644
--- a/src/netconf_access.hpp
+++ b/src/netconf_access.hpp
@@ -35,12 +35,8 @@
     ~NetconfAccess() override;
     Tree getItems(const std::string& path) override;
     void setLeaf(const std::string& path, leaf_data_ value) override;
-    void createPresenceContainer(const std::string& path) override;
-    void deletePresenceContainer(const std::string& path) override;
-    void createListInstance(const std::string& path) override;
-    void deleteListInstance(const std::string& path) override;
-    void createLeafListInstance(const std::string& path) override;
-    void deleteLeafListInstance(const std::string& path) override;
+    void createItem(const std::string& path) override;
+    void deleteItem(const std::string& path) override;
     void moveItem(const std::string& path, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
     void commitChanges() override;
     void discardChanges() override;
diff --git a/src/python_netconf.cpp b/src/python_netconf.cpp
index 76dc559..19c096f 100644
--- a/src/python_netconf.cpp
+++ b/src/python_netconf.cpp
@@ -68,10 +68,8 @@
                     "server"_a, "port"_a=830, "username"_a, "interactive_auth"_a)
             .def("getItems", &NetconfAccess::getItems, "xpath"_a)
             .def("setLeaf", &NetconfAccess::setLeaf, "xpath"_a, "value"_a)
-            .def("createPresenceContainer", &NetconfAccess::createPresenceContainer, "xpath"_a)
-            .def("deletePresenceContainer", &NetconfAccess::deletePresenceContainer, "xpath"_a)
-            .def("createListInstance", &NetconfAccess::createListInstance, "xpath"_a)
-            .def("deleteListInstance", &NetconfAccess::deleteListInstance, "xpath"_a)
+            .def("createItem", &NetconfAccess::createItem, "xpath"_a)
+            .def("deleteItem", &NetconfAccess::deleteItem, "xpath"_a)
             .def("executeRpc", &NetconfAccess::executeRpc, "rpc"_a, "input"_a=DatastoreAccess::Tree{})
             .def("commitChanges", &NetconfAccess::commitChanges)
             ;
diff --git a/src/sysrepo_access.cpp b/src/sysrepo_access.cpp
index d8a64da..70b8823 100644
--- a/src/sysrepo_access.cpp
+++ b/src/sysrepo_access.cpp
@@ -235,7 +235,7 @@
     }
 }
 
-void SysrepoAccess::createPresenceContainer(const std::string& path)
+void SysrepoAccess::createItem(const std::string& path)
 {
     try {
         m_session->set_item(path.c_str());
@@ -244,43 +244,7 @@
     }
 }
 
-void SysrepoAccess::deletePresenceContainer(const std::string& path)
-{
-    try {
-        m_session->delete_item(path.c_str());
-    } catch (sysrepo::sysrepo_exception& ex) {
-        reportErrors();
-    }
-}
-
-void SysrepoAccess::createLeafListInstance(const std::string& path)
-{
-    try {
-        m_session->set_item(path.c_str());
-    } catch (sysrepo::sysrepo_exception& ex) {
-        reportErrors();
-    }
-}
-
-void SysrepoAccess::deleteLeafListInstance(const std::string& path)
-{
-    try {
-        m_session->delete_item(path.c_str());
-    } catch (sysrepo::sysrepo_exception& ex) {
-        reportErrors();
-    }
-}
-
-void SysrepoAccess::createListInstance(const std::string& path)
-{
-    try {
-        m_session->set_item(path.c_str());
-    } catch (sysrepo::sysrepo_exception& ex) {
-        reportErrors();
-    }
-}
-
-void SysrepoAccess::deleteListInstance(const std::string& path)
+void SysrepoAccess::deleteItem(const std::string& path)
 {
     try {
         m_session->delete_item(path.c_str());
@@ -313,7 +277,7 @@
         if (m_schema->nodeType(source) == yang::NodeTypes::LeafList) {
             destPathStr = stripLeafListValueFromPath(source) + "[.='" + leafDataToString(relative.m_path.at(".")) + "']";
         } else {
-            destPathStr = stripLastListInstanceFromPath(source) + instanceToString(m_schema->dataNodeFromPath(source)->node_module()->name(), relative.m_path);
+            destPathStr = stripLastListInstanceFromPath(source) + instanceToString(relative.m_path, m_schema->dataNodeFromPath(source)->node_module()->name());
         }
     }
     m_session->move_item(source.c_str(), toSrMoveOp(move), destPathStr.c_str());
diff --git a/src/sysrepo_access.hpp b/src/sysrepo_access.hpp
index 6e66eea..4d87847 100644
--- a/src/sysrepo_access.hpp
+++ b/src/sysrepo_access.hpp
@@ -30,12 +30,8 @@
     SysrepoAccess(const std::string& appname, const Datastore datastore);
     Tree getItems(const std::string& path) override;
     void setLeaf(const std::string& path, leaf_data_ value) override;
-    void createPresenceContainer(const std::string& path) override;
-    void deletePresenceContainer(const std::string& path) override;
-    void createListInstance(const std::string& path) override;
-    void deleteListInstance(const std::string& path) override;
-    void createLeafListInstance(const std::string& path) override;
-    void deleteLeafListInstance(const std::string& path) override;
+    void createItem(const std::string& path) override;
+    void deleteItem(const std::string& path) override;
     void moveItem(const std::string& source, std::variant<yang::move::Absolute, yang::move::Relative> move) override;
     Tree executeRpc(const std::string& path, const Tree& input) override;
 
diff --git a/src/utils.cpp b/src/utils.cpp
index cf789b7..e151417 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -243,12 +243,13 @@
     return res;
 }
 
-std::string instanceToString(const std::string& modName, const ListInstance& instance)
+std::string instanceToString(const ListInstance& instance, const std::optional<std::string>& modName)
 {
     std::string instanceStr;
+    auto modulePrefix = modName ? *modName + ":" : "";
     for (const auto& [key, value] : instance) {
         using namespace std::string_literals;
-        instanceStr += "[" + modName + ":" + key + "=" + escapeListKeyString(leafDataToString(value)) + "]";
+        instanceStr += "[" + modulePrefix + key + "=" + escapeListKeyString(leafDataToString(value)) + "]";
     }
     return instanceStr;
 }
diff --git a/src/utils.hpp b/src/utils.hpp
index 0196ff8..ec3f3c8 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -27,5 +27,5 @@
 schemaPath_ anyPathToSchemaPath(const boost::variant<dataPath_, schemaPath_, module_>& path);
 std::string stripLeafListValueFromPath(const std::string& path);
 std::string stripLastListInstanceFromPath(const std::string& path);
-// The string includes module name prefixes.
-std::string instanceToString(const std::string& modName, const ListInstance& instance);
+// The second argument controls whether module prefixes should be added to the keys.
+std::string instanceToString(const ListInstance& instance, const std::optional<std::string>& modName = std::nullopt);
diff --git a/tests/data_query.cpp b/tests/data_query.cpp
index 5adc661..f075b4b 100644
--- a/tests/data_query.cpp
+++ b/tests/data_query.cpp
@@ -46,9 +46,9 @@
 
         SECTION("example-schema:person")
         {
-            datastore.createListInstance("/example-schema:person[name='Vaclav']");
-            datastore.createListInstance("/example-schema:person[name='Tomas']");
-            datastore.createListInstance("/example-schema:person[name='Jan Novak']");
+            datastore.createItem("/example-schema:person[name='Vaclav']");
+            datastore.createItem("/example-schema:person[name='Tomas']");
+            datastore.createItem("/example-schema:person[name='Jan Novak']");
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"person"}});
             expected = {
                 {{"name", std::string{"Jan Novak"}}},
@@ -66,9 +66,9 @@
 
         SECTION("example-schema:selectedNumbers")
         {
-            datastore.createListInstance("/example-schema:selectedNumbers[value='45']");
-            datastore.createListInstance("/example-schema:selectedNumbers[value='99']");
-            datastore.createListInstance("/example-schema:selectedNumbers[value='127']");
+            datastore.createItem("/example-schema:selectedNumbers[value='45']");
+            datastore.createItem("/example-schema:selectedNumbers[value='99']");
+            datastore.createItem("/example-schema:selectedNumbers[value='127']");
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"selectedNumbers"}});
             expected = {
                 {{"value", int8_t{127}}},
@@ -79,9 +79,9 @@
 
         SECTION("example-schema:animalWithColor")
         {
-            datastore.createListInstance("/example-schema:animalWithColor[name='Dog'][color='brown']");
-            datastore.createListInstance("/example-schema:animalWithColor[name='Dog'][color='white']");
-            datastore.createListInstance("/example-schema:animalWithColor[name='Cat'][color='grey']");
+            datastore.createItem("/example-schema:animalWithColor[name='Dog'][color='brown']");
+            datastore.createItem("/example-schema:animalWithColor[name='Dog'][color='white']");
+            datastore.createItem("/example-schema:animalWithColor[name='Cat'][color='grey']");
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"animalWithColor"}});
             expected = {
                 {{"name", std::string{"Cat"}}, {"color", std::string{"grey"}}},
@@ -92,7 +92,7 @@
 
         SECTION("example-schema:animalWithColor - quotes in values")
         {
-            datastore.createListInstance("/example-schema:animalWithColor[name='D\"o\"g'][color=\"b'r'own\"]");
+            datastore.createItem("/example-schema:animalWithColor[name='D\"o\"g'][color=\"b'r'own\"]");
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"animalWithColor"}});
             expected = {
                 {{"name", std::string{"D\"o\"g"}}, {"color", std::string{"b'r'own"}}}
@@ -101,9 +101,9 @@
 
         SECTION("example-schema:ports")
         {
-            datastore.createListInstance("/example-schema:ports[name='A']");
-            datastore.createListInstance("/example-schema:ports[name='B']");
-            datastore.createListInstance("/example-schema:ports[name='E']");
+            datastore.createItem("/example-schema:ports[name='A']");
+            datastore.createItem("/example-schema:ports[name='B']");
+            datastore.createItem("/example-schema:ports[name='E']");
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"ports"}});
             expected = {
                 {{"name", enum_{"A"}}},
@@ -114,17 +114,17 @@
 
         SECTION("example-schema:org/example:people - nested list")
         {
-            datastore.createListInstance("/example-schema:org[department='accounting']");
-            datastore.createListInstance("/example-schema:org[department='sales']");
-            datastore.createListInstance("/example-schema:org[department='programmers']");
-            datastore.createListInstance("/example-schema:org[department='accounting']/people[name='Alice']");
-            datastore.createListInstance("/example-schema:org[department='accounting']/people[name='Bob']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']/computers[type='laptop']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Alice']/computers[type='server']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='PC']");
-            datastore.createListInstance("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='server']");
+            datastore.createItem("/example-schema:org[department='accounting']");
+            datastore.createItem("/example-schema:org[department='sales']");
+            datastore.createItem("/example-schema:org[department='programmers']");
+            datastore.createItem("/example-schema:org[department='accounting']/people[name='Alice']");
+            datastore.createItem("/example-schema:org[department='accounting']/people[name='Bob']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Alice']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Cyril']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Alice']/computers[type='laptop']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Alice']/computers[type='server']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='PC']");
+            datastore.createItem("/example-schema:org[department='sales']/people[name='Cyril']/computers[type='server']");
 
             SECTION("outer list")
             {
@@ -215,8 +215,8 @@
 
         SECTION("/other-module:parking-lot/example-schema:cars - list coming from an augment")
         {
-            datastore.createListInstance("/other-module:parking-lot/example-schema:cars[id='1']");
-            datastore.createListInstance("/other-module:parking-lot/example-schema:cars[id='2']");
+            datastore.createItem("/other-module:parking-lot/example-schema:cars[id='1']");
+            datastore.createItem("/other-module:parking-lot/example-schema:cars[id='2']");
 
             listPath.m_nodes.push_back(dataNode_{{"other-module"}, container_{"parking-lot"}});
             listPath.m_nodes.push_back(dataNode_{{"example-schema"}, list_{"cars"}});
diff --git a/tests/datastore_access.cpp b/tests/datastore_access.cpp
index b9870f3..e9490ba 100644
--- a/tests/datastore_access.cpp
+++ b/tests/datastore_access.cpp
@@ -11,7 +11,15 @@
 
 #ifdef sysrepo_BACKEND
 #include "sysrepo_access.hpp"
+using OnInvalidSchemaPathCreate = DatastoreException;
+using OnInvalidSchemaPathDelete = void;
+using OnInvalidSchemaPathMove = sysrepo::sysrepo_exception;
+using OnKeyNotFound = void;
 #elif defined(netconf_BACKEND)
+using OnInvalidSchemaPathCreate = std::runtime_error;
+using OnInvalidSchemaPathDelete = std::runtime_error;
+using OnInvalidSchemaPathMove = std::runtime_error;
+using OnKeyNotFound = std::runtime_error;
 #include "netconf_access.hpp"
 #include "netopeer_vars.hpp"
 #else
@@ -33,6 +41,26 @@
     IMPLEMENT_CONST_MOCK1(get_data);
 };
 
+namespace {
+template <class ...> constexpr std::false_type always_false [[maybe_unused]] {};
+template <class Exception, typename Callable> void catching(const Callable& what) {
+
+    if constexpr (std::is_same_v<Exception, void>) {
+        what();
+    } else if constexpr (std::is_same<Exception, std::runtime_error>()) {
+        // cannot use REQUIRE_THROWS_AS(..., Exception) directly because that one
+        // needs an extra `typename` deep in the bowels of doctest
+        REQUIRE_THROWS_AS(what(), std::runtime_error);
+    } else if constexpr (std::is_same<Exception, DatastoreException>()) {
+        REQUIRE_THROWS_AS(what(), DatastoreException);
+    } else if constexpr (std::is_same<Exception, sysrepo::sysrepo_exception>()) {
+        REQUIRE_THROWS_AS(what(), sysrepo::sysrepo_exception);
+    } else {
+        static_assert(always_false<Exception>); // https://stackoverflow.com/a/53945549/2245623
+    }
+}
+}
+
 TEST_CASE("setting/getting values")
 {
     trompeloeil::sequence seq1;
@@ -118,10 +146,32 @@
         datastore.commitChanges();
     }
 
+    SECTION("set a string, then delete it")
+    {
+        REQUIRE_CALL(mock, write("/example-schema:leafString", std::nullopt, "blah"s));
+        datastore.setLeaf("/example-schema:leafString", "blah"s);
+        datastore.commitChanges();
+        DatastoreAccess::Tree expected{{"/example-schema:leafString", "blah"s}};
+        REQUIRE(datastore.getItems("/example-schema:leafString") == expected);
+
+        REQUIRE_CALL(mock, write("/example-schema:leafString", "blah"s, std::nullopt));
+        datastore.deleteItem("/example-schema:leafString");
+        datastore.commitChanges();
+        expected.clear();
+        REQUIRE(datastore.getItems("/example-schema:leafString") == expected);
+    }
+
+    SECTION("set a non-existing leaf")
+    {
+        catching<OnInvalidSchemaPathCreate>([&]{
+            datastore.setLeaf("/example-schema:non-existing", "what"s);
+        });
+    }
+
     SECTION("create presence container")
     {
         REQUIRE_CALL(mock, write("/example-schema:pContainer", std::nullopt, ""s));
-        datastore.createPresenceContainer("/example-schema:pContainer");
+        datastore.createItem("/example-schema:pContainer");
         datastore.commitChanges();
     }
 
@@ -130,17 +180,37 @@
         {
             REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']", std::nullopt, ""s));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']/name", std::nullopt, "Nguyen"s));
-            datastore.createListInstance("/example-schema:person[name='Nguyen']");
+            datastore.createItem("/example-schema:person[name='Nguyen']");
             datastore.commitChanges();
         }
         {
             REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']", ""s, std::nullopt));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Nguyen']/name", "Nguyen"s, std::nullopt));
-            datastore.deleteListInstance("/example-schema:person[name='Nguyen']");
+            datastore.deleteItem("/example-schema:person[name='Nguyen']");
             datastore.commitChanges();
         }
     }
 
+    SECTION("deleting non-existing list keys")
+    {
+        catching<OnKeyNotFound>([&]{
+            datastore.deleteItem("/example-schema:person[name='non existing']");
+            datastore.commitChanges();
+        });
+    }
+
+    SECTION("accessing non-existing schema nodes as a list")
+    {
+        catching<OnInvalidSchemaPathCreate>([&]{
+            datastore.createItem("/example-schema:non-existing-list[xxx='blah']");
+            datastore.commitChanges();
+        });
+        catching<OnInvalidSchemaPathDelete>([&]{
+            datastore.deleteItem("/example-schema:non-existing-list[xxx='non existing']");
+            datastore.commitChanges();
+        });
+    }
+
     SECTION("leafref pointing to a key of a list")
     {
         {
@@ -150,9 +220,9 @@
             REQUIRE_CALL(mock, write("/example-schema:person[name='Elfi']/name", std::nullopt, "Elfi"s));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']", std::nullopt, ""s));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Kolafa']/name", std::nullopt, "Kolafa"s));
-            datastore.createListInstance("/example-schema:person[name='Dan']");
-            datastore.createListInstance("/example-schema:person[name='Elfi']");
-            datastore.createListInstance("/example-schema:person[name='Kolafa']");
+            datastore.createItem("/example-schema:person[name='Dan']");
+            datastore.createItem("/example-schema:person[name='Elfi']");
+            datastore.createItem("/example-schema:person[name='Kolafa']");
             datastore.commitChanges();
         }
 
@@ -241,9 +311,9 @@
             REQUIRE_CALL(mock, write("/example-schema:person[name='Michal']/name", std::nullopt, "Michal"s));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']", std::nullopt, ""s));
             REQUIRE_CALL(mock, write("/example-schema:person[name='Petr']/name", std::nullopt, "Petr"s));
-            datastore.createListInstance("/example-schema:person[name='Jan']");
-            datastore.createListInstance("/example-schema:person[name='Michal']");
-            datastore.createListInstance("/example-schema:person[name='Petr']");
+            datastore.createItem("/example-schema:person[name='Jan']");
+            datastore.createItem("/example-schema:person[name='Michal']");
+            datastore.createItem("/example-schema:person[name='Petr']");
             datastore.commitChanges();
         }
         DatastoreAccess::Tree expected{
@@ -266,7 +336,7 @@
 
         {
             REQUIRE_CALL(mock, write("/example-schema:pContainer", std::nullopt, ""s));
-            datastore.createPresenceContainer("/example-schema:pContainer");
+            datastore.createItem("/example-schema:pContainer");
             datastore.commitChanges();
         }
         expected = {
@@ -277,13 +347,29 @@
         // Make sure it's not there after we delete it
         {
             REQUIRE_CALL(mock, write("/example-schema:pContainer", ""s, std::nullopt));
-            datastore.deletePresenceContainer("/example-schema:pContainer");
+            datastore.deleteItem("/example-schema:pContainer");
             datastore.commitChanges();
         }
         expected = {};
         REQUIRE(datastore.getItems("/example-schema:pContainer") == expected);
     }
 
+    SECTION("creating a non-existing schema node as a container")
+    {
+        catching<OnInvalidSchemaPathCreate>([&]{
+            datastore.createItem("/example-schema:non-existing-presence-container");
+            datastore.commitChanges();
+        });
+    }
+
+    SECTION("deleting a non-existing schema node as a container or leaf")
+    {
+        catching<OnInvalidSchemaPathDelete>([&]{
+            datastore.deleteItem("/example-schema:non-existing-presence-container");
+            datastore.commitChanges();
+        });
+    }
+
     SECTION("nested presence container")
     {
         DatastoreAccess::Tree expected;
@@ -292,7 +378,7 @@
         {
             REQUIRE_CALL(mock, write("/example-schema:inventory", std::nullopt, ""s));
             REQUIRE_CALL(mock, write("/example-schema:inventory/stuff", std::nullopt, ""s));
-            datastore.createPresenceContainer("/example-schema:inventory/stuff");
+            datastore.createItem("/example-schema:inventory/stuff");
             datastore.commitChanges();
         }
         expected = {
@@ -302,7 +388,7 @@
         {
             REQUIRE_CALL(mock, write("/example-schema:inventory", ""s, std::nullopt));
             REQUIRE_CALL(mock, write("/example-schema:inventory/stuff", ""s, std::nullopt));
-            datastore.deletePresenceContainer("/example-schema:inventory/stuff");
+            datastore.deleteItem("/example-schema:inventory/stuff");
             datastore.commitChanges();
         }
         expected = {};
@@ -392,8 +478,8 @@
         DatastoreAccess::Tree expected;
         REQUIRE_CALL(mock, write("/example-schema:addresses", std::nullopt, "0.0.0.0"s));
         REQUIRE_CALL(mock, write("/example-schema:addresses", std::nullopt, "127.0.0.1"s));
-        datastore.createLeafListInstance("/example-schema:addresses[.='0.0.0.0']");
-        datastore.createLeafListInstance("/example-schema:addresses[.='127.0.0.1']");
+        datastore.createItem("/example-schema:addresses[.='0.0.0.0']");
+        datastore.createItem("/example-schema:addresses[.='127.0.0.1']");
         datastore.commitChanges();
         expected = {
             {"/example-schema:addresses", special_{SpecialValue::LeafList}},
@@ -403,7 +489,7 @@
         REQUIRE(datastore.getItems("/example-schema:addresses") == expected);
 
         REQUIRE_CALL(mock, write("/example-schema:addresses", "0.0.0.0"s, std::nullopt));
-        datastore.deleteLeafListInstance("/example-schema:addresses[.='0.0.0.0']");
+        datastore.deleteItem("/example-schema:addresses[.='0.0.0.0']");
         datastore.commitChanges();
         expected = {
             {"/example-schema:addresses", special_{SpecialValue::LeafList}},
@@ -412,12 +498,33 @@
         REQUIRE(datastore.getItems("/example-schema:addresses") == expected);
 
         REQUIRE_CALL(mock, write("/example-schema:addresses", "127.0.0.1"s, std::nullopt));
-        datastore.deleteLeafListInstance("/example-schema:addresses[.='127.0.0.1']");
+        datastore.deleteItem("/example-schema:addresses[.='127.0.0.1']");
         datastore.commitChanges();
         expected = {};
         REQUIRE(datastore.getItems("/example-schema:addresses") == expected);
     }
 
+    SECTION("deleting a non-existing leaf-list")
+    {
+        catching<OnKeyNotFound>([&]{
+            datastore.deleteItem("/example-schema:addresses[.='non-existing']");
+            datastore.commitChanges();
+        });
+    }
+
+    SECTION("accessing a non-existing schema node as a leaf-list")
+    {
+        catching<OnInvalidSchemaPathCreate>([&]{
+            datastore.createItem("/example-schema:non-existing[.='non-existing']");
+            datastore.commitChanges();
+        });
+
+        catching<OnInvalidSchemaPathDelete>([&]{
+            datastore.deleteItem("/example-schema:non-existing[.='non-existing']");
+            datastore.commitChanges();
+        });
+    }
+
     SECTION("copying data from startup refreshes the data")
     {
         {
@@ -442,9 +549,9 @@
             REQUIRE_CALL(mock, write("/example-schema:protocols", std::nullopt, "pop3"s));
             REQUIRE_CALL(mock, write("/example-schema:protocols", "http"s, "ftp"s));
             REQUIRE_CALL(mock, write("/example-schema:protocols", "ftp"s, "pop3"s));
-            datastore.createLeafListInstance("/example-schema:protocols[.='http']");
-            datastore.createLeafListInstance("/example-schema:protocols[.='ftp']");
-            datastore.createLeafListInstance("/example-schema:protocols[.='pop3']");
+            datastore.createItem("/example-schema:protocols[.='http']");
+            datastore.createItem("/example-schema:protocols[.='ftp']");
+            datastore.createItem("/example-schema:protocols[.='pop3']");
             datastore.commitChanges();
             expected = {
                 {"/example-schema:protocols", special_{SpecialValue::LeafList}},
@@ -517,6 +624,14 @@
         }
     }
 
+    SECTION("moving non-existing schema nodes")
+    {
+        catching<OnInvalidSchemaPathMove>([&]{
+            datastore.moveItem("/example-schema:non-existing", yang::move::Absolute::Begin);
+            datastore.commitChanges();
+        });
+    }
+
     SECTION("moving list instances")
     {
         DatastoreAccess::Tree expected;
@@ -530,9 +645,9 @@
             REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']", std::nullopt, ""s));
             REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']/name", std::nullopt, "Adam"s));
             REQUIRE_CALL(mock, write("/example-schema:players[name='Adam']", ""s, ""s));
-            datastore.createListInstance("/example-schema:players[name='John']");
-            datastore.createListInstance("/example-schema:players[name='Eve']");
-            datastore.createListInstance("/example-schema:players[name='Adam']");
+            datastore.createItem("/example-schema:players[name='John']");
+            datastore.createItem("/example-schema:players[name='Eve']");
+            datastore.createItem("/example-schema:players[name='Adam']");
             datastore.commitChanges();
             expected = {
                 {"/example-schema:players[name='John']", special_{SpecialValue::List}},
@@ -615,6 +730,33 @@
         }
     }
 
+    SECTION("getting /")
+    {
+        {
+            REQUIRE_CALL(mock, write("/example-schema:leafInt32", std::nullopt, "64"s));
+            datastore.setLeaf("/example-schema:leafInt32", 64);
+            datastore.commitChanges();
+        }
+
+        DatastoreAccess::Tree expected{
+        // Sysrepo always returns containers when getting values, but
+        // libnetconf does not. This is fine by the YANG standard:
+        // https://tools.ietf.org/html/rfc7950#section-7.5.7 Furthermore,
+        // NetconfAccess implementation actually only iterates over leafs,
+        // so even if libnetconf did include containers, they wouldn't get
+        // shown here anyway. With sysrepo2, this won't be necessary,
+        // because it'll use the same data structure as libnetconf, so the
+        // results will be consistent.
+#ifdef sysrepo_BACKEND
+                                                   {"/example-schema:inventory", special_{SpecialValue::Container}},
+                                                   {"/example-schema:lol", special_{SpecialValue::Container}},
+#endif
+                                                   {"/example-schema:leafInt32", 64}};
+        auto items = datastore.getItems("/");
+        // This tests if we at least get the data WE added.
+        REQUIRE(std::all_of(expected.begin(), expected.end(), [items] (const auto& item) { return std::find(items.begin(), items.end(), item) != items.end(); }));
+    }
+
     waitForCompletionAndBitMore(seq1);
 }
 
@@ -678,52 +820,60 @@
 #error "Unknown backend"
 #endif
 
-    std::string rpc;
-    DatastoreAccess::Tree input, output;
+    SECTION("valid")
+    {
+        std::string rpc;
+        DatastoreAccess::Tree input, output;
 
-    SECTION("noop") {
-        rpc = "/example-schema:noop";
+        SECTION("noop") {
+            rpc = "/example-schema:noop";
+        }
+
+        SECTION("small nuke") {
+            rpc = "/example-schema:launch-nukes";
+            input = {
+                {"description", "dummy"s},
+                {"payload/kilotons", uint64_t{333'666}},
+            };
+            // no data are returned
+        }
+
+        SECTION("small nuke") {
+            rpc = "/example-schema:launch-nukes";
+            input = {
+                {"description", "dummy"s},
+                {"payload/kilotons", uint64_t{4}},
+            };
+            output = {
+                {"blast-radius", uint32_t{33'666}},
+                {"actual-yield", uint64_t{5}},
+            };
+        }
+
+        SECTION("with lists") {
+            rpc = "/example-schema:launch-nukes";
+            input = {
+                {"payload/kilotons", uint64_t{6}},
+                {"cities/targets[city='Prague']/city", "Prague"s},
+            };
+            output = {
+                {"blast-radius", uint32_t{33'666}},
+                {"actual-yield", uint64_t{7}},
+                {"damaged-places", special_{SpecialValue::PresenceContainer}},
+                {"damaged-places/targets[city='London']", special_{SpecialValue::List}},
+                {"damaged-places/targets[city='London']/city", "London"s},
+                {"damaged-places/targets[city='Berlin']", special_{SpecialValue::List}},
+                {"damaged-places/targets[city='Berlin']/city", "Berlin"s},
+            };
+        }
+
+        REQUIRE(datastore.executeRpc(rpc, input) == output);
     }
 
-    SECTION("small nuke") {
-        rpc = "/example-schema:launch-nukes";
-        input = {
-            {"description", "dummy"s},
-            {"payload/kilotons", uint64_t{333'666}},
-        };
-        // no data are returned
+    SECTION("non-existing RPC")
+    {
+        REQUIRE_THROWS_AS(datastore.executeRpc("/example-schema:non-existing", DatastoreAccess::Tree{}), std::runtime_error);
     }
 
-    SECTION("small nuke") {
-        rpc = "/example-schema:launch-nukes";
-        input = {
-            {"description", "dummy"s},
-            {"payload/kilotons", uint64_t{4}},
-        };
-        output = {
-            {"blast-radius", uint32_t{33'666}},
-            {"actual-yield", uint64_t{5}},
-        };
-    }
-
-    SECTION("with lists") {
-        rpc = "/example-schema:launch-nukes";
-        input = {
-            {"payload/kilotons", uint64_t{6}},
-            {"cities/targets[city='Prague']/city", "Prague"s},
-        };
-        output = {
-            {"blast-radius", uint32_t{33'666}},
-            {"actual-yield", uint64_t{7}},
-            {"damaged-places", special_{SpecialValue::PresenceContainer}},
-            {"damaged-places/targets[city='London']", special_{SpecialValue::List}},
-            {"damaged-places/targets[city='London']/city", "London"s},
-            {"damaged-places/targets[city='Berlin']", special_{SpecialValue::List}},
-            {"damaged-places/targets[city='Berlin']/city", "Berlin"s},
-        };
-    }
-
-    REQUIRE(datastore.executeRpc(rpc, input) == output);
-
     waitForCompletionAndBitMore(seq1);
 }
diff --git a/tests/datastoreaccess_mock.hpp b/tests/datastoreaccess_mock.hpp
index 354dd44..8252d04 100644
--- a/tests/datastoreaccess_mock.hpp
+++ b/tests/datastoreaccess_mock.hpp
@@ -21,12 +21,8 @@
 class MockDatastoreAccess : public trompeloeil::mock_interface<DatastoreAccess> {
     IMPLEMENT_MOCK1(getItems);
     IMPLEMENT_MOCK2(setLeaf);
-    IMPLEMENT_MOCK1(createPresenceContainer);
-    IMPLEMENT_MOCK1(deletePresenceContainer);
-    IMPLEMENT_MOCK1(createLeafListInstance);
-    IMPLEMENT_MOCK1(deleteLeafListInstance);
-    IMPLEMENT_MOCK1(createListInstance);
-    IMPLEMENT_MOCK1(deleteListInstance);
+    IMPLEMENT_MOCK1(createItem);
+    IMPLEMENT_MOCK1(deleteItem);
     IMPLEMENT_MOCK2(moveItem);
     IMPLEMENT_MOCK2(executeRpc);
 
diff --git a/tests/interpreter.cpp b/tests/interpreter.cpp
index 3acef2b..d1e6d26 100644
--- a/tests/interpreter.cpp
+++ b/tests/interpreter.cpp
@@ -335,22 +335,22 @@
         SECTION("list instance")
         {
             inputPath.m_nodes = {dataNode_{{"mod"}, listElement_{"department", {{"name", "engineering"s}}}}};
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createListInstance("/mod:department[name='engineering']")));
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteListInstance("/mod:department[name='engineering']")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:department[name='engineering']")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:department[name='engineering']")));
         }
 
         SECTION("leaflist instance")
         {
             inputPath.m_nodes = {dataNode_{{"mod"}, leafListElement_{"addresses", "127.0.0.1"s}}};
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createLeafListInstance("/mod:addresses[.='127.0.0.1']")));
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteLeafListInstance("/mod:addresses[.='127.0.0.1']")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:addresses[.='127.0.0.1']")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:addresses[.='127.0.0.1']")));
         }
 
         SECTION("presence container")
         {
             inputPath.m_nodes = {dataNode_{{"mod"}, container_{"pContainer"}}};
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createPresenceContainer("/mod:pContainer")));
-            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deletePresenceContainer("/mod:pContainer")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, createItem("/mod:pContainer")));
+            expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:pContainer")));
         }
 
         create_ createCmd;
@@ -361,6 +361,14 @@
         toInterpret.push_back(deleteCmd);
     }
 
+    SECTION("delete a leaf")
+    {
+        expectations.push_back(NAMED_REQUIRE_CALL(datastore, deleteItem("/mod:someLeaf")));
+        delete_ deleteCmd;
+        deleteCmd.m_path = {Scope::Absolute, {dataNode_{{"mod"}, leaf_{"someLeaf"}}, }};
+        toInterpret.push_back(deleteCmd);
+    }
+
     SECTION("commit")
     {
         expectations.push_back(NAMED_REQUIRE_CALL(datastore, commitChanges()));
diff --git a/tests/leaf_editing.cpp b/tests/leaf_editing.cpp
index 1a55fa1..35e2ddc 100644
--- a/tests/leaf_editing.cpp
+++ b/tests/leaf_editing.cpp
@@ -617,4 +617,15 @@
         REQUIRE_THROWS_AS(parser.parseCommand(input, errorStream), InvalidCommandException);
         REQUIRE(errorStream.str().find(expectedError) != std::string::npos);
     }
+
+    SECTION("deleting a leaf")
+    {
+        delete_ expected;
+        input = "delete mod:leafString";
+        expected.m_path.m_nodes.push_back(dataNode_{module_{"mod"}, leaf_("leafString")});
+
+        command_ command = parser.parseCommand(input, errorStream);
+        REQUIRE(command.type() == typeid(delete_));
+        REQUIRE(boost::get<delete_>(command) == expected);
+    }
 }