Browse Source

WIP: cpp17

cpp17-init-dingo-fail
chodak166 5 months ago
parent
commit
74955aa3f9
  1. 101
      README.md
  2. 1
      cpp17/CMakeLists.txt
  3. 81
      cpp17/README.md
  4. 2
      cpp17/TODO.md
  5. 2
      cpp17/lib/CMakeLists.txt
  6. 30
      cpp17/tests/CMakeLists.txt
  7. 365
      cpp17/tests/integration/FileItemRepository.test.cpp
  8. 312
      cpp17/tests/integration/FileUserRepository.test.cpp
  9. 564
      openapi.yaml

101
README.md

@ -55,45 +55,48 @@ AutoStore/
├── Extern
│ ├── <jwt-lib, http-client, etc.>
│ └── <...downloaded libraries and git submodules>
└── Src
├── Domain/
│ ├── Entities/
│ │ ├── User
│ │ └── Item
│ └── Services/
│ └── ExpirationPolicy
├── Application/
│ ├── UseCases/
│ │ ├── RegisterUser
│ │ ├── LoginUser
│ │ ├── AddItem
│ │ ├── GetItem
│ │ ├── DeleteItem
│ │ └── HandleExpiredItems
│ ├── Interfaces/
│ │ ├── IUserRepository
│ │ ├── IItemRepository
│ │ ├── IAuthService
│ │ └── IClock
│ ├── Dto/
│ └── Services/
├── Infrastructure/
│ ├── Repositories/
│ │ ├── FileUserRepository
│ │ └── FileItemRepository
│ ├── Adapters/
│ │ ├── JwtAuthAdapter
│ │ ├── OrderUrlHttpClient
│ │ ├── SystemClockImpl
│ │ └── <... some extern lib adapters>
│ └── Helpers/
│ └── <... DRY helpers>
└── WebApi/
├── Controllers/
│ ├── StoreController
│ └── UserController
└── Auth/
└── JwtMiddleware
├── Src
│ ├── Domain/
│ │ ├── Entities/
│ │ │ ├── User
│ │ │ └── Item
│ │ └── Services/
│ │ └── ExpirationPolicy
│ ├── Application/
│ │ ├── UseCases/
│ │ │ ├── RegisterUser
│ │ │ ├── LoginUser
│ │ │ ├── AddItem
│ │ │ ├── GetItem
│ │ │ ├── DeleteItem
│ │ │ └── HandleExpiredItems
│ │ ├── Interfaces/
│ │ │ ├── IUserRepository
│ │ │ ├── IItemRepository
│ │ │ ├── IAuthService
│ │ │ └── IClock
│ │ ├── Dto/
│ │ └── Services/
│ ├── Infrastructure/
│ │ ├── Repositories/
│ │ │ ├── FileUserRepository
│ │ │ └── FileItemRepository
│ │ ├── Adapters/
│ │ │ ├── JwtAuthAdapter
│ │ │ ├── OrderUrlHttpClient
│ │ │ ├── SystemClockImpl
│ │ │ └── <... some extern lib adapters>
│ │ └── Helpers/
│ │ └── <... DRY helpers>
│ └── WebApi/
│ ├── Controllers/
│ │ ├── StoreController
│ │ └── UserController
│ └── Auth/
│ └── JwtMiddleware
└── Tests
├── Unit/
└── Integration/
```
## Build and Run
@ -106,3 +109,23 @@ docker compose up
to build and run the application.
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.
## API Endpoints
See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.).
Here's a summary of example API endpoints:
| Endpoint | Method | Description |
|-------------------------|--------|--------------------------------------|
| `/register` | POST | Register a new user account |
| `/login` | POST | Authenticate user and get JWT token |
| `/users` | GET | Get list of all users |
| `/users/{id}` | GET | Get user by ID |
| `/users/{id}` | POST | Create new user (admin) |
| `/users/{id}` | PUT | Update user details |
| `/users/{id}` | DELETE | Delete user account |
| `/items` | GET | Get user's items |
| `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |

1
cpp17/CMakeLists.txt

