diff --git a/src/mesh/MeshLineNodeBoundary.cpp b/src/mesh/MeshLineNodeBoundary.cpp
index c7e3bb97ae7d786294dbd3d24bd8209b600404b0..71ff3e65a96e122a01f8a9a246837c07be7e6a14 100644
--- a/src/mesh/MeshLineNodeBoundary.cpp
+++ b/src/mesh/MeshLineNodeBoundary.cpp
@@ -15,21 +15,21 @@ MeshLineNodeBoundary<Dimension>::_checkBoundaryIsLine(const TinyVector<Dimension
 
   const NodeValue<const Rd>& xr = mesh.xr();
 
-  Rdxd P = Rdxd{identity} - tensorProduct(direction, direction);
+  const Rdxd P = Rdxd{identity} - tensorProduct(direction, direction);
 
   bool is_bad = false;
-  parallel_for(this->m_node_list.size(), [=, &is_bad](int node_id) {
-    const Rd& x    = xr[this->m_node_list[node_id]];
-    const Rd delta = x - origin;
-    if (dot(P * delta, direction) > 1E-13 * length) {
+  parallel_for(this->m_node_list.size(), [=, &is_bad](int i_node) {
+    const Rd& x    = xr[this->m_node_list[i_node]];
+    const Rd delta = P * (x - origin);
+    if (dot(delta, delta) > 1E-13 * length) {
       is_bad = true;
     }
   });
 
   if (parallel::allReduceOr(is_bad)) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": boundary is not a line!";
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
+        << "\": boundary is not a line!";
     throw NormalError(ost.str());
   }
 }
@@ -48,8 +48,8 @@ MeshLineNodeBoundary<2>::_getDirection(const Mesh<Connectivity<2>>& mesh)
 
   if (xmin == xmax) {
     std::ostringstream ost;
-    ost << "invalid boundary " << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
-        << ": unable to compute direction";
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
+        << "\": unable to compute direction";
     throw NormalError(ost.str());
   }
 
@@ -92,6 +92,12 @@ MeshLineNodeBoundary<3>::_getDirection(const Mesh<Connectivity<3>>& mesh)
   }
 
   const double length = l2Norm(direction);
+  if (length == 0) {
+    std::ostringstream ost;
+    ost << "invalid boundary \"" << rang::fgB::yellow << this->m_boundary_name << rang::style::reset
+        << "\": unable to compute direction";
+    throw NormalError(ost.str());
+  }
   direction *= 1. / length;
 
   this->_checkBoundaryIsLine(direction, xmin, length, mesh);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 85c99ead032dccef8ad4bb68269495ac3da88951..6db69f436ba9fcae7ae5957a4edddf14be12cf05 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -178,6 +178,7 @@ add_executable (mpi_unit_tests
   test_MeshFaceBoundary.cpp
   test_MeshFlatFaceBoundary.cpp
   test_MeshFlatNodeBoundary.cpp
+  test_MeshLineNodeBoundary.cpp
   test_MeshNodeBoundary.cpp
   test_Messenger.cpp
   test_OFStream.cpp
diff --git a/tests/test_MeshLineNodeBoundary.cpp b/tests/test_MeshLineNodeBoundary.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..af3bb4e66ecbbb6f107a2ee3a97a08f3917db2d0
--- /dev/null
+++ b/tests/test_MeshLineNodeBoundary.cpp
@@ -0,0 +1,1258 @@
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+#include <algebra/TinyMatrix.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/Mesh.hpp>
+#include <mesh/MeshLineNodeBoundary.hpp>
+#include <mesh/NamedBoundaryDescriptor.hpp>
+#include <mesh/NumberedBoundaryDescriptor.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("MeshLineNodeBoundary", "[mesh]")
+{
+  auto is_same = [](const auto& a, const auto& b) -> bool {
+    if (a.size() > 0 and b.size() > 0) {
+      return (a[0] == b[0]);
+    } else {
+      return (a.size() == b.size());
+    }
+  };
+
+  auto get_node_list_from_tag = [](const size_t tag, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagNumber() == tag) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  auto get_node_list_from_name = [](const std::string& name, const auto& connectivity) -> Array<const NodeId> {
+    for (size_t i = 0; i < connectivity.template numberOfRefItemList<ItemType::node>(); ++i) {
+      const auto& ref_node_list = connectivity.template refItemList<ItemType::node>(i);
+      const RefId ref_id        = ref_node_list.refId();
+      if (ref_id.tagName() == name) {
+        return ref_node_list.list();
+      }
+    }
+    return {};
+  };
+
+  SECTION("aligned axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMINYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                                "error: invalid boundary \"XMAXYMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                                "error: invalid boundary \"XMINYMAX\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX\": unable to compute direction");
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 3: {
+              direction = R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(8)),
+                                "error: invalid boundary \"XMINYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(9)),
+                                "error: invalid boundary \"XMINYMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                                "error: invalid boundary \"XMAXYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                                "error: invalid boundary \"XMAXYMAX\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMIN")),
+                                "error: invalid boundary \"XMINYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAX")),
+                                "error: invalid boundary \"XMINYMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMIN")),
+                                "error: invalid boundary \"XMAXYMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAX")),
+                                "error: invalid boundary \"XMAXYMAX\": unable to compute direction");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(0)),
+                              "error: invalid boundary \"XMIN\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(1)),
+                              "error: invalid boundary \"XMAX\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(2)),
+                              "error: invalid boundary \"YMIN\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(3)),
+                              "error: invalid boundary \"YMAX\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(4)),
+                              "error: invalid boundary \"ZMIN\": boundary is not a line!");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(5)),
+                              "error: invalid boundary \"ZMAX\": boundary is not a line!");
+
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(10)),
+                              "error: invalid boundary \"XMINYMINZMIN\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(11)),
+                              "error: invalid boundary \"XMAXYMINZMIN\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(12)),
+                              "error: invalid boundary \"XMAXYMAXZMIN\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(13)),
+                              "error: invalid boundary \"XMINYMAXZMIN\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(14)),
+                              "error: invalid boundary \"XMINYMINZMAX\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(15)),
+                              "error: invalid boundary \"XMAXYMINZMAX\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(16)),
+                              "error: invalid boundary \"XMAXYMAXZMAX\": unable to compute direction");
+          REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(17)),
+                              "error: invalid boundary \"XMINYMAXZMAX\": unable to compute direction");
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (const auto& name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX\": unable to compute direction");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+        const MeshType& mesh   = *p_mesh;
+
+        const ConnectivityType& connectivity = mesh.connectivity();
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(22)),
+                                "error: invalid boundary \"XMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(23)),
+                                "error: invalid boundary \"XMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(24)),
+                                "error: invalid boundary \"ZMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(25)),
+                                "error: invalid boundary \"ZMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(26)),
+                                "error: invalid boundary \"YMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(27)),
+                                "error: invalid boundary \"YMIN\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(40)),
+                                "error: invalid boundary \"XMINYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(41)),
+                                "error: invalid boundary \"XMAXYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(42)),
+                                "error: invalid boundary \"XMINYMAXZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(43)),
+                                "error: invalid boundary \"XMINYMAXZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(44)),
+                                "error: invalid boundary \"XMINYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(45)),
+                                "error: invalid boundary \"XMAXYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(47)),
+                                "error: invalid boundary \"XMAXYMAXZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(51)),
+                                "error: invalid boundary \"XMAXYMAXZMIN\": unable to compute direction");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMIN")),
+                                "error: invalid boundary \"XMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAX")),
+                                "error: invalid boundary \"XMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMIN")),
+                                "error: invalid boundary \"YMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAX")),
+                                "error: invalid boundary \"YMAX\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMIN")),
+                                "error: invalid boundary \"ZMIN\": boundary is not a line!");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("ZMAX")),
+                                "error: invalid boundary \"ZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMIN")),
+                                "error: invalid boundary \"XMINYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMIN")),
+                                "error: invalid boundary \"XMAXYMINZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMIN")),
+                                "error: invalid boundary \"XMAXYMAXZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMIN")),
+                                "error: invalid boundary \"XMINYMAXZMIN\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMINZMAX")),
+                                "error: invalid boundary \"XMINYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMINZMAX")),
+                                "error: invalid boundary \"XMAXYMINZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMAXYMAXZMAX")),
+                                "error: invalid boundary \"XMAXYMAXZMAX\": unable to compute direction");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("XMINYMAXZMAX")),
+                                "error: invalid boundary \"XMINYMAXZMAX\": unable to compute direction");
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("rotated axis")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      const double theta = 0.3;
+      const TinyMatrix<2> R{std::cos(theta), -std::sin(theta),   //
+                            std::sin(theta), std::cos(theta)};
+
+      SECTION("cartesian 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {0, 1, 2, 3};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 0: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R2> rotated_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 3, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 2: {
+              direction = R * R2{0, -1};
+              break;
+            }
+            case 3: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            case 4: {
+              direction = R * R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN", "YMAX"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R * R2{0, -1};
+            } else if (name == "XMAX") {
+              direction = R * R2{0, -1};
+            } else if (name == "YMIN") {
+              direction = R * R2{1, 0};
+            } else if (name == "YMAX") {
+              direction = R * R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      const double theta = 0.3;
+      const double phi   = 0.4;
+      const TinyMatrix<3> R =
+        TinyMatrix<3>{std::cos(theta), -std::sin(theta), 0, std::sin(theta), std::cos(theta), 0, 0, 0, 1} *
+        TinyMatrix<3>{0, std::cos(phi), -std::sin(phi), 0, std::sin(phi), std::cos(phi), 1, 0, 0};
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 28:
+            case 29:
+            case 30:
+            case 31: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<R3> rotated_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { rotated_xr[node_id] = R * xr[node_id]; });
+
+        MeshType mesh{p_mesh->shared_connectivity(), rotated_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R * R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R * R3{0, 1, 0};
+              break;
+            }
+            case 36:
+            case 37:
+            case 38:
+            case 39: {
+              direction = R * R3{-1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX",
+                                                  "XMINZMIN", "XMINZMAX", "XMAXZMAX", "XMINZMAX",
+                                                  "YMINZMIN", "YMINZMAX", "YMAXZMAX", "YMAXZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R * R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R * R3{0, 1, 0};
+            } else if ((name == "YMINZMIN") or (name == "YMINZMAX") or (name == "YMAXZMIN") or (name == "YMAXZMAX")) {
+              direction = R * R3{-1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+        }
+      }
+    }
+  }
+
+  SECTION("curved mesh")
+  {
+    SECTION("2D")
+    {
+      static constexpr size_t Dimension = 2;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R2 = TinyVector<2>;
+
+      auto curve = [](const R2& X) -> R2 { return R2{X[0], (1 + X[0] * X[0]) * X[1]}; };
+
+      SECTION("hybrid 2d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        NodeValue<TinyVector<2>> curved_xr{connectivity};
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {1, 2, 4};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R2 direction = zero;
+
+            switch (tag) {
+            case 1: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 2: {
+              direction = R2{0, 1};
+              break;
+            }
+            case 4: {
+              direction = R2{1, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(3);
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor),
+                                "error: invalid boundary \"YMAX\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMIN", "XMAX", "YMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            R2 direction = zero;
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            if (name == "XMIN") {
+              direction = R2{0, 1};
+            } else if (name == "XMAX") {
+              direction = R2{0, 1};
+            } else if (name == "YMIN") {
+              direction = R2{1, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            NamedBoundaryDescriptor named_boundary_descriptor("YMAX");
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, named_boundary_descriptor),
+                                "error: invalid boundary \"YMAX\": boundary is not a line!");
+          }
+        }
+      }
+    }
+
+    SECTION("3D")
+    {
+      static constexpr size_t Dimension = 3;
+
+      using ConnectivityType = Connectivity<Dimension>;
+      using MeshType         = Mesh<ConnectivityType>;
+
+      using R3 = TinyVector<3>;
+
+      SECTION("cartesian 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().cartesian3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * (X[1] + 1), (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {20, 21, 22, 23, 24, 25, 26, 27, 28};
+
+          for (auto tag : tag_set) {
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 20:
+            case 21:
+            case 22:
+            case 23: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 24:
+            case 25:
+            case 26:
+            case 27: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 28: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(29)),
+                                "error: invalid boundary \"YMINZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(30)),
+                                "error: invalid boundary \"YMAXZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(31)),
+                                "error: invalid boundary \"YMAXZMIN\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN\": boundary is not a line!");
+          }
+        }
+      }
+
+      SECTION("hybrid 3d")
+      {
+        std::shared_ptr p_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+        const ConnectivityType& connectivity = p_mesh->connectivity();
+
+        auto xr = p_mesh->xr();
+
+        auto curve = [](const R3& X) -> R3 {
+          return R3{X[0], (1 + X[0] * X[0]) * X[1], (1 - 0.2 * X[0] * X[0]) * X[2]};
+        };
+
+        NodeValue<R3> curved_xr{connectivity};
+
+        parallel_for(
+          connectivity.numberOfNodes(), PUGS_LAMBDA(const NodeId node_id) { curved_xr[node_id] = curve(xr[node_id]); });
+
+        MeshType mesh{p_mesh->shared_connectivity(), curved_xr};
+
+        {
+          const std::set<size_t> tag_set = {28, 29, 30, 31, 32, 33, 34, 35, 36};
+
+          for (auto tag : tag_set) {
+            NumberedBoundaryDescriptor numbered_boundary_descriptor(tag);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, numbered_boundary_descriptor);
+
+            auto node_list = get_node_list_from_tag(tag, connectivity);
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            switch (tag) {
+            case 30:
+            case 31:
+            case 34:
+            case 35: {
+              direction = R3{0, 0, -1};
+              break;
+            }
+            case 28:
+            case 29:
+            case 32:
+            case 33: {
+              direction = R3{0, -1, 0};
+              break;
+            }
+            case 36:
+            // case 37:
+            // case 38:
+            case 39: {
+              direction = R3{1, 0, 0};
+              break;
+            }
+            default: {
+              FAIL("unexpected tag number");
+            }
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(37)),
+                                "error: invalid boundary \"YMINZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(38)),
+                                "error: invalid boundary \"YMAXZMIN\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NumberedBoundaryDescriptor(39)),
+                                "error: invalid boundary \"YMAXZMAX\": boundary is not a line!");
+          }
+        }
+
+        {
+          const std::set<std::string> name_set = {"XMINYMIN", "XMINYMAX", "XMAXYMIN", "XMAXYMAX", "XMINZMIN",
+                                                  "XMINZMAX", "XMAXZMAX", "XMINZMAX", "YMINZMIN"};
+
+          for (auto name : name_set) {
+            NamedBoundaryDescriptor named_boundary_descriptor(name);
+            const auto& node_boundary = getMeshLineNodeBoundary(mesh, named_boundary_descriptor);
+
+            auto node_list = get_node_list_from_name(name, connectivity);
+
+            REQUIRE(is_same(node_boundary.nodeList(), node_list));
+
+            R3 direction = zero;
+
+            if ((name == "XMINYMIN") or (name == "XMINYMAX") or (name == "XMAXYMIN") or (name == "XMAXYMAX")) {
+              direction = R3{0, 0, -1};
+            } else if ((name == "XMINZMIN") or (name == "XMINZMAX") or (name == "XMAXZMIN") or (name == "XMAXZMAX")) {
+              direction = R3{0, -1, 0};
+            } else if ((name == "YMINZMIN")) {
+              direction = R3{1, 0, 0};
+            } else {
+              FAIL("unexpected name: " + name);
+            }
+
+            REQUIRE(l2Norm(node_boundary.direction() - direction) == Catch::Approx(0).margin(1E-13));
+          }
+
+          {
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMAX")),
+                                "error: invalid boundary \"YMAXZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMINZMAX")),
+                                "error: invalid boundary \"YMINZMAX\": boundary is not a line!");
+
+            REQUIRE_THROWS_WITH(getMeshLineNodeBoundary(mesh, NamedBoundaryDescriptor("YMAXZMIN")),
+                                "error: invalid boundary \"YMAXZMIN\": boundary is not a line!");
+          }
+        }
+      }
+    }
+  }
+}