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 "autostore/Version.h" |
||||
#include <httplib.h> |
||||
#include <stdexcept> |
||||
#include <regex> |
||||
#include <thread> |
||||
#include <queue> |
||||
#include <mutex> |
||||
#include <condition_variable> |
||||
#include <atomic> |
||||
#include <chrono> |
||||
#include <memory> |
||||
#include <unordered_map> |
||||
|
||||
namespace nxl::autostore::infrastructure { |
||||
|
||||
HttpOrderService::HttpOrderService(ILoggerPtr logger) : log{std::move(logger)} |
||||
{} |
||||
namespace { |
||||
|
||||
void HttpOrderService::orderItem(const domain::Item& item) |
||||
constexpr int MAX_RETRIES = 3; |
||||
constexpr int CONNECTION_TIMEOUT_SECONDS = 5; |
||||
constexpr int READ_TIMEOUT_SECONDS = 5; |
||||
constexpr int WRITE_TIMEOUT_SECONDS = 5; |
||||
constexpr char CONTENT_TYPE_JSON[] = "application/json"; |
||||
|
||||
std::pair<std::string, std::string> parseUrl(const std::string& url) |
||||
{ |
||||
if (item.orderUrl.empty()) { |
||||
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||
static const std::regex url_regex( |
||||
R"(^(https?:\/\/)?([^\/:]+)(?::(\d+))?(\/[^\?]*)?(\?.*)?$)"); |
||||
|
||||
std::smatch matches; |
||||
if (!std::regex_match(url, matches, url_regex) || matches.size() < 5) { |
||||
throw std::runtime_error("Invalid URL format: " + url); |
||||
} |
||||
|
||||
std::string host = matches[2].str(); |
||||
std::string port = matches[3].str(); |
||||
std::string path = matches[4].str(); |
||||
std::string query = matches[5].str(); |
||||
|
||||
if (!port.empty()) { |
||||
host += ":" + port; |
||||
} |
||||
|
||||
std::string payload = |
||||
R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}"; |
||||
sendPostRequest(item.orderUrl, payload); |
||||
if (path.empty()) { |
||||
path = "/"; |
||||
} |
||||
|
||||
path += query; |
||||
return {host, path}; |
||||
} |
||||
|
||||
void HttpOrderService::sendPostRequest(std::string_view url, |
||||
std::string_view payload) |
||||
std::string createOrderPayload(const domain::Item& item) |
||||
{ |
||||
// Escape JSON special characters in strings
|
||||
auto escapeJson = [](const std::string& str) { |
||||
std::string escaped; |
||||
escaped.reserve(str.size() + 10); // Reserve extra space for escapes
|
||||
|
||||
for (char c : str) { |
||||
switch (c) { |
||||
case '"': |
||||
escaped += "\\\""; |
||||
break; |
||||
case '\\': |
||||
escaped += "\\\\"; |
||||
break; |
||||
case '\b': |
||||
escaped += "\\b"; |
||||
break; |
||||
case '\f': |
||||
escaped += "\\f"; |
||||
break; |
||||
case '\n': |
||||
escaped += "\\n"; |
||||
break; |
||||
case '\r': |
||||
escaped += "\\r"; |
||||
break; |
||||
case '\t': |
||||
escaped += "\\t"; |
||||
break; |
||||
default: |
||||
escaped += c; |
||||
break; |
||||
} |
||||
} |
||||
return escaped; |
||||
}; |
||||
|
||||
return R"({"itemName": ")" + escapeJson(item.name) + R"(", "itemId": ")" |
||||
+ escapeJson(item.id) + "\"}"; |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
struct OrderRequest |
||||
{ |
||||
std::string url; |
||||
std::string payload; |
||||
int retryCount = 0; |
||||
std::chrono::system_clock::time_point nextAttemptTime; |
||||
|
||||
OrderRequest() = default; |
||||
OrderRequest(std::string url, std::string payload, int rc = 0, |
||||
std::chrono::system_clock::time_point nat = |
||||
std::chrono::system_clock::now()) |
||||
: url{std::move(url)}, payload{std::move(payload)}, retryCount{rc}, |
||||
nextAttemptTime(nat) |
||||
{} |
||||
}; |
||||
|
||||
class HttpOrderService::Impl |
||||
{ |
||||
public: |
||||
explicit Impl(ILoggerPtr logger) |
||||
: log{std::move(logger)}, shutdownRequested{false} |
||||
{ |
||||
if (!log) { |
||||
throw std::invalid_argument("Logger cannot be null"); |
||||
} |
||||
|
||||
userAgent = "Autostore/" + nxl::getVersionString(); |
||||
workerThread = std::thread(&Impl::processQueue, this); |
||||
} |
||||
|
||||
~Impl() |
||||
{ |
||||
shutdown(); |
||||
if (workerThread.joinable()) { |
||||
workerThread.join(); |
||||
} |
||||
} |
||||
|
||||
void enqueueOrder(const std::string& url, std::string payload) |
||||
{ |
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
if (shutdownRequested) { |
||||
throw std::runtime_error( |
||||
"Service is shutting down, cannot enqueue new orders"); |
||||
} |
||||
orderQueue.emplace(url, std::move(payload)); |
||||
} |
||||
queueCondition.notify_one(); |
||||
} |
||||
|
||||
private: |
||||
void shutdown() |
||||
{ |
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
shutdownRequested = true; |
||||
} |
||||
queueCondition.notify_one(); |
||||
} |
||||
|
||||
bool shouldShutdown() const |
||||
{ |
||||
return shutdownRequested && orderQueue.empty(); |
||||
} |
||||
|
||||
bool isRequestReady(const OrderRequest& request) const |
||||
{ |
||||
return request.nextAttemptTime <= std::chrono::system_clock::now(); |
||||
} |
||||
|
||||
void processQueue() |
||||
{ |
||||
while (true) { |
||||
std::unique_lock<std::mutex> lock(queueMutex); |
||||
|
||||
// Wait for orders or shutdown signal
|
||||
queueCondition.wait( |
||||
lock, [this] { return !orderQueue.empty() || shutdownRequested; }); |
||||
|
||||
if (shouldShutdown()) { |
||||
break; |
||||
} |
||||
|
||||
if (orderQueue.empty()) { |
||||
continue; |
||||
} |
||||
|
||||
// Check if the front request is ready to be processed
|
||||
if (!isRequestReady(orderQueue.front())) { |
||||
// Wait until the next attempt time
|
||||
auto waitTime = |
||||
orderQueue.front().nextAttemptTime - std::chrono::system_clock::now(); |
||||
if (waitTime > std::chrono::milliseconds(0)) { |
||||
queueCondition.wait_for(lock, waitTime); |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
// Extract request for processing
|
||||
OrderRequest request = std::move(orderQueue.front()); |
||||
orderQueue.pop(); |
||||
|
||||
// Release lock before processing to avoid blocking other operations
|
||||
lock.unlock(); |
||||
|
||||
processRequest(request); |
||||
} |
||||
} |
||||
|
||||
void processRequest(OrderRequest& request) |
||||
{ |
||||
try { |
||||
sendPostRequest(request.url, request.payload); |
||||
log->i("Order request sent successfully to: %s", request.url.c_str()); |
||||
} catch (const std::exception& e) { |
||||
log->e("Failed to send order request to %s: %s", request.url.c_str(), |
||||
e.what()); |
||||
handleFailedRequest(request); |
||||
} |
||||
} |
||||
|
||||
void handleFailedRequest(OrderRequest& request) |
||||
{ |
||||
if (request.retryCount < MAX_RETRIES) { |
||||
request.retryCount++; |
||||
// Exponential backoff: 1s, 2s, 4s, 8s...
|
||||
auto delay = std::chrono::seconds(1 << (request.retryCount - 1)); |
||||
request.nextAttemptTime = std::chrono::system_clock::now() + delay; |
||||
|
||||
log->w("Retrying order request to %s (attempt %d/%d) in %ld seconds", |
||||
request.url.c_str(), request.retryCount, MAX_RETRIES, |
||||
delay.count()); |
||||
|
||||
{ |
||||
std::lock_guard<std::mutex> lock(queueMutex); |
||||
if (!shutdownRequested) { |
||||
orderQueue.push(std::move(request)); |
||||
} |
||||
} |
||||
queueCondition.notify_one(); |
||||
} else { |
||||
log->e("Max retries exceeded for order request to: %s", |
||||
request.url.c_str()); |
||||
} |
||||
} |
||||
|
||||
std::shared_ptr<httplib::Client> getOrCreateClient(const std::string& host) |
||||
{ |
||||
std::lock_guard<std::mutex> lock(clientsMutex); |
||||
|
||||
auto it = clients.find(host); |
||||
if (it != clients.end()) { |
||||
// Check if client is still valid
|
||||
auto client = it->second.lock(); |
||||
if (client) { |
||||
return client; |
||||
} else { |
||||
// Remove expired weak_ptr
|
||||
clients.erase(it); |
||||
} |
||||
} |
||||
|
||||
// Create new client
|
||||
auto client = std::make_shared<httplib::Client>(host); |
||||
configureClient(*client); |
||||
clients[host] = client; |
||||
return client; |
||||
} |
||||
|
||||
void configureClient(httplib::Client& client) |
||||
{ |
||||
client.set_connection_timeout(CONNECTION_TIMEOUT_SECONDS, 0); |
||||
client.set_read_timeout(READ_TIMEOUT_SECONDS, 0); |
||||
client.set_write_timeout(WRITE_TIMEOUT_SECONDS, 0); |
||||
|
||||
// Enable keep-alive for better performance
|
||||
client.set_keep_alive(true); |
||||
|
||||
// Set reasonable limits
|
||||
client.set_compress(true); |
||||
} |
||||
|
||||
void sendPostRequest(const std::string& url, const std::string& payload) |
||||
{ |
||||
auto [host, path] = parseUrl(url); |
||||
auto client = getOrCreateClient(host); |
||||
|
||||
httplib::Headers headers = {{"Content-Type", CONTENT_TYPE_JSON}, |
||||
{"User-Agent", userAgent}, |
||||
{"Accept", CONTENT_TYPE_JSON}}; |
||||
|
||||
log->i("Sending POST request to: %s%s", host.c_str(), path.c_str()); |
||||
log->v(1, "Payload: %s", payload.c_str()); |
||||
|
||||
auto res = client->Post(path, headers, payload, CONTENT_TYPE_JSON); |
||||
|
||||
if (!res) { |
||||
throw std::runtime_error("Failed to connect to: " + host); |
||||
} |
||||
|
||||
log->v(2, "Response status: %d", res->status); |
||||
log->v(3, "Response body: %s", res->body.c_str()); |
||||
|
||||
if (res->status < 200 || res->status >= 300) { |
||||
std::string error_msg = |
||||
"HTTP request failed with status: " + std::to_string(res->status) |
||||
+ " for URL: " + url; |
||||
if (!res->body.empty()) { |
||||
error_msg += " Response: " + res->body; |
||||
} |
||||
throw std::runtime_error(error_msg); |
||||
} |
||||
} |
||||
|
||||
ILoggerPtr log; |
||||
std::queue<OrderRequest> orderQueue; |
||||
std::mutex queueMutex; |
||||
std::condition_variable queueCondition; |
||||
std::thread workerThread; |
||||
std::atomic<bool> shutdownRequested; |
||||
|
||||
// Use weak_ptr to allow automatic cleanup of unused clients
|
||||
std::unordered_map<std::string, std::weak_ptr<httplib::Client>> clients; |
||||
std::mutex clientsMutex; |
||||
std::string userAgent; |
||||
}; |
||||
|
||||
HttpOrderService::HttpOrderService(ILoggerPtr logger) |
||||
: impl{std::make_unique<Impl>(std::move(logger))} |
||||
{} |
||||
|
||||
HttpOrderService::~HttpOrderService() = default; |
||||
|
||||
void HttpOrderService::orderItem(const domain::Item& item) |
||||
{ |
||||
// In a real implementation, this would use an HTTP client library
|
||||
// For now, we'll simulate the HTTP call
|
||||
log->i("POST request to: %s", url); |
||||
log->v(1, "Payload: %s", payload); |
||||
if (item.orderUrl.empty()) { |
||||
throw std::runtime_error("Order URL is empty for item: " + item.name); |
||||
} |
||||
|
||||
// Simulate HTTP error handling
|
||||
if (url.find("error") != std::string::npos) { |
||||
throw std::runtime_error("Failed to send order request to: " |
||||
+ std::string(url)); |
||||
if (item.orderUrl.find("://") == std::string::npos) { |
||||
throw std::runtime_error("Invalid URL format for item: " + item.name |
||||
+ " (missing protocol)"); |
||||
} |
||||
|
||||
std::string payload = createOrderPayload(item); |
||||
impl->enqueueOrder(item.orderUrl, std::move(payload)); |
||||
} |
||||
|
||||
} // namespace nxl::autostore::infrastructure
|
||||
@ -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 |
||||
|
||||
#include "webapi/controllers/BaseController.h" |
||||
#include "application/commands/AddItem.h" |
||||
#include <httplib.h> // TODO: forward declaration |
||||
#include "application/queries/ListItems.h" |
||||
#include "application/queries/GetItem.h" |
||||
#include "application/commands/DeleteItem.h" |
||||
#include "application/interfaces/IAuthService.h" |
||||
#include "infrastructure/helpers/JsonItem.h" |
||||
#include <httplib.h> |
||||
|
||||
namespace nxl::autostore::webapi { |
||||
|
||||
class StoreController |
||||
class StoreController : public BaseController |
||||
{ |
||||
public: |
||||
struct Context |
||||
{ |
||||
application::AddItem addItemUc; |
||||
application::ListItems listItemsUc; |
||||
application::GetItem getItemUc; |
||||
application::DeleteItem deleteItemUc; |
||||
application::IAuthService& authService; |
||||
}; |
||||
|
||||
StoreController(Context&& context); |
||||
|
||||
void registerRoutes(httplib::Server& server); |
||||
protected: |
||||
std::vector<RouteConfig> getRoutes() const override; |
||||
|
||||
private: |
||||
void addItem(const httplib::Request& req, httplib::Response& res); |
||||
void listItems(const httplib::Request& req, httplib::Response& res); |
||||
void getItem(const httplib::Request& req, httplib::Response& res); |
||||
void deleteItem(const httplib::Request& req, httplib::Response& res); |
||||
|
||||
private: |
||||
Context context; |
||||
void assertUserId(std::optional<domain::User::Id_t> userId) const; |
||||
}; |
||||
|
||||
} // namespace nxl::autostore::webapi
|
||||
@ -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