@ -11,3 +11,4 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
add_subdirectory(lib)
add_subdirectory(app)
add_subdirectory(tests)

81
cpp17/README.md

@ -55,45 +55,48 @@ AutoStore/
├── Extern
│ ├── <jwt-lib, http-client, etc.>
│ └── <...downloaded libraries and git submodules>
└── Src
├── Domain/
│ ├── Entities/
│ │ ├── User
│ │ └── Item
│ └── Services/
│ └── ExpirationPolicy
├── Application/
│ ├── UseCases/
│ │ ├── RegisterUser
│ │ ├── LoginUser
│ │ ├── AddItem
│ │ ├── GetItem
│ │ ├── DeleteItem
│ │ └── HandleExpiredItems
│ ├── Interfaces/
│ │ ├── IUserRepository
│ │ ├── IItemRepository
│ │ ├── IAuthService
│ │ └── IClock
│ ├── Dto/
│ └── Services/
├── Infrastructure/
│ ├── Repositories/
│ │ ├── FileUserRepository
│ │ └── FileItemRepository
│ ├── Adapters/
│ │ ├── JwtAuthAdapter
│ │ ├── OrderUrlHttpClient
│ │ ├── SystemClockImpl
│ │ └── <... some extern lib adapters>
│ └── Helpers/
│ └── <... DRY helpers>
└── WebApi/
├── Controllers/
│ ├── StoreController
│ └── UserController
└── Auth/
└── JwtMiddleware
├── Src
│ ├── Domain/
│ │ ├── Entities/
│ │ │ ├── User
│ │ │ └── Item
│ │ └── Services/
│ │ └── ExpirationPolicy
│ ├── Application/
│ │ ├── UseCases/
│ │ │ ├── RegisterUser
│ │ │ ├── LoginUser
│ │ │ ├── AddItem
│ │ │ ├── GetItem
│ │ │ ├── DeleteItem
│ │ │ └── HandleExpiredItems
│ │ ├── Interfaces/
│ │ │ ├── IUserRepository
│ │ │ ├── IItemRepository
│ │ │ ├── IAuthService
│ │ │ └── IClock
│ │ ├── Dto/
│ │ └── Services/
│ ├── Infrastructure/
│ │ ├── Repositories/
│ │ │ ├── FileUserRepository
│ │ │ └── FileItemRepository
│ │ ├── Adapters/
│ │ │ ├── JwtAuthAdapter
│ │ │ ├── OrderUrlHttpClient
│ │ │ ├── SystemClockImpl
│ │ │ └── <... some extern lib adapters>
│ │ └── Helpers/
│ │ └── <... DRY helpers>
│ └── WebApi/
│ ├── Controllers/
│ │ ├── StoreController
│ │ └── UserController
│ └── Auth/
│ └── JwtMiddleware
└── Tests
├── Unit/
└── Integration/
```
## Build and Run

2
cpp17/TODO.md

@ -18,7 +18,7 @@ This document outlines the steps to implement the C++17 version of the AutoStore
- [x] Create `lib/CMakeLists.txt` to build a static library.
- [x] In `lib/include/autostore`, define the public interface for the `App` to use.
- [x] Create a dummy `AutoStore` class in `lib/include/autostore/AutoStore.h` and a source file in `lib/src/AutoStore.cpp`.
- [ ] Define initail classes for core domain and application logic inside the library (e.g., `ItemRepository`, `UserService`, etc.) to establish the architecture. These will be mostly private to the library initially and implemented later.
- [ ] Define initial classes for core domain and application logic inside the library (e.g., `ItemRepository`, `UserService`, etc.) to establish the architecture. These will be mostly private to the library initially and implemented later.
- [ ] Ensure the project compiles and links successfully with the dummy implementations.
## Phase 3: Application (`app`) - Dummy Implementation

2
cpp17/lib/CMakeLists.txt

@ -32,12 +32,10 @@ target_sources(${TARGET_NAME}
# Find dependencies
find_package(httplib CONFIG REQUIRED)
find_package(Catch2 CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(${TARGET_NAME}
PUBLIC
httplib::httplib
Catch2::Catch2WithMain
nlohmann_json::nlohmann_json
)

30
cpp17/tests/CMakeLists.txt

@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.20)
enable_testing()
find_package(Catch2 CONFIG REQUIRED)
# Macro to create a test executable
function(add_integration_test TEST_NAME SOURCE_FILE)
add_executable(${TEST_NAME}
${SOURCE_FILE}
)
target_link_libraries(${TEST_NAME}
PRIVATE
AutoStoreLib
Catch2::Catch2WithMain
)
target_include_directories(${TEST_NAME}
PRIVATE
${PROJECT_SOURCE_DIR}/lib/include
)
# Add test to CTest
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endfunction()
# Create test executables
add_integration_test(FileUserRepositoryTest integration/FileUserRepository.test.cpp)
add_integration_test(FileItemRepositoryTest integration/FileItemRepository.test.cpp)

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

@ -0,0 +1,365 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <autostore/infrastructure/repositories/FileItemRepository.h>
#include <autostore/domain/entities/Item.h>
#include <filesystem>
#include <fstream>
#include <optional>
#include <chrono>
using namespace nxl::autostore;
using Catch::Matchers::Equals;
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_ITEM_NAME_2 = "anotheritem";
constexpr const char* TEST_ORDER_URL_1 = "https://example.com/order1";
constexpr const char* TEST_ORDER_URL_2 = "https://example.com/order2";
constexpr const char* TEST_USER_ID_1 = "user123";
constexpr const char* TEST_USER_ID_2 = "user456";
constexpr const char* NON_EXISTENT_ID = "nonexistent";
constexpr const char* NON_EXISTENT_USER_ID = "nonexistentuser";
constexpr const char* TEST_DIR_NAME = "autostore_test";
constexpr const char* TEST_DB_FILE_NAME = "test_items.json";
// Helper function to create a test item with default values
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))
{
domain::Item item;
item.id = id;
item.name = name;
item.orderUrl = orderUrl;
item.userId = userId;
item.expirationDate = expirationDate;
return item;
}
// Helper function to create a second test item
domain::Item createSecondTestItem()
{
return createTestItem(
TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2, TEST_USER_ID_2,
std::chrono::system_clock::now() + std::chrono::hours(48));
}
// 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 item properties match expected values
void verifyItemProperties(const domain::Item& item,
const std::string& expectedId,
const std::string& expectedName,
const std::string& expectedOrderUrl,
const std::string& expectedUserId)
{
REQUIRE(item.id == expectedId);
REQUIRE(item.name == expectedName);
REQUIRE(item.orderUrl == expectedOrderUrl);
REQUIRE(item.userId == expectedUserId);
}
// Helper function to verify item properties match default test item values
void verifyDefaultTestItem(const domain::Item& item)
{
verifyItemProperties(item, TEST_ITEM_ID_1, TEST_ITEM_NAME_1, TEST_ORDER_URL_1,
TEST_USER_ID_1);
}
// Helper function to verify item properties match second test item values
void verifySecondTestItem(const domain::Item& item)
{
verifyItemProperties(item, TEST_ITEM_ID_2, TEST_ITEM_NAME_2, TEST_ORDER_URL_2,
TEST_USER_ID_2);
}
} // namespace Test
TEST_CASE("FileItemRepository Integration Tests",
"[integration][FileItemRepository]")
{
// Setup test environment
std::string testDbPath = Test::setupTestEnvironment();
SECTION("when a new item is saved then it can be found by id")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
// When
repository.save(testItem);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
REQUIRE(foundItem.has_value());
Test::verifyDefaultTestItem(*foundItem);
}
SECTION("when a new item is saved then it can be found by user")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
// When
repository.save(testItem);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 1);
Test::verifyDefaultTestItem(userItems[0]);
}
SECTION("when multiple items are saved then findAll returns all items")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem = Test::createSecondTestItem();
// When
repository.save(firstItem);
repository.save(secondItem);
// Then
auto allItems = repository.findAll();
REQUIRE(allItems.size() == 2);
// Verify both items are present (order doesn't matter)
bool foundFirst = false;
bool foundSecond = false;
for (const auto& item : allItems) {
if (item.id == Test::TEST_ITEM_ID_1) {
Test::verifyDefaultTestItem(item);
foundFirst = true;
} else if (item.id == Test::TEST_ITEM_ID_2) {
Test::verifySecondTestItem(item);
foundSecond = true;
}
}
REQUIRE(foundFirst);
REQUIRE(foundSecond);
}
SECTION("when multiple items for same user are saved then findByUser returns "
"all user items")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem =
Test::createTestItem("item789", "thirditem", "https://example.com/order3",
Test::TEST_USER_ID_1);
// When
repository.save(firstItem);
repository.save(secondItem);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
REQUIRE(userItems.size() == 2);
// Verify both items are present (order doesn't matter)
bool foundFirst = false;
bool foundSecond = false;
for (const auto& item : userItems) {
if (item.id == Test::TEST_ITEM_ID_1) {
Test::verifyDefaultTestItem(item);
foundFirst = true;
} else if (item.id == "item789") {
REQUIRE(item.name == "thirditem");
REQUIRE(item.orderUrl == "https://example.com/order3");
REQUIRE(item.userId == Test::TEST_USER_ID_1);
foundSecond = true;
}
}
REQUIRE(foundFirst);
REQUIRE(foundSecond);
}
SECTION("when an existing item is saved then it is updated")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
testItem.name = "updateditemname";
testItem.orderUrl = "https://updated.example.com/order";
testItem.userId = Test::TEST_USER_ID_2;
repository.save(testItem);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
REQUIRE(foundItem.has_value());
REQUIRE(foundItem->id == Test::TEST_ITEM_ID_1);
REQUIRE(foundItem->name == "updateditemname");
REQUIRE(foundItem->orderUrl == "https://updated.example.com/order");
REQUIRE(foundItem->userId == Test::TEST_USER_ID_2);
}
SECTION("when an item is removed then it cannot be found by id")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
// Then
auto foundItem = repository.findById(Test::TEST_ITEM_ID_1);
REQUIRE_FALSE(foundItem.has_value());
}
SECTION("when an item is removed then it is not in findByUser")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
// Then
auto userItems = repository.findByUser(Test::TEST_USER_ID_1);
REQUIRE(userItems.empty());
}
SECTION("when an item is removed then it is not in findAll")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item firstItem = Test::createTestItem();
domain::Item secondItem = Test::createSecondTestItem();
repository.save(firstItem);
repository.save(secondItem);
// When
repository.remove(Test::TEST_ITEM_ID_1);
// Then
auto allItems = repository.findAll();
REQUIRE(allItems.size() == 1);
Test::verifySecondTestItem(allItems[0]);
}
SECTION(
"when findById is called with non-existent id then it returns nullopt")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
// When
auto foundItem = repository.findById(Test::NON_EXISTENT_ID);
// Then
REQUIRE_FALSE(foundItem.has_value());
}
SECTION("when findByUser is called with non-existent user id then it returns "
"empty vector")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
auto userItems = repository.findByUser(Test::NON_EXISTENT_USER_ID);
// Then
REQUIRE(userItems.empty());
}
SECTION("when remove is called with non-existent id then it does nothing")
{
// Given
infrastructure::FileItemRepository repository(testDbPath);
domain::Item testItem = Test::createTestItem();
repository.save(testItem);
// When
repository.remove(Test::NON_EXISTENT_ID);
// Then
auto allItems = repository.findAll();
REQUIRE(allItems.size() == 1);
Test::verifyDefaultTestItem(allItems[0]);
}
SECTION(
"when repository is created with existing data file then it loads the data")
{
// Given
{
infrastructure::FileItemRepository firstRepository(testDbPath);
domain::Item testItem = Test::createTestItem();
firstRepository.save(testItem);
}
// When
infrastructure::FileItemRepository secondRepository(testDbPath);
// Then
auto foundItem = secondRepository.findById(Test::TEST_ITEM_ID_1);
REQUIRE(foundItem.has_value());
Test::verifyDefaultTestItem(*foundItem);
}
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::FileItemRepository repository(nonExistentDbPath);
// Then
auto allItems = repository.findAll();
REQUIRE(allItems.empty());
}
// Clean up test environment
Test::cleanupTestEnvironment();
}

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

@ -0,0 +1,312 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <autostore/infrastructure/repositories/FileUserRepository.h>
#include <autostore/domain/entities/User.h>
#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();
}

564
openapi.yaml

@ -0,0 +1,564 @@
openapi: 3.0.3
info:
title: AutoStore API
description: API for the AutoStore system - a system to store items with expiration dates that automatically orders new items when they expire.
version: 1.0.0
servers:
- url: http://localhost:3000/api/v1
description: Development server
paths:
/register:
post:
summary: Register a new user
description: Creates a new user account and returns a JWT token
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- username
- password
properties:
username:
type: string
description: User's username or email
password:
type: string
description: User's password
responses:
'201':
description: User successfully registered
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: object
properties:
user:
$ref: '#/components/schemas/User'
token:
type: string
description: JWT token for authentication
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'409':
description: Username already exists
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/login:
post:
summary: User login
description: Authenticates a user and returns a JWT token
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- username
- password
properties:
username:
type: string
description: User's username or email
password:
type: string
description: User's password
responses:
'200':
description: Login successful
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: object
properties:
user:
$ref: '#/components/schemas/User'
token:
type: string
description: JWT token for authentication
'401':
description: Invalid credentials
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/users:
get:
summary: Get list of users
description: Returns a list of all users (requires authentication)
security:
- bearerAuth: []
responses:
'200':
description: List of users
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/users/{id}:
get:
summary: Get user by ID
description: Returns a specific user by their ID (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
responses:
'200':
description: User details
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
post:
summary: Create a new user
description: Creates a new user (admin functionality, requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'409':
description: User already exists
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
put:
summary: Update a user
description: Updates an existing user (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInput'
responses:
'200':
description: User updated successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
delete:
summary: Delete a user
description: Deletes an existing user (requires authentication)
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: User ID
schema:
type: string
responses:
'204':
description: User deleted successfully
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/items:
get:
summary: Get list of items
description: Returns a list of all items for the authenticated user
security:
- bearerAuth: []
responses:
'200':
description: List of items
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Item'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
post:
summary: Create a new item
description: Creates a new item for the authenticated user
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ItemInput'
responses:
'201':
description: Item created successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/Item'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
/items/{id}:
get:
summary: Get item by ID
description: Returns a specific item by its ID
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: Item ID
schema:
type: string
responses:
'200':
description: Item details
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/Item'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: Item not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
put:
summary: Update an item
description: Updates an existing item
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: Item ID
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ItemInput'
responses:
'200':
description: Item updated successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/JsendSuccess'
- type: object
properties:
data:
$ref: '#/components/schemas/Item'
'400':
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: Item not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
delete:
summary: Delete an item
description: Deletes an existing item
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
description: Item ID
schema:
type: string
responses:
'204':
description: Item deleted successfully
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
'404':
description: Item not found
content:
application/json:
schema:
$ref: '#/components/schemas/JsendError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
JsendSuccess:
type: object
properties:
status:
type: string
example: success
data:
type: object
description: Response data
JsendError:
type: object
properties:
status:
type: string
example: error
message:
type: string
description: Error message
code:
type: integer
description: Error code
data:
type: object
description: Additional error data
User:
type: object
properties:
id:
type: string
description: User ID
username:
type: string
description: User's username or email
UserInput:
type: object
required:
- username
- password
properties:
username:
type: string
description: User's username or email
password:
type: string
description: User's password
Item:
type: object
properties:
id:
type: string
description: Item ID
name:
type: string
description: Item name
expirationDate:
type: string
format: date-time
description: Item expiration date
orderUrl:
type: string
format: uri
description: URL to send order request when item expires
userId:
type: string
description: ID of the user who owns this item
ItemInput:
type: object
required:
- name
- expirationDate
- orderUrl
properties:
name:
type: string
description: Item name
expirationDate:
type: string
format: date-time
description: Item expiration date
orderUrl:
type: string
format: uri
description: URL to send order request when item expires
Loading…
Cancel
Save