Browse Source

Added c++ use cases, tests and basic authentication mock

cpp17-init
chodak166 4 months ago
parent
commit
67edb98056
  1. 30
      README.md
  2. 19
      cpp17/CMakePresets.json
  3. 35
      cpp17/README.md
  4. 16
      cpp17/app/CMakeLists.txt
  5. 12
      cpp17/app/defaults/users.json
  6. 5
      cpp17/app/src/App.cpp
  7. 2
      cpp17/app/src/Main.cpp
  8. 48
      cpp17/app/src/OsHelpers.h
  9. 4
      cpp17/doc/add-item-sequence.md
  10. 28
      cpp17/lib/CMakeLists.txt
  11. 11
      cpp17/lib/include/autostore/AutoStore.h
  12. 2
      cpp17/lib/include/autostore/ILogger.h
  13. 59
      cpp17/lib/src/AutoStore.cpp
  14. 0
      cpp17/lib/src/Version.h.in
  15. 10
      cpp17/lib/src/application/commands/AddItem.cpp
  16. 6
      cpp17/lib/src/application/commands/AddItem.h
  17. 19
      cpp17/lib/src/application/commands/DeleteItem.cpp
  18. 21
      cpp17/lib/src/application/commands/DeleteItem.h
  19. 29
      cpp17/lib/src/application/commands/HandleExpiredItems.cpp
  20. 30
      cpp17/lib/src/application/commands/HandleExpiredItems.h
  21. 20
      cpp17/lib/src/application/commands/LoginUser.cpp
  22. 20
      cpp17/lib/src/application/commands/LoginUser.h
  23. 16
      cpp17/lib/src/application/exceptions/AutoStoreExceptions.h
  24. 6
      cpp17/lib/src/application/interfaces/IAuthService.h
  25. 20
      cpp17/lib/src/application/interfaces/IBlocker.h
  26. 14
      cpp17/lib/src/application/interfaces/IClock.h
  27. 10
      cpp17/lib/src/application/interfaces/IItemRepository.h
  28. 28
      cpp17/lib/src/application/interfaces/IThreadManager.h
  29. 18
      cpp17/lib/src/application/interfaces/ITimeProvider.h
  30. 23
      cpp17/lib/src/application/interfaces/IUserRepository.h
  31. 21
      cpp17/lib/src/application/queries/GetItem.cpp
  32. 23
      cpp17/lib/src/application/queries/GetItem.h
  33. 14
      cpp17/lib/src/application/queries/ListItems.cpp
  34. 21
      cpp17/lib/src/application/queries/ListItems.h
  35. 3
      cpp17/lib/src/domain/entities/User.h
  36. 55
      cpp17/lib/src/infrastructure/adapters/CvBlocker.cpp
  37. 33
      cpp17/lib/src/infrastructure/adapters/CvBlocker.h
  38. 17
      cpp17/lib/src/infrastructure/adapters/SystemClock.h
  39. 43
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.cpp
  40. 27
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.h
  41. 22
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.cpp
  42. 15
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.h
  43. 92
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.cpp
  44. 31
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.h
  45. 2
      cpp17/lib/src/infrastructure/helpers/JsonItem.cpp
  46. 37
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.cpp
  47. 20
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.h
  48. 365
      cpp17/lib/src/infrastructure/http/HttpOrderService.cpp
  49. 4
      cpp17/lib/src/infrastructure/http/HttpOrderService.h
  50. 37
      cpp17/lib/src/infrastructure/http/HttpServer.cpp
  51. 4
      cpp17/lib/src/infrastructure/http/HttpServer.h
  52. 14
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  53. 9
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  54. 130
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp
  55. 30
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.h
  56. 138
      cpp17/lib/src/infrastructure/services/TaskScheduler.cpp
  57. 82
      cpp17/lib/src/infrastructure/services/TaskScheduler.h
  58. 58
      cpp17/lib/src/webapi/controllers/AuthController.cpp
  59. 26
      cpp17/lib/src/webapi/controllers/AuthController.h
  60. 23
      cpp17/lib/src/webapi/controllers/BaseController.cpp
  61. 86
      cpp17/lib/src/webapi/controllers/BaseController.h
  62. 133
      cpp17/lib/src/webapi/controllers/StoreController.cpp
  63. 23
      cpp17/lib/src/webapi/controllers/StoreController.h
  64. 14
      cpp17/tests/CMakeLists.txt
  65. 57
      cpp17/tests/helpers/AddItemTestHelpers.h
  66. 113
      cpp17/tests/integration/FileItemRepository.test.cpp
  67. 312
      cpp17/tests/integration/FileUserRepository.test.cpp
  68. 19
      cpp17/tests/mocks/MockBlocker.h
  69. 22
      cpp17/tests/mocks/MockItemRepository.h
  70. 14
      cpp17/tests/mocks/MockOrderService.h
  71. 24
      cpp17/tests/mocks/MockThreadManager.h
  72. 16
      cpp17/tests/mocks/MockTimeProvider.h
  73. 51
      cpp17/tests/mocks/TestLogger.h
  74. 223
      cpp17/tests/unit/AddItem.test.cpp
  75. 326
      cpp17/tests/unit/TaskScheduler.test.cpp
  76. 4
      cpp17/vcpkg.json
  77. 250
      openapi.yaml
  78. 8
      testing/http-echo-server/docker-compose.yml
  79. 22
      testing/tavern/export.sh
  80. 5
      testing/tavern/requirements.txt
  81. 13
      testing/tavern/tavern-run-all.sh
  82. 16
      testing/tavern/tavern-run-single.sh
  83. 14
      testing/tavern/test_plans/includes.yaml
  84. 200
      testing/tavern/test_plans/items_api.tavern.yaml

30
README.md

@ -19,10 +19,11 @@ A system to store items with expiration dates. When items expire, new ones are a
3. **When an item expires, a new item of the same type is automatically ordered.**
4. **Expired items can be added to the store, triggering immediate ordering.**
5. **Every item belongs to a user.**
6. **Only the item's owner can manage it.**
#### Application Requirements
1. **Users can register and log in to obtain a JWT.**
1. **Users can log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".**
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
@ -30,17 +31,20 @@ A system to store items with expiration dates. When items expire, new ones are a
6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).**
**Note:** For simplicity, user CRUD is skipped. Integrate with an OP (OpenID Provider) service like Keycloak, Authentic, or Zitadel, or mock authentication with a simple Docker service. Alternatively, simply authenticate a predefined user and return a JWT on login.
---
## Layer Boundaries
| Layer | Responsibility | Internal Dependencies | External Dependencies |
|------------------|--------------------------------------------------------------- |----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
| Layer | Responsibility | Internal Dependencies | External Dependencies |
|-------------------|--------------------------------------------------------------- |-----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
---
@ -117,15 +121,11 @@ Here's a summary of example API endpoints:
| Endpoint | Method | Description |
|-------------------------|--------|--------------------------------------|
| `/register` | POST | Register a new user account |
| `/login` | POST | Authenticate user and get JWT token |
| `/users` | GET | Get list of all users |
| `/users/{id}` | GET | Get user by ID |
| `/users/{id}` | POST | Create new user (admin) |
| `/users/{id}` | PUT | Update user details |
| `/users/{id}` | DELETE | Delete user account |
| `/login` | POST | Authenticate user and get JWT token |
| `/items` | GET | Get user's items |
| `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |
Suggested base URL is `http://localhost:8080/api/v1/`.

19
cpp17/CMakePresets.json

@ -2,18 +2,31 @@
"version": 3,
"configurePresets": [
{
"name": "default",
"name": "debug",
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE"
}
},
{
"name": "release",
"toolchainFile": "${env:VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_EXPORT_COMPILE_COMMANDS": "TRUE"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default",
"name": "debug",
"configurePreset": "debug",
"jobs": 8
},
{
"name": "release",
"configurePreset": "release",
"jobs": 8
}
]

35
cpp17/README.md

@ -22,7 +22,7 @@ A system to store items with expiration dates. When items expire, new ones are a
#### Application Requirements
1. **Users can register and log in to obtain a JWT.**
1. **Users can log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".**
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
@ -30,17 +30,20 @@ A system to store items with expiration dates. When items expire, new ones are a
6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).**
**Note:** For simplicity, user CRUD is skipped. Integrate with an OP (OpenID Provider) service like Keycloak, Authentic, or Zitadel, or mock authentication with a simple Docker service. Alternatively, simply authenticate a predefined user and return a JWT on login.
---
## Layer Boundaries
| Layer | Responsibility | Internal Dependencies | External Dependencies |
|------------------|--------------------------------------------------------------- |----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| Layer | Responsibility | Internal Dependencies | External Dependencies |
|-------------------|--------------------------------------------------------------- |----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
---
@ -74,7 +77,7 @@ AutoStore/
│ │ │ ├── IUserRepository
│ │ │ ├── IItemRepository
│ │ │ ├── IAuthService
│ │ │ └── IClock
│ │ │ └── ITimeProvider
│ │ ├── Dto/
│ │ └── Services/
│ ├── Infrastructure/
@ -109,3 +112,19 @@ docker compose up
to build and run the application.
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.
## API Endpoints
See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.).
Here's a summary of example API endpoints:
| Endpoint | Method | Description |
|-------------------------|--------|--------------------------------------|
| `/login` | POST | Authenticate user and get JWT token |
| `/items` | GET | Get user's items |
| `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |
Suggested base URL is `http://localhost:8080/api/v1/`.

