diff --git a/cpp/cmake/tests.cmake b/cpp/cmake/tests.cmake
index a7fe771e..e5f0ea79 100644
--- a/cpp/cmake/tests.cmake
+++ b/cpp/cmake/tests.cmake
@@ -43,6 +43,7 @@ set(LIBLOOT_SRC_TESTS_INTERFACE_H_FILES
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/game_interface_test.h"
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/is_compatible_test.h"
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/metadata/file_test.h"
+ "${PROJECT_SOURCE_DIR}/src/tests/api/interface/metadata/filename_test.h"
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/metadata/group_test.h"
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/metadata/location_test.h"
"${PROJECT_SOURCE_DIR}/src/tests/api/interface/metadata/message_test.h"
diff --git a/cpp/src/tests/api/interface/main.cpp b/cpp/src/tests/api/interface/main.cpp
index 7b326f65..b0c18ab1 100644
--- a/cpp/src/tests/api/interface/main.cpp
+++ b/cpp/src/tests/api/interface/main.cpp
@@ -32,6 +32,7 @@
#include "tests/api/interface/game_interface_test.h"
#include "tests/api/interface/is_compatible_test.h"
#include "tests/api/interface/metadata/file_test.h"
+#include "tests/api/interface/metadata/filename_test.h"
#include "tests/api/interface/metadata/group_test.h"
#include "tests/api/interface/metadata/location_test.h"
#include "tests/api/interface/metadata/message_content_test.h"
diff --git a/cpp/src/tests/api/interface/metadata/filename_test.h b/cpp/src/tests/api/interface/metadata/filename_test.h
new file mode 100644
index 00000000..f5926032
--- /dev/null
+++ b/cpp/src/tests/api/interface/metadata/filename_test.h
@@ -0,0 +1,147 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2026 Oliver Hamlet
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_METADATA_FILENAME_TEST
+#define LOOT_TESTS_API_INTERFACE_METADATA_FILENAME_TEST
+
+#include
+
+#include "loot/metadata/filename.h"
+
+namespace loot::test {
+TEST(Filename, defaultConstructorShouldInitialiseEmptyString) {
+ Filename filename;
+
+ EXPECT_EQ("", std::string(filename));
+}
+
+TEST(Filename, stringConstructorShouldStoreGivenString) {
+ Filename filename("name");
+
+ EXPECT_EQ("name", std::string(filename));
+}
+
+TEST(Filename, equalityShouldBeCaseInsensitive) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_TRUE(filename1 == filename2);
+
+ filename1 = Filename("name");
+ filename2 = Filename("Name");
+
+ EXPECT_TRUE(filename1 == filename2);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_FALSE(filename1 == filename2);
+}
+
+TEST(Filename, inequalityShouldBeTheInverseOfEquality) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_FALSE(filename1 != filename2);
+
+ filename1 = Filename("name");
+ filename2 = Filename("Name");
+
+ EXPECT_FALSE(filename1 != filename2);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_TRUE(filename1 != filename2);
+}
+
+TEST(Filename,
+ lessThanOperatorShouldUseCaseInsensitiveLexicographicalComparison) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_FALSE(filename1 < filename2);
+ EXPECT_FALSE(filename2 < filename1);
+
+ filename1 = Filename("name");
+ filename2 = Filename("Name");
+
+ EXPECT_FALSE(filename1 < filename2);
+ EXPECT_FALSE(filename2 < filename1);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_TRUE(filename1 < filename2);
+ EXPECT_FALSE(filename2 < filename1);
+}
+
+TEST(Filename, shouldAllowComparisonUsingGreaterThanOperator) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_FALSE(filename1 > filename2);
+ EXPECT_FALSE(filename2 > filename1);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_FALSE(filename1 > filename2);
+ EXPECT_TRUE(filename2 > filename1);
+}
+
+TEST(
+ Filename,
+ lessThanOrEqualToOperatorShouldReturnTrueIfFirstFilenameIsNotGreaterThanSecondFilename) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_TRUE(filename1 <= filename2);
+ EXPECT_TRUE(filename2 <= filename1);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_TRUE(filename1 <= filename2);
+ EXPECT_FALSE(filename2 <= filename1);
+}
+
+TEST(
+ Filename,
+ greaterThanOrEqualToOperatorShouldReturnTrueIfFirstFilenameIsNotLessThanSecondFilename) {
+ Filename filename1("name");
+ Filename filename2("name");
+
+ EXPECT_TRUE(filename1 >= filename2);
+ EXPECT_TRUE(filename2 >= filename1);
+
+ filename1 = Filename("name1");
+ filename2 = Filename("name2");
+
+ EXPECT_FALSE(filename1 >= filename2);
+ EXPECT_TRUE(filename2 >= filename1);
+}
+}
+
+#endif
diff --git a/cpp/src/tests/api/interface/metadata/plugin_metadata_test.h b/cpp/src/tests/api/interface/metadata/plugin_metadata_test.h
index 19df6e1d..02e36e28 100644
--- a/cpp/src/tests/api/interface/metadata/plugin_metadata_test.h
+++ b/cpp/src/tests/api/interface/metadata/plugin_metadata_test.h
@@ -47,6 +47,11 @@ TEST(
EXPECT_FALSE(plugin.GetGroup());
}
+TEST(PluginMetadata, stringConstructorShouldTrimDotGhostExtension) {
+ EXPECT_EQ("a.esp", PluginMetadata("a.esp").GetName());
+ EXPECT_EQ("a.esp", PluginMetadata("a.esp.ghost").GetName());
+}
+
TEST(PluginMetadata,
nameMatchesShouldUseCaseInsensitiveNameComparisonForNonRegexNames) {
PluginMetadata plugin(BLANK_ESM);
@@ -70,6 +75,23 @@ TEST(PluginMetadata,
EXPECT_FALSE(plugin.NameMatches(BLANK_DIFFERENT_ESM));
}
+TEST(PluginMetadata,
+ nameMatchesShouldThrowIfThePluginNameContainsInvalidRegex) {
+ PluginMetadata plugin("invalid(\\.esp");
+
+ try {
+ plugin.NameMatches("blank.esm");
+ FAIL();
+ } catch (const std::runtime_error& e) {
+ EXPECT_STREQ(
+ "encountered a regex error: Unbalanced parenthesis: Unbalanced "
+ "parenthesis",
+ e.what());
+ } catch (...) {
+ FAIL();
+ }
+}
+
TEST(PluginMetadata, mergeMetadataShouldNotChangeName) {
PluginMetadata plugin1(BLANK_ESM);
PluginMetadata plugin2(BLANK_DIFFERENT_ESM);