joinPaths: support adding the trailing slash

Change-Id: I8bcaf08dd17d331c4922d1242f872961631c7caa
Co-authored-by: Jan Kundrát <jan.kundrat@cesnet.cz>
diff --git a/src/utils.cpp b/src/utils.cpp
index d0e8639..978e0a9 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -11,10 +11,26 @@
 
 std::string joinPaths(const std::string& prefix, const std::string& suffix)
 {
-    if (prefix.empty() || suffix.empty() || prefix == "/")
-        return prefix + suffix;
-    else
-        return prefix + '/' + suffix;
+    // These two if statements are essential for the algorithm:
+    // The first one solves joining nothing and a relative path - the algorithm
+    // down below adds a leading slash, turning it into an absolute path.
+    // The second one would always add a trailing slash to the path.
+    if (prefix.empty()) {
+        return suffix;
+    }
+
+    if (suffix.empty()) {
+        return prefix;
+    }
+
+    // Otherwise, strip slashes where the join is going to happen. This will
+    // also change "/" to "", but the return statement takes care of that and
+    // inserts the slash again.
+    auto prefixWithoutTrailingSlash = !prefix.empty() && prefix.back() == '/' ? prefix.substr(0, prefix.length() - 1) : prefix;
+    auto suffixWithoutLeadingSlash = !suffix.empty() && suffix.front() == '/' ? suffix.substr(1) : suffix;
+
+    // And join the result with a slash.
+    return prefixWithoutTrailingSlash + '/' + suffixWithoutLeadingSlash;
 }
 
 std::string stripLastNodeFromPath(const std::string& path)
diff --git a/tests/utils.cpp b/tests/utils.cpp
index a6a982b..7717480 100644
--- a/tests/utils.cpp
+++ b/tests/utils.cpp
@@ -22,4 +22,61 @@
         REQUIRE((filterByPrefix(set, "polivkax") == std::set<std::string>{}));
         REQUIRE((filterByPrefix(set, "co") == std::set<std::string>{"copak", "coze"}));
     }
+
+    SECTION("joinPaths") {
+        std::string prefix, suffix, result;
+
+        SECTION("regular") {
+            prefix = "/example:a";
+            suffix = "leaf";
+            result = "/example:a/leaf";
+        }
+
+        SECTION("no prefix - absolute path") {
+            suffix = "/example:a/leaf";
+            result = "/example:a/leaf";
+        }
+
+        SECTION("no prefix - relative path") {
+            suffix = "example:a/leaf";
+            result = "example:a/leaf";
+        }
+
+        SECTION("no suffix") {
+            prefix = "/example:a/leaf";
+            result = "/example:a/leaf";
+        }
+
+        SECTION("at root") {
+            prefix = "/";
+            suffix = "example:a";
+            result = "/example:a";
+        }
+
+        SECTION("trailing slash") {
+            prefix = "/example:a";
+            suffix = "/";
+            result = "/example:a/";
+        }
+
+        SECTION("prefix ends with slash") {
+            prefix = "/example:a/";
+            suffix = "leaf";
+            result = "/example:a/leaf";
+        }
+
+        SECTION("suffix starts with slash") {
+            prefix = "/example:a";
+            suffix = "/leaf";
+            result = "/example:a/leaf";
+        }
+
+        SECTION("slashes all the way to eleven") {
+            prefix = "/example:a/";
+            suffix = "/leaf";
+            result = "/example:a/leaf";
+        }
+
+        REQUIRE(joinPaths(prefix, suffix) == result);
+    }
 }