16
cpp17/app/CMakeLists.txt

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(AutoStoreApp LANGUAGES CXX VERSION 0.1.0)
project(AutoStoreApp LANGUAGES CXX)
set(TARGET_NAME AutoStore)
set(CMAKE_CXX_STANDARD 17)
@ -8,8 +8,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(spdlog CONFIG REQUIRED)
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/Version.h)
set(SOURCES
src/Main.cpp
src/App.cpp
@ -27,10 +25,12 @@ target_include_directories(${TARGET_NAME}
${CMAKE_BINARY_DIR}
)
# for docker test
# target_compile_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++)
# target_link_options(${TARGET_NAME} PRIVATE -static-libgcc -static-libstdc++)
# Create data directory and copy defalut users.json for development
add_custom_command(
TARGET ${TARGET_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data" && ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_LIST_DIR}/defaults/users.json"
"${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/data/users.json"
)
target_link_libraries(${TARGET_NAME} PRIVATE ${LIBRARIES})
# add_subdirectory(tests/unit)

12
cpp17/app/defaults/users.json

@ -0,0 +1,12 @@
[
{
"username": "admin",
"password": "admin",
"id": "1000"
},
{
"username": "user",
"password": "user",
"id": "1001"
}
]

5
cpp17/app/src/App.cpp

@ -1,5 +1,6 @@
#include "App.h"
#include "SpdLogger.h"
#include "OsHelpers.h"
#include <iostream>
#include <filesystem>
@ -17,15 +18,13 @@ App::App(int argc, char** argv)
signal(SIGINT, App::handleSignal);
signal(SIGTERM, App::handleSignal);
std::filesystem::create_directories("data");
auto spdLogger = spdlog::stdout_color_mt("console");
spdLogger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v");
spdLogger->set_level(spdlog::level::debug);
log = logger = std::make_shared<SpdLogger>(spdLogger, 9);
autoStore = std::make_unique<AutoStore>(
AutoStore::Config{
.dataPath = "data",
.dataPath = os::getApplicationDirectory() + "/data",
.host = "0.0.0.0",
.port = 8080,
},

2
cpp17/app/src/Main.cpp

@ -1,5 +1,5 @@
#include "App.h"
#include "Version.h"
#include "autostore/Version.h"
#include <iostream>
int main(int argc, char** argv)

48
cpp17/app/src/OsHelpers.h

@ -0,0 +1,48 @@
#pragma once
#include <string>
#ifdef _WIN32
#include <windows.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
namespace nxl::os {
std::string getApplicationDirectory()
{
char exePath[PATH_MAX];
GetModuleFileNameA(NULL, exePath, PATH_MAX);
std::string exeDir = std::string(dirname(exePath));
return exeDir;
}
} // namespace nxl::os
#else
#include <sys/types.h>
#include <unistd.h>
#include <libgen.h>
namespace nxl::os {
std::string getApplicationDirectory()
{
char result[PATH_MAX] = {0};
std::string path;
ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
if (count != -1) {
result[count] = '\0';
path = dirname(result);
} else {
path = "./";
}
return path;
}
} // namespace nxl::os
#endif

4
cpp17/doc/add-item-sequence.md

@ -4,7 +4,7 @@
sequenceDiagram
participant Controller as StoreController
participant UseCase as AddItem Use Case
participant Clock as IClock
participant Clock as ITimeProvider
participant Policy as ExpirationPolicy
participant OrderService as OrderingService
participant HttpClient as HttpClient
@ -12,7 +12,7 @@ sequenceDiagram
Controller->>UseCase: execute(item)
UseCase->>Clock: getCurrentTime()
UseCase->>Clock: now()
Clock-->>UseCase: DateTime
UseCase->>Policy: IsExpired(item, currentTime)

28
cpp17/lib/CMakeLists.txt

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(AutoStoreLib)
project(AutoStoreLib LANGUAGES CXX VERSION 0.1.0)
set(TARGET_NAME AutoStoreLib)
set(CMAKE_CXX_STANDARD 17)
@ -8,17 +8,32 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find dependencies
find_package(httplib CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(jwt-cpp CONFIG REQUIRED)
configure_file(src/Version.h.in ${CMAKE_BINARY_DIR}/autostore/Version.h)
add_library(${TARGET_NAME} STATIC
src/AutoStore.cpp
src/infrastructure/repositories/FileUserRepository.cpp
src/application/queries/GetItem.cpp
src/application/queries/ListItems.cpp
src/application/commands/AddItem.cpp
src/application/commands/HandleExpiredItems.cpp
src/application/commands/DeleteItem.cpp
src/application/commands/LoginUser.cpp
src/infrastructure/repositories/FileItemRepository.cpp
src/infrastructure/http/HttpServer.cpp
src/infrastructure/http/HttpJwtMiddleware.cpp
src/infrastructure/http/HttpOrderService.cpp
src/infrastructure/helpers/Jsend.cpp
src/infrastructure/helpers/JsonItem.cpp
src/infrastructure/auth/FileJwtAuthService.cpp
src/infrastructure/services/TaskScheduler.cpp
src/infrastructure/adapters/CvBlocker.cpp
src/infrastructure/adapters/SystemThreadManager.cpp
src/infrastructure/adapters/SystemTimeProvider.cpp
src/webapi/controllers/BaseController.cpp
src/webapi/controllers/StoreController.cpp
src/application/commands/AddItem.cpp
src/webapi/controllers/AuthController.cpp
src/AutoStore.cpp
)
target_include_directories(${TARGET_NAME}
@ -26,6 +41,7 @@ target_include_directories(${TARGET_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/include/autostore
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}
)
target_sources(${TARGET_NAME}
@ -34,12 +50,12 @@ target_sources(${TARGET_NAME}
BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/include
FILES
include/autostore/AutoStore.h
include/autostore/ILogger.h
)
target_link_libraries(${TARGET_NAME}
PUBLIC
httplib::httplib
nlohmann_json::nlohmann_json
jwt-cpp::jwt-cpp
)

11
cpp17/lib/include/autostore/AutoStore.h

@ -10,20 +10,24 @@ namespace nxl::autostore {
namespace application {
class IItemRepository;
class IClock;
class ITimeProvider;
class IOrderService;
class IAuthService;
} // namespace application
namespace infrastructure {
class HttpServer;
class TaskScheduler;
} // namespace infrastructure
namespace webapi {
class StoreController;
class AuthController;
} // namespace webapi
namespace application {
class AddItem;
class LoginUser;
} // namespace application
class AutoStore
@ -48,10 +52,13 @@ private:
ILoggerPtr log;
std::unique_ptr<infrastructure::HttpServer> httpServer;
std::unique_ptr<infrastructure::TaskScheduler> taskScheduler;
std::unique_ptr<webapi::StoreController> storeController;
std::unique_ptr<webapi::AuthController> authController;
std::unique_ptr<application::IItemRepository> itemRepository;
std::unique_ptr<application::IClock> clock;
std::unique_ptr<application::ITimeProvider> clock;
std::unique_ptr<application::IOrderService> orderService;
std::unique_ptr<application::IAuthService> authService;
};
} // namespace nxl::autostore

2
cpp17/lib/include/autostore/ILogger.h

@ -10,7 +10,7 @@ namespace nxl::autostore {
template <typename T> auto to_printf_arg(T&& arg)
{
if constexpr (std::is_same_v<std::decay_t<T>, std::string_view>) {
return std::string(arg).c_str();
return arg.data();
} else if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
return arg.c_str();
} else {

59
cpp17/lib/src/AutoStore.cpp

@ -1,15 +1,24 @@
#include "AutoStore.h"
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/adapters/SystemClock.h"
#include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/http/HttpOrderService.h"
#include "infrastructure/auth/FileJwtAuthService.h"
#include "webapi/controllers/StoreController.h"
#include "webapi/controllers/AuthController.h"
#include "infrastructure/http/HttpServer.h"
#include "infrastructure/services/TaskScheduler.h"
#include "application/commands/HandleExpiredItems.h"
#include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/adapters/SystemThreadManager.h"
#include "infrastructure/adapters/CvBlocker.h"
#include <iostream>
#include <filesystem>
#include <memory>
namespace nxl::autostore {
using namespace infrastructure;
AutoStore::AutoStore(Config config, ILoggerPtr logger)
: config{std::move(config)}, log{std::move(logger)}
{}
@ -29,22 +38,44 @@ bool AutoStore::initialize()
// Initialize repositories and services
std::string itemsDbPath =
std::filesystem::path(config.dataPath) / "items.json";
itemRepository =
std::make_unique<infrastructure::FileItemRepository>(itemsDbPath);
itemRepository = std::make_unique<FileItemRepository>(itemsDbPath);
clock = std::make_unique<SystemTimeProvider>();
orderService = std::make_unique<HttpOrderService>(log);
// Initialize auth service
std::string usersDbPath =
std::filesystem::path(config.dataPath) / "users.json";
authService = std::make_unique<FileJwtAuthService>(usersDbPath);
clock = std::make_unique<infrastructure::SystemClock>();
// Initialize dependencies for task scheduler
auto timeProvider = std::make_shared<SystemTimeProvider>();
auto threadManager = std::make_shared<SystemThreadManager>();
auto blocker = std::make_shared<CvBlocker>();
orderService = std::make_unique<infrastructure::HttpOrderService>(log);
// Initialize task scheduler (for handling expired items)
taskScheduler = std::make_unique<TaskScheduler>(log, timeProvider,
threadManager, blocker);
// Initialize HTTP server
httpServer = std::make_unique<infrastructure::HttpServer>(log);
httpServer = std::make_unique<HttpServer>(log, *authService);
// Initialize store controller
storeController = std::make_unique<webapi::StoreController>(
webapi::StoreController::Context{
application::AddItem{*itemRepository, *clock, *orderService}});
application::AddItem{*itemRepository, *clock, *orderService},
application::ListItems{*itemRepository},
application::GetItem{*itemRepository},
application::DeleteItem{*itemRepository}, *authService});
// Initialize auth controller
authController = std::make_unique<webapi::AuthController>(
webapi::AuthController::Context{application::LoginUser{*authService}});
log->info("Data path: %s", config.dataPath);
log->info("AutoStore initialized successfully, handling expired items...");
log->info("AutoStore initialized successfully");
return true;
} catch (const std::exception& e) {
log->error("Failed to initialize AutoStore: %s", e.what());
@ -57,7 +88,17 @@ bool AutoStore::start()
log->info("Starting AutoStore services...");
try {
taskScheduler->schedule(
[this]() {
application::HandleExpiredItems{*itemRepository, *clock, *orderService}
.execute();
},
00, 00, 00, // midnight (00:00:00)
TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::OnStart);
taskScheduler->start();
storeController->registerRoutes(httpServer->getServer());
authController->registerRoutes(httpServer->getServer());
if (!httpServer->start(config.port, config.host)) {
log->error("Failed to start HTTP server");
@ -67,7 +108,7 @@ bool AutoStore::start()
log->info("AutoStore services started successfully");
log->info("HTTP server listening on http://%s:%d", config.host,
config.port);
log->info("API endpoint: POST http://%s:%d/api/items", config.host,
log->info("API endpoint: POST http://%s:%d/api/v1", config.host,
config.port);
return true;

0
cpp17/app/src/Version.h.in → cpp17/lib/src/Version.h.in

10
cpp17/lib/src/application/commands/AddItem.cpp

@ -3,17 +3,23 @@
namespace nxl::autostore::application {
AddItem::AddItem(IItemRepository& itemRepository, IClock& clock,
AddItem::AddItem(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService)
: itemRepository(itemRepository), clock(clock), orderService(orderService)
{}
domain::Item::Id_t AddItem::execute(domain::Item&& item)
{
const auto currentTime = clock.getCurrentTime();
if (item.userId == domain::User::NULL_ID) {
throw std::runtime_error("User ID not provided");
}
const auto currentTime = clock.now();
if (expirationPolicy.isExpired(item, currentTime)) {
item.id = domain::Item::NULL_ID;
orderService.orderItem(item);
return item.id; // TODO: test that
}
return itemRepository.save(item);
}

6
cpp17/lib/src/application/commands/AddItem.h

@ -3,7 +3,7 @@
#include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include "application/interfaces/IItemRepository.h"
#include "application/interfaces/IClock.h"
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IOrderService.h"
namespace nxl::autostore::application {
@ -13,13 +13,13 @@ class AddItem
public:
virtual ~AddItem() = default;
AddItem(IItemRepository& itemRepository, IClock& clock,
AddItem(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService);
domain::Item::Id_t execute(domain::Item&& item);
private:
IItemRepository& itemRepository;
IClock& clock;
ITimeProvider& clock;
IOrderService& orderService;
domain::ItemExpirationPolicy expirationPolicy;
};

19
cpp17/lib/src/application/commands/DeleteItem.cpp

@ -0,0 +1,19 @@
#include "DeleteItem.h"
#include "application/exceptions/AutoStoreExceptions.h"
namespace nxl::autostore::application {
DeleteItem::DeleteItem(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
void DeleteItem::execute(domain::Item::Id_t id, domain::User::Id_t ownerId)
{
auto item = itemRepository.findById(id);
if (!item || item->userId != ownerId) {
throw ItemNotFoundException("Item not found");
}
itemRepository.remove(id);
}
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/commands/DeleteItem.h

@ -0,0 +1,21 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <string_view>
namespace nxl::autostore::application {
class DeleteItem
{
public:
virtual ~DeleteItem() = default;
explicit DeleteItem(IItemRepository& itemRepository);
void execute(domain::Item::Id_t id, domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

29
cpp17/lib/src/application/commands/HandleExpiredItems.cpp

@ -0,0 +1,29 @@
#include "HandleExpiredItems.h"
#include <stdexcept>
namespace nxl::autostore::application {
HandleExpiredItems::HandleExpiredItems(IItemRepository& itemRepository,
ITimeProvider& clock,
IOrderService& orderService)
: itemRepository(itemRepository), clock(clock), orderService(orderService)
{}
uint16_t HandleExpiredItems::execute()
{
const auto currentTime = clock.now();
auto items = itemRepository.findWhere([&](const domain::Item& i) {
return expirationPolicy.isExpired(i, currentTime);
});
// remove expired one and order a new one
for (auto& item : items) {
itemRepository.remove(item.id);
orderService.orderItem(item);
}
return items.size();
}
} // namespace nxl::autostore::application

30
cpp17/lib/src/application/commands/HandleExpiredItems.h

@ -0,0 +1,30 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/polices/ItemExpirationPolicy.h"
#include "application/interfaces/IItemRepository.h"
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IOrderService.h"
namespace nxl::autostore::application {
class HandleExpiredItems
{
public:
virtual ~HandleExpiredItems() = default;
HandleExpiredItems(IItemRepository& itemRepository, ITimeProvider& clock,
IOrderService& orderService);
/**
* @returns number of expired items
*/
uint16_t execute();
private:
IItemRepository& itemRepository;
ITimeProvider& clock;
IOrderService& orderService;
domain::ItemExpirationPolicy expirationPolicy;
};
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/commands/LoginUser.cpp

@ -0,0 +1,20 @@
#include "LoginUser.h"
#include <stdexcept>
namespace nxl::autostore::application {
LoginUser::LoginUser(IAuthService& authService) : authService(authService) {}
std::string LoginUser::execute(std::string_view username,
std::string_view password)
{
auto userId = authService.authenticateUser(username, password);
if (!userId) {
throw std::runtime_error("Invalid username or password");
}
// Generate a token for the authenticated user
return authService.generateToken(*userId);
}
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/commands/LoginUser.h

@ -0,0 +1,20 @@
#pragma once
#include "domain/entities/User.h"
#include "application/interfaces/IAuthService.h"
namespace nxl::autostore::application {
class LoginUser
{
public:
virtual ~LoginUser() = default;
LoginUser(IAuthService& authService);
std::string execute(std::string_view username, std::string_view password);
private:
IAuthService& authService;
};
} // namespace nxl::autostore::application

16
cpp17/lib/src/application/exceptions/AutoStoreExceptions.h

@ -0,0 +1,16 @@
#pragma once
#include <stdexcept>
#include <string>
namespace nxl::autostore::application {
class ItemNotFoundException : public std::runtime_error
{
public:
explicit ItemNotFoundException(const std::string& message)
: std::runtime_error(message)
{}
};
} // namespace nxl::autostore::application

6
cpp17/lib/src/application/interfaces/IAuthService.h

@ -1,5 +1,6 @@
#pragma once
#include "domain/entities/User.h"
#include <string>
#include <string_view>
#include <optional>
@ -10,8 +11,11 @@ class IAuthService
{
public:
virtual ~IAuthService() = default;
virtual std::optional<domain::User::Id_t>
authenticateUser(std::string_view username, std::string_view password) = 0;
virtual std::string generateToken(std::string_view userId) = 0;
virtual std::optional<std::string> validateToken(std::string_view token) = 0;
virtual std::optional<domain::User::Id_t>
extractUserId(std::string_view token) = 0;
};
} // namespace nxl::autostore::application

20
cpp17/lib/src/application/interfaces/IBlocker.h

@ -0,0 +1,20 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class IBlocker
{
public:
using TimePoint = std::chrono::system_clock::time_point;
virtual ~IBlocker() = default;
virtual void block() = 0;
virtual void blockFor(const std::chrono::milliseconds& duration) = 0;
virtual void blockUntil(const TimePoint& timePoint) = 0;
virtual void notify() = 0;
virtual bool isBlocked() = 0;
virtual bool wasNotified() = 0;
};
} // namespace nxl::autostore::application

14
cpp17/lib/src/application/interfaces/IClock.h

@ -1,14 +0,0 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class IClock
{
public:
virtual ~IClock() = default;
virtual std::chrono::system_clock::time_point getCurrentTime() const = 0;
};
} // namespace nxl::autostore::application

10
cpp17/lib/src/application/interfaces/IItemRepository.h

@ -2,6 +2,7 @@
#include "domain/entities/Item.h"
#include <optional>
#include <functional>
#include <string>
#include <string_view>
#include <vector>
@ -13,10 +14,11 @@ class IItemRepository
public:
virtual ~IItemRepository() = default;
virtual domain::Item::Id_t save(const domain::Item& item) = 0;
virtual std::optional<domain::Item> findById(std::string_view id) = 0;
virtual std::vector<domain::Item> findByUser(std::string_view userId) = 0;
virtual std::vector<domain::Item> findAll() = 0;
virtual void remove(std::string_view id) = 0;
virtual std::optional<domain::Item> findById(domain::Item::Id_t id) = 0;
virtual std::vector<domain::Item> findByOwner(domain::User::Id_t ownerId) = 0;
virtual std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) = 0;
virtual void remove(domain::Item::Id_t id) = 0;
};
} // namespace nxl::autostore::application

28
cpp17/lib/src/application/interfaces/IThreadManager.h

@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include <thread>
namespace nxl::autostore::application {
class IThreadManager
{
public:
class ThreadHandle
{
public:
virtual ~ThreadHandle() = default;
virtual void join() = 0;
virtual bool joinable() const = 0;
};
using ThreadHandlePtr = std::unique_ptr<ThreadHandle>;
virtual ~IThreadManager() = default;
virtual ThreadHandlePtr createThread(std::function<void()> func) = 0;
virtual std::thread::id getCurrentThreadId() const = 0;
virtual void sleep(const std::chrono::milliseconds& duration) = 0;
};
} // namespace nxl::autostore::application

18
cpp17/lib/src/application/interfaces/ITimeProvider.h

@ -0,0 +1,18 @@
#pragma once
#include <chrono>
namespace nxl::autostore::application {
class ITimeProvider
{
public:
using Clock = std::chrono::system_clock;
virtual ~ITimeProvider() = default;
virtual Clock::time_point now() const = 0;
virtual std::tm to_tm(const Clock::time_point& timePoint) const = 0;
virtual Clock::time_point from_tm(const std::tm& tm) const = 0;
};
} // namespace nxl::autostore::application

23
cpp17/lib/src/application/interfaces/IUserRepository.h

@ -1,23 +0,0 @@
#pragma once
#include "domain/entities/User.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace nxl::autostore::application {
class IUserRepository
{
public:
virtual ~IUserRepository() = default;
virtual void save(const domain::User& user) = 0;
virtual std::optional<domain::User> findById(std::string_view id) = 0;
virtual std::optional<domain::User>
findByUsername(std::string_view username) = 0;
virtual std::vector<domain::User> findAll() = 0;
virtual void remove(std::string_view id) = 0;
};
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/queries/GetItem.cpp

@ -0,0 +1,21 @@
#include "GetItem.h"
#include "../exceptions/AutoStoreExceptions.h"
namespace nxl::autostore::application {
GetItem::GetItem(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
std::optional<domain::Item> GetItem::execute(domain::Item::Id_t id,
domain::User::Id_t ownerId)
{
auto item = itemRepository.findById(id);
// act as not found when ownerId does not match for security
if (!item || item->userId != ownerId) {
throw ItemNotFoundException("Item not found");
}
return item;
}
} // namespace nxl::autostore::application

23
cpp17/lib/src/application/queries/GetItem.h

@ -0,0 +1,23 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <optional>
#include <string_view>
namespace nxl::autostore::application {
class GetItem
{
public:
virtual ~GetItem() = default;
explicit GetItem(IItemRepository& itemRepository);
std::optional<domain::Item> execute(domain::Item::Id_t id,
domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

14
cpp17/lib/src/application/queries/ListItems.cpp

@ -0,0 +1,14 @@
#include "ListItems.h"
namespace nxl::autostore::application {
ListItems::ListItems(IItemRepository& itemRepository)
: itemRepository(itemRepository)
{}
std::vector<domain::Item> ListItems::execute(domain::User::Id_t ownerId)
{
return itemRepository.findByOwner(ownerId);
}
} // namespace nxl::autostore::application

21
cpp17/lib/src/application/queries/ListItems.h

@ -0,0 +1,21 @@
#pragma once
#include "domain/entities/Item.h"
#include "application/interfaces/IItemRepository.h"
#include <vector>
namespace nxl::autostore::application {
class ListItems
{
public:
virtual ~ListItems() = default;
explicit ListItems(IItemRepository& itemRepository);
std::vector<domain::Item> execute(domain::User::Id_t ownerId);
private:
IItemRepository& itemRepository;
};
} // namespace nxl::autostore::application

3
cpp17/lib/src/domain/entities/User.h

@ -7,9 +7,10 @@ namespace nxl::autostore::domain {
struct User
{
using Id_t = std::string;
inline static const Id_t NULL_ID{""};
Id_t id;
std::string username;
std::string passwordHash;
std::string password;
};
} // namespace nxl::autostore::domain

55
cpp17/lib/src/infrastructure/adapters/CvBlocker.cpp

@ -0,0 +1,55 @@
#include "CvBlocker.h"
namespace nxl::autostore::infrastructure {
void CvBlocker::block()
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait(lock);
blocked = false;
}
void CvBlocker::blockFor(const std::chrono::milliseconds& duration)
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait_for(lock, duration);
blocked = false;
}
void CvBlocker::blockUntil(const TimePoint& timePoint)
{
blockUntilTimePoint(timePoint);
}
void CvBlocker::notify()
{
notified = true;
conditionVar.notify_all();
}
bool CvBlocker::isBlocked()
{
return blocked;
}
bool CvBlocker::wasNotified()
{
return notified;
}
template <class Clock, class Duration>
void CvBlocker::blockUntilTimePoint(
const std::chrono::time_point<Clock, Duration>& timePoint)
{
notified = false;
std::unique_lock<std::mutex> lock(mutex);
blocked = true;
conditionVar.wait_until(lock, timePoint);
blocked = false;
}
} // namespace nxl::autostore::infrastructure

33
cpp17/lib/src/infrastructure/adapters/CvBlocker.h

@ -0,0 +1,33 @@
#pragma once
#include "application/interfaces/IBlocker.h"
#include <condition_variable>
#include <mutex>
#include <atomic>
namespace nxl::autostore::infrastructure {
class CvBlocker : public application::IBlocker
{
public:
~CvBlocker() override = default;
void block() override;
void blockFor(const std::chrono::milliseconds& duration) override;
void blockUntil(const TimePoint& timePoint) override;
void notify() override;
bool isBlocked() override;
bool wasNotified() override;
private:
template <class Clock, class Duration>
void blockUntilTimePoint(
const std::chrono::time_point<Clock, Duration>& timePoint);
private:
std::condition_variable conditionVar;
std::mutex mutex;
std::atomic<bool> notified{false};
std::atomic<bool> blocked{false};
};
} // namespace nxl::autostore::infrastructure

17
cpp17/lib/src/infrastructure/adapters/SystemClock.h

@ -1,17 +0,0 @@
#pragma once
#include "application/interfaces/IClock.h"
#include <chrono>
namespace nxl::autostore::infrastructure {
class SystemClock : public application::IClock
{
public:
std::chrono::system_clock::time_point getCurrentTime() const override
{
return std::chrono::system_clock::now();
}
};
} // namespace nxl::autostore::infrastructure

43
cpp17/lib/src/infrastructure/adapters/SystemThreadManager.cpp

@ -0,0 +1,43 @@
#include "SystemThreadManager.h"
namespace nxl::autostore::infrastructure {
SystemThreadManager::SystemThreadHandle::SystemThreadHandle(
std::thread&& thread)
: thread{std::move(thread)}
{}
SystemThreadManager::SystemThreadHandle::~SystemThreadHandle()
{
if (thread.joinable()) {
thread.join();
}
}
void SystemThreadManager::SystemThreadHandle::join()
{
thread.join();
}
bool SystemThreadManager::SystemThreadHandle::joinable() const
{
return thread.joinable();
}
application::IThreadManager::ThreadHandlePtr
SystemThreadManager::createThread(std::function<void()> func)
{
return std::make_unique<SystemThreadHandle>(std::thread(func));
}
std::thread::id SystemThreadManager::getCurrentThreadId() const
{
return std::this_thread::get_id();
}
void SystemThreadManager::sleep(const std::chrono::milliseconds& duration)
{
std::this_thread::sleep_for(duration);
}
} // namespace nxl::autostore::infrastructure

27
cpp17/lib/src/infrastructure/adapters/SystemThreadManager.h

@ -0,0 +1,27 @@
#pragma once
#include "application/interfaces/IThreadManager.h"
namespace nxl::autostore::infrastructure {
class SystemThreadManager : public application::IThreadManager
{
public:
class SystemThreadHandle : public ThreadHandle
{
public:
explicit SystemThreadHandle(std::thread&& thread);
~SystemThreadHandle() override;
void join() override;
bool joinable() const override;
private:
std::thread thread;
};
ThreadHandlePtr createThread(std::function<void()> func) override;
std::thread::id getCurrentThreadId() const override;
void sleep(const std::chrono::milliseconds& duration) override;
};
} // namespace nxl::autostore::infrastructure

22
cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.cpp

@ -0,0 +1,22 @@
#include "SystemTimeProvider.h"
namespace nxl::autostore::infrastructure {
SystemTimeProvider::Clock::time_point SystemTimeProvider::now() const
{
return Clock::now();
}
std::tm SystemTimeProvider::to_tm(const Clock::time_point& timePoint) const
{
auto time_t_now = Clock::to_time_t(timePoint);
return *std::localtime(&time_t_now);
}
SystemTimeProvider::Clock::time_point
SystemTimeProvider::from_tm(const std::tm& tm) const
{
return Clock::from_time_t(std::mktime(const_cast<std::tm*>(&tm)));
}
} // namespace nxl::autostore::infrastructure

15
cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.h

@ -0,0 +1,15 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
namespace nxl::autostore::infrastructure {
class SystemTimeProvider : public application::ITimeProvider
{
public:
Clock::time_point now() const override;
std::tm to_tm(const Clock::time_point& timePoint) const override;
Clock::time_point from_tm(const std::tm& tm) const override;
};
} // namespace nxl::autostore::infrastructure

92
cpp17/lib/src/infrastructure/auth/FileJwtAuthService.cpp

@ -0,0 +1,92 @@
#include "FileJwtAuthService.h"
#include <jwt-cpp/jwt.h>
#include <fstream>
#include <nlohmann/json.hpp>
namespace nxl::autostore::infrastructure {
namespace {
// hardcoded secret key for demo purposes
constexpr const char* secretKey{"secret-key"};
} // namespace
/**
* @note Normally that would be generated by the OP
*/
std::string FileJwtAuthService::generateToken(std::string_view userId)
{
auto token =
jwt::create()
.set_issuer("autostore")
.set_type("JWS")
.set_payload_claim("sub", jwt::claim(std::string(userId)))
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24))
.sign(jwt::algorithm::hs256{secretKey});
return token;
}
std::optional<domain::User::Id_t>
FileJwtAuthService::extractUserId(std::string_view token)
{
// Check cache first
std::string tokenStr(token);
auto cacheIt = uidCache.find(tokenStr);
if (cacheIt != uidCache.end()) {
return cacheIt->second;
}
try {
auto decoded = jwt::decode(tokenStr);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{secretKey})
.with_issuer("autostore");
verifier.verify(decoded);
auto subClaim = decoded.get_payload_claim("sub");
auto userId = subClaim.as_string();
if (uidCache.size() >= MAX_UID_CACHE_SIZE) {
// Remove the oldest entry (first element) to make space
uidCache.erase(uidCache.begin());
}
uidCache[tokenStr] = userId;
return userId;
} catch (const std::exception& e) {
return std::nullopt;
}
}
/**
* @note Normally that wouldn't be this app's concern
*/
std::optional<domain::User::Id_t>
FileJwtAuthService::authenticateUser(std::string_view username,
std::string_view password)
{
try {
std::ifstream file(dbPath);
if (!file.is_open()) {
return std::nullopt;
}
nlohmann::json usersJson;
file >> usersJson;
for (const auto& userJson : usersJson) {
if (userJson["username"] == username
&& userJson["password"] == password) {
return userJson["id"].get<std::string>();
}
}
return std::nullopt;
} catch (const std::exception& e) {
return std::nullopt;
}
}
} // namespace nxl::autostore::infrastructure

31
cpp17/lib/src/infrastructure/auth/FileJwtAuthService.h

@ -0,0 +1,31 @@
#pragma once
#include "application/interfaces/IAuthService.h"
#include <unordered_map>
#include <string>
#include <string_view>
#include <optional>
namespace nxl::autostore::infrastructure {
class FileJwtAuthService : public application::IAuthService
{
public:
FileJwtAuthService(std::string dbPath) : dbPath{std::move(dbPath)} {}
std::string generateToken(std::string_view userId) override;
std::optional<domain::User::Id_t>
extractUserId(std::string_view token) override;
std::optional<domain::User::Id_t>
authenticateUser(std::string_view username,
std::string_view password) override;
private:
static constexpr size_t MAX_UID_CACHE_SIZE = 1024;
std::string dbPath;
std::unordered_map<std::string, domain::User::Id_t> uidCache;
};
} // namespace nxl::autostore::infrastructure

2
cpp17/lib/src/infrastructure/helpers/JsonItem.cpp

@ -17,7 +17,7 @@ domain::Item JsonItem::fromJsonObj(const nlohmann::json& j)
item.id = j.value("id", "");
item.name = j.value("name", "");
item.orderUrl = j.value("orderUrl", "");
item.userId = j.value("userId", "default-user");
item.userId = j.value("userId", domain::User::NULL_ID);
if (j["expirationDate"].is_number()) {
// Handle numeric timestamp

37
cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.cpp

@ -0,0 +1,37 @@
#include "HttpJwtMiddleware.h"
namespace nxl::autostore::infrastructure {
httplib::Server::HandlerResponse HttpJwtMiddleware::authenticate(
const httplib::Request& req, httplib::Response& res,
application::IAuthService& authService, ILoggerPtr log)
{
log->v(1, "Pre-request handler: %s", req.path);
// Skip authentication for login endpoint
if (req.path == "/api/v1/login") {
log->v(1, "Skipping authentication for login endpoint");
return httplib::Server::HandlerResponse::Unhandled;
}
auto it = req.headers.find("Authorization");
if (it != req.headers.end()) {
auto authHeader = it->second;
if (authHeader.find("Bearer ") == 0) {
auto token = authHeader.substr(7); // Remove "Bearer " prefix
if (authService.extractUserId(token)) {
log->v(1, "Authorized request");
return httplib::Server::HandlerResponse::Unhandled;
}
}
}
log->v(1, "Unauthorized request");
res.status = 401;
res.set_content(
R"({"status":"error","message":"Unauthorized - Invalid or missing token"})",
"application/json");
return httplib::Server::HandlerResponse::Handled;
}
} // namespace nxl::autostore::infrastructure

20
cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.h

@ -0,0 +1,20 @@
#pragma once
#include "application/interfaces/IAuthService.h"
#include "autostore/ILogger.h"
#include <httplib.h>
namespace nxl::autostore::infrastructure {
class HttpJwtMiddleware
{
public:
HttpJwtMiddleware() = default;
~HttpJwtMiddleware() = default;
static httplib::Server::HandlerResponse
authenticate(const httplib::Request& req, httplib::Response& res,
application::IAuthService& authService, ILoggerPtr logger);
};
} // namespace nxl::autostore::infrastructure

365
cpp17/lib/src/infrastructure/http/HttpOrderService.cpp

@ -1,35 +1,364 @@
/**
* HTTP-based order service implementation with retry logic and
* connection pooling
*
* FLOW OVERVIEW:
* 1. orderItem() validates input and enqueues order request
* 2. Background worker thread processes queue sequentially (FIFO)
* 3. Failed requests are retried with exponential backoff (1s, 2s, 4s...)
* 4. HTTP clients are cached per host and auto-cleaned when unused
* 5. Service shuts down gracefully, completing queued orders
*
* IMPORTANT LIMITATIONS:
* - Uses single worker thread - retries of failed requests will block
* processing of new orders until retry delay expires
* - Not suitable for time-critical operations due to sequential processing
* - Designed for fire-and-forget order notifications, not real-time
* transactions
*/
#include "HttpOrderService.h"
#include "autostore/Version.h"
#include <httplib.h>
#include <stdexcept>
#include <regex>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <chrono>
#include <memory>
#include <unordered_map>
namespace nxl::autostore::infrastructure {
HttpOrderService::HttpOrderService(ILoggerPtr logger) : log{std::move(logger)}
{}
namespace {
void HttpOrderService::orderItem(const domain::Item& item)
constexpr int MAX_RETRIES = 3;
constexpr int CONNECTION_TIMEOUT_SECONDS = 5;
constexpr int READ_TIMEOUT_SECONDS = 5;
constexpr int WRITE_TIMEOUT_SECONDS = 5;
constexpr char CONTENT_TYPE_JSON[] = "application/json";
std::pair<std::string, std::string> parseUrl(const std::string& url)
{
if (item.orderUrl.empty()) {
throw std::runtime_error("Order URL is empty for item: " + item.name);
static const std::regex url_regex(
R"(^(https?:\/\/)?([^\/:]+)(?::(\d+))?(\/[^\?]*)?(\?.*)?$)");
std::smatch matches;
if (!std::regex_match(url, matches, url_regex) || matches.size() < 5) {
throw std::runtime_error("Invalid URL format: " + url);
}
std::string host = matches[2].str();
std::string port = matches[3].str();
std::string path = matches[4].str();
std::string query = matches[5].str();
if (!port.empty()) {
host += ":" + port;
}
std::string payload =
R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}";
sendPostRequest(item.orderUrl, payload);
if (path.empty()) {
path = "/";
}
path += query;
return {host, path};
}
void HttpOrderService::sendPostRequest(std::string_view url,
std::string_view payload)
std::string createOrderPayload(const domain::Item& item)
{
// Escape JSON special characters in strings
auto escapeJson = [](const std::string& str) {
std::string escaped;
escaped.reserve(str.size() + 10); // Reserve extra space for escapes
for (char c : str) {
switch (c) {
case '"':
escaped += "\\\"";
break;
case '\\':
escaped += "\\\\";
break;
case '\b':
escaped += "\\b";
break;
case '\f':
escaped += "\\f";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
escaped += c;
break;
}
}
return escaped;
};
return R"({"itemName": ")" + escapeJson(item.name) + R"(", "itemId": ")"
+ escapeJson(item.id) + "\"}";
}
} // namespace
struct OrderRequest
{
std::string url;
std::string payload;
int retryCount = 0;
std::chrono::system_clock::time_point nextAttemptTime;
OrderRequest() = default;
OrderRequest(std::string url, std::string payload, int rc = 0,
std::chrono::system_clock::time_point nat =
std::chrono::system_clock::now())
: url{std::move(url)}, payload{std::move(payload)}, retryCount{rc},
nextAttemptTime(nat)
{}
};
class HttpOrderService::Impl
{
public:
explicit Impl(ILoggerPtr logger)
: log{std::move(logger)}, shutdownRequested{false}
{
if (!log) {
throw std::invalid_argument("Logger cannot be null");
}
userAgent = "Autostore/" + nxl::getVersionString();
workerThread = std::thread(&Impl::processQueue, this);
}
~Impl()
{
shutdown();
if (workerThread.joinable()) {
workerThread.join();
}
}
void enqueueOrder(const std::string& url, std::string payload)
{
{
std::lock_guard<std::mutex> lock(queueMutex);
if (shutdownRequested) {
throw std::runtime_error(
"Service is shutting down, cannot enqueue new orders");
}
orderQueue.emplace(url, std::move(payload));
}
queueCondition.notify_one();
}
private:
void shutdown()
{
{
std::lock_guard<std::mutex> lock(queueMutex);
shutdownRequested = true;
}
queueCondition.notify_one();
}
bool shouldShutdown() const
{
return shutdownRequested && orderQueue.empty();
}
bool isRequestReady(const OrderRequest& request) const
{
return request.nextAttemptTime <= std::chrono::system_clock::now();
}
void processQueue()
{
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
// Wait for orders or shutdown signal
queueCondition.wait(
lock, [this] { return !orderQueue.empty() || shutdownRequested; });
if (shouldShutdown()) {
break;
}
if (orderQueue.empty()) {
continue;
}
// Check if the front request is ready to be processed
if (!isRequestReady(orderQueue.front())) {
// Wait until the next attempt time
auto waitTime =
orderQueue.front().nextAttemptTime - std::chrono::system_clock::now();
if (waitTime > std::chrono::milliseconds(0)) {
queueCondition.wait_for(lock, waitTime);
}
continue;
}
// Extract request for processing
OrderRequest request = std::move(orderQueue.front());
orderQueue.pop();
// Release lock before processing to avoid blocking other operations
lock.unlock();
processRequest(request);
}
}
void processRequest(OrderRequest& request)
{
try {
sendPostRequest(request.url, request.payload);
log->i("Order request sent successfully to: %s", request.url.c_str());
} catch (const std::exception& e) {
log->e("Failed to send order request to %s: %s", request.url.c_str(),
e.what());
handleFailedRequest(request);
}
}
void handleFailedRequest(OrderRequest& request)
{
if (request.retryCount < MAX_RETRIES) {
request.retryCount++;
// Exponential backoff: 1s, 2s, 4s, 8s...
auto delay = std::chrono::seconds(1 << (request.retryCount - 1));
request.nextAttemptTime = std::chrono::system_clock::now() + delay;
log->w("Retrying order request to %s (attempt %d/%d) in %ld seconds",
request.url.c_str(), request.retryCount, MAX_RETRIES,
delay.count());
{
std::lock_guard<std::mutex> lock(queueMutex);
if (!shutdownRequested) {
orderQueue.push(std::move(request));
}
}
queueCondition.notify_one();
} else {
log->e("Max retries exceeded for order request to: %s",
request.url.c_str());
}
}
std::shared_ptr<httplib::Client> getOrCreateClient(const std::string& host)
{
std::lock_guard<std::mutex> lock(clientsMutex);
auto it = clients.find(host);
if (it != clients.end()) {
// Check if client is still valid
auto client = it->second.lock();
if (client) {
return client;
} else {
// Remove expired weak_ptr
clients.erase(it);
}
}
// Create new client
auto client = std::make_shared<httplib::Client>(host);
configureClient(*client);
clients[host] = client;
return client;
}
void configureClient(httplib::Client& client)
{
client.set_connection_timeout(CONNECTION_TIMEOUT_SECONDS, 0);
client.set_read_timeout(READ_TIMEOUT_SECONDS, 0);
client.set_write_timeout(WRITE_TIMEOUT_SECONDS, 0);
// Enable keep-alive for better performance
client.set_keep_alive(true);
// Set reasonable limits
client.set_compress(true);
}
void sendPostRequest(const std::string& url, const std::string& payload)
{
auto [host, path] = parseUrl(url);
auto client = getOrCreateClient(host);
httplib::Headers headers = {{"Content-Type", CONTENT_TYPE_JSON},
{"User-Agent", userAgent},
{"Accept", CONTENT_TYPE_JSON}};
log->i("Sending POST request to: %s%s", host.c_str(), path.c_str());
log->v(1, "Payload: %s", payload.c_str());
auto res = client->Post(path, headers, payload, CONTENT_TYPE_JSON);
if (!res) {
throw std::runtime_error("Failed to connect to: " + host);
}
log->v(2, "Response status: %d", res->status);
log->v(3, "Response body: %s", res->body.c_str());
if (res->status < 200 || res->status >= 300) {
std::string error_msg =
"HTTP request failed with status: " + std::to_string(res->status)
+ " for URL: " + url;
if (!res->body.empty()) {
error_msg += " Response: " + res->body;
}
throw std::runtime_error(error_msg);
}
}
ILoggerPtr log;
std::queue<OrderRequest> orderQueue;
std::mutex queueMutex;
std::condition_variable queueCondition;
std::thread workerThread;
std::atomic<bool> shutdownRequested;
// Use weak_ptr to allow automatic cleanup of unused clients
std::unordered_map<std::string, std::weak_ptr<httplib::Client>> clients;
std::mutex clientsMutex;
std::string userAgent;
};
HttpOrderService::HttpOrderService(ILoggerPtr logger)
: impl{std::make_unique<Impl>(std::move(logger))}
{}
HttpOrderService::~HttpOrderService() = default;
void HttpOrderService::orderItem(const domain::Item& item)
{
// In a real implementation, this would use an HTTP client library
// For now, we'll simulate the HTTP call
log->i("POST request to: %s", url);
log->v(1, "Payload: %s", payload);
if (item.orderUrl.empty()) {
throw std::runtime_error("Order URL is empty for item: " + item.name);
}
// Simulate HTTP error handling
if (url.find("error") != std::string::npos) {
throw std::runtime_error("Failed to send order request to: "
+ std::string(url));
if (item.orderUrl.find("://") == std::string::npos) {
throw std::runtime_error("Invalid URL format for item: " + item.name
+ " (missing protocol)");
}
std::string payload = createOrderPayload(item);
impl->enqueueOrder(item.orderUrl, std::move(payload));
}
} // namespace nxl::autostore::infrastructure

4
cpp17/lib/src/infrastructure/http/HttpOrderService.h

@ -10,11 +10,15 @@ class HttpOrderService : public application::IOrderService
{
public:
explicit HttpOrderService(ILoggerPtr logger);
virtual ~HttpOrderService();
void orderItem(const domain::Item& item) override;
private:
ILoggerPtr log;
void sendPostRequest(std::string_view url, std::string_view payload);
class Impl;
std::unique_ptr<Impl> impl;
};
} // namespace nxl::autostore::infrastructure

37
cpp17/lib/src/infrastructure/http/HttpServer.cpp

@ -1,4 +1,5 @@
#include "infrastructure/http/HttpServer.h"
#include "infrastructure/http/HttpJwtMiddleware.h"
#include <iostream>
#include <future>
@ -8,7 +9,10 @@ namespace {
constexpr std::chrono::seconds defaultStartupTimeout{5};
}
HttpServer::HttpServer(ILoggerPtr logger) : log{std::move(logger)} {}
HttpServer::HttpServer(ILoggerPtr logger,
application::IAuthService& authService)
: log{std::move(logger)}, authService{authService}
{}
HttpServer::~HttpServer()
{
@ -23,16 +27,12 @@ bool HttpServer::start(int port, std::string_view host)
return true;
}
// std::cout << "Starting HTTP server on " << host << ":" << port <<
// std::endl;
log->info("Starting HTTP server on %s:%d...", host.data(), port);
std::promise<bool> startupPromise;
std::future<bool> startupFuture = startupPromise.get_future();
serverThread = std::thread([host, port, this, &startupPromise]() {
// std::cout << "Server thread started, binding to " << host << ":" << port
// << std::endl;
log->v(1, "Server thread started, binding to %s:%d...", host, port);
try {
@ -42,24 +42,29 @@ bool HttpServer::start(int port, std::string_view host)
return;
}
server.set_logger([this](const httplib::Request& req, const auto& res) {
log->v(1, "Request: %s\n%s", req.path, req.body);
log->v(1, "Response: %d, %s", res.status, res.body);
});
// JWT authentication middleware
server.set_pre_request_handler(
[this](const httplib::Request& req,
httplib::Response& res) -> httplib::Server::HandlerResponse {
return HttpJwtMiddleware::authenticate(req, res, authService, log);
});
// Signal that the server has bound to the port
startupPromise.set_value(true);
// std::cout << "Server thread listening on " << host << ":" << port
// << std::endl;
log->info("Server thread listening on %s:%d", host, port);
// Start listening for connections
bool listenResult = server.listen_after_bind();
// std::cout << "Server stopped, listen result: " << listenResult
// << std::endl;
log->info("Server stopped, listen result: %d", listenResult);
} catch (const std::exception& e) {
// std::cerr << "Server thread exception: " << e.what() << std::endl;
log->error("Server thread exception: %s", e.what());
startupPromise.set_exception(std::current_exception());
} catch (...) {
// std::cerr << "Server thread unknown exception" << std::endl;
log->error("Server thread unknown exception");
startupPromise.set_exception(std::current_exception());
}
@ -68,7 +73,6 @@ bool HttpServer::start(int port, std::string_view host)
// Wait for the server to start (with a timeout)
if (startupFuture.wait_for(defaultStartupTimeout)
== std::future_status::timeout) {
// std::cerr << "Failed to start HTTP server - timeout" << std::endl;
log->error("Failed to start HTTP server - timeout");
return false;
}
@ -76,19 +80,15 @@ bool HttpServer::start(int port, std::string_view host)
try {
// Check if the server bound to the port successfully
if (!startupFuture.get()) {
// std::cerr << "Failed to start HTTP server - could not bind to port"
// << std::endl;
log->error("Failed to start HTTP server - could not bind to port");
return false;
}
} catch (const std::exception& e) {
// std::cerr << "Failed to start HTTP server - " << e.what() << std::endl;
log->error("Failed to start HTTP server - %s", e.what());
return false;
}
running = true;
// std::cout << "HTTP server is running" << std::endl;
log->info("HTTP server is running");
return true;
}
@ -99,17 +99,14 @@ void HttpServer::stop()
return;
}
// std::cout << "Stopping HTTP server..." << std::endl;
log->info("Stopping HTTP server...");
server.stop();
if (serverThread.joinable()) {
// std::cout << "Waiting for server thread to join..." << std::endl;
log->info("Waiting for server thread to join...");
serverThread.join();
}
running = false;
// std::cout << "HTTP server stopped" << std::endl;
log->info("HTTP server stopped");
}

4
cpp17/lib/src/infrastructure/http/HttpServer.h

@ -1,6 +1,7 @@
#pragma once
#include "autostore/ILogger.h"
#include "application/interfaces/IAuthService.h"
#include <httplib.h>
#include <memory>
#include <string>
@ -11,7 +12,7 @@ namespace nxl::autostore::infrastructure {
class HttpServer
{
public:
explicit HttpServer(ILoggerPtr logger);
HttpServer(ILoggerPtr logger, application::IAuthService& authService);
~HttpServer();
bool start(int port = 8080, std::string_view host = "0.0.0.0");
@ -22,6 +23,7 @@ public:
private:
ILoggerPtr log;
application::IAuthService& authService;
bool running{false};
httplib::Server server;
std::thread serverThread;

14
cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp

@ -59,7 +59,7 @@ domain::Item::Id_t FileItemRepository::save(const domain::Item& item)
return id;
}
std::optional<domain::Item> FileItemRepository::findById(std::string_view id)
std::optional<domain::Item> FileItemRepository::findById(domain::Item::Id_t id)
{
std::lock_guard<std::mutex> lock(mtx);
auto it = std::find_if(items.begin(), items.end(),
@ -72,7 +72,7 @@ std::optional<domain::Item> FileItemRepository::findById(std::string_view id)
}
std::vector<domain::Item>
FileItemRepository::findByUser(std::string_view userId)
FileItemRepository::findByOwner(domain::User::Id_t userId)
{
std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> userItems;
@ -81,13 +81,17 @@ FileItemRepository::findByUser(std::string_view userId)
return userItems;
}
std::vector<domain::Item> FileItemRepository::findAll()
std::vector<domain::Item> FileItemRepository::findWhere(
std::function<bool(const domain::Item&)> predicate)
{
std::lock_guard<std::mutex> lock(mtx);
return items;
std::vector<domain::Item> matchedItems;
std::copy_if(items.begin(), items.end(), std::back_inserter(matchedItems),
predicate);
return matchedItems;
}
void FileItemRepository::remove(std::string_view id)
void FileItemRepository::remove(domain::Item::Id_t id)
{
std::lock_guard<std::mutex> lock(mtx);
items.erase(std::remove_if(items.begin(), items.end(),

9
cpp17/lib/src/infrastructure/repositories/FileItemRepository.h

@ -12,10 +12,11 @@ class FileItemRepository : public application::IItemRepository
public:
explicit FileItemRepository(std::string_view dbPath);
domain::Item::Id_t save(const domain::Item& item) override;
std::optional<domain::Item> findById(std::string_view id) override;
std::vector<domain::Item> findByUser(std::string_view userId) override;
std::vector<domain::Item> findAll() override;
void remove(std::string_view id) override;
std::optional<domain::Item> findById(domain::Item::Id_t id) override;
std::vector<domain::Item> findByOwner(domain::User::Id_t userId) override;
std::vector<domain::Item>
findWhere(std::function<bool(const domain::Item&)> predicate) override;
void remove(domain::Item::Id_t id) override;
private:
void load();

130
cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp

@ -1,130 +0,0 @@
#include "infrastructure/repositories/FileUserRepository.h"
#include "nlohmann/json.hpp"
#include <fstream>
#include <algorithm>
namespace nxl::autostore::infrastructure {
namespace {
// Helper functions for JSON serialization
inline void userToJson(nlohmann::json& j, const domain::User& u)
{
j = nlohmann::json{
{"id", u.id}, {"username", u.username}, {"passwordHash", u.passwordHash}};
}
inline void jsonToUser(const nlohmann::json& j, domain::User& u)
{
j.at("id").get_to(u.id);
j.at("username").get_to(u.username);
j.at("passwordHash").get_to(u.passwordHash);
}
// Helper functions for vector serialization
inline nlohmann::json usersToJson(const std::vector<domain::User>& users)
{
nlohmann::json j = nlohmann::json::array();
for (const auto& user : users) {
nlohmann::json userJson;
userToJson(userJson, user);
j.push_back(userJson);
}
return j;
}
inline std::vector<domain::User> jsonToUsers(const nlohmann::json& j)
{
std::vector<domain::User> users;
for (const auto& userJson : j) {
domain::User user;
jsonToUser(userJson, user);
users.push_back(user);
}
return users;
}
} // namespace
FileUserRepository::FileUserRepository(std::string_view dbPath) : dbPath(dbPath)
{
load();
}
void FileUserRepository::save(const domain::User& user)
{
std::lock_guard<std::mutex> lock(mtx);
auto it =
std::find_if(users.begin(), users.end(),
[&](const domain::User& u) { return u.id == user.id; });
if (it != users.end()) {
*it = user;
} else {
users.push_back(user);
}
persist();
}
std::optional<domain::User> FileUserRepository::findById(std::string_view id)
{
std::lock_guard<std::mutex> lock(mtx);
auto it = std::find_if(users.begin(), users.end(),
[&](const domain::User& u) { return u.id == id; });
if (it != users.end()) {
return *it;
}
return std::nullopt;
}
std::optional<domain::User>
FileUserRepository::findByUsername(std::string_view username)
{
std::lock_guard<std::mutex> lock(mtx);
auto it =
std::find_if(users.begin(), users.end(),
[&](const domain::User& u) { return u.username == username; });
if (it != users.end()) {
return *it;
}
return std::nullopt;
}
std::vector<domain::User> FileUserRepository::findAll()
{
std::lock_guard<std::mutex> lock(mtx);
return users;
}
void FileUserRepository::remove(std::string_view id)
{
std::lock_guard<std::mutex> lock(mtx);
users.erase(std::remove_if(users.begin(), users.end(),
[&](const domain::User& u) { return u.id == id; }),
users.end());
persist();
}
void FileUserRepository::load()
{
std::lock_guard<std::mutex> lock(mtx);
std::ifstream file(dbPath);
if (file.is_open()) {
nlohmann::json j;
file >> j;
users = jsonToUsers(j);
}
}
void FileUserRepository::persist()
{
std::ofstream file(dbPath);
if (file.is_open()) {
nlohmann::json j = usersToJson(users);
file << j.dump(4);
}
}
} // namespace nxl::autostore::infrastructure

30
cpp17/lib/src/infrastructure/repositories/FileUserRepository.h

@ -1,30 +0,0 @@
#pragma once
#include "application/interfaces/IUserRepository.h"
#include <string>
#include <vector>
#include <mutex>
namespace nxl::autostore::infrastructure {
class FileUserRepository : public application::IUserRepository
{
public:
explicit FileUserRepository(std::string_view dbPath);
void save(const domain::User& user) override;
std::optional<domain::User> findById(std::string_view id) override;
std::optional<domain::User>
findByUsername(std::string_view username) override;
std::vector<domain::User> findAll() override;
void remove(std::string_view id) override;
private:
void load();
void persist();
std::string dbPath;
std::vector<domain::User> users;
std::mutex mtx;
};
} // namespace nxl::autostore::infrastructure

138
cpp17/lib/src/infrastructure/services/TaskScheduler.cpp

@ -0,0 +1,138 @@
#include "TaskScheduler.h"
#include <sstream>
#include <iomanip>
#include <random>
#include <stdexcept>
#include <thread>
namespace nxl::autostore::infrastructure {
namespace {
std::chrono::system_clock::time_point
today(uint8_t hour, uint8_t minute, uint8_t second,
const application::ITimeProvider& timeProvider)
{
using namespace std::chrono;
using days = duration<int, std::ratio<86400>>;
auto now = timeProvider.now();
auto midnight = time_point_cast<system_clock::duration>(floor<days>(now));
auto offset = hours{hour} + minutes{minute} + seconds{second};
return midnight + offset;
}
} // namespace
TaskScheduler::TaskScheduler(
ILoggerPtr logger, std::shared_ptr<application::ITimeProvider> timeProvider,
std::shared_ptr<application::IThreadManager> threadManager,
std::shared_ptr<application::IBlocker> blocker)
: logger{std::move(logger)}, timeProvider{std::move(timeProvider)},
threadManager{std::move(threadManager)}, blocker{std::move(blocker)},
running(false), stopRequested(false)
{}
void TaskScheduler::schedule(TaskFunction task, int hour, int minute,
int second, RunMode mode)
{
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0
|| second > 59) {
throw std::invalid_argument("Invalid time parameters");
}
if ((mode & RunMode::Forever) && (mode & RunMode::Once)) {
throw std::invalid_argument(
"Forever and Once modes are mutually exclusive");
}
std::lock_guard<std::mutex> lock(tasksMutex);
tasks.emplace_back(std::move(task), hour, minute, second, mode);
}
void TaskScheduler::start()
{
if (running) {
return;
}
running = true;
stopRequested = false;
threadHandle = threadManager->createThread([this]() {
logger->info("TaskScheduler thread started");
while (!stopRequested) {
auto now = timeProvider->now();
bool shouldExecuteOnStart = false;
{
std::lock_guard<std::mutex> lock(tasksMutex);
for (auto& task : tasks) {
if (stopRequested)
break;
bool executeNow = false;
if ((task.mode & RunMode::OnStart) && !task.executed) {
executeNow = true;
shouldExecuteOnStart = true;
} else if (task.mode & RunMode::Once
|| task.mode & RunMode::Forever) {
if (!task.executed || (task.mode & RunMode::Forever)) {
auto taskTime =
today(task.hour, task.minute, task.second, *timeProvider);
if (taskTime <= now) {
if (task.mode & RunMode::Forever) {
taskTime += std::chrono::hours(24);
}
executeNow = true;
}
task.nextExecution = taskTime;
}
}
if (executeNow) {
try {
task.function();
task.executed = true;
logger->info("Task executed successfully");
} catch (const std::exception& e) {
logger->error("Task execution failed: %s", e.what());
}
}
}
}
if (shouldExecuteOnStart) {
continue;
}
if (!stopRequested) {
blocker->blockFor(std::chrono::milliseconds(100));
}
}
running = false;
logger->info("TaskScheduler thread stopped");
});
}
void TaskScheduler::stop()
{
if (!running) {
return;
}
stopRequested = true;
blocker->notify();
if (threadHandle && threadHandle->joinable()) {
threadHandle->join();
}
}
} // namespace nxl::autostore::infrastructure

82
cpp17/lib/src/infrastructure/services/TaskScheduler.h

@ -0,0 +1,82 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
#include "application/interfaces/IThreadManager.h"
#include "application/interfaces/IBlocker.h"
#include <autostore/ILogger.h>
#include <functional>
#include <chrono>
#include <vector>
#include <atomic>
#include <memory>
#include <mutex>
namespace nxl::autostore::infrastructure {
class TaskScheduler
{
public:
/**
* @note Forever and Once are mutually exclusive
*/
enum class RunMode { OnStart = 1, Forever = 2, Once = 4 };
using TaskFunction = std::function<void()>;
struct ScheduledTask
{
TaskFunction function;
int hour;
int minute;
int second;
RunMode mode;
bool executed;
application::ITimeProvider::Clock::time_point nextExecution;
ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md)
: function(std::move(t)), hour(h), minute(m), second(s), mode(md),
executed(false)
{}
};
TaskScheduler(ILoggerPtr logger,
std::shared_ptr<application::ITimeProvider> timeProvider,
std::shared_ptr<application::IThreadManager> threadManager,
std::shared_ptr<application::IBlocker> blocker);
TaskScheduler(const TaskScheduler&) = delete;
TaskScheduler& operator=(const TaskScheduler&) = delete;
virtual ~TaskScheduler() = default;
void schedule(TaskFunction task, int hour, int minute, int second,
RunMode mode);
void start();
void stop();
private:
ILoggerPtr logger;
std::shared_ptr<application::ITimeProvider> timeProvider;
std::shared_ptr<application::IThreadManager> threadManager;
std::shared_ptr<application::IBlocker> blocker;
std::vector<ScheduledTask> tasks;
std::mutex tasksMutex;
std::atomic<bool> running;
std::atomic<bool> stopRequested;
std::unique_ptr<application::IThreadManager::ThreadHandle> threadHandle;
};
constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a,
TaskScheduler::RunMode b)
{
return static_cast<TaskScheduler::RunMode>(static_cast<int>(a)
| static_cast<int>(b));
}
constexpr int operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b)
{
return static_cast<int>(a) & static_cast<int>(b);
}
} // namespace nxl::autostore::infrastructure

58
cpp17/lib/src/webapi/controllers/AuthController.cpp

@ -0,0 +1,58 @@
#include "webapi/controllers/AuthController.h"
#include "infrastructure/helpers/JsonItem.h"
#include "infrastructure/helpers/Jsend.h"
#include "application/commands/LoginUser.h"
#include <nlohmann/json.hpp>
namespace nxl::autostore::webapi {
AuthController::AuthController(Context&& context)
: BaseController(std::move(context))
{}
std::vector<BaseController::RouteConfig> AuthController::getRoutes() const
{
return {{"/api/v1/login", "POST",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<AuthController*>(this)->loginUser(req, res);
}}};
}
void AuthController::loginUser(const httplib::Request& req,
httplib::Response& res)
{
try {
if (req.body.empty()) {
sendError(res, "Request body is empty",
httplib::StatusCode::BadRequest_400);
return;
}
auto requestBody = nlohmann::json::parse(req.body);
std::string username = requestBody.value("username", "");
std::string password = requestBody.value("password", "");
if (username.empty() || password.empty()) {
sendError(res, "Username and password are required",
httplib::StatusCode::BadRequest_400);
return;
}
try {
std::string token =
getContext<Context>().loginUserUc.execute(username, password);
nlohmann::json responseData = nlohmann::json::object();
responseData["token"] = token;
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
sendError(res, "Authentication failed: " + std::string(e.what()),
httplib::StatusCode::Unauthorized_401);
}
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::BadRequest_400);
}
}
} // namespace nxl::autostore::webapi

26
cpp17/lib/src/webapi/controllers/AuthController.h

@ -0,0 +1,26 @@
#pragma once
#include "webapi/controllers/BaseController.h"
#include "application/commands/LoginUser.h"
#include <httplib.h>
namespace nxl::autostore::webapi {
class AuthController : public BaseController
{
public:
struct Context
{
application::LoginUser loginUserUc;
};
AuthController(Context&& context);
protected:
std::vector<RouteConfig> getRoutes() const override;
private:
void loginUser(const httplib::Request& req, httplib::Response& res);
};
} // namespace nxl::autostore::webapi

23
cpp17/lib/src/webapi/controllers/BaseController.cpp

@ -0,0 +1,23 @@
#include "webapi/controllers/BaseController.h"
namespace nxl::autostore::webapi {
void BaseController::registerRoutes(httplib::Server& server)
{
auto routes = getRoutes();
for (const auto& route : routes) {
if (route.method == "GET") {
server.Get(route.path, route.handler);
} else if (route.method == "POST") {
server.Post(route.path, route.handler);
} else if (route.method == "PUT") {
server.Put(route.path, route.handler);
} else if (route.method == "DELETE") {
server.Delete(route.path, route.handler);
} else if (route.method == "PATCH") {
server.Patch(route.path, route.handler);
}
}
}
} // namespace nxl::autostore::webapi

86
cpp17/lib/src/webapi/controllers/BaseController.h

@ -0,0 +1,86 @@
#pragma once
#include "infrastructure/helpers/Jsend.h"
#include <httplib.h>
#include <functional>
#include <string_view>
#include <nlohmann/json.hpp>
namespace nxl::autostore::webapi {
class BaseController
{
public:
using HttpRequestHandler =
std::function<void(const httplib::Request&, httplib::Response&)>;
struct RouteConfig
{
std::string path;
std::string method;
HttpRequestHandler handler;
};
template <typename Context>
BaseController(Context&& context)
: contextStorage(
std::make_unique<ContextHolder<Context>>(std::move(context)))
{}
virtual ~BaseController() = default;
void registerRoutes(httplib::Server& server);
protected:
virtual std::vector<RouteConfig> getRoutes() const = 0;
void sendError(httplib::Response& res, std::string_view message, int status)
{
res.status = status;
res.set_content(infrastructure::Jsend::error(message, status),
"application/json");
}
template <typename T> T& getContext()
{
return static_cast<ContextHolder<T>*>(contextStorage.get())->getContext();
}
std::string extractUserToken(const httplib::Request& req)
{
auto header = req.get_header_value("Authorization");
if (header.empty()) {
throw std::runtime_error("Authorization header is missing");
}
if (header.substr(0, 7) != "Bearer ") {
throw std::runtime_error("Authorization header is invalid");
}
return header.substr(7);
}
template <typename T, typename U>
std::optional<U> extractUserId(const httplib::Request& req)
{
auto token = extractUserToken(req);
return getContext<T>().authService.extractUserId(token);
}
private:
struct ContextHolderBase
{
virtual ~ContextHolderBase() = default;
};
template <typename T> struct ContextHolder : ContextHolderBase
{
ContextHolder(T&& ctx) : context(std::move(ctx)) {}
T& getContext() { return context; }
T context;
};
std::unique_ptr<ContextHolderBase> contextStorage;
};
} // namespace nxl::autostore::webapi

133
cpp17/lib/src/webapi/controllers/StoreController.cpp

@ -2,51 +2,126 @@
#include "infrastructure/helpers/JsonItem.h"
#include "infrastructure/helpers/Jsend.h"
#include "application/commands/AddItem.h"
#include "application/queries/ListItems.h"
#include "application/queries/GetItem.h"
#include "application/commands/DeleteItem.h"
#include "application/exceptions/AutoStoreExceptions.h"
#include <optional>
namespace nxl::autostore::webapi {
using infrastructure::Jsend;
using infrastructure::JsonItem;
StoreController::StoreController(Context&& context)
: context{std::move(context)}
: BaseController(std::move(context))
{}
void StoreController::registerRoutes(httplib::Server& server)
std::vector<BaseController::RouteConfig> StoreController::getRoutes() const
{
server.Post("/api/items",
[this](const httplib::Request& req, httplib::Response& res) {
this->addItem(req, res);
});
return {{"/api/v1/items", "POST",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->addItem(req, res);
}},
{"/api/v1/items", "GET",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->listItems(req, res);
}},
{"/api/v1/items/(.*)", "GET",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->getItem(req, res);
}},
{"/api/v1/items/(.*)", "DELETE",
[this](const httplib::Request& req, httplib::Response& res) {
const_cast<StoreController*>(this)->deleteItem(req, res);
}}};
}
void StoreController::addItem(const httplib::Request& req,
httplib::Response& res)
{
try {
if (req.body.empty()) {
res.status = 400;
res.set_content(Jsend::error("Request body is empty", 400),
"application/json");
return;
}
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto item = infrastructure::JsonItem::fromJson(req.body);
item.userId = userId.value();
auto itemId = getContext<Context>().addItemUc.execute(std::move(item));
nlohmann::json responseData = nlohmann::json::object();
responseData["id"] = itemId;
res.status = httplib::StatusCode::Created_201;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::listItems(const httplib::Request& req,
httplib::Response& res)
{
try {
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto items = getContext<Context>().listItemsUc.execute(userId.value());
auto item = JsonItem::fromJson(req.body);
try {
nlohmann::json responseData = nlohmann::json::object();
responseData["id"] = context.addItemUc.execute(std::move(item));
res.status = 201;
res.set_content(Jsend::success(responseData), "application/json");
} catch (const std::exception& e) {
res.status = 500;
res.set_content(
Jsend::error("Failed to add item: " + std::string(e.what()),
res.status),
"application/json");
nlohmann::json responseData = nlohmann::json::array();
for (const auto& item : items) {
responseData.push_back(infrastructure::JsonItem::toJsonObj(item));
}
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const std::exception& e) {
res.status = 400;
res.set_content(Jsend::error(e.what(), res.status), "application/json");
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::getItem(const httplib::Request& req,
httplib::Response& res)
{
try {
auto itemId = req.matches[1];
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
auto item = getContext<Context>().getItemUc.execute(itemId, userId.value());
auto responseData = infrastructure::JsonItem::toJsonObj(*item);
res.status = httplib::StatusCode::OK_200;
res.set_content(infrastructure::Jsend::success(responseData),
"application/json");
} catch (const nxl::autostore::application::ItemNotFoundException& e) {
sendError(res, e.what(), httplib::StatusCode::NotFound_404);
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::deleteItem(const httplib::Request& req,
httplib::Response& res)
{
try {
auto itemId = req.matches[1];
auto userId = extractUserId<Context, domain::User::Id_t>(req);
assertUserId(userId);
getContext<Context>().deleteItemUc.execute(itemId, userId.value());
nlohmann::json responseData = nlohmann::json::object();
responseData["message"] = "Item deleted successfully";
res.status = httplib::StatusCode::NoContent_204;
// Actually, no content should follow 204 response
// res.set_content(infrastructure::Jsend::success(responseData),
// "application/json");
} catch (const nxl::autostore::application::ItemNotFoundException& e) {
sendError(res, e.what(), httplib::StatusCode::NotFound_404);
} catch (const std::exception& e) {
sendError(res, e.what(), httplib::StatusCode::InternalServerError_500);
}
}
void StoreController::assertUserId(
std::optional<domain::User::Id_t> userId) const
{
if (!userId) {
throw std::runtime_error("User ID not found in request");
}
}

23
cpp17/lib/src/webapi/controllers/StoreController.h

@ -1,27 +1,40 @@
#pragma once
#include "webapi/controllers/BaseController.h"
#include "application/commands/AddItem.h"
#include <httplib.h> // TODO: forward declaration
#include "application/queries/ListItems.h"
#include "application/queries/GetItem.h"
#include "application/commands/DeleteItem.h"
#include "application/interfaces/IAuthService.h"
#include "infrastructure/helpers/JsonItem.h"
#include <httplib.h>
namespace nxl::autostore::webapi {
class StoreController
class StoreController : public BaseController
{
public:
struct Context
{
application::AddItem addItemUc;
application::ListItems listItemsUc;
application::GetItem getItemUc;
application::DeleteItem deleteItemUc;
application::IAuthService& authService;
};
StoreController(Context&& context);
void registerRoutes(httplib::Server& server);
protected:
std::vector<RouteConfig> getRoutes() const override;
private:
void addItem(const httplib::Request& req, httplib::Response& res);
void listItems(const httplib::Request& req, httplib::Response& res);
void getItem(const httplib::Request& req, httplib::Response& res);
void deleteItem(const httplib::Request& req, httplib::Response& res);
private:
Context context;
void assertUserId(std::optional<domain::User::Id_t> userId) const;
};
} // namespace nxl::autostore::webapi

14
cpp17/tests/CMakeLists.txt

@ -3,9 +3,11 @@ cmake_minimum_required(VERSION 3.20)
enable_testing()
find_package(Catch2 CONFIG REQUIRED)
find_package(trompeloeil CONFIG REQUIRED)
# Macro to create a test executable
function(add_integration_test TEST_NAME SOURCE_FILE)
# Macro to create test executable
function(add_test_target TEST_NAME SOURCE_FILE)
add_executable(${TEST_NAME}
${SOURCE_FILE}
)
@ -14,12 +16,14 @@ function(add_integration_test TEST_NAME SOURCE_FILE)
PRIVATE
AutoStoreLib
Catch2::Catch2WithMain
trompeloeil::trompeloeil
)
target_include_directories(${TEST_NAME}
PRIVATE
${PROJECT_SOURCE_DIR}/lib/include
${PROJECT_SOURCE_DIR}/lib/src
${PROJECT_SOURCE_DIR}/tests
)
# Add test to CTest
@ -27,5 +31,7 @@ function(add_integration_test TEST_NAME SOURCE_FILE)
endfunction()
# Create test executables
add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp)
add_integration_test(FileItemRepositoryTest integration/FileItemRepository.test.cpp)
add_test_target(FileItemRepositoryTest integration/FileItemRepository.test.cpp)
# add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp)
add_test_target(AddItemTest unit/AddItem.test.cpp)
add_test_target(TaskSchedulerTest unit/TaskScheduler.test.cpp)

57
cpp17/tests/helpers/AddItemTestHelpers.h

@ -0,0 +1,57 @@
#pragma once
#include "domain/entities/Item.h"
#include "domain/entities/User.h"
#include <chrono>
#include <string>
namespace nxl::autostore::domain {
// Equality operator for Item to make trompeloeil work
inline bool operator==(const Item& lhs, const Item& rhs)
{
return lhs.id == rhs.id && lhs.name == rhs.name
&& lhs.orderUrl == rhs.orderUrl && lhs.userId == rhs.userId
&& lhs.expirationDate == rhs.expirationDate;
}
} // namespace nxl::autostore::domain
namespace test {
constexpr const char* TEST_ITEM_ID_1 = "item123";
constexpr const char* TEST_ITEM_ID_2 = "item456";
constexpr const char* TEST_ITEM_NAME_1 = "testitem";
constexpr const char* TEST_ORDER_URL_1 = "https://example.com/order1";
constexpr const char* TEST_USER_ID_1 = "user123";
constexpr const char* TEST_USER_ID_2 = "user456";
// Fixed test timepoint: 2020-01-01 12:00
constexpr std::chrono::system_clock::time_point TEST_TIMEPOINT_NOW =
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000));
// Helper function to create a test item with default values
nxl::autostore::domain::Item
createTestItem(const std::string& id = TEST_ITEM_ID_1,
const std::string& name = TEST_ITEM_NAME_1,
const std::string& orderUrl = TEST_ORDER_URL_1,
const std::string& userId = TEST_USER_ID_1,
const std::chrono::system_clock::time_point& expirationDate =
std::chrono::system_clock::now() + std::chrono::hours(24))
{
nxl::autostore::domain::Item item;
item.id = id;
item.name = name;
item.orderUrl = orderUrl;
item.userId = userId;
item.expirationDate = expirationDate;
return item;
}
// Helper function to create an expired test item
nxl::autostore::domain::Item createExpiredTestItem()
{
return createTestItem(TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1,
TEST_USER_ID_1,
TEST_TIMEPOINT_NOW - std::chrono::hours(1));
}
} // namespace test

113
cpp17/tests/integration/FileItemRepository.test.cpp

@ -117,12 +117,15 @@ TEST_CASE("FileItemRepository Integration Tests",
domain::Item testItem = Test::createTestItem();
// When
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
auto foundItem = repository.findById(savedItemId);
REQUIRE(foundItem.has_value());
Test::verifyDefaultTestItem(*foundItem);
REQUIRE(foundItem->id == savedItemId);
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1);
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1);
}
SECTION("when a new item is saved then it can be found by user")
@ -132,12 +135,15 @@ TEST_CASE("FileItemRepository Integration Tests",
domain::Item testItem = Test::createTestItem();
// When
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 1);
Test::verifyDefaultTestItem(userItems[0]);
REQUIRE(userItems[0].id == savedItemId);
REQUIRE(userItems[0].name == Test::TEST_ITEM_NAME_1);
REQUIRE(userItems[0].orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(userItems[0].userId == Test::TEST_USER_ID_1);
}
SECTION("when multiple items are saved then findAll returns all items")
@ -148,11 +154,12 @@ TEST_CASE("FileItemRepository Integration Tests",
domain::Item secondItem = Test::createSecondTestItem();
// When
repository.save(firstItem);
repository.save(secondItem);
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// Then
auto allItems = repository.findAll();
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 2);
// Verify both items are present (order doesn't matter)
@ -160,11 +167,15 @@ TEST_CASE("FileItemRepository Integration Tests",
bool foundSecond = false;
for (const auto& item : allItems) {
if (item.id == Test::TEST_ITEM_ID_1) {
Test::verifyDefaultTestItem(item);
if (item.id == firstItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_1);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundFirst = true;
} else if (item.id == Test::TEST_ITEM_ID_2) {
Test::verifySecondTestItem(item);
} else if (item.id == secondItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_2);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_2);
REQUIRE(item.userId == Test::TEST_USER_ID_2);
foundSecond = true;
}
}
@ -184,11 +195,11 @@ TEST_CASE("FileItemRepository Integration Tests",
Test::TEST_USER_ID_1);
// When
repository.save(firstItem);
repository.save(secondItem);
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 2);
// Verify both items are present (order doesn't matter)
@ -196,10 +207,12 @@ TEST_CASE("FileItemRepository Integration Tests",
bool foundSecond = false;
for (const auto& item : userItems) {
if (item.id == Test::TEST_ITEM_ID_1) {
Test::verifyDefaultTestItem(item);
if (item.id == firstItemId) {
REQUIRE(item.name == Test::TEST_ITEM_NAME_1);
REQUIRE(item.orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundFirst = true;
} else if (item.id == "item789") {
} else if (item.id == secondItemId) {
REQUIRE(item.name == "thirditem");
REQUIRE(item.orderUrl == "https://example.com/order3");
REQUIRE(item.userId == Test::TEST_USER_ID_1);
@ -216,18 +229,20 @@ TEST_CASE("FileItemRepository Integration Tests",
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// When
testItem.id = savedItemId;
testItem.name = "updateditemname";
testItem.orderUrl = "https://updated.example.com/order";
testItem.userId = Test::TEST_USER_ID_2;
repository.save(testItem);
auto updatedItemId = repository.save(testItem);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
REQUIRE(savedItemId == updatedItemId); // ID should not change for updates
auto foundItem = repository.findById(updatedItemId);
REQUIRE(foundItem.has_value());
REQUIRE(foundItem->id == Test::TEST_ITEM_ID_1);
REQUIRE(foundItem->id == updatedItemId);
REQUIRE(foundItem->name == "updateditemname");
REQUIRE(foundItem->orderUrl == "https://updated.example.com/order");
REQUIRE(foundItem->userId == Test::TEST_USER_ID_2);
@ -238,13 +253,13 @@ TEST_CASE("FileItemRepository Integration Tests",
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
repository.remove(savedItemId);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
auto foundItem = repository.findById(savedItemId);
REQUIRE_FALSE(foundItem.has_value());
}
@ -253,13 +268,13 @@ TEST_CASE("FileItemRepository Integration Tests",
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
repository.remove(savedItemId);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
auto userItems = repository.findByOwner(Test::TEST_USER_ID_1);
REQUIRE(userItems.empty());
}
@ -269,16 +284,20 @@ TEST_CASE("FileItemRepository Integration Tests",
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem = Test::createSecondTestItem();
repository.save(firstItem);
repository.save(secondItem);
auto firstItemId = repository.save(firstItem);
auto secondItemId = repository.save(secondItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
repository.remove(firstItemId);
// Then
auto allItems = repository.findAll();
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 1);
Test::verifySecondTestItem(allItems[0]);
REQUIRE(allItems[0].id == secondItemId);
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_2);
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_2);
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_2);
}
SECTION(
@ -303,45 +322,52 @@ TEST_CASE("FileItemRepository Integration Tests",
repository.save(testItem);
// When
auto userItems = repository.findByUser(Test::NON_EXISTENT_USER_ID);
auto userItems = repository.findByOwner(Test::NON_EXISTENT_USER_ID);
// Then
REQUIRE(userItems.empty());
}
SECTION("when remove is called with non-existent id then it does nothing")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
auto savedItemId = repository.save(testItem);
// When
repository.remove(Test::NON_EXISTENT_ID);
// Then
auto allItems = repository.findAll();
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.size() == 1);
Test::verifyDefaultTestItem(allItems[0]);
REQUIRE(allItems[0].id == savedItemId);
REQUIRE(allItems[0].name == Test::TEST_ITEM_NAME_1);
REQUIRE(allItems[0].orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(allItems[0].userId == Test::TEST_USER_ID_1);
}
SECTION(
"when repository is created with existing data file then it loads the data")
{
// Given
std::string savedItemId;
{
infrastructure::FileItemRepository firstRepository(testDbPath);
domain::Item testItem = Test::createTestItem();
firstRepository.save(testItem);
savedItemId = firstRepository.save(testItem);
}
// When
infrastructure::FileItemRepository secondRepository(testDbPath);
// Then
auto foundItem = secondRepository.findById(Test::TEST_ITEM_ID_1);
auto foundItem = secondRepository.findById(savedItemId);
REQUIRE(foundItem.has_value());
Test::verifyDefaultTestItem(*foundItem);
REQUIRE(foundItem->id == savedItemId);
REQUIRE(foundItem->name == Test::TEST_ITEM_NAME_1);
REQUIRE(foundItem->orderUrl == Test::TEST_ORDER_URL_1);
REQUIRE(foundItem->userId == Test::TEST_USER_ID_1);
}
SECTION("when repository is created with non-existent data file then it "
@ -356,7 +382,8 @@ TEST_CASE("FileItemRepository Integration Tests",
infrastructure::FileItemRepository repository(nonExistentDbPath);
// Then
auto allItems = repository.findAll();
auto allItems =
repository.findWhere([](const domain::Item&) { return true; });
REQUIRE(allItems.empty());
}

312
cpp17/tests/integration/FileUserRepository.test.cpp

@ -1,312 +0,0 @@
#include "infrastructure/repositories/FileUserRepository.h"
#include "domain/entities/User.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
using namespace nxl::autostore;
using Catch::Matchers::Equals;
namespace Test {
// Constants for magic strings and numbers
constexpr const char* TEST_USER_ID_1 = "user123";
constexpr const char* TEST_USER_ID_2 = "user456";
constexpr const char* TEST_USERNAME_1 = "testuser";
constexpr const char* TEST_USERNAME_2 = "anotheruser";
constexpr const char* TEST_PASSWORD_HASH_1 = "hashedpassword123";
constexpr const char* TEST_PASSWORD_HASH_2 = "hashedpassword456";
constexpr const char* NON_EXISTENT_ID = "nonexistent";
constexpr const char* NON_EXISTENT_USERNAME = "nonexistentuser";
constexpr const char* TEST_DIR_NAME = "autostore_test";
constexpr const char* TEST_DB_FILE_NAME = "test_users.json";
// Helper function to create a test user with default values
domain::User
createTestUser(const std::string& id = TEST_USER_ID_1,
const std::string& username = TEST_USERNAME_1,
const std::string& passwordHash = TEST_PASSWORD_HASH_1)
{
domain::User user;
user.id = id;
user.username = username;
user.passwordHash = passwordHash;
return user;
}
// Helper function to create a second test user
domain::User createSecondTestUser()
{
return createTestUser(TEST_USER_ID_2, TEST_USERNAME_2, TEST_PASSWORD_HASH_2);
}
// Helper function to set up test environment
std::string setupTestEnvironment()
{
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / TEST_DIR_NAME;
std::filesystem::create_directories(testDir);
std::string testDbPath = (testDir / TEST_DB_FILE_NAME).string();
// Clean up any existing test file
if (std::filesystem::exists(testDbPath)) {
std::filesystem::remove(testDbPath);
}
return testDbPath;
}
// Helper function to clean up test environment
void cleanupTestEnvironment()
{
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / TEST_DIR_NAME;
if (std::filesystem::exists(testDir)) {
std::filesystem::remove_all(testDir);
}
}
// Helper function to verify user properties match expected values
void verifyUserProperties(const domain::User& user,
const std::string& expectedId,
const std::string& expectedUsername,
const std::string& expectedPasswordHash)
{
REQUIRE(user.id == expectedId);
REQUIRE(user.username == expectedUsername);
REQUIRE(user.passwordHash == expectedPasswordHash);
}
// Helper function to verify user properties match default test user values
void verifyDefaultTestUser(const domain::User& user)
{
verifyUserProperties(user, TEST_USER_ID_1, TEST_USERNAME_1,
TEST_PASSWORD_HASH_1);
}
// Helper function to verify user properties match second test user values
void verifySecondTestUser(const domain::User& user)
{
verifyUserProperties(user, TEST_USER_ID_2, TEST_USERNAME_2,
TEST_PASSWORD_HASH_2);
}
} // namespace Test
TEST_CASE("FileUserRepository Integration Tests",
"[integration][FileUserRepository]")
{
// Setup test environment
std::string testDbPath = Test::setupTestEnvironment();
SECTION("when a new user is saved then it can be found by id")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
// When
repository.save(testUser);
// Then
auto foundUser = repository.findById(Test::TEST_USER_ID_1);
REQUIRE(foundUser.has_value());
Test::verifyDefaultTestUser(*foundUser);
}
SECTION("when a new user is saved then it can be found by username")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
// When
repository.save(testUser);
// Then
auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1);
REQUIRE(foundUser.has_value());
Test::verifyDefaultTestUser(*foundUser);
}
SECTION("when multiple users are saved then findAll returns all users")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User firstUser = Test::createTestUser();
domain::User secondUser = Test::createSecondTestUser();
// When
repository.save(firstUser);
repository.save(secondUser);
// Then
auto allUsers = repository.findAll();
REQUIRE(allUsers.size() == 2);
// Verify both users are present (order doesn't matter)
bool foundFirst = false;
bool foundSecond = false;
for (const auto& user : allUsers) {
if (user.id == Test::TEST_USER_ID_1) {
Test::verifyDefaultTestUser(user);
foundFirst = true;
} else if (user.id == Test::TEST_USER_ID_2) {
Test::verifySecondTestUser(user);
foundSecond = true;
}
}
REQUIRE(foundFirst);
REQUIRE(foundSecond);
}
SECTION("when an existing user is saved then it is updated")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
repository.save(testUser);
// When
testUser.username = "updatedusername";
testUser.passwordHash = "updatedpasswordhash";
repository.save(testUser);
// Then
auto foundUser = repository.findById(Test::TEST_USER_ID_1);
REQUIRE(foundUser.has_value());
REQUIRE(foundUser->id == Test::TEST_USER_ID_1);
REQUIRE(foundUser->username == "updatedusername");
REQUIRE(foundUser->passwordHash == "updatedpasswordhash");
}
SECTION("when a user is removed then it cannot be found by id")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
repository.save(testUser);
// When
repository.remove(Test::TEST_USER_ID_1);
// Then
auto foundUser = repository.findById(Test::TEST_USER_ID_1);
REQUIRE_FALSE(foundUser.has_value());
}
SECTION("when a user is removed then it cannot be found by username")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
repository.save(testUser);
// When
repository.remove(Test::TEST_USER_ID_1);
// Then
auto foundUser = repository.findByUsername(Test::TEST_USERNAME_1);
REQUIRE_FALSE(foundUser.has_value());
}
SECTION("when a user is removed then it is not in findAll")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User firstUser = Test::createTestUser();
domain::User secondUser = Test::createSecondTestUser();
repository.save(firstUser);
repository.save(secondUser);
// When
repository.remove(Test::TEST_USER_ID_1);
// Then
auto allUsers = repository.findAll();
REQUIRE(allUsers.size() == 1);
Test::verifySecondTestUser(allUsers[0]);
}
SECTION(
"when findById is called with non-existent id then it returns nullopt")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
// When
auto foundUser = repository.findById(Test::NON_EXISTENT_ID);
// Then
REQUIRE_FALSE(foundUser.has_value());
}
SECTION("when findByUsername is called with non-existent username then it "
"returns nullopt")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
// When
auto foundUser = repository.findByUsername(Test::NON_EXISTENT_USERNAME);
// Then
REQUIRE_FALSE(foundUser.has_value());
}
SECTION("when remove is called with non-existent id then it does nothing")
{
// Given
infrastructure::FileUserRepository repository(testDbPath);
domain::User testUser = Test::createTestUser();
repository.save(testUser);
// When
repository.remove(Test::NON_EXISTENT_ID);
// Then
auto allUsers = repository.findAll();
REQUIRE(allUsers.size() == 1);
Test::verifyDefaultTestUser(allUsers[0]);
}
SECTION(
"when repository is created with existing data file then it loads the data")
{
// Given
{
infrastructure::FileUserRepository firstRepository(testDbPath);
domain::User testUser = Test::createTestUser();
firstRepository.save(testUser);
}
// When
infrastructure::FileUserRepository secondRepository(testDbPath);
// Then
auto foundUser = secondRepository.findById(Test::TEST_USER_ID_1);
REQUIRE(foundUser.has_value());
Test::verifyDefaultTestUser(*foundUser);
}
SECTION("when repository is created with non-existent data file then it "
"starts empty")
{
// Given
std::filesystem::path testDir =
std::filesystem::temp_directory_path() / Test::TEST_DIR_NAME;
std::string nonExistentDbPath = (testDir / "nonexistent.json").string();
// When
infrastructure::FileUserRepository repository(nonExistentDbPath);
// Then
auto allUsers = repository.findAll();
REQUIRE(allUsers.empty());
}
// Clean up test environment
Test::cleanupTestEnvironment();
}

19
cpp17/tests/mocks/MockBlocker.h

@ -0,0 +1,19 @@
#pragma once
#include "application/interfaces/IBlocker.h"
#include <trompeloeil.hpp>
namespace test {
class MockBlocker : public nxl::autostore::application::IBlocker
{
public:
MAKE_MOCK0(block, void(), override);
MAKE_MOCK1(blockFor, void(const std::chrono::milliseconds&), override);
MAKE_MOCK1(blockUntil, void(const TimePoint&), override);
MAKE_MOCK0(notify, void(), override);
MAKE_MOCK0(isBlocked, bool(), override);
MAKE_MOCK0(wasNotified, bool(), override);
};
} // namespace test

22
cpp17/tests/mocks/MockItemRepository.h

@ -0,0 +1,22 @@
#pragma once
#include "application/interfaces/IItemRepository.h"
#include <trompeloeil.hpp>
namespace test {
using nxl::autostore::domain::Item;
using nxl::autostore::domain::User;
class MockItemRepository : public nxl::autostore::application::IItemRepository
{
public:
MAKE_MOCK1(save, Item::Id_t(const Item&), override);
MAKE_MOCK1(findById, std::optional<Item>(Item::Id_t), override);
MAKE_MOCK1(findByOwner, std::vector<Item>(User::Id_t), override);
MAKE_MOCK1(findWhere, std::vector<Item>(std::function<bool(const Item&)>),
override);
MAKE_MOCK1(remove, void(Item::Id_t), override);
};
} // namespace test

14
cpp17/tests/mocks/MockOrderService.h

@ -0,0 +1,14 @@
#pragma once
#include "application/interfaces/IOrderService.h"
#include <trompeloeil.hpp>
namespace test {
class MockOrderService : public nxl::autostore::application::IOrderService
{
public:
MAKE_MOCK1(orderItem, void(const nxl::autostore::domain::Item&), override);
};
} // namespace test

24
cpp17/tests/mocks/MockThreadManager.h

@ -0,0 +1,24 @@
#pragma once
#include "application/interfaces/IThreadManager.h"
#include <trompeloeil.hpp>
namespace test {
class MockThreadHandle
: public nxl::autostore::application::IThreadManager::ThreadHandle
{
public:
MAKE_MOCK0(join, void(), override);
MAKE_CONST_MOCK0(joinable, bool(), override);
};
class MockThreadManager : public nxl::autostore::application::IThreadManager
{
public:
MAKE_MOCK1(createThread, ThreadHandlePtr(std::function<void()>), override);
MAKE_CONST_MOCK0(getCurrentThreadId, std::thread::id(), override);
MAKE_MOCK1(sleep, void(const std::chrono::milliseconds&), override);
};
} // namespace test

16
cpp17/tests/mocks/MockTimeProvider.h

@ -0,0 +1,16 @@
#pragma once
#include "application/interfaces/ITimeProvider.h"
#include <trompeloeil.hpp>
namespace test {
class MockTimeProvider : public nxl::autostore::application::ITimeProvider
{
public:
MAKE_MOCK0(now, Clock::time_point(), const override);
MAKE_MOCK1(to_tm, std::tm(const Clock::time_point&), const override);
MAKE_MOCK1(from_tm, Clock::time_point(const std::tm&), const override);
};
} // namespace test

51
cpp17/tests/mocks/TestLogger.h

@ -0,0 +1,51 @@
#pragma once
#include <autostore/ILogger.h>
#include <iostream>
#include <mutex>
namespace test {
class TestLogger : public nxl::autostore::ILogger
{
public:
TestLogger() = default;
virtual ~TestLogger() = default;
void log(LogLevel level, std::string_view message) override;
void vlog(int8_t, std::string_view message) override;
private:
std::mutex mutex_;
};
void TestLogger::log(LogLevel level, std::string_view message)
{
std::lock_guard<std::mutex> lock(mutex_);
const char* levelStr = "";
switch (level) {
case LogLevel::Info:
levelStr = "INFO";
break;
case LogLevel::Warning:
levelStr = "WARNING";
break;
case LogLevel::Error:
levelStr = "ERROR";
break;
case LogLevel::Debug:
levelStr = "DEBUG";
break;
case LogLevel::Verbose:
levelStr = "VERBOSE";
break;
}
std::cout << "[" << levelStr << "] " << message << std::endl;
}
void TestLogger::vlog(int8_t level, std::string_view message)
{
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[V" << static_cast<int>(level) << "] " << message << std::endl;
}
} // namespace test

223
cpp17/tests/unit/AddItem.test.cpp

@ -0,0 +1,223 @@
#include "application/commands/AddItem.h"
#include "domain/entities/Item.h"
#include "mocks/MockItemRepository.h"
#include "mocks/MockTimeProvider.h"
#include "mocks/MockOrderService.h"
#include "helpers/AddItemTestHelpers.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <trompeloeil.hpp>
#include <memory>
#include <stdexcept>
using trompeloeil::_;
using namespace nxl::autostore;
using namespace std::chrono;
TEST_CASE("AddItem Unit Tests", "[unit][AddItem]")
{
test::MockItemRepository mockRepository;
test::MockTimeProvider mockClock;
test::MockOrderService mockOrderService;
SECTION(
"when user id is present and item is not expired then the item is saved")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == expectedItemId);
}
SECTION("when item has null user id then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
testItem.userId = domain::User::NULL_ID;
FORBID_CALL(mockRepository, save(_));
FORBID_CALL(mockClock, now());
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when item is expired then the order is placed")
{
// Given
auto testItem = test::createExpiredTestItem();
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Order was placed (verified by REQUIRE_CALL above)
}
SECTION("when item is expired then null id is returned")
{
// Given
auto testItem = test::createExpiredTestItem();
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == domain::Item::NULL_ID);
}
SECTION("when item expiration date is exactly current time then the order is "
"placed")
{
// Given
auto testItem = test::createTestItem();
testItem.expirationDate = test::TEST_TIMEPOINT_NOW;
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Order was placed (verified by REQUIRE_CALL above)
}
SECTION("when item expiration date is exactly current time then null id is "
"returned")
{
// Given
auto testItem = test::createTestItem();
testItem.expirationDate = test::TEST_TIMEPOINT_NOW;
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_));
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == domain::Item::NULL_ID);
}
SECTION("when item expiration date is in the future then the item is saved")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
addItem.execute(std::move(testItem));
// Then
// Item was saved (verified by REQUIRE_CALL above)
}
SECTION(
"when item expiration date is in the future then the item id is returned")
{
// Given
auto testItem = test::createTestItem();
auto expectedItemId = "saved_item_id";
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).RETURN(expectedItemId);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When
auto resultItemId = addItem.execute(std::move(testItem));
// Then
REQUIRE(resultItemId == expectedItemId);
}
SECTION(
"when repository save throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
auto expectedException = std::runtime_error("Repository error");
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockRepository, save(testItem)).THROW(expectedException);
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when order service throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createExpiredTestItem();
auto expectedException = std::runtime_error("Order service error");
REQUIRE_CALL(mockClock, now()).RETURN(test::TEST_TIMEPOINT_NOW);
REQUIRE_CALL(mockOrderService, orderItem(_)).THROW(expectedException);
FORBID_CALL(mockRepository, save(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
SECTION("when clock throws exception then a runtime error is thrown")
{
// Given
auto testItem = test::createTestItem();
auto expectedException = std::runtime_error("Clock error");
REQUIRE_CALL(mockClock, now()).THROW(expectedException);
FORBID_CALL(mockRepository, save(_));
FORBID_CALL(mockOrderService, orderItem(_));
application::AddItem addItem(mockRepository, mockClock, mockOrderService);
// When & Then
REQUIRE_THROWS_AS(addItem.execute(std::move(testItem)), std::runtime_error);
}
}

326
cpp17/tests/unit/TaskScheduler.test.cpp

@ -0,0 +1,326 @@
#include "infrastructure/services/TaskScheduler.h"
#include "mocks/TestLogger.h"
#include "mocks/MockTimeProvider.h"
#include "mocks/MockThreadManager.h"
#include "mocks/MockBlocker.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <memory>
#include <atomic>
using trompeloeil::_;
using namespace nxl::autostore;
using namespace std::chrono;
using nxl::autostore::infrastructure::TaskScheduler;
namespace test {
// Fixed test timepoint: 2020-01-01 12:00
constexpr std::chrono::system_clock::time_point TIMEPOINT_NOW =
std::chrono::system_clock::time_point(std::chrono::seconds(1577880000));
} // namespace test
TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
{
// Common mock objects that all sections can use
auto logger = std::make_shared<test::TestLogger>();
auto timeProvider = std::make_shared<test::MockTimeProvider>();
auto threadMgr = std::make_shared<test::MockThreadManager>();
auto blocker = std::make_shared<test::MockBlocker>();
SECTION("when start is called then createThread is called")
{
// Given
// Expect createThread to be called
REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
// When
scheduler.start();
}
SECTION("when scheduler is created then it is not running")
{
// When
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
// Then - calling stop on a non-running scheduler should not cause issues
// and no thread operations should be called
FORBID_CALL(*threadMgr, createThread(_));
scheduler.stop();
}
SECTION("when task is scheduled with OnStart mode then it executes "
"immediately after start")
{
// Given
bool taskExecuted = false;
std::function<void()> threadFn;
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>())
.LR_SIDE_EFFECT(threadFn = std::move(_1));
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
FORBID_CALL(*blocker, blockFor(_));
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
auto taskFunction = [&]() {
taskExecuted = true;
scheduler.stop(); // prevent infinite loop in threadFn
};
// When
scheduler.schedule(taskFunction, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.start();
threadFn();
// Then
REQUIRE(taskExecuted);
scheduler.stop();
}
SECTION(
"when task is scheduled with Once mode then it executes at specified time")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
bool taskExecuted = false;
std::function<void()> threadFn;
auto currentTime = test::TIMEPOINT_NOW; // current "now", starts at 12:00
std::chrono::seconds timeDelta{5};
std::chrono::milliseconds actualDelay{0};
auto initialTime = test::TIMEPOINT_NOW;
auto expectedExecutionTime = initialTime + timeDelta;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - return initial time first, then execution time
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls, save delay value
ALLOW_CALL(*blocker, blockFor(_))
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow
);
ALLOW_CALL(*blocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
auto taskFunction = [&]() {
taskExecuted = true;
scheduler.stop(); // prevent infinite loop in threadFn
};
// When
scheduler.schedule(taskFunction, 12, 0, timeDelta.count(),
TaskScheduler::RunMode::Once);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(taskExecuted);
REQUIRE(actualDelay == timeDelta);
}
SECTION("when task is scheduled with Forever and OnStart mode then it "
"executes repeatedly")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
int executionCount = 0;
auto currentTime = test::TIMEPOINT_NOW;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*blocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
auto taskFunction = [&]() {
executionCount++;
if (executionCount >= 3) {
scheduler.stop(); // stop after 3 executions
}
};
// When
scheduler.schedule(taskFunction, 0, 0, 0,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(executionCount >= 3);
}
SECTION("when invalid time parameters are provided then exception is thrown")
{
// Given
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
// When & Then - invalid hour
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, -1, 0, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 24, 0, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
// When & Then - invalid minute
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, -1, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 60, 0, TaskScheduler::RunMode::Once),
std::invalid_argument);
// When & Then - invalid second
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 0, -1, TaskScheduler::RunMode::Once),
std::invalid_argument);
REQUIRE_THROWS_AS(
scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once),
std::invalid_argument);
}
// std::invalid_argument);
// }
SECTION("when invalid mode combination is used then exception is thrown")
{
// Given
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
// When & Then
REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::Once),
std::invalid_argument);
}
SECTION("when multiple tasks are scheduled then all execute")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
bool task1Executed = false;
bool task2Executed = false;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
// Allow blocker calls
ALLOW_CALL(*blocker, blockFor(_));
ALLOW_CALL(*blocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
auto taskFunction1 = [&]() { task1Executed = true; };
auto taskFunction2 = [&]() {
task2Executed = true;
scheduler.stop(); // stop after both tasks have had a chance to execute
};
// When
scheduler.schedule(taskFunction1, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.schedule(taskFunction2, 0, 0, 0, TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(task1Executed);
REQUIRE(task2Executed);
}
// }
SECTION("when task is scheduled with Forever mode then it repeats")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
int executionCount = 0;
auto currentTime = test::TIMEPOINT_NOW;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - simulate time advancing
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*blocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker);
auto taskFunction = [&]() {
executionCount++;
if (executionCount >= 2) {
scheduler.stop(); // stop after 2 executions
}
};
// Schedule task to run at a specific time (not immediately) and repeat
// forever This ensures the task doesn't get stuck in an infinite OnStart
// loop
scheduler.schedule(taskFunction, 12, 0, 1, TaskScheduler::RunMode::Forever);
// When
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then
REQUIRE(executionCount >= 2);
}
// }
}

4
cpp17/vcpkg.json

@ -4,7 +4,9 @@
"dependencies": [
"cpp-httplib",
"nlohmann-json",
"jwt-cpp",
"spdlog",
"catch2"
"catch2",
"trompeloeil"
]
}

250
openapi.yaml

@ -7,56 +7,6 @@ servers:
- url: http://localhost:3000/api/v1
description: Development server
paths:
/register:
post:
summary: Register a new user
description: Creates a new user account and returns a JWT token
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- username
- password
properties:
username:
type: string
description: User's username or email
password:
type: string
description: User's password
responses:
'201':
description: User successfully registered
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: object
properties:
user:
$ref: '#/components/schemas/User'
token:
type: string
description: JWT token for authentication
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'409':
description: Username already exists
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/login:
post:
@ -103,193 +53,6 @@ paths:
schema:
$ref: '#/components/schemas/JsendError'
/users:
get:
summary: Get list of users
description: Returns a list of all users (requires authentication)
security:
- bearerAuth: []
responses:
'200':
description: List of users
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/users/{id}:
get:
summary: Get user by ID
description: Returns a specific user by their ID (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
responses:
'200':
description: User details
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
post:
summary: Create a new user
description: Creates a new user (admin functionality, requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'409':
description: User already exists
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
put:
summary: Update a user
description: Updates an existing user (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'200':
description: User updated successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
delete:
summary: Delete a user
description: Deletes an existing user (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
responses:
'204':
description: User deleted successfully
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/items:
get:
@ -510,19 +273,6 @@ components:
type: string
description: User's username or email
UserInput:
type: object
required:
- username
- password
properties:
username:
type: string
description: User's username or email
password:
type: string
description: User's password
Item:
type: object
properties:

8
testing/http-echo-server/docker-compose.yml

@ -0,0 +1,8 @@
version: '3'
services:
http-echo-listener:
image: mendhak/http-https-echo:37
environment:
- HTTP_PORT=8888
ports:
- "8888:8888"

22
testing/tavern/export.sh

@ -0,0 +1,22 @@
if [ -d .venv ]; then
source .venv/bin/activate
else
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fi
export TEST_SERVER_ADDRESS="127.0.0.1"
export TEST_SERVER_PORT="8080"
export TEST_API_BASE="api/v1"
export TEST_ORDER_URL="http://192.168.20.2:8888/"
export TEST_USER1_ID="1000"
export TEST_USER1_LOGIN="admin"
export TEST_USER1_PASSWORD="admin"
export TEST_USER2_ID="1001"
export TEST_USER2_LOGIN="user"
export TEST_USER2_PASSWORD="user"
export TEST_WRONG_USER_ID="999"
export TEST_ITEM_ID="secret"

5
testing/tavern/requirements.txt

@ -0,0 +1,5 @@
pyyaml
tavern
tavern[mqtt]
tavern[grpc]
allure-pytest

13
testing/tavern/tavern-run-all.sh

@ -0,0 +1,13 @@
#!/usr/bin/env bash
if [ -z "$TEST_SERVER_ADDRESS" ]; then
source export.sh
fi
# tavern-ci --alluredir=reports test_plans/users_api.tavern.yaml
tavern-ci --alluredir=reports test_plans/items_api.tavern.yaml
allure generate --clean --single-file --output /tmp/vm-allure-report --name index.html reports
# allure package: https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb

16
testing/tavern/tavern-run-single.sh

@ -0,0 +1,16 @@
#!/usr/bin/env bash
if [ -z "$1" ]; then
echo "Usage: $0 <test plan>"
exit 1
fi
if [ -z "$TEST_SERVER_ADDRESS" ]; then
source export.sh
fi
tavern-ci --alluredir=reports $1
allure generate --clean --single-file --output /tmp/vm-allure-report --name index.html reports
# allure package: https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb

14
testing/tavern/test_plans/includes.yaml

@ -0,0 +1,14 @@
variables:
server_address: "{tavern.env_vars.TEST_SERVER_ADDRESS}"
server_port: "{tavern.env_vars.TEST_SERVER_PORT}"
api_base: "{tavern.env_vars.TEST_API_BASE}"
order_url: "{tavern.env_vars.TEST_ORDER_URL}"
user1_id: "{tavern.env_vars.TEST_USER1_ID}"
user1_login: "{tavern.env_vars.TEST_USER1_LOGIN}"
user1_password: "{tavern.env_vars.TEST_USER1_PASSWORD}"
user2_id: "{tavern.env_vars.TEST_USER2_ID}"
user2_login: "{tavern.env_vars.TEST_USER2_LOGIN}"
user2_password: "{tavern.env_vars.TEST_USER2_PASSWORD}"
wrong_user_id: "{tavern.env_vars.TEST_WRONG_USER_ID}"
item_id: "{tavern.env_vars.TEST_ITEM_ID}"

200
testing/tavern/test_plans/items_api.tavern.yaml

@ -0,0 +1,200 @@
test_name: "Items management"
includes:
- !include includes.yaml
strict:
- headers:off
- json:off
stages:
- name: "Login as user1 and get JWT token"
request:
url: "http://{server_address}:{server_port}/{api_base}/login"
method: POST
json:
username: "{user1_login}"
password: "{user1_password}"
response:
status_code: 200
json:
status: success
data:
token: !anything
save:
json:
user_token: "data.token"
- name: "Add non-expired item 1"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: POST
headers:
Authorization: "Bearer {user_token}"
json:
name: "Tavern Test Item"
expirationDate: "2050-08-10T14:00:00"
orderUrl: "{order_url}"
response:
status_code: 201
json:
status: success
data:
id: !anything
save:
json:
item_id: "data.id"
- name: "Add non-expired item 2"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: POST
headers:
Authorization: "Bearer {user_token}"
json:
name: "Tavern Test Item"
expirationDate: "2050-08-10T14:00:00"
orderUrl: "{order_url}"
response:
status_code: 201
json:
status: success
data:
id: !anything
save:
json:
item_id2: "data.id"
- name: "Add expired item"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: POST
headers:
Authorization: "Bearer {user_token}"
json:
name: "Tavern Test Item"
expirationDate: "2000-08-10T14:00:00"
orderUrl: "{order_url}"
response:
status_code: 201
json:
status: success
data:
id: !anything
- name: "Get item list"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: GET
headers:
Authorization: "Bearer {user_token}"
response:
status_code: 200
json:
status: "success"
data: !anylist
- name: "Get single existing item"
request:
url: "http://{server_address}:{server_port}/{api_base}/items/{item_id}"
method: GET
headers:
Authorization: "Bearer {user_token}"
response:
status_code: 200
json:
status: "success"
data:
id: !anything
- name: "Get single non-existing item"
request:
url: "http://{server_address}:{server_port}/{api_base}/items/9999"
method: GET
headers:
Authorization: "Bearer {user_token}"
response:
status_code: 404
- name: "Delete item"
request:
url: "http://{server_address}:{server_port}/{api_base}/items/{item_id}"
method: DELETE
headers:
Authorization: "Bearer {user_token}"
response:
status_code: 204
# login as user2 and test item access restrictions
- name: "Login as user2 and get JWT token"
request:
url: "http://{server_address}:{server_port}/{api_base}/login"
method: POST
json:
username: "{user2_login}"
password: "{user2_password}"
response:
status_code: 200
json:
status: success
data:
token: !anything
save:
json:
user2_token: "data.token"
- name: "User2 tries to access item2 created by user1 (should fail)"
request:
url: "http://{server_address}:{server_port}/{api_base}/items/{item_id2}"
method: GET
headers:
Authorization: "Bearer {user2_token}"
response:
status_code: 404
- name: "User2 tries to delete item2 created by user1 (should fail)"
request:
url: "http://{server_address}:{server_port}/{api_base}/items/{item_id2}"
method: DELETE
headers:
Authorization: "Bearer {user2_token}"
response:
status_code: 404
- name: "User2 adds own item"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: POST
headers:
Authorization: "Bearer {user2_token}"
json:
name: "User2 Tavern Test Item"
expirationDate: "2050-08-10T14:00:00"
orderUrl: "{order_url}"
response:
status_code: 201
json:
status: success
data:
id: !anything
save:
json:
user2_item_id: "data.id"
- name: "User2 gets item list (should only see own items)"
request:
url: "http://{server_address}:{server_port}/{api_base}/items"
method: GET
headers:
Authorization: "Bearer {user2_token}"
response:
status_code: 200
json:
status: "success"
data:
- !anydict
id: "{user2_item_id}"
name: "User2 Tavern Test Item"
Loading…
Cancel
Save