diff --git a/.github/workflows/change.yml b/.github/workflows/change.yml index b49e3d5..f496412 100644 --- a/.github/workflows/change.yml +++ b/.github/workflows/change.yml @@ -30,6 +30,10 @@ jobs: uses: ./.github/workflows/analysis.yml needs: dev-build secrets: inherit + test: + uses: ./.github/workflows/test.yml + needs: dev-build + secrets: inherit deploy: uses: ./.github/workflows/deploy.yml needs: prod-build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e744e4f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Run CTest +on: workflow_call + +jobs: + run-ctest: + name: Run CTest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Download Docker image from artifacts + uses: actions/download-artifact@v4 + with: + name: bxt-development-image + path: distfiles + - name: Load Docker image + run: docker load -i distfiles/bxt-development.tar + - uses: addnab/docker-run-action@v3 + with: + image: anydistro/bxt-development:latest + options: -v ${{ github.workspace }}:/src + run: | + cd /src/build + ctest --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 4954bfb..2de5b8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ ################################################################################ cmake_minimum_required(VERSION 3.23) project(bxt C CXX) +include(CTest) list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) @@ -12,6 +13,7 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++ -Wall -Wextra") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++ -lc++abi") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") set(FETCHCONTENT_QUIET FALSE) option(BXT_EXPERIMENTAL_COPY_MOVE "Enable experimental copy/move operations" OFF) diff --git a/CMakePresets.json b/CMakePresets.json index 9b10888..20476be 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -20,7 +20,9 @@ "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "BUILD_TESTING": "ON", + "CONAN_INSTALL_ARGS": "--build=missing;-o testing=True" } }, { @@ -34,7 +36,8 @@ "name": "release", "inherits": "base", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "BUID_TESTING": "OFF" } } ], diff --git a/conanfile.py b/conanfile.py index faa0b06..216cab6 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,7 +5,13 @@ class BxtConanFile(ConanFile): settings = "os", "arch", "compiler", "build_type" generators = "CMakeDeps" - + options = { + "testing": [True, False], + } + default_options = { + "testing": False, + } + def requirements(self): # to link to them you need to change cmake/deps.cmake self.requires("openssl/3.3.1") @@ -27,6 +33,10 @@ def requirements(self): self.requires("cereal/1.3.2") self.requires("libcoro/0.12.1") self.requires("scope-lite/0.2.0") + + if self.options.testing: + print("Testing enabled") + self.requires("catch2/3.7.0") def configure(self): self.options["boost/*"].shared = True diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 83c794e..6be4210 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -9,7 +9,12 @@ project(bxtd LANGUAGES CXX) ################################################################################ add_subdirectory(swagger) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() + file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp") +list(FILTER SOURCES EXCLUDE REGEX "tests/.*") ################################################################################ # Executable Configuration @@ -41,4 +46,4 @@ target_link_libraries(${PROJECT_NAME} PRIVATE reflectcpp ) -add_dependencies(${PROJECT_NAME} deploy_swagger) +add_dependencies(${PROJECT_NAME} deploy_swagger daemon_tests) diff --git a/daemon/core/domain/entities/Section.h b/daemon/core/domain/entities/Section.h index 904dd7f..4380a31 100644 --- a/daemon/core/domain/entities/Section.h +++ b/daemon/core/domain/entities/Section.h @@ -52,6 +52,7 @@ class Section { std::string string() const { return fmt::format("{}/{}/{}", branch(), repository(), architecture()); } + auto operator<=>(Section const& other) const = default; private: Name m_branch; diff --git a/daemon/core/domain/services/PermissionMatcher.cpp b/daemon/core/domain/services/PermissionMatcher.cpp index e04ab19..73b2267 100644 --- a/daemon/core/domain/services/PermissionMatcher.cpp +++ b/daemon/core/domain/services/PermissionMatcher.cpp @@ -6,6 +6,8 @@ */ #include "PermissionMatcher.h" +#include + namespace bxt::Core::Domain::PermissionMatcher { bool match(Permission const& lh, Permission const& rh) { @@ -23,6 +25,11 @@ bool match(Permission const& lh, Permission const& rh) { } } + if (ltags.size() != rtags.size() && !std::ranges::contains(ltags, "*") + && !std::ranges::contains(rtags, "*")) { + return false; + } + return true; } diff --git a/daemon/core/domain/value_objects/PackageVersion.h b/daemon/core/domain/value_objects/PackageVersion.h index 2e00416..2f8a273 100644 --- a/daemon/core/domain/value_objects/PackageVersion.h +++ b/daemon/core/domain/value_objects/PackageVersion.h @@ -44,6 +44,10 @@ struct PackageVersion { return compare(*this, rh); }; + auto operator==(PackageVersion const& rh) const { + return compare(*this, rh) == 0; + } + static ParseResult from_string(std::string_view str); std::string string() const; diff --git a/daemon/event_log/domain/entities/PackageLogEntry.h b/daemon/event_log/domain/entities/PackageLogEntry.h index 975df2f..6840b8a 100644 --- a/daemon/event_log/domain/entities/PackageLogEntry.h +++ b/daemon/event_log/domain/entities/PackageLogEntry.h @@ -46,6 +46,8 @@ class PackageLogEntry { return m_version; } + bool operator==(PackageLogEntry const& other) const = default; + private: LogEntryType m_type; Core::Domain::Section m_section; diff --git a/daemon/event_log/domain/entities/PackageUpdateLogEntry.h b/daemon/event_log/domain/entities/PackageUpdateLogEntry.h index 1c3e80a..a6e3aae 100644 --- a/daemon/event_log/domain/entities/PackageUpdateLogEntry.h +++ b/daemon/event_log/domain/entities/PackageUpdateLogEntry.h @@ -26,6 +26,8 @@ class PackageUpdateLogEntry { return previous_package_log_entry; } + bool operator==(PackageUpdateLogEntry const& other) const = default; + private: PackageLogEntry package_log_entry; PackageLogEntry previous_package_log_entry; diff --git a/daemon/tests/CMakeLists.txt b/daemon/tests/CMakeLists.txt new file mode 100644 index 0000000..c8f1ead --- /dev/null +++ b/daemon/tests/CMakeLists.txt @@ -0,0 +1,53 @@ +################################################################################ +# Test Configuration +################################################################################ + +find_package(Catch2 REQUIRED) + +################################################################################ +# Test Sources +################################################################################ + +file(GLOB_RECURSE TEST_SOURCES "src/unit/*/**.cpp") +file(GLOB_RECURSE BXT_SOURCES "../*/**.cpp") + +################################################################################ +# Test Executable +################################################################################ + +add_executable(daemon_tests + ${TEST_SOURCES} ${BXT_SOURCES} +) + +target_link_libraries(daemon_tests PRIVATE + Catch2::Catch2WithMain + deps + reflectcpp + Dexode::EventBus +) + +target_include_directories(daemon_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../ + ${lmdbxx_SOURCE_DIR}/include +) + +set_target_properties(daemon_tests PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/tests +) + +get_target_property(TESTS_RUNTIME_OUTPUT_DIRECTORY daemon_tests RUNTIME_OUTPUT_DIRECTORY) + +message("Tests will be run from ${TESTS_RUNTIME_OUTPUT_DIRECTORY}") + +################################################################################ +# Test Data +################################################################################ + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data DESTINATION ${TESTS_RUNTIME_OUTPUT_DIRECTORY}) + +################################################################################ +# CTest Integration +################################################################################ +include(Catch) + +catch_discover_tests(daemon_tests WORKING_DIRECTORY ${TESTS_RUNTIME_OUTPUT_DIRECTORY}) diff --git a/daemon/tests/data/dummy-1-1-any.pkg.tar.zst b/daemon/tests/data/dummy-1-1-any.pkg.tar.zst new file mode 100644 index 0000000..eb54621 Binary files /dev/null and b/daemon/tests/data/dummy-1-1-any.pkg.tar.zst differ diff --git a/daemon/tests/data/dummy-1-1-any.pkg.tar.zst.sig b/daemon/tests/data/dummy-1-1-any.pkg.tar.zst.sig new file mode 100644 index 0000000..060ef82 Binary files /dev/null and b/daemon/tests/data/dummy-1-1-any.pkg.tar.zst.sig differ diff --git a/daemon/tests/src/unit/core/domain/entities/PackageTest.cpp b/daemon/tests/src/unit/core/domain/entities/PackageTest.cpp new file mode 100644 index 0000000..29e47a4 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/entities/PackageTest.cpp @@ -0,0 +1,27 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/entities/Package.h" + +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("Package", "[core][domain][entities]") { + SECTION("Parse file name") { + REQUIRE(Package::parse_file_name("package-1.0.0-1-x86_64.pkg.tar.zst").value() + == "package"); + REQUIRE(Package::parse_file_name("package-1.0.0-1-any.pkg.tar.zst").value() == "package"); + REQUIRE(Package::parse_file_name("lib32-package-1.0.0-1-x86_64.pkg.tar.zst").value() + == "lib32-package"); + + // Invalid cases + REQUIRE(Package::parse_file_name("package.pkg.tar.zst") == std::nullopt); + REQUIRE(Package::parse_file_name("1.0.0-1-x86_64.pkg.tar.zst") == std::nullopt); + REQUIRE(Package::parse_file_name("package-") == std::nullopt); + REQUIRE(Package::parse_file_name("package-1.0.0-1.pkg.tar.zst") == std::nullopt); + } +} diff --git a/daemon/tests/src/unit/core/domain/entities/SectionTest.cpp b/daemon/tests/src/unit/core/domain/entities/SectionTest.cpp new file mode 100644 index 0000000..f233d96 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/entities/SectionTest.cpp @@ -0,0 +1,49 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/entities/Section.h" + +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("Section entity", "[core][domain][entities]") { + SECTION("Construction and getters") { + Section section(Name("stable"), Name("core"), Name("x86_64")); + + REQUIRE(section.branch() == Name("stable")); + REQUIRE(section.repository() == Name("core")); + REQUIRE(section.architecture() == Name("x86_64")); + } + + SECTION("Setters") { + Section section(Name("stable"), Name("core"), Name("x86_64")); + + section.set_branch(Name("testing")); + REQUIRE(section.branch() == Name("testing")); + + section.set_repository(Name("extra")); + REQUIRE(section.repository() == Name("extra")); + + section.set_architecture(Name("aarch64")); + REQUIRE(section.architecture() == Name("aarch64")); + } + + SECTION("ID generation") { + Section section(Name("stable"), Name("core"), Name("x86_64")); + REQUIRE(section.id() == "stable/core/x86_64"); + } + + SECTION("String representation") { + Section section(Name("stable"), Name("core"), Name("x86_64")); + REQUIRE(section.string() == "stable/core/x86_64"); + } + + SECTION("to_string function") { + Section section(Name("stable"), Name("core"), Name("x86_64")); + REQUIRE(bxt::to_string(section) == "stable/core/x86_64"); + } +} diff --git a/daemon/tests/src/unit/core/domain/entities/UserTest.cpp b/daemon/tests/src/unit/core/domain/entities/UserTest.cpp new file mode 100644 index 0000000..8b3ea64 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/entities/UserTest.cpp @@ -0,0 +1,59 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/entities/User.h" + +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("User entity", "[core][domain][entities]") { + SECTION("Construction and getters") { + User user(Name("testuser"), "password123"); + + REQUIRE(user.name() == Name("testuser")); + REQUIRE(user.password() == "password123"); + REQUIRE(user.permissions().empty()); + } + + SECTION("Setters") { + User user(Name("testuser"), "password123"); + + user.set_name("newuser"); + REQUIRE(user.name() == Name("newuser")); + + user.set_password("newpassword"); + REQUIRE(user.password() == "newpassword"); + + std::set new_permissions = {Permission("read"), Permission("write")}; + user.set_permissions(new_permissions); + REQUIRE(user.permissions() == new_permissions); + } + + SECTION("ID") { + User user(Name("testuser"), "password123"); + REQUIRE(user.id() == Name("testuser")); + } + + SECTION("Permission checking") { + User user(Name("testuser"), "password123"); + std::set permissions = {Permission("sections.*.*.*"), + Permission("packages.get.stable.core.x86_64")}; + user.set_permissions(permissions); + + REQUIRE(user.has_permission("sections.stable.core.x86_64")); + REQUIRE(user.has_permission("packages.get.stable.core.x86_64")); + REQUIRE_FALSE(user.has_permission("packages.snap.stable.core.x86_64")); + REQUIRE_FALSE(user.has_permission("users.add")); + } + + SECTION("Default constructor") { + User default_user; + REQUIRE(default_user.name() == Name("Unnamed")); + REQUIRE(default_user.password().empty()); + REQUIRE(default_user.permissions().empty()); + } +} diff --git a/daemon/tests/src/unit/core/domain/enums/PoolLocationTest.cpp b/daemon/tests/src/unit/core/domain/enums/PoolLocationTest.cpp new file mode 100644 index 0000000..51f7620 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/enums/PoolLocationTest.cpp @@ -0,0 +1,52 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/enums/PoolLocation.h" + +#include +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("PoolLocation enum", "[core][domain][enums]") { + SECTION("to_string conversion") { + REQUIRE(bxt::to_string(PoolLocation::Sync) == "sync"); + REQUIRE(bxt::to_string(PoolLocation::Overlay) == "overlay"); + REQUIRE(bxt::to_string(PoolLocation::Automated) == "automated"); + REQUIRE_THROWS(bxt::to_string(PoolLocation::Unknown)); + } + + SECTION("select_preferred_pool_location") { + SECTION("Empty map") { + std::map empty_map; + auto result = select_preferred_pool_location(empty_map); + REQUIRE_FALSE(result.has_value()); + } + + SECTION("Single element map") { + std::map single_map = {{PoolLocation::Sync, 1}}; + auto result = select_preferred_pool_location(single_map); + REQUIRE(result.has_value()); + REQUIRE(*result == PoolLocation::Sync); + } + + SECTION("Multiple elements map") { + std::map multi_map = { + {PoolLocation::Sync, 1}, {PoolLocation::Overlay, 2}, {PoolLocation::Automated, 3}}; + auto result = select_preferred_pool_location(multi_map); + REQUIRE(result.has_value()); + REQUIRE(*result == PoolLocation::Overlay); + } + + SECTION("Map with Unknown location") { + std::map map_with_unknown = { + {PoolLocation::Unknown, 0}, {PoolLocation::Automated, 1}, {PoolLocation::Sync, 2}}; + auto result = select_preferred_pool_location(map_with_unknown); + REQUIRE(result.has_value()); + REQUIRE(*result == PoolLocation::Automated); + } + } +} diff --git a/daemon/tests/src/unit/core/domain/services/PermissionMatcherTest.cpp b/daemon/tests/src/unit/core/domain/services/PermissionMatcherTest.cpp new file mode 100644 index 0000000..81e1d2d --- /dev/null +++ b/daemon/tests/src/unit/core/domain/services/PermissionMatcherTest.cpp @@ -0,0 +1,62 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/services/PermissionMatcher.h" + +#include "core/domain/value_objects/Permission.h" + +#include +using namespace bxt::Core::Domain; + +TEST_CASE("PermissionMatcher", "[core][domain][services]") { + SECTION("Exact match") { + Permission p1("a.b.c"); + Permission p2("a.b.c"); + REQUIRE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Wildcard match") { + Permission p1("a.*.c"); + Permission p2("a.b.c"); + REQUIRE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Partial match with wildcard") { + Permission p1("a.*"); + Permission p2("a.b.c"); + REQUIRE(PermissionMatcher::match(p1, p2)); + } + + SECTION("No match") { + Permission p1("a.b.c"); + Permission p2("x.y.z"); + REQUIRE_FALSE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Different length, no match") { + Permission p1("a.b"); + Permission p2("a.b.c"); + REQUIRE_FALSE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Different length with wildcard, match") { + Permission p1("a.b.*"); + Permission p2("a.b.c.d"); + REQUIRE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Empty permissions") { + Permission p1(""); + Permission p2(""); + REQUIRE(PermissionMatcher::match(p1, p2)); + } + + SECTION("Single wildcard") { + Permission p1("*"); + Permission p2("a.b.c"); + REQUIRE(PermissionMatcher::match(p1, p2)); + } +} diff --git a/daemon/tests/src/unit/core/domain/value_objects/NameTest.cpp b/daemon/tests/src/unit/core/domain/value_objects/NameTest.cpp new file mode 100644 index 0000000..3892b14 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/value_objects/NameTest.cpp @@ -0,0 +1,41 @@ + +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/value_objects/Name.h" + +#include +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("Name", "[core][domain][value_objects]") { + SECTION("Construction and string conversion") { + Name name("TestName"); + REQUIRE(static_cast(name) == "TestName"); + } + + SECTION("Empty name throws exception") { + REQUIRE_THROWS(Name("")); + } + + SECTION("Comparison") { + Name name1("Name1"); + Name name2("Name1"); + Name name3("Name2"); + + REQUIRE(name1 == name2); + REQUIRE(name1 != name3); + REQUIRE(name1 < name3); + REQUIRE(name3 > name1); + } + + SECTION("Formatting") { + Name name("FormatTest"); + std::string formatted = fmt::format("{}", name); + REQUIRE(formatted == "FormatTest"); + } +} diff --git a/daemon/tests/src/unit/core/domain/value_objects/PackageArchitectureTest.cpp b/daemon/tests/src/unit/core/domain/value_objects/PackageArchitectureTest.cpp new file mode 100644 index 0000000..eb5c4c5 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/value_objects/PackageArchitectureTest.cpp @@ -0,0 +1,44 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/value_objects/PackageArchitecture.h" + +#include +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("PackageArchitecture value object", "[core][domain][value_objects]") { + SECTION("Default constructor") { + PackageArchitecture arch; + REQUIRE(std::string(arch) == "any"); + } + + SECTION("Constructor with non-empty string") { + PackageArchitecture arch("x86_64"); + REQUIRE(std::string(arch) == "x86_64"); + } + + SECTION("Constructor with empty string") { + PackageArchitecture arch(""); + REQUIRE(std::string(arch) == "any"); + } + + SECTION("Implicit conversion to string") { + PackageArchitecture arch("arm64"); + std::string arch_str = arch; + REQUIRE(arch_str == "arm64"); + } + + SECTION("Comparison") { + PackageArchitecture arch1("x86_64"); + PackageArchitecture arch2("x86_64"); + PackageArchitecture arch3("arm64"); + + REQUIRE(std::string(arch1) == std::string(arch2)); + REQUIRE(std::string(arch1) != std::string(arch3)); + } +} diff --git a/daemon/tests/src/unit/core/domain/value_objects/PackagePoolEntryTest.cpp b/daemon/tests/src/unit/core/domain/value_objects/PackagePoolEntryTest.cpp new file mode 100644 index 0000000..3417316 --- /dev/null +++ b/daemon/tests/src/unit/core/domain/value_objects/PackagePoolEntryTest.cpp @@ -0,0 +1,58 @@ + +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/value_objects/PackagePoolEntry.h" + +#include +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("PackagePoolEntry", "[core][domain][value_objects]") { + SECTION("Parse valid file path") { + auto cwd = std::filesystem::absolute("."); + std::filesystem::path file_path = "data/dummy-1-1-any.pkg.tar.zst"; + auto result = PackagePoolEntry::parse_file_path(file_path, std::nullopt); + REQUIRE(result.has_value()); + auto entry = result.value(); + REQUIRE(entry.file_path() == file_path); + REQUIRE(entry.version().string() == "1-1"); + } + + SECTION("Parse file path with signature") { + std::filesystem::path file_path = "data/dummy-1-1-any.pkg.tar.zst"; + std::filesystem::path sig_path = "data/dummy-1-1-any.pkg.tar.zst.sig"; + auto result = PackagePoolEntry::parse_file_path(file_path, sig_path); + REQUIRE(result.has_value()); + auto entry = result.value(); + REQUIRE(entry.file_path() == file_path); + REQUIRE(entry.signature_path() == sig_path); + } + + SECTION("Parse invalid file path") { + std::filesystem::path file_path = "invalid-package-name"; + auto result = PackagePoolEntry::parse_file_path(file_path, std::nullopt); + REQUIRE_FALSE(result.has_value()); + REQUIRE(result.error().error_code + == PackagePoolEntry::ParsingError::ErrorCode::InvalidFilename); + } + + SECTION("Accessors") { + std::filesystem::path file_path = "data/dummy-1-1-x86_64.pkg.tar.zst"; + std::filesystem::path sig_path = "data/dummy-1-1-x86_64.pkg.tar.zst.sig"; + bxt::Utilities::AlpmDb::Desc desc; + PackageVersion version = PackageVersion::from_string("1.0.0-1").value(); + + PackagePoolEntry entry(file_path, sig_path, desc, version); + + REQUIRE(entry.file_path() == file_path); + REQUIRE(entry.signature_path() == sig_path); + REQUIRE(entry.version().string() == version.string()); + REQUIRE(entry.desc().desc == desc.desc); + REQUIRE(entry.desc().files == desc.files); + } +} diff --git a/daemon/tests/src/unit/core/domain/value_objects/PackageVersionTest.cpp b/daemon/tests/src/unit/core/domain/value_objects/PackageVersionTest.cpp new file mode 100644 index 0000000..45b60bf --- /dev/null +++ b/daemon/tests/src/unit/core/domain/value_objects/PackageVersionTest.cpp @@ -0,0 +1,66 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/value_objects/PackageVersion.h" + +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("PackageVersion", "[core][domain][value_objects]") { + SECTION("Parse valid version string") { + auto result = PackageVersion::from_string("1:2.3.4-5"); + REQUIRE(result.has_value()); + auto version = result.value(); + REQUIRE(version.epoch == Name("1")); + REQUIRE(version.version == Name("2.3.4")); + REQUIRE(version.release.has_value()); + REQUIRE(version.release.value() == Name("5")); + } + + SECTION("Parse version string without epoch") { + auto result = PackageVersion::from_string("2.3.4-5"); + REQUIRE(result.has_value()); + auto version = result.value(); + REQUIRE(version.epoch == Name("0")); + REQUIRE(version.version == Name("2.3.4")); + REQUIRE(version.release.has_value()); + REQUIRE(version.release.value() == Name("5")); + } + + SECTION("Parse version string without release") { + auto result = PackageVersion::from_string("1:2.3.4"); + REQUIRE(result.has_value()); + auto version = result.value(); + REQUIRE(version.epoch == Name("1")); + REQUIRE(version.version == Name("2.3.4")); + REQUIRE_FALSE(version.release.has_value()); + } + + SECTION("Compare versions") { + auto v1 = PackageVersion::from_string("1:2.3.4-5").value(); + auto v2 = PackageVersion::from_string("1:2.3.4-6").value(); + auto v3 = PackageVersion::from_string("2:1.0.0-1").value(); + + REQUIRE(v1 < v2); + REQUIRE(v2 < v3); + REQUIRE(v1 < v3); + } + + SECTION("Version to string") { + auto version = PackageVersion::from_string("1:2.3.4-5").value(); + REQUIRE(version.string() == "1:2.3.4-5"); + + auto version_no_epoch = PackageVersion::from_string("2.3.4-5").value(); + REQUIRE(version_no_epoch.string() == "2.3.4-5"); + + auto version_no_release = PackageVersion::from_string("1:2.3.4").value(); + REQUIRE(version_no_release.string() == "1:2.3.4"); + + auto version_simple = PackageVersion::from_string("2.3.4").value(); + REQUIRE(version_simple.string() == "2.3.4"); + } +} diff --git a/daemon/tests/src/unit/core/domain/value_objects/PermissionTest.cpp b/daemon/tests/src/unit/core/domain/value_objects/PermissionTest.cpp new file mode 100644 index 0000000..128441e --- /dev/null +++ b/daemon/tests/src/unit/core/domain/value_objects/PermissionTest.cpp @@ -0,0 +1,60 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "core/domain/value_objects/Permission.h" + +#include + +using namespace bxt::Core::Domain; + +TEST_CASE("Permission", "[core][domain][value_objects]") { + SECTION("Create permission from string") { + Permission permission("sections.stable.core.x86_64"); + REQUIRE(permission.tags().size() == 4); + REQUIRE(permission.tags()[0] == "sections"); + REQUIRE(permission.tags()[1] == "stable"); + REQUIRE(permission.tags()[2] == "core"); + REQUIRE(permission.tags()[3] == "x86_64"); + } + + SECTION("Convert permission to string") { + Permission permission("packages.get.unstable.extra.arm"); + std::string permission_str = permission; + REQUIRE(permission_str == "packages.get.unstable.extra.arm"); + } + + SECTION("Compare permissions") { + Permission p1("users.add"); + Permission p2("users.remove"); + Permission p3("users.add"); + + REQUIRE(p1 < p2); + REQUIRE(p1 == p3); + REQUIRE(p2 > p1); + } + + SECTION("Create permission with single tag") { + Permission permission("logs"); + REQUIRE(permission.tags().size() == 1); + REQUIRE(permission.tags()[0] == "logs"); + } + + SECTION("Create permission with multiple tags") { + Permission permission("packages.commit.testing.community.aarch64"); + REQUIRE(permission.tags().size() == 5); + REQUIRE(permission.tags()[0] == "packages"); + REQUIRE(permission.tags()[1] == "commit"); + REQUIRE(permission.tags()[2] == "testing"); + REQUIRE(permission.tags()[3] == "community"); + REQUIRE(permission.tags()[4] == "aarch64"); + } + + SECTION("Create permission with empty string") { + Permission permission(""); + REQUIRE(permission.tags().size() == 1); + REQUIRE(permission.tags()[0].empty()); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/CommitLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/CommitLogEntryTest.cpp new file mode 100644 index 0000000..5910479 --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/CommitLogEntryTest.cpp @@ -0,0 +1,58 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/CommitLogEntry.h" + +#include "helpers.h" + +#include + +using namespace bxt::EventLog::Domain; +using namespace bxt::Core::Domain; + +// Helper functions to create PackageLogEntry objects + +TEST_CASE("CommitLogEntry entity", "[event_log][domain][entities]") { + SECTION("Construction and getters") { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + std::vector added = {bxt::tests::create_pkg_entry1(), + bxt::tests::create_pkg_entry2()}; + std::vector deleted = {bxt::tests::create_pkg_entry3()}; + std::vector moved = {PackageUpdateLogEntry( + bxt::tests::create_pkg_entry4(), bxt::tests::create_pkg_entry4())}; + std::vector copied = {PackageUpdateLogEntry( + bxt::tests::create_pkg_entry5(), bxt::tests::create_pkg_entry5())}; + + CommitLogEntry entry(std::chrono::system_clock::now(), "John Doe", added, deleted, moved, + copied); + + REQUIRE(entry.commiter_name() == "John Doe"); + REQUIRE(entry.added() == added); + REQUIRE(entry.deleted() == deleted); + REQUIRE(entry.moved() == moved); + REQUIRE(entry.copied() == copied); + REQUIRE(entry.type() == EventLogEntryType::Commit); + } + + SECTION("Time comparison") { + auto time1 = std::chrono::system_clock::now(); + auto time2 = time1 + std::chrono::hours(1); + + CommitLogEntry entry1(time1, "Alice", {}, {}, {}, {}); + CommitLogEntry entry2(time2, "Bob", {}, {}, {}, {}); + + REQUIRE(entry1.time() < entry2.time()); + } + + SECTION("Empty collections") { + CommitLogEntry entry(std::chrono::system_clock::now(), "Empty", {}, {}, {}, {}); + + REQUIRE(entry.added().empty()); + REQUIRE(entry.deleted().empty()); + REQUIRE(entry.moved().empty()); + REQUIRE(entry.copied().empty()); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/DeployLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/DeployLogEntryTest.cpp new file mode 100644 index 0000000..ea59e3c --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/DeployLogEntryTest.cpp @@ -0,0 +1,47 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/DeployLogEntry.h" + +#include "helpers.h" + +#include + +using namespace bxt::EventLog::Domain; +using namespace bxt::Core::Domain; + +TEST_CASE("DeployLogEntry entity", "[event_log][domain][entities]") { + SECTION("Construction and getters") { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + std::vector added = {bxt::tests::create_pkg_entry1(), + bxt::tests::create_pkg_entry2()}; + + DeployLogEntry entry(std::chrono::system_clock::now(), + "https://example.com/actions/run/12345", added); + + REQUIRE(entry.runner_url() == "https://example.com/actions/run/12345"); + REQUIRE(entry.added() == added); + + REQUIRE(entry.type() == EventLogEntryType::Deploy); + } + + SECTION("Time comparison") { + auto time1 = std::chrono::system_clock::now(); + auto time2 = time1 + std::chrono::hours(1); + + DeployLogEntry entry1(time1, "https://example.com/actions/run/12345", {}); + DeployLogEntry entry2(time2, "https://example.com/actions/run/54321", {}); + + REQUIRE(entry1.time() < entry2.time()); + } + + SECTION("Empty collections") { + DeployLogEntry entry(std::chrono::system_clock::now(), + "https://example.com/actions/run/00000", {}); + + REQUIRE(entry.added().empty()); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/EventLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/EventLogEntryTest.cpp new file mode 100644 index 0000000..b7ab3ca --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/EventLogEntryTest.cpp @@ -0,0 +1,43 @@ + +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/EventLogEntry.h" + +#include +#include + +using namespace bxt::EventLog::Domain; + +TEST_CASE("EventLogEntryBase", "[event_log][domain][entities]") { + SECTION("Construction and basic properties") { + auto now = std::chrono::system_clock::now(); + EventLogEntryBase entry {EventLogEntryType::Commit, now}; + + REQUIRE(entry.id() == std::to_string(now.time_since_epoch().count())); + REQUIRE(entry.time() == now); + REQUIRE(entry.type() == EventLogEntryType::Commit); + } + + SECTION("Different types") { + auto now = std::chrono::system_clock::now(); + EventLogEntryBase commit_entry {EventLogEntryType::Commit, now}; + EventLogEntryBase deploy_entry {EventLogEntryType::Deploy, now}; + + REQUIRE(commit_entry.type() == EventLogEntryType::Commit); + REQUIRE(deploy_entry.type() == EventLogEntryType::Deploy); + } + + SECTION("Unique IDs for different time points") { + auto time1 = std::chrono::system_clock::now(); + auto time2 = time1 + std::chrono::seconds(1); + + EventLogEntryBase entry1 {EventLogEntryType::Commit, time1}; + EventLogEntryBase entry2 {EventLogEntryType::Deploy, time2}; + + REQUIRE(entry1.id() != entry2.id()); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/PackageLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/PackageLogEntryTest.cpp new file mode 100644 index 0000000..faea994 --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/PackageLogEntryTest.cpp @@ -0,0 +1,29 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/PackageLogEntry.h" + +#include "core/domain/entities/Section.h" +#include "core/domain/enums/PoolLocation.h" +#include "core/domain/value_objects/PackageVersion.h" + +#include + +using namespace bxt::Core::Domain; +using namespace bxt::EventLog::Domain; + +TEST_CASE("PackageLogEntry", "[event_log][domain][entities]") { + SECTION("Construction and basic properties") { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + PackageLogEntry entry {LogEntryType::Add, section, "test-package", PoolLocation::Sync, + PackageVersion::from_string("1.0.0-1").value()}; + + REQUIRE(entry.name() == "test-package"); + REQUIRE(entry.version()->string() == "1.0.0-1"); + REQUIRE(entry.section().string() == "stable/core/x86_64"); + REQUIRE(entry.location() == PoolLocation::Sync); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/PackageUpdateLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/PackageUpdateLogEntryTest.cpp new file mode 100644 index 0000000..877cad7 --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/PackageUpdateLogEntryTest.cpp @@ -0,0 +1,54 @@ + +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/PackageUpdateLogEntry.h" + +#include "core/domain/entities/Section.h" +#include "core/domain/enums/PoolLocation.h" +#include "core/domain/value_objects/PackageVersion.h" + +#include + +using namespace bxt::Core::Domain; +using namespace bxt::EventLog::Domain; + +TEST_CASE("PackageUpdateLogEntry", "[event_log][domain][entities]") { + SECTION("Construction and basic properties") { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + PackageLogEntry old_entry {LogEntryType::Add, section, "test-package", PoolLocation::Sync, + PackageVersion::from_string("1.0.0-1").value()}; + PackageLogEntry new_entry {LogEntryType::Update, section, "test-package", + PoolLocation::Sync, + PackageVersion::from_string("1.1.0-1").value()}; + + PackageUpdateLogEntry update_entry {new_entry, old_entry}; + + REQUIRE(update_entry.package().name() == "test-package"); + REQUIRE(update_entry.package().version()->string() == "1.1.0-1"); + REQUIRE(update_entry.package().section().string() == "stable/core/x86_64"); + REQUIRE(update_entry.package().location() == PoolLocation::Sync); + + REQUIRE(update_entry.previous_package().name() == "test-package"); + REQUIRE(update_entry.previous_package().version()->string() == "1.0.0-1"); + REQUIRE(update_entry.previous_package().section().string() == "stable/core/x86_64"); + REQUIRE(update_entry.previous_package().location() == PoolLocation::Sync); + } + + SECTION("Equality comparison") { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + PackageLogEntry old_entry {LogEntryType::Add, section, "test-package", PoolLocation::Sync, + PackageVersion::from_string("1.0.0-1").value()}; + PackageLogEntry new_entry {LogEntryType::Update, section, "test-package", + PoolLocation::Sync, + PackageVersion::from_string("1.1.0-1").value()}; + + PackageUpdateLogEntry update_entry1 {new_entry, old_entry}; + PackageUpdateLogEntry update_entry2 {new_entry, old_entry}; + + REQUIRE(update_entry1 == update_entry2); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/SyncLogEntryTest.cpp b/daemon/tests/src/unit/event_log/domain/entities/SyncLogEntryTest.cpp new file mode 100644 index 0000000..be605b3 --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/SyncLogEntryTest.cpp @@ -0,0 +1,63 @@ + +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ +#include "event_log/domain/entities/SyncLogEntry.h" + +#include + +using namespace bxt::EventLog::Domain; +using namespace bxt::Core::Domain; + +// Helper functions to create PackageLogEntry objects +PackageLogEntry create_pkg_entry1() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg1", PoolLocation::Sync, + PackageVersion::from_string("1.0").value()}; +} + +PackageLogEntry create_pkg_entry2() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg2", PoolLocation::Sync, + PackageVersion::from_string("2.0").value()}; +} + +PackageLogEntry create_pkg_entry3() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Remove, section, "pkg3", PoolLocation::Sync, + PackageVersion::from_string("3.0").value()}; +} + +TEST_CASE("SyncLogEntry entity", "[event_log][domain][entities]") { + SECTION("Construction and getters") { + std::vector added = {create_pkg_entry1(), create_pkg_entry2()}; + std::vector deleted = {create_pkg_entry3()}; + + SyncLogEntry entry(std::chrono::system_clock::now(), "John Doe", added, deleted); + + REQUIRE(entry.sync_trigger_username() == "John Doe"); + REQUIRE(entry.added() == added); + REQUIRE(entry.deleted() == deleted); + REQUIRE(entry.type() == EventLogEntryType::Sync); + } + + SECTION("Time comparison") { + auto time1 = std::chrono::system_clock::now(); + auto time2 = time1 + std::chrono::hours(1); + + SyncLogEntry entry1(time1, "Alice", {}, {}); + SyncLogEntry entry2(time2, "Bob", {}, {}); + + REQUIRE(entry1.time() < entry2.time()); + } + + SECTION("Empty collections") { + SyncLogEntry entry(std::chrono::system_clock::now(), "Empty", {}, {}); + + REQUIRE(entry.added().empty()); + REQUIRE(entry.deleted().empty()); + } +} diff --git a/daemon/tests/src/unit/event_log/domain/entities/helpers.h b/daemon/tests/src/unit/event_log/domain/entities/helpers.h new file mode 100644 index 0000000..c0f0e7b --- /dev/null +++ b/daemon/tests/src/unit/event_log/domain/entities/helpers.h @@ -0,0 +1,46 @@ +/* === This file is part of bxt === + * + * SPDX-FileCopyrightText: 2024 Artem Grinev + * SPDX-License-Identifier: AGPL-3.0-or-later + * + */ + +#pragma once +#include "core/domain/entities/Section.h" +#include "core/domain/value_objects/Name.h" +#include "event_log/domain/entities/PackageLogEntry.h" + +namespace bxt::tests { +using namespace bxt::Core::Domain; +using namespace bxt::EventLog::Domain; +inline PackageLogEntry create_pkg_entry1() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg1", PoolLocation::Sync, + PackageVersion::from_string("1.0").value()}; +} + +inline PackageLogEntry create_pkg_entry2() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg2", PoolLocation::Sync, + PackageVersion::from_string("2.0").value()}; +} + +inline PackageLogEntry create_pkg_entry3() { + Section section {Name {"stable"}, Name {"core"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Remove, section, "pkg3", PoolLocation::Sync, + PackageVersion::from_string("3.0").value()}; +} + +inline PackageLogEntry create_pkg_entry4() { + Section section {Name {"testing"}, Name {"extra"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg4", PoolLocation::Sync, + PackageVersion::from_string("4.0").value()}; +} + +inline PackageLogEntry create_pkg_entry5() { + Section section {Name {"unstable"}, Name {"community"}, Name {"x86_64"}}; + return PackageLogEntry {LogEntryType::Add, section, "pkg5", PoolLocation::Sync, + PackageVersion::from_string("5.0").value()}; +} + +} // namespace bxt::tests