84 changed files with 2946 additions and 976 deletions
@ -0,0 +1,12 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"username": "admin", |
||||||
|
"password": "admin", |
||||||
|
"id": "1000" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"username": "user", |
||||||
|
"password": "user", |
||||||
|
"id": "1001" |
||||||
|
} |
||||||
|
] |
||||||
@ -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 |
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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 "HttpOrderService.h" |
||||||
|
#include "autostore/Version.h" |
||||||
|
#include <httplib.h> |
||||||
#include <stdexcept> |
#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 { |
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()) { |
static const std::regex url_regex( |
||||||
throw std::runtime_error("Order URL is empty for item: " + item.name); |
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 payload = |
std::string host = matches[2].str(); |
||||||
R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}"; |
std::string port = matches[3].str(); |
||||||
sendPostRequest(item.orderUrl, payload); |
std::string path = matches[4].str(); |
||||||
|
std::string query = matches[5].str(); |
||||||
|
|
||||||
|
if (!port.empty()) { |
||||||
|
host += ":" + port; |
||||||
|
} |
||||||
|
|
||||||
|
if (path.empty()) { |
||||||
|
path = "/"; |
||||||
|
} |
||||||
|
|
||||||
|
path += query; |
||||||
|
return {host, path}; |
||||||
} |
} |
||||||
|
|
||||||
void HttpOrderService::sendPostRequest(std::string_view url, |
std::string createOrderPayload(const domain::Item& item) |
||||||
std::string_view payload) |
|
||||||
{ |
{ |
||||||
// In a real implementation, this would use an HTTP client library
|
// Escape JSON special characters in strings
|
||||||
// For now, we'll simulate the HTTP call
|
auto escapeJson = [](const std::string& str) { |
||||||
log->i("POST request to: %s", url); |
std::string escaped; |
||||||
log->v(1, "Payload: %s", payload); |
escaped.reserve(str.size() + 10); // Reserve extra space for escapes
|
||||||
|
|
||||||
// Simulate HTTP error handling
|
for (char c : str) { |
||||||
if (url.find("error") != std::string::npos) { |
switch (c) { |
||||||
throw std::runtime_error("Failed to send order request to: " |
case '"': |
||||||
+ std::string(url)); |
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) |
||||||
|
{ |
||||||
|
if (item.orderUrl.empty()) { |
||||||
|
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||||
|
} |
||||||
|
|
||||||
|
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
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -1,27 +1,40 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
|
#include "webapi/controllers/BaseController.h" |
||||||
#include "application/commands/AddItem.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 { |
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
class StoreController |
class StoreController : public BaseController |
||||||
{ |
{ |
||||||
public: |
public: |
||||||
struct Context |
struct Context |
||||||
{ |
{ |
||||||
application::AddItem addItemUc; |
application::AddItem addItemUc; |
||||||
|
application::ListItems listItemsUc; |
||||||
|
application::GetItem getItemUc; |
||||||
|
application::DeleteItem deleteItemUc; |
||||||
|
application::IAuthService& authService; |
||||||
}; |
}; |
||||||
|
|
||||||
StoreController(Context&& context); |
StoreController(Context&& context); |
||||||
|
|
||||||
void registerRoutes(httplib::Server& server); |
protected: |
||||||
|
std::vector<RouteConfig> getRoutes() const override; |
||||||
|
|
||||||
private: |
private: |
||||||
void addItem(const httplib::Request& req, httplib::Response& res); |
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: |
void assertUserId(std::optional<domain::User::Id_t> userId) const; |
||||||
Context context; |
|
||||||
}; |
}; |
||||||
|
|
||||||
} // namespace nxl::autostore::webapi
|
} // namespace nxl::autostore::webapi
|
||||||
@ -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
|
||||||
@ -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(); |
|
||||||
} |
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
// }
|
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
version: '3' |
||||||
|
services: |
||||||
|
http-echo-listener: |
||||||
|
image: mendhak/http-https-echo:37 |
||||||
|
environment: |
||||||
|
- HTTP_PORT=8888 |
||||||
|
ports: |
||||||
|
- "8888:8888" |
||||||
@ -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" |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
pyyaml |
||||||
|
tavern |
||||||
|
tavern[mqtt] |
||||||
|
tavern[grpc] |
||||||
|
allure-pytest |
||||||
@ -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 |
||||||
@ -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 |
||||||
@ -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}" |
||||||
@ -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…
Reference in new issue