Compare commits

..

12 Commits

  1. 5
      .gitignore
  2. 2
      LICENSE
  3. 89
      README.md
  4. 254
      cpp17/.clang-tidy
  5. 19
      cpp17/CMakePresets.json
  6. 123
      cpp17/README.md
  7. 55
      cpp17/TODO.md
  8. 17
      cpp17/app/CMakeLists.txt
  9. 13
      cpp17/app/defaults/users.json
  10. 26
      cpp17/app/src/App.cpp
  11. 2
      cpp17/app/src/App.h
  12. 2
      cpp17/app/src/Main.cpp
  13. 48
      cpp17/app/src/OsHelpers.h
  14. 49
      cpp17/app/src/SpdLogger.h
  15. 0
      cpp17/app/src/Version.h.in
  16. 49
      cpp17/doc/add-item-sequence.md
  17. 82
      cpp17/doc/architecture-overview.md
  18. 25
      cpp17/doc/request-sequence.md
  19. 4
      cpp17/docker/.dockerignore
  20. 23
      cpp17/docker/Dockerfile
  21. 4
      cpp17/docker/docker-compose.yml
  22. 32
      cpp17/lib/CMakeLists.txt
  23. 54
      cpp17/lib/include/autostore/AutoStore.h
  24. 98
      cpp17/lib/include/autostore/ILogger.h
  25. 127
      cpp17/lib/src/AutoStore.cpp
  26. 45
      cpp17/lib/src/DiContainer.cpp
  27. 102
      cpp17/lib/src/DiContainer.h
  28. 22
      cpp17/lib/src/application/commands/AddItem.cpp
  29. 9
      cpp17/lib/src/application/commands/AddItem.h
  30. 19
      cpp17/lib/src/application/commands/DeleteItem.cpp
  31. 21
      cpp17/lib/src/application/commands/DeleteItem.h
  32. 29
      cpp17/lib/src/application/commands/HandleExpiredItems.cpp
  33. 30
      cpp17/lib/src/application/commands/HandleExpiredItems.h
  34. 20
      cpp17/lib/src/application/commands/LoginUser.cpp
  35. 20
      cpp17/lib/src/application/commands/LoginUser.h
  36. 16
      cpp17/lib/src/application/exceptions/AutoStoreExceptions.h
  37. 6
      cpp17/lib/src/application/interfaces/IAuthService.h
  38. 20
      cpp17/lib/src/application/interfaces/IBlocker.h
  39. 14
      cpp17/lib/src/application/interfaces/IClock.h
  40. 13
      cpp17/lib/src/application/interfaces/IItemRepository.h
  41. 28
      cpp17/lib/src/application/interfaces/IThreadManager.h
  42. 18
      cpp17/lib/src/application/interfaces/ITimeProvider.h
  43. 23
      cpp17/lib/src/application/interfaces/IUserRepository.h
  44. 34
      cpp17/lib/src/application/presenters/GenericPresenters.h
  45. 10
      cpp17/lib/src/application/presenters/StorePresenters.h
  46. 21
      cpp17/lib/src/application/queries/GetItem.cpp
  47. 23
      cpp17/lib/src/application/queries/GetItem.h
  48. 14
      cpp17/lib/src/application/queries/ListItems.cpp
  49. 21
      cpp17/lib/src/application/queries/ListItems.h
  50. 229
      cpp17/lib/src/application/services/TaskScheduler.cpp
  51. 85
      cpp17/lib/src/application/services/TaskScheduler.h
  52. 2
      cpp17/lib/src/domain/entities/User.h
  53. 141
      cpp17/lib/src/domain/helpers/Specification.cpp
  54. 198
      cpp17/lib/src/domain/helpers/Specification.h
  55. 17
      cpp17/lib/src/domain/polices/ItemExpirationPolicy.h
  56. 55
      cpp17/lib/src/infrastructure/adapters/CvBlocker.cpp
  57. 33
      cpp17/lib/src/infrastructure/adapters/CvBlocker.h
  58. 17
      cpp17/lib/src/infrastructure/adapters/SystemClock.h
  59. 43
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.cpp
  60. 27
      cpp17/lib/src/infrastructure/adapters/SystemThreadManager.h
  61. 22
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.cpp
  62. 15
      cpp17/lib/src/infrastructure/adapters/SystemTimeProvider.h
  63. 96
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.cpp
  64. 31
      cpp17/lib/src/infrastructure/auth/FileJwtAuthService.h
  65. 2
      cpp17/lib/src/infrastructure/helpers/Jsend.cpp
  66. 2
      cpp17/lib/src/infrastructure/helpers/Jsend.h
  67. 4
      cpp17/lib/src/infrastructure/helpers/JsonItem.cpp
  68. 2
      cpp17/lib/src/infrastructure/helpers/JsonItem.h
  69. 37
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.cpp
  70. 20
      cpp17/lib/src/infrastructure/http/HttpJwtMiddleware.h
  71. 366
      cpp17/lib/src/infrastructure/http/HttpOrderService.cpp
  72. 12
      cpp17/lib/src/infrastructure/http/HttpOrderService.h
  73. 116
      cpp17/lib/src/infrastructure/http/HttpServer.cpp
  74. 8
      cpp17/lib/src/infrastructure/http/HttpServer.h
  75. 25
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.cpp
  76. 11
      cpp17/lib/src/infrastructure/repositories/FileItemRepository.h
  77. 130
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.cpp
  78. 30
      cpp17/lib/src/infrastructure/repositories/FileUserRepository.h
  79. 58
      cpp17/lib/src/webapi/controllers/AuthController.cpp
  80. 26
      cpp17/lib/src/webapi/controllers/AuthController.h
  81. 23
      cpp17/lib/src/webapi/controllers/BaseController.cpp
  82. 86
      cpp17/lib/src/webapi/controllers/BaseController.h
  83. 140
      cpp17/lib/src/webapi/controllers/StoreController.cpp
  84. 31
      cpp17/lib/src/webapi/controllers/StoreController.h
  85. 15
      cpp17/tests/CMakeLists.txt
  86. 57
      cpp17/tests/helpers/AddItemTestHelpers.h
  87. 113
      cpp17/tests/integration/FileItemRepository.test.cpp
  88. 312
      cpp17/tests/integration/FileUserRepository.test.cpp
  89. 19
      cpp17/tests/mocks/MockBlocker.h
  90. 24
      cpp17/tests/mocks/MockItemRepository.h
  91. 14
      cpp17/tests/mocks/MockOrderService.h
  92. 24
      cpp17/tests/mocks/MockThreadManager.h
  93. 16
      cpp17/tests/mocks/MockTimeProvider.h
  94. 51
      cpp17/tests/mocks/TestLogger.h
  95. 223
      cpp17/tests/unit/AddItem.test.cpp
  96. 692
      cpp17/tests/unit/Specification.test.cpp
  97. 436
      cpp17/tests/unit/TaskScheduler.test.cpp
  98. 10
      cpp17/vcpkg.json
  99. 31
      golang/.devcontainer/Dockerfile
  100. 25
      golang/.devcontainer/devcontainer.json
  101. Some files were not shown because too many files have changed in this diff Show More

5
.gitignore vendored

@ -21,6 +21,10 @@ tmp
*.dylib *.dylib
*.dll *.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries # Compiled Static libraries
*.lai *.lai
*.la *.la
@ -236,4 +240,3 @@ gradle-app.setting
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
reference-*

2
LICENSE

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 chodak166 Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

89
README.md

@ -4,7 +4,7 @@ This repository hosts multiple implementations of the same back-end application.
Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology. Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology.
Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or atomic transactions unless intentionally focusing on those patterns for comparison). Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison).
--- ---
@ -19,11 +19,10 @@ A system to store items with expiration dates. When items expire, new ones are a
3. **When an item expires, a new item of the same type is automatically ordered.** 3. **When an item expires, a new item of the same type is automatically ordered.**
4. **Expired items can be added to the store, triggering immediate ordering.** 4. **Expired items can be added to the store, triggering immediate ordering.**
5. **Every item belongs to a user.** 5. **Every item belongs to a user.**
6. **Only the item's owner can manage it.**
#### Application Requirements #### Application Requirements
1. **Users can log in to obtain a JWT.** 1. **Users can register and log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.** 2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".** 3. **Each item has an associated "order URL".**
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.** 4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
@ -31,20 +30,17 @@ A system to store items with expiration dates. When items expire, new ones are a
6. **Upon startup, the system must verify expiration dates for all items.** 6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).** 7. **Persistent storage must be used (file, database, etc.).**
**Note:** For simplicity, user CRUD is skipped. Integrate with an OP (OpenID Provider) service like Keycloak, Authentic, or Zitadel, or mock authentication with a simple Docker service. Alternatively, simply authenticate a predefined user and return a JWT on login.
--- ---
## Layer Boundaries ## Layer Boundaries
| Layer | Responsibility | Internal Dependencies | External Dependencies | | Layer | Responsibility | Internal Dependencies | External Dependencies |
|-------------------|--------------------------------------------------------------- |-----------------------|-----------------------| |------------------|--------------------------------------------------------------- |----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) | | **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal | | **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) | | **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others | | **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.| | **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
--- ---
@ -52,38 +48,35 @@ A system to store items with expiration dates. When items expire, new ones are a
```plaintext ```plaintext
AutoStore/ AutoStore/
├── App # app assembly ├── App
│ ├── Main │ ├── Main
│ ├── AppConfig │ ├── AppConfig
│ └── ... │ └── ...
├── Extern ├── Extern
│ ├── <package manager files> │ ├── <jwt-lib, http-client, etc.>
│ └── <...downloaded libraries and git submodules> │ └── <...downloaded libraries and git submodules>
├── Src # internal/lib/src ├── Src
│ ├── Domain/ │ ├── Domain/
│ │ ├── Entities/ │ │ ├── Entities/
│ │ │ ├── User │ │ │ ├── User
│ │ │ └── Item │ │ │ └── Item
│ │ └── Specifications/ │ │ └── Services/
│ │ └── ItemExpirationSpec # domain knowledge (from domain experts) │ │ └── ExpirationPolicy
│ ├── Application/ │ ├── Application/
│ │ ├── Commands/ # use cases │ │ ├── UseCases/
│ │ │ ├── Login │ │ │ ├── RegisterUser
│ │ │ ├── LoginUser
│ │ │ ├── AddItem │ │ │ ├── AddItem
│ │ │ ├── GetItem
│ │ │ ├── DeleteItem │ │ │ ├── DeleteItem
│ │ │ └── HandleExpiredItems │ │ │ └── HandleExpiredItems
│ │ ├── Queries/ # use cases (read only)
│ │ │ ├── GetItem
│ │ │ └── ListItems
│ │ ├── Interfaces/ │ │ ├── Interfaces/
│ │ │ ├── IUserRepository │ │ │ ├── IUserRepository
│ │ │ ├── IItemRepository │ │ │ ├── IItemRepository
│ │ │ ├── IAuthService │ │ │ ├── IAuthService
│ │ │ └── IDateProvider │ │ │ └── IClock
│ │ ├── Dto/ # data transfer objects (fields mappings, validation, etc.) │ │ ├── Dto/
│ │ └── Services/ │ │ └── Services/
│ │ ├── UserInitializationService
│ │ └── ExpirationScheduler
│ ├── Infrastructure/ │ ├── Infrastructure/
│ │ ├── Repositories/ │ │ ├── Repositories/
│ │ │ ├── FileUserRepository │ │ │ ├── FileUserRepository
@ -91,12 +84,11 @@ AutoStore/
│ │ ├── Adapters/ │ │ ├── Adapters/
│ │ │ ├── JwtAuthAdapter │ │ │ ├── JwtAuthAdapter
│ │ │ ├── OrderUrlHttpClient │ │ │ ├── OrderUrlHttpClient
│ │ │ ├── SystemDateProvider │ │ │ ├── SystemClockImpl
│ │ │ └── <... some extern lib adapters> │ │ │ └── <... some extern lib adapters>
│ │ └── Helpers/ │ │ └── Helpers/
│ │ └── <... DRY helpers> │ │ └── <... DRY helpers>
│ ├── Cli # presentation, optional command line use case caller │ └── WebApi/
│ └── WebApi/ # presentation, REST (controllers, middlewares, etc.)
│ ├── Controllers/ │ ├── Controllers/
│ │ ├── StoreController │ │ ├── StoreController
│ │ └── UserController │ │ └── UserController
@ -107,28 +99,14 @@ AutoStore/
└── Integration/ └── Integration/
``` ```
---
## Domain Knowledge and Repository Queries
Business rules like expiration checks (`expirationDate <= currentDate`) represent domain knowledge that **must have a single source of truth**. This logic might evolve (e.g., to `<= currentDate - N days` or vary by item type) and should never be duplicated across the codebase.
While simple predicates (like `findWhere(predicate<bool>(Item))`) work for in-memory repositories, SQL-based repositories need to translate these rules into efficient WHERE clauses. One solution (not too over-engineered) would be to pass DTO or value object ready to be put into queries (e.g., `fetchExpiredItems(calculatedConditionFields)`).
But for fun and possible UI search, consider implementing a specification pattern with a simple condition abstraction that exposes the business rule as composable conditions (field, operator, value).
This allows domain services to define rules once, use cases to apply them consistently, and repositories to translate them into optimal queries by interpreting the conditions according to their storage mechanism. Avoid duplicating the business logic in repository implementations - instead, let repositories consume the specification and build their queries accordingly. This aims to overcome to-repo-and-back drawbacks depicted in *Evans, Eric (2003). Domain Driven Design. Final Manuscript*.
---
## Build and Run ## Build and Run
Each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run: Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run:
```bash ```bash
cd docker && docker compose up --build docker compose up
``` ```
to build, test and run the application. to build and run the application.
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions. Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.
@ -139,18 +117,15 @@ Here's a summary of example API endpoints:
| Endpoint | Method | Description | | Endpoint | Method | Description |
|-------------------------|--------|--------------------------------------| |-------------------------|--------|--------------------------------------|
| `/login` | POST | Authenticate user and get JWT token | | `/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` | GET | Get user's items |
| `/items` | POST | Create new item | | `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID | | `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details | | `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item | | `/items/{id}` | DELETE | Delete item |
Suggested base URL is `http://localhost:50080/api/v1/`.
## Testing
- Each implementation should include its own **unit tests** and **integration tests**, which must run automatically during the Docker image build.
- Implementation-independent functional tests are provided in `testing/tavern/`
- Tavern API tests (requests and assertions) must pass for every implementation to ensure consistent behavior across all technology stacks.
- For debugging and verifying the automatic ordering feature, use the helper service in `testing/http-echo-server/` which provides a simple Docker Compose setup that listens on port 8888 and logs all incoming POST requests, allowing you to observe when expired items trigger order notifications.

254
cpp17/.clang-tidy

@ -1,254 +0,0 @@
---
Checks: >-
-*,
bugprone-assert-side-effect,
bugprone-bad-signal-to-kill-thread,
bugprone-bool-pointer-implicit-conversion,
bugprone-branch-clone,
bugprone-copy-constructor-init,
bugprone-dangling-handle,
bugprone-dynamic-static-initializers,
bugprone-exception-escape,
bugprone-forward-declaration-namespace,
bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-infinite-loop,
bugprone-integer-division,
bugprone-macro-parentheses,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-misplaced-widening-cast,
bugprone-move-forwarding-reference,
bugprone-multiple-statement-macro,
bugprone-not-null-terminated-result,
bugprone-parent-virtual-call,
bugprone-posix-return,
bugprone-signed-char-misuse,
bugprone-sizeof-container,
bugprone-sizeof-expression,
bugprone-spuriously-wake-up-functions,
bugprone-string-constructor,
bugprone-string-integer-assignment,
bugprone-string-literal-with-embedded-nul,
bugprone-suspicious-enum-usage,
bugprone-suspicious-include,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-self-assignment,
bugprone-unhandled-self-assignment,
bugprone-unused-raii,
bugprone-use-after-move,
cert-dcl21-cpp,
cert-dcl50-cpp,
cert-dcl58-cpp,
cert-env33-c,
cert-err34-c,
cert-err52-cpp,
cert-err58-cpp,
cert-err60-cpp,
cert-flp30-c,
cert-mem57-cpp,
cert-msc50-cpp,
cert-msc51-cpp,
cert-oop57-cpp,
cert-oop58-cpp,
clang-analyzer-core.CallAndMessage,
clang-analyzer-core.DivideZero,
clang-analyzer-core.*,
clang-analyzer-cplusplus.*,
clang-analyzer-deadcode.*,
clang-analyzer-nullability.*,
clang-analyzer-optin.*,
clang-analyzer-valist.*,
clang-analyzer-security.*,
cppcoreguidelines-avoid-goto,
cppcoreguidelines-avoid-non-const-global-variables,
cppcoreguidelines-init-variables,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-macro-usage,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-no-malloc,
cppcoreguidelines-owning-memory,
cppcoreguidelines-pro-bounds-array-to-pointer-decay,
cppcoreguidelines-pro-bounds-constant-array-index,
cppcoreguidelines-pro-bounds-pointer-arithmetic,
cppcoreguidelines-pro-type-const-cast,
cppcoreguidelines-pro-type-cstyle-cast,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-reinterpret-cast,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-pro-type-union-access,
cppcoreguidelines-pro-type-vararg,
cppcoreguidelines-slicing,
cppcoreguidelines-special-member-functions,
google-build-namespaces,
google-default-arguments,
google-explicit-constructor,
google-build-using-namespace,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
google-runtime-operator,
hicpp-exception-baseclass,
hicpp-multiway-paths-covered,
hicpp-no-assembler,
hicpp-signed-bitwise,
llvm-namespace-comment,
misc-definitions-in-headers,
misc-misplaced-const,
misc-new-delete-overloads,
misc-no-recursion,
misc-non-copyable-objects,
misc-non-private-member-variables-in-classes,
misc-redundant-expression,
misc-static-assert,
misc-throw-by-value-catch-by-reference,
misc-unconventional-assign-operator,
misc-uniqueptr-reset-release,
misc-unused-parameters,
misc-unused-using-decls,
misc-unused-alias-decls,
modernize-avoid-bind,
modernize-avoid-c-arrays,
modernize-concat-nested-namespaces,
modernize-deprecated-headers,
modernize-deprecated-ios-base-aliases,
modernize-loop-convert,
modernize-make-shared,
modernize-make-unique,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-auto-ptr,
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-auto,
modernize-use-bool-literals,
modernize-use-default-member-init,
modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-use-nodiscard,
modernize-use-noexcept,
modernize-use-nullptr,
modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
modernize-use-using,
performance-faster-string-find,
performance-for-range-copy,
performance-implicit-conversion-in-loop,
performance-inefficient-algorithm,
performance-inefficient-string-concatenation,
performance-inefficient-vector-operation,
performance-move-const-arg,
performance-move-constructor-init,
performance-no-automatic-move,
performance-noexcept-move-constructor,
performance-trivially-destructible,
performance-type-promotion-in-math-fn,
performance-unnecessary-copy-initialization,
performance-unnecessary-value-param,
readability-avoid-const-params-in-decls,
readability-braces-around-statements,
readability-const-return-type,
readability-container-size-empty,
readability-delete-null-pointer,
readability-deleted-default,
readability-else-after-return,
readability-function-size,
readability-identifier-naming,
readability-implicit-bool-conversion,
readability-inconsistent-declaration-parameter-name,
readability-isolate-declaration,
readability-magic-numbers,
readability-make-member-function-const,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-named-parameter,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-redundant-function-ptr-dereference,
readability-redundant-member-init,
readability-redundant-preprocessor,
readability-redundant-smartptr-get,
readability-redundant-string-cstr,
readability-redundant-string-init,
readability-simplify-boolean-expr,
readability-simplify-subscript-expr,
readability-static-accessed-through-instance,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-uppercase-literal-suffix,
readability-use-anyofallof
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
AnalyzeTemporaryDtors: false
FormatStyle: none
CheckOptions:
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: 1
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: readability-function-size.LineThreshold
value: 50
- key: readability-function-size.StatementThreshold
value: 800
- key: readability-function-size.BranchThreshold
value: 10
- key: readability-function-size.ParameterThreshold
value: 6
- key: readability-function-size.NestingThreshold
value: 15
- key: readability-function-size.VariableThreshold
value: 10
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.ClassMemberCase
value: camelBack
- key: readability-identifier-naming.ClassMethodCase
value: camelBack
- key: readability-identifier-naming.MethodCase
value: camelBack
- key: readability-identifier-naming.ConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.LocalConstantCase
value: camelBack
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: camelBack
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: 1
...

19
cpp17/CMakePresets.json

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

123
cpp17/README.md

@ -1,44 +1,111 @@
# Ovierview # About this Repository
Read top-level `README.md` for more information on this repository. This repository hosts multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
# Authentication Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology.
No external service is used. JWT tokens are created and verified using `jwt-cpp` library. Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison).
Default, pre-defined user databse is a simple json file (`app/defaults/users.json`).
# Build and Run ---
```bash ### Project Idea: AutoStore
cd docker
docker compose build
docker compose up
```
Note: do not use this for development. See `.devcontainer` directory for development setup. A system to store items with expiration dates. When items expire, new ones are automatically ordered by making a POST request to the configured order URL.
For non-container development, see Dockerfile and replicate the steps. Simple `build-and-test.sh` #### Business Rules (Domain)
script would look like this:
```bash 1. **Each item has a name and an expiration date.**
#/bin/bash 2. **Expired items are automatically removed from the store.**
3. **When an item expires, a new item of the same type is automatically ordered.**
4. **Expired items can be added to the store, triggering immediate ordering.**
5. **Every item belongs to a user.**
#### Application Requirements
1. **Users can register and log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".**
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
5. **This call should occur immediately when the item's expiration date is reached, or when an expired item is added.**
6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).**
OUT_DIR=./out ---
DEBUG_DIR=$OUT_DIR/build/debug
cmake -DCMAKE_BUILD_TYPE=Debug \ ## Layer Boundaries
-DCMAKE_EXPORT_COMPILE_COMMANDS=TRUE \
-DCMAKE_TOOLCHAIN_FILE=/opt/vcpkg/scripts/buildsystems/vcpkg.cmake \
-S /workspace -B $DEBUG_DIR
cd "$DEBUG_DIR" | Layer | Responsibility | Internal Dependencies | External Dependencies |
cmake --build . -- -j8 |------------------|--------------------------------------------------------------- |----------------------|-----------------------|
ctest --output-on-failure . | **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
---
### Possible directory layout (will vary from tech to tech)
```plaintext
AutoStore/
├── App
│ ├── Main
│ ├── AppConfig
│ └── ...
├── 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
└── Tests
├── Unit/
└── Integration/
``` ```
# Testing ## Build and Run
Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run:
Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it. ```bash
docker compose up
```
to build and run the application.
Use top-level testing/tavern scripts to run functional tests. Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.

55
cpp17/TODO.md

@ -0,0 +1,55 @@
# C++17 AutoStore Implementation Plan
This document outlines the steps to implement the C++17 version of the AutoStore application. Implemented classes should use `nxl::` namespace prefix.
## Phase 1: Project Scaffolding & Build System
- [x] Initialize a CMake project structure.
- [x] Set up the root `CMakeLists.txt` to manage the `app` and `lib` subdirectories.
- [x] Create the `lib` directory for the static library.
- [x] Create the `app` directory for the executable.
- [x] Configure `vcpkg` for dependency management and integrate it with CMake.
- [x] Add a dependency for an HTTP library (e.g., `cpp-httplib`) via `vcpkg`.
- [x] Add a dependency for a testing framework (e.g., `catch2`) via `vcpkg`.
## Phase 2: Library (`lib`) - Dummy Implementation
- [x] Create the directory structure for the library: `lib/src`, `lib/include`.
- [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 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
- [ ] Create the directory structure for the application: `app/src`.
- [x] Create `app/CMakeLists.txt` to build the executable.
- [ ] Link the `app` against the `lib` static library.
- [ ] Implement the main `App` class in `app/src/App.h` and `app/src/App.cpp`.
- [ ] The `App` class will have a constructor `App(int argc, char** argv)` and an `exec()` method.
- [ ] Implement signal handling (for `SIGINT`, `SIGTERM`) in the `App` class for graceful shutdown.
- [x] In `app/src/Main.cpp`, instantiate and run the `App` class.
- [ ] Ensure the project compiles and links successfully with the dummy implementations.
## Phase 4: Core Logic Implementation
- [ ] Implement the Domain layer in `lib/src/domain`.
- [ ] Implement the Application layer in `lib/src/application`.
- [ ] Implement the Infrastructure layer in `lib/src/infrastructure` (e.g., file-based persistence, HTTP client for ordering).
- [ ] Implement the Presentation layer (HTTP API) using the chosen HTTP library.
- [ ] Implement the startup logic to check for expired items.
- [ ] Implement a background mechanism (e.g., a thread) to periodically check for expired items.
## Phase 5: Testing
- [ ] Set up a `tests` directory.
- [ ] Create `tests/CMakeLists.txt` to build the test runner.
- [ ] Write unit tests for the Domain layer.
- [ ] Write unit tests for the Application layer, using mocks for infrastructure interfaces.
- [ ] Write integration tests for the Infrastructure layer.
## Phase 6: Containerization
- [x] Create a `Dockerfile` to build the C++ application in a container.
- [x] Create a `docker-compose.yml` file to easily build and run the application.

17
cpp17/app/CMakeLists.txt

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

13
cpp17/app/defaults/users.json

@ -1,13 +0,0 @@
[
{
"username": "admin",
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"id": "1000"
},
{
"username": "user",
"password": "04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb",
"id": "1001"
}
]

26
cpp17/app/src/App.cpp

@ -1,34 +1,20 @@
#include "App.h" #include "App.h"
#include "SpdLogger.h"
#include "OsHelpers.h"
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
namespace nxl { namespace nxl {
using nxl::autostore::AutoStore;
std::condition_variable App::exitCv; std::condition_variable App::exitCv;
std::mutex App::mtx; std::mutex App::mtx;
bool App::shouldExit = false; bool App::shouldExit = false;
nxl::autostore::ILoggerPtr log{nullptr};
App::App(int argc, char** argv) App::App(int argc, char** argv)
{ {
signal(SIGINT, App::handleSignal); signal(SIGINT, App::handleSignal);
signal(SIGTERM, App::handleSignal); signal(SIGTERM, App::handleSignal);
auto spdLogger = spdlog::stdout_color_mt("console"); std::filesystem::create_directories("data");
spdLogger->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v"); autoStore = std::make_unique<nxl::autostore::AutoStore>("data");
spdLogger->set_level(spdlog::level::debug);
log = logger = std::make_shared<SpdLogger>(spdLogger, 9);
autoStore = std::make_unique<AutoStore>(
AutoStore::Config{
.dataPath = os::getApplicationDirectory() + "/data",
.host = "0.0.0.0",
.port = 50080,
},
logger);
if (!autoStore->initialize()) { if (!autoStore->initialize()) {
std::cerr << "Failed to initialize AutoStore" << std::endl; std::cerr << "Failed to initialize AutoStore" << std::endl;
@ -45,7 +31,7 @@ int App::exec()
return 1; return 1;
} }
logger->info("AutoStore is running. Press Ctrl+C to stop."); std::cout << "AutoStore is running. Press Ctrl+C to stop." << std::endl;
std::unique_lock<std::mutex> lock(mtx); std::unique_lock<std::mutex> lock(mtx);
exitCv.wait(lock, [] { return shouldExit; }); exitCv.wait(lock, [] { return shouldExit; });
@ -57,10 +43,8 @@ int App::exec()
void App::handleSignal(int signum) void App::handleSignal(int signum)
{ {
if (log) { std::cout << "\nCaught signal " << signum << ". Graceful shutdown."
log->info("Caught signal %d. Graceful shutdown.", signum); << std::endl;
}
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
shouldExit = true; shouldExit = true;

2
cpp17/app/src/App.h

@ -7,7 +7,6 @@
#include <thread> #include <thread>
#include <memory> #include <memory>
#include <autostore/AutoStore.h> #include <autostore/AutoStore.h>
#include <autostore/ILogger.h>
namespace nxl { namespace nxl {
@ -25,7 +24,6 @@ private:
static bool shouldExit; static bool shouldExit;
std::unique_ptr<nxl::autostore::AutoStore> autoStore; std::unique_ptr<nxl::autostore::AutoStore> autoStore;
autostore::ILoggerPtr logger;
}; };
} // namespace nxl } // namespace nxl

2
cpp17/app/src/Main.cpp

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

48
cpp17/app/src/OsHelpers.h

@ -1,48 +0,0 @@
#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

49
cpp17/app/src/SpdLogger.h

@ -1,49 +0,0 @@
#pragma once
#include <autostore/ILogger.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
class SpdLogger : public nxl::autostore::ILogger
{
public:
explicit SpdLogger(std::shared_ptr<spdlog::logger> logger, int8_t vlevel)
: logger{std::move(logger)}, vlevel{vlevel}
{}
protected:
void log(LogLevel level, std::string_view message) override
{
switch (level) {
case LogLevel::Info:
logger->info(message);
break;
case LogLevel::Warning:
logger->warn(message);
break;
case LogLevel::Error:
logger->error(message);
break;
case LogLevel::Debug:
logger->debug(message);
break;
}
}
void vlog(int8_t level, std::string_view message) override
{
if (level > vlevel) {
return;
}
logger->log(spdlog::level::info,
"[V:" + std::to_string(level) + "] " + std::string(message));
}
std::shared_ptr<spdlog::logger> getLogger() const { return logger; }
void setVLevel(int8_t level) { vlevel = level; }
private:
int8_t vlevel{-1};
std::shared_ptr<spdlog::logger> logger;
};

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

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

@ -2,57 +2,32 @@
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Client as HTTP Client
participant Middleware as HttpJwtMiddleware
participant Server as HttpServer
participant Controller as StoreController participant Controller as StoreController
participant AuthService as IAuthService participant UseCase as AddItem Use Case
participant UseCase as AddItem participant Clock as IClock
participant Clock as ITimeProvider participant Policy as ExpirationPolicy
participant Policy as ItemExpirationPolicy participant OrderService as OrderingService
participant OrderService as IOrderService participant HttpClient as HttpClient
participant HttpClient as HttpOrderService
participant Repo as IItemRepository participant Repo as IItemRepository
Client->>Server: POST /items with JWT
Server->>Middleware: Validate token
Middleware-->>Server: token valid
Server->>Controller: Forward request
Controller->>AuthService: Extract user ID from token
AuthService-->>Controller: User ID
Controller->>Controller: Parse request body to Item
Controller->>UseCase: execute(item) Controller->>UseCase: execute(item)
UseCase->>Clock: now() UseCase->>Clock: getCurrentTime()
Clock-->>UseCase: current time Clock-->>UseCase: DateTime
UseCase->>Policy: isExpired(item, currentTime) UseCase->>Policy: IsExpired(item, currentTime)
Policy-->>UseCase: boolean Policy-->>UseCase: boolean
alt Item is expired alt Item is expired
UseCase->>OrderService: orderItem(item) UseCase->>OrderService: orderItem(item)
OrderService->>HttpClient: POST to order URL OrderService->>HttpClient: POST to order URL
HttpClient-->>OrderService: Response HttpClient-->>OrderService: Response
OrderService-->>UseCase: void OrderService-->>UseCase: OrderResult
end end
UseCase->>Repo: save(item) UseCase->>Repo: save(item)
Repo->>Repo: Persist to file storage Repo->>Repo: Persist to storage
Repo-->>UseCase: Item ID Repo-->>UseCase: Saved Item ID
UseCase-->>Controller: Item ID
Controller->>Controller: Build success response
Controller-->>Client: 201 Created with Item ID
alt Error occurs UseCase-->>Controller: Result (success/error)
UseCase-->>Controller: Exception
Controller->>Controller: Build error response
Controller-->>Client: 4xx/5xx error
end
alt Authentication fails
Middleware-->>Client: 401 Unauthorized
end
``` ```

82
cpp17/doc/architecture-overview.md

@ -1,82 +0,0 @@
# AutoStore Architecture Overview
## Layer Boundaries
```mermaid
graph TB
subgraph PL[Presentation Layer]
A[StoreController]
B[AuthController]
end
subgraph AL[Application Layer]
E[AddItem Use Case]
F[DeleteItem Use Case]
G[LoginUser Use Case]
H[GetItem Use Case]
I[ListItems Use Case]
J[HandleExpiredItems Use Case]
K[TaskScheduler]
L[IItemRepository]
M[IAuthService]
N[IOrderService]
O[ITimeProvider]
P[IThreadManager]
end
subgraph DL[Domain Layer]
Q[Item]
R[User]
S[ItemExpirationPolicy]
end
subgraph IL[Infrastructure Layer]
C[HttpServer]
D[HttpJwtMiddleware]
T[FileItemRepository]
U[FileJwtAuthService]
V[HttpOrderService]
W[SystemTimeProvider]
X[SystemThreadManager]
Y[CvBlocker]
end
```
## Component Dependencies
```mermaid
graph LR
StoreController --> AddItem
StoreController --> DeleteItem
StoreController --> GetItem
StoreController --> ListItems
StoreController --> IAuthService
AuthController --> LoginUser
AddItem --> IItemRepository
AddItem --> ITimeProvider
AddItem --> IOrderService
AddItem --> ItemExpirationPolicy
DeleteItem --> IItemRepository
LoginUser --> IAuthService
GetItem --> IItemRepository
ListItems --> IItemRepository
HandleExpiredItems --> IItemRepository
HandleExpiredItems --> ITimeProvider
HandleExpiredItems --> IOrderService
HandleExpiredItems --> ItemExpirationPolicy
TaskScheduler --> ITimeProvider
TaskScheduler --> IThreadManager
IItemRepository --> Item
IAuthService --> User
IOrderService --> Item
ItemExpirationPolicy --> Item
ItemExpirationPolicy --> ITimeProvider

25
cpp17/doc/request-sequence.md

@ -0,0 +1,25 @@
# General request sequence with authentication
```mermaid
sequenceDiagram
participant Client as HTTP Client
participant Router as Request Router
participant Auth as JwtMiddleware
participant Controller as Controller
participant UseCase as Use Case
Client->>Router: POST /api/items (with JWT)
Router->>Auth: Forward request
alt Authentication successful
Auth->>Auth: Validate JWT
Auth->>Controller: Forward authenticated request
Controller->>Controller: Parse request body to DTO
Controller->>UseCase: execute()
UseCase-->>Controller: Result (success/error)
Controller->>Controller: Convert result to HTTP response
Controller-->>Client: HTTP Response (2xx)
else Authentication fails
Auth-->>Client: 401 Unauthorized
end
```

4
cpp17/docker/.dockerignore

@ -1,4 +0,0 @@
.devcontainer
.out
build
build-*

23
cpp17/docker/Dockerfile

@ -1,28 +1,13 @@
FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS base
WORKDIR /workspace WORKDIR /workspace
COPY ../CMakeLists.txt .
COPY ../vcpkg.json .
RUN vcpkg install
# Cche stays valid if only code changes
COPY .. . COPY .. .
# generate and build
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \
-H/workspace -B/workspace/build -G Ninja -H/workspace -B/workspace/build -G Ninja
RUN cmake --build /workspace/build --config Release --target all -j 8 -- RUN cmake --build /workspace/build --config Release --target all -j 6 --
# run tests
RUN cd /workspace/build && ctest --output-on-failure .
FROM ubuntu:24.04 AS runtime
WORKDIR /app
COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore
COPY --from=builder /workspace/build/bin/data ./data
CMD ["./AutoStore"] CMD ["/workspace/build/bin/AutoStore"]

4
cpp17/docker/docker-compose.yml

@ -3,8 +3,6 @@ services:
app: app:
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: Docker/Dockerfile
image: autostore-build-cpp-vcpkg-img image: autostore-build-cpp-vcpkg-img
container_name: autostore-build-cpp-vcpkg container_name: autostore-build-cpp-vcpkg
ports:
- 50080:50080

32
cpp17/lib/CMakeLists.txt

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

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

@ -1,6 +1,5 @@
#pragma once #pragma once
#include "autostore/ILogger.h"
#include <memory> #include <memory>
#include <string> #include <string>
#include <string_view> #include <string_view>
@ -8,40 +7,19 @@
namespace nxl::autostore { namespace nxl::autostore {
namespace application { namespace di {
class IItemRepository; class DiContainer;
class ITimeProvider; }
class IOrderService;
class IAuthService;
class IThreadManager;
class TaskScheduler;
} // namespace application
namespace infrastructure {
class HttpServer;
} // namespace infrastructure
namespace webapi { namespace webapi {
class StoreController; class StoreController;
class AuthController; }
} // namespace webapi
namespace application {
class AddItem;
class LoginUser;
} // namespace application
class AutoStore class AutoStore
{ {
public: public:
struct Config AutoStore(std::string_view dataPath, int port = 8080,
{ std::string_view host = "0.0.0.0");
std::string dataPath;
std::string host{"0.0.0.0"};
uint16_t port{50080};
};
AutoStore(Config config, ILoggerPtr logger);
~AutoStore(); ~AutoStore();
bool initialize(); bool initialize();
@ -49,19 +27,15 @@ public:
void stop(); void stop();
private: private:
Config config; int port;
ILoggerPtr log; std::string host;
std::string dataPath;
std::unique_ptr<infrastructure::HttpServer> httpServer; bool initialized;
std::unique_ptr<application::TaskScheduler> taskScheduler; std::thread serverThread;
bool serverRunning;
std::unique_ptr<di::DiContainer> diContainer;
std::unique_ptr<webapi::StoreController> storeController; std::unique_ptr<webapi::StoreController> storeController;
std::unique_ptr<webapi::AuthController> authController;
std::unique_ptr<application::IItemRepository> itemRepository;
std::unique_ptr<application::ITimeProvider> clock;
std::unique_ptr<application::IOrderService> orderService;
std::unique_ptr<application::IAuthService> authService;
std::unique_ptr<application::ITimeProvider> timeProvider;
std::unique_ptr<application::IThreadManager> threadManager;
}; };
} // namespace nxl::autostore } // namespace nxl::autostore

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

@ -1,98 +0,0 @@
#pragma once
#include <memory>
#include <string>
#include <utility>
#include <cstdint>
#include <string_view>
#include <cstdio>
namespace nxl::autostore {
template <typename T> auto to_printf_arg(T&& arg)
{
if constexpr (std::is_same_v<std::decay_t<T>, std::string_view>) {
return arg.data();
} else if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
return arg.c_str();
} else {
return std::forward<T>(arg);
}
}
#define DEFINE_LOG_METHOD(name, level) \
void name(const char* message) \
{ \
log(LogLevel::level, std::string_view(message)); \
} \
template <typename... Args> void name(const char* format, Args&&... args) \
{ \
_log(LogLevel::level, format, -1, std::forward<Args>(args)...); \
}
#define DEFINE_LOGGER_ALIAS(original, alias) \
template <typename... Args> void alias(const char* format, Args&&... args) \
{ \
original(format, std::forward<Args>(args)...); \
}
class ILogger
{
public:
virtual ~ILogger() = default;
DEFINE_LOG_METHOD(info, Info)
DEFINE_LOG_METHOD(warning, Warning)
DEFINE_LOG_METHOD(error, Error)
DEFINE_LOG_METHOD(debug, Debug)
void verbose(int8_t level, const char* message) { vlog(level, message); }
template <typename... Args>
void verbose(int8_t level, const char* format, Args&&... args)
{
_log(LogLevel::Verbose, format, level, std::forward<Args>(args)...);
}
// Aliases defined using macro
DEFINE_LOGGER_ALIAS(info, i)
DEFINE_LOGGER_ALIAS(warning, w)
DEFINE_LOGGER_ALIAS(error, e)
DEFINE_LOGGER_ALIAS(debug, d)
void v(int8_t level, const char* message) { vlog(level, message); }
template <typename... Args>
void v(int8_t level, const char* format, Args&&... args)
{
_log(LogLevel::Verbose, format, level, std::forward<Args>(args)...);
}
protected:
enum class LogLevel { Info, Warning, Error, Debug, Verbose };
virtual void log(LogLevel level, std::string_view message) = 0;
virtual void vlog(int8_t level, std::string_view message) = 0;
private:
template <typename... Args>
void _log(LogLevel level, const char* format, int8_t vlevel = -1,
Args&&... args)
{
// Create a lambda that captures the converted arguments
auto format_message = [format](auto&&... args) {
// Calculate the required size
size_t size = std::snprintf(nullptr, 0, format, args...) + 1;
// Format the message
std::string msg;
msg.resize(size);
std::snprintf(&msg[0], size, format, args...);
msg.pop_back();
return msg;
};
// Call the lambda with the converted arguments
std::string msg =
format_message(to_printf_arg(std::forward<Args>(args))...);
vlevel == -1 ? log(level, msg) : vlog(vlevel, msg);
}
};
// Undefine the macro to avoid polluting the namespace
#undef DEFINE_LOGGER_ALIAS
using ILoggerPtr = std::shared_ptr<ILogger>;
} // namespace nxl::autostore

127
cpp17/lib/src/AutoStore.cpp

@ -1,132 +1,97 @@
#include "AutoStore.h" #include "AutoStore.h"
#include "DiContainer.h"
#include "infrastructure/repositories/FileItemRepository.h" #include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/adapters/SystemTimeProvider.h" #include "infrastructure/adapters/SystemClock.h"
#include "infrastructure/http/HttpOrderService.h" #include "infrastructure/http/HttpOrderService.h"
#include "infrastructure/auth/FileJwtAuthService.h"
#include "webapi/controllers/StoreController.h" #include "webapi/controllers/StoreController.h"
#include "webapi/controllers/AuthController.h"
#include "infrastructure/http/HttpServer.h" #include "infrastructure/http/HttpServer.h"
#include "application/services/TaskScheduler.h"
#include "application/commands/HandleExpiredItems.h"
#include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/adapters/SystemThreadManager.h"
#include "infrastructure/adapters/CvBlocker.h"
#include <iostream> #include <iostream>
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
namespace nxl::autostore { namespace nxl::autostore {
using namespace infrastructure; AutoStore::AutoStore(std::string_view dataPath, int port, std::string_view host)
using namespace application; : dataPath(dataPath), port(port), host(host), initialized(false),
serverRunning(false)
AutoStore::AutoStore(Config config, ILoggerPtr logger)
: config{std::move(config)}, log{std::move(logger)}
{} {}
AutoStore::~AutoStore() AutoStore::~AutoStore()
{ {
if (httpServer && httpServer->isRunning()) { if (serverRunning) {
stop(); stop();
} }
} }
bool AutoStore::initialize() bool AutoStore::initialize()
{ {
try { std::cout << "Initializing AutoStore with data path: " << dataPath
std::filesystem::create_directories(config.dataPath); << ", host: " << host << ", port: " << port << std::endl;
// Initialize repositories and services
std::string itemsDbPath =
std::filesystem::path(config.dataPath) / "items.json";
itemRepository = std::make_unique<FileItemRepository>(itemsDbPath);
clock = std::make_unique<SystemTimeProvider>();
orderService = std::make_unique<HttpOrderService>(log);
// Initialize auth service
std::string usersDbPath =
std::filesystem::path(config.dataPath) / "users.json";
authService = std::make_unique<FileJwtAuthService>(usersDbPath);
// Initialize dependencies for task scheduler
timeProvider = std::make_unique<SystemTimeProvider>();
threadManager = std::make_unique<SystemThreadManager>();
auto blocker = std::make_unique<CvBlocker>();
// Initialize task scheduler (for handling expired items)
taskScheduler = std::make_unique<TaskScheduler>(
log, *timeProvider, *threadManager, std::move(blocker));
// Initialize HTTP server
httpServer = std::make_unique<HttpServer>(log, *authService);
// Initialize store controller try {
storeController = std::make_unique<webapi::StoreController>( std::filesystem::create_directories(dataPath);
webapi::StoreController::Context{
application::AddItem{*itemRepository, *clock, *orderService},
application::ListItems{*itemRepository},
application::GetItem{*itemRepository},
application::DeleteItem{*itemRepository}, *authService});
// Initialize auth controller
authController = std::make_unique<webapi::AuthController>(
webapi::AuthController::Context{application::LoginUser{*authService}});
log->info("Data path: %s", config.dataPath); diContainer = std::make_unique<di::DiContainer>();
log->info("AutoStore initialized successfully, handling expired items..."); storeController = std::make_unique<webapi::StoreController>(*diContainer);
initialized = true;
std::cout << "AutoStore initialized successfully" << std::endl;
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
log->error("Failed to initialize AutoStore: %s", e.what()); std::cerr << "Failed to initialize AutoStore: " << e.what() << std::endl;
return false; return false;
} }
} }
bool AutoStore::start() bool AutoStore::start()
{ {
log->info("Starting AutoStore services..."); if (!initialized) {
std::cerr << "AutoStore not initialized. Call initialize() first."
<< std::endl;
return false;
}
std::cout << "Starting AutoStore services..." << std::endl;
try { try {
taskScheduler->schedule( auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>();
[this]() { storeController->registerRoutes(httpServer.getServer());
application::HandleExpiredItems{*itemRepository, *clock, *orderService}
.execute(); if (!httpServer.start(port, host)) {
}, std::cerr << "Failed to start HTTP server" << std::endl;
00, 00, 00, // midnight (00:00:00)
TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::OnStart);
taskScheduler->start();
storeController->registerRoutes(httpServer->getServer());
authController->registerRoutes(httpServer->getServer());
if (!httpServer->start(config.port, config.host)) {
log->error("Failed to start HTTP server");
return false; return false;
} }
log->info("AutoStore services started successfully"); serverRunning = true;
log->info("HTTP server listening on http://%s:%d", config.host, std::cout << "AutoStore services started successfully" << std::endl;
config.port); std::cout << "HTTP server listening on http://" << host << ":" << port
log->info("API endpoint: POST http://%s:%d/api/v1", config.host, << std::endl;
config.port); std::cout << "API endpoint: POST http://" << host << ":" << port
<< "/api/items" << std::endl;
return true; return true;
} catch (const std::exception& e) { } catch (const std::exception& e) {
log->error("Failed to start AutoStore services: %s", e.what()); std::cerr << "Failed to start AutoStore services: " << e.what()
<< std::endl;
return false; return false;
} }
} }
void AutoStore::stop() void AutoStore::stop()
{ {
log->info("Stopping AutoStore services..."); if (!serverRunning) {
return;
}
if (httpServer && httpServer->isRunning()) { std::cout << "Stopping AutoStore services..." << std::endl;
httpServer->stop();
if (diContainer) {
auto& httpServer = diContainer->resolveRef<infrastructure::HttpServer>();
httpServer.stop();
} }
log->info("AutoStore services stopped");
serverRunning = false;
std::cout << "AutoStore services stopped" << std::endl;
} }
} // namespace nxl::autostore } // namespace nxl::autostore

45
cpp17/lib/src/DiContainer.cpp

@ -0,0 +1,45 @@
#include "DiContainer.h"
#include "infrastructure/repositories/FileItemRepository.h"
#include "infrastructure/adapters/SystemClock.h"
#include "infrastructure/http/HttpOrderService.h"
#include "infrastructure/http/HttpServer.h"
#include "application/commands/AddItem.h"
#include "webapi/controllers/StoreController.h"
#include <filesystem>
namespace nxl::autostore::di {
DiContainer::DiContainer()
{
registerDependencies();
}
void DiContainer::registerDependencies()
{
// Register shared references
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::FileItemRepository>,
dingo::interface<application::IItemRepository>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::SystemClock>,
dingo::interface<application::IClock>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::HttpOrderService>,
dingo::interface<application::IOrderService>>();
container.register_type<dingo::scope<dingo::shared>,
dingo::storage<infrastructure::HttpServer>>();
container.register_indexed_type<dingo::scope<dingo::shared>,
dingo::storage<application::AddItem>,
dingo::interface<application::AddItem>>(
std::string("AddItem"));
// test:
auto uc = container.resolve<application::AddItem>(
std::string("AddItem")); // throws on start
}
} // namespace nxl::autostore::di

102
cpp17/lib/src/DiContainer.h

@ -0,0 +1,102 @@
#pragma once
#include <dingo/container.h>
#include <dingo/factory/constructor.h>
#include <dingo/storage/external.h>
#include <dingo/storage/shared.h>
#include <dingo/index/unordered_map.h>
#include <memory>
#include <string>
#include <filesystem>
#include <tuple>
// Forward declarations
namespace nxl::autostore {
class AutoStore;
}
namespace nxl::autostore::infrastructure {
class FileItemRepository;
class SystemClock;
class HttpOrderService;
class HttpServer;
} // namespace nxl::autostore::infrastructure
namespace nxl::autostore::application {
class AddItem;
}
namespace nxl::autostore::webapi {
class StoreController;
}
namespace nxl::autostore::di {
// Declare traits with std::string based index for named resolution
struct container_traits : dingo::dynamic_container_traits
{
using index_definition_type =
std::tuple<std::tuple<std::string, dingo::index_type::unordered_map>>;
};
/**
* @brief Dependency Injection Container for AutoStore application
*
* This class wraps the dingo container and provides a simplified interface
* for registering and resolving dependencies in the AutoStore application.
*/
class DiContainer
{
public:
/**
* @brief Construct a new DiContainer object
*/
DiContainer();
/**
* @brief Destroy the DiContainer object
*/
~DiContainer() = default;
/**
* @brief Register all application dependencies
*
* @param dataPath Path to the data directory
* @param port HTTP server port
* @param host HTTP server host
*/
void registerDependencies();
/**
* @brief Resolve a dependency by type
*
* @tparam T Type to resolve
* @return Instance of the resolved type
*/
template <typename T> T resolve() { return container.resolve<T>(); }
/**
* @brief Resolve a dependency by type as a shared pointer
*
* @tparam T Type to resolve
* @return Shared pointer to the resolved type
*/
template <typename T> std::shared_ptr<T> resolveShared()
{
return container.resolve<std::shared_ptr<T>>();
}
/**
* @brief Resolve a dependency by type as a reference
*
* @tparam T Type to resolve
* @return Reference to the resolved type
*/
template <typename T> T& resolveRef() { return container.resolve<T&>(); }
private:
dingo::container<container_traits> container;
};
} // namespace nxl::autostore::di

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,14 @@
#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

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

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

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

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

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

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

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

@ -0,0 +1,23 @@
#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

34
cpp17/lib/src/application/presenters/GenericPresenters.h

@ -0,0 +1,34 @@
#pragma once
#include <functional>
#include <string>
namespace nxl::autostore::application {
struct OpResult
{
bool success;
std::string message;
bool operator==(const OpResult& other) const
{
return success == other.success && message == other.message;
}
};
struct ErrorResult : public OpResult
{
ErrorResult(std::string message) : OpResult({false, message}) {}
};
struct SuccessResult : public OpResult
{
SuccessResult(std::string message) : OpResult({true, message}) {}
};
using BoolPresenter = std::function<void(bool)>;
using IntPresenter = std::function<void(int)>;
using DoublePresenter = std::function<void(double)>;
using StringPresenter = std::function<void(std::string)>;
} // namespace nxl::autostore::application

10
cpp17/lib/src/application/presenters/StorePresenters.h

@ -0,0 +1,10 @@
#pragma once
#include "domain/entities/Item.h"
#include <functional>
namespace nxl::autostore::application {
using ItemPresenter = std::function<void(const domain::Item& item)>;
} // namespace nxl::autostore::application

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

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

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

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

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

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

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

@ -1,21 +0,0 @@
#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

229
cpp17/lib/src/application/services/TaskScheduler.cpp

@ -1,229 +0,0 @@
#include "TaskScheduler.h"
#include <stdexcept>
namespace nxl::autostore::application {
namespace {
using Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point;
using Duration = Clock::duration;
using Hours = std::chrono::hours;
using Minutes = std::chrono::minutes;
using Seconds = std::chrono::seconds;
using Milliseconds = std::chrono::milliseconds;
using Days = std::chrono::duration<int, std::ratio<86400>>;
using RunMode = TaskScheduler::RunMode;
bool isValidTime(int hour, int minute, int second)
{
return (hour >= 0 && hour <= 23) && (minute >= 0 && minute <= 59)
&& (second >= 0 && second <= 59);
}
bool areModesMutuallyExclusive(RunMode mode)
{
return (static_cast<int>(mode) & static_cast<int>(RunMode::Forever))
&& (static_cast<int>(mode) & static_cast<int>(RunMode::Once));
}
TimePoint todayAt(uint8_t hour, uint8_t minute, uint8_t second,
const ITimeProvider& timeProvider)
{
auto now = timeProvider.now();
auto midnight =
std::chrono::time_point_cast<Duration>(std::chrono::floor<Days>(now));
auto offset = Hours{hour} + Minutes{minute} + Seconds{second};
return midnight + offset;
}
bool shouldExecuteOnStart(const TaskScheduler::ScheduledTask& task)
{
return (static_cast<int>(task.mode) & static_cast<int>(RunMode::OnStart))
&& !task.executed;
}
TimePoint calculateNextExecutionTime(const TaskScheduler::ScheduledTask& task,
const ITimeProvider& timeProvider,
TimePoint now)
{
auto taskTime = todayAt(task.hour, task.minute, task.second, timeProvider);
if (taskTime <= now) {
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
taskTime += Hours(24);
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& task.executed) {
return TimePoint{};
}
}
return taskTime;
}
bool shouldExecuteBasedOnTime(const TaskScheduler::ScheduledTask& task,
TimePoint now)
{
if (task.nextExecution == TimePoint{}) {
return false;
}
return task.nextExecution <= now;
}
void executeTask(TaskScheduler::ScheduledTask& task, ILogger& logger,
const ITimeProvider& timeProvider, TimePoint& nextWakeupTime)
{
try {
task.function();
task.executed = true;
logger.info("Task executed successfully");
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
auto nextTaskTime =
todayAt(task.hour, task.minute, task.second, timeProvider);
nextTaskTime += Hours(24);
task.nextExecution = nextTaskTime;
if (nextTaskTime < nextWakeupTime) {
nextWakeupTime = nextTaskTime;
}
}
} catch (const std::exception& e) {
logger.error("Task execution failed: %s", e.what());
}
}
void processTasks(std::vector<TaskScheduler::ScheduledTask>& tasks,
ILogger& logger, const ITimeProvider& timeProvider,
std::atomic<bool>& stopRequested, TimePoint now,
bool& hasOnStartTask, TimePoint& nextWakeupTime)
{
for (auto& task : tasks) {
if (stopRequested) {
break;
}
bool executeNow = false;
if (shouldExecuteOnStart(task)) {
executeNow = true;
hasOnStartTask = true;
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
|| (static_cast<int>(task.mode)
& static_cast<int>(RunMode::Forever))) {
if (task.nextExecution == TimePoint{}) {
auto taskTime = calculateNextExecutionTime(task, timeProvider, now);
if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& taskTime == TimePoint{} && !task.executed) {
executeNow = true;
} else if (taskTime != TimePoint{}) {
task.nextExecution = taskTime;
}
}
if (!executeNow && shouldExecuteBasedOnTime(task, now)) {
executeNow = true;
}
if (!executeNow && task.nextExecution < nextWakeupTime) {
nextWakeupTime = task.nextExecution;
}
}
if (executeNow) {
executeTask(task, logger, timeProvider, nextWakeupTime);
}
}
}
void waitForNextTask(IBlocker& blocker, std::atomic<bool>& stopRequested,
TimePoint now, TimePoint nextWakeupTime)
{
if (!stopRequested && nextWakeupTime > now) {
auto waitDuration =
std::chrono::duration_cast<Milliseconds>(nextWakeupTime - now);
auto maxWait = Minutes(1);
if (waitDuration > maxWait) {
waitDuration = maxWait;
}
blocker.blockFor(waitDuration);
}
}
} // namespace
TaskScheduler::TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
IThreadManager& threadManager,
std::unique_ptr<IBlocker> blocker)
: logger{std::move(logger)}, timeProvider{timeProvider},
threadManager{threadManager}, blocker{std::move(blocker)}
{}
void TaskScheduler::schedule(TaskFunction task, int hour, int minute,
int second, RunMode mode)
{
if (!isValidTime(hour, minute, second)) {
throw std::invalid_argument("Invalid time parameters");
}
if (areModesMutuallyExclusive(mode)) {
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;
auto nextWakeupTime = now + Hours(24);
{
std::lock_guard<std::mutex> lock(tasksMutex);
processTasks(tasks, *logger, timeProvider, stopRequested, now,
shouldExecuteOnStart, nextWakeupTime);
}
if (shouldExecuteOnStart) {
continue;
}
waitForNextTask(*blocker, stopRequested, now, nextWakeupTime);
}
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::application

85
cpp17/lib/src/application/services/TaskScheduler.h

@ -1,85 +0,0 @@
#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::application {
class TaskScheduler
{
public:
enum class RunMode {
OnStart = 1,
Forever = 2,
OnStartThenForever = 3, // OnStart | Forever
Once = 4,
OnStartThenOnce = 5 // OnStart | Once
};
using TaskFunction = std::function<void()>;
using TimePoint = application::ITimeProvider::Clock::time_point;
using ThreadHandle = application::IThreadManager::ThreadHandle;
struct ScheduledTask
{
TaskFunction function;
int hour;
int minute;
int second;
RunMode mode;
bool executed = false;
TimePoint nextExecution{};
ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md)
: function(std::move(t)), hour(h), minute(m), second(s), mode(md)
{}
};
TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
IThreadManager& threadManager,
std::unique_ptr<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;
ITimeProvider& timeProvider;
IThreadManager& threadManager;
std::unique_ptr<IBlocker> blocker;
std::vector<ScheduledTask> tasks;
std::mutex tasksMutex;
std::atomic<bool> running{false};
std::atomic<bool> stopRequested{false};
std::unique_ptr<ThreadHandle> threadHandle;
};
constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a,
TaskScheduler::RunMode b)
{
return static_cast<TaskScheduler::RunMode>(static_cast<uint8_t>(a)
| static_cast<uint8_t>(b));
}
constexpr uint8_t operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b)
{
return static_cast<uint8_t>(a) & static_cast<uint8_t>(b);
}
} // namespace nxl::autostore::application

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

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

141
cpp17/lib/src/domain/helpers/Specification.cpp

@ -1,141 +0,0 @@
#include "Specification.h"
#include <stdexcept>
namespace nxl::helpers {
// Condition render implementation
std::string Condition::render(const Renderer& renderer) const
{
std::string opStr;
switch (op) {
case ComparisonOp::EQ:
opStr = renderer.opEq;
break;
case ComparisonOp::NE:
opStr = renderer.opNe;
break;
case ComparisonOp::LT:
opStr = renderer.opLt;
break;
case ComparisonOp::LE:
opStr = renderer.opLe;
break;
case ComparisonOp::GT:
opStr = renderer.opGt;
break;
case ComparisonOp::GE:
opStr = renderer.opGe;
break;
case ComparisonOp::LIKE:
opStr = renderer.opLike;
break;
case ComparisonOp::IS_NULL:
opStr = renderer.opIsNull;
break;
case ComparisonOp::IS_NOT_NULL:
opStr = renderer.opIsNotNull;
break;
}
std::string result = field + " " + opStr;
if (value) {
result += " " + renderer.formatValue(*value);
}
return result;
}
// ConditionGroup render implementation
std::string ConditionGroup::render(const Renderer& renderer) const
{
if (children.empty())
return "";
std::string opStr = (op == LogicalOp::AND) ? renderer.opAnd : renderer.opOr;
std::string result = renderer.groupStart;
bool first = true;
for (const auto& child : children) {
if (!first)
result += " " + opStr + " ";
result += child->render(renderer);
first = false;
}
result += renderer.groupEnd;
return result;
}
// SpecificationBuilder implementation
SpecificationBuilder::SpecificationBuilder()
{
root = std::make_unique<ConditionGroup>(LogicalOp::AND);
current = root.get();
}
SpecificationBuilder& SpecificationBuilder::field(const std::string& name)
{
lastField = name;
return *this;
}
SpecificationBuilder& SpecificationBuilder::like(std::string pattern)
{
addCondition(ComparisonOp::LIKE, std::move(pattern));
return *this;
}
SpecificationBuilder& SpecificationBuilder::isNull()
{
addCondition(ComparisonOp::IS_NULL);
return *this;
}
SpecificationBuilder& SpecificationBuilder::isNotNull()
{
addCondition(ComparisonOp::IS_NOT_NULL);
return *this;
}
SpecificationBuilder& SpecificationBuilder::andGroup()
{
startGroup(LogicalOp::AND);
return *this;
}
SpecificationBuilder& SpecificationBuilder::orGroup()
{
startGroup(LogicalOp::OR);
return *this;
}
SpecificationBuilder& SpecificationBuilder::endGroup()
{
if (stack.empty())
return *this;
current = stack.back();
stack.pop_back();
return *this;
}
std::unique_ptr<ISpecificationExpr> SpecificationBuilder::build()
{
return std::move(root);
}
void SpecificationBuilder::startGroup(LogicalOp opType)
{
auto newGroup = std::make_unique<ConditionGroup>(opType);
auto* newGroupPtr = newGroup.get();
current->add(std::move(newGroup));
stack.push_back(current);
current = newGroupPtr;
}
// Helper function
SpecificationBuilder makeSpecification()
{
return SpecificationBuilder();
}
} // namespace nxl::helpers

198
cpp17/lib/src/domain/helpers/Specification.h

@ -1,198 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <variant>
#include <optional>
#include <functional>
#include <any>
#include <stdexcept>
namespace nxl::helpers {
class ISpecificationExpr;
class Condition;
class ConditionGroup;
enum class ComparisonOp { EQ, NE, LT, LE, GT, GE, LIKE, IS_NULL, IS_NOT_NULL };
enum class LogicalOp { AND, OR };
class ISpecificationExpr
{
public:
virtual ~ISpecificationExpr() = default;
virtual std::string render(const struct Renderer& renderer) const = 0;
};
class ConditionValue
{
public:
template <typename T> ConditionValue(T value) : value_(std::move(value)) {}
const std::any& get() const { return value_; }
template <typename T> const T& as() const
{
return std::any_cast<const T&>(value_);
}
template <typename T> bool is() const { return value_.type() == typeid(T); }
private:
std::any value_;
};
class Condition : public ISpecificationExpr
{
std::string field;
ComparisonOp op;
std::optional<ConditionValue> value; // Optional for operators like IS_NULL
public:
Condition(std::string f, ComparisonOp o, std::optional<ConditionValue> v = {})
: field(std::move(f)), op(o), value(std::move(v))
{}
const std::string& getField() const { return field; }
ComparisonOp getOp() const { return op; }
const std::optional<ConditionValue>& getValue() const { return value; }
std::string render(const struct Renderer& renderer) const override;
};
// Logical group of conditions
class ConditionGroup : public ISpecificationExpr
{
LogicalOp op;
std::vector<std::unique_ptr<ISpecificationExpr>> children;
public:
ConditionGroup(LogicalOp o) : op(o) {}
void add(std::unique_ptr<ISpecificationExpr> expr)
{
children.push_back(std::move(expr));
}
LogicalOp getOp() const { return op; }
const std::vector<std::unique_ptr<ISpecificationExpr>>& getChildren() const
{
return children;
}
std::string render(const struct Renderer& renderer) const override;
};
// Renderer interface - defines how to render operators and grouping
struct Renderer
{
std::string opEq;
std::string opNe;
std::string opLt;
std::string opLe;
std::string opGt;
std::string opGe;
std::string opLike;
std::string opIsNull;
std::string opIsNotNull;
std::string opAnd;
std::string opOr;
std::string groupStart;
std::string groupEnd;
// Value formatting function
std::function<std::string(const ConditionValue&)> formatValue;
};
// Fluent builder for specifications
class SpecificationBuilder
{
std::unique_ptr<ConditionGroup> root;
ConditionGroup* current;
std::vector<ConditionGroup*> stack;
std::string lastField;
public:
SpecificationBuilder();
// Set the field for the next condition
SpecificationBuilder& field(const std::string& name);
template <typename T> SpecificationBuilder& equals(T value)
{
addCondition(ComparisonOp::EQ, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& notEquals(T value)
{
addCondition(ComparisonOp::NE, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& lessThan(T value)
{
addCondition(ComparisonOp::LT, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& lessOrEqual(T value)
{
addCondition(ComparisonOp::LE, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& greaterThan(T value)
{
addCondition(ComparisonOp::GT, std::move(value));
return *this;
}
template <typename T> SpecificationBuilder& greaterOrEqual(T value)
{
addCondition(ComparisonOp::GE, std::move(value));
return *this;
}
SpecificationBuilder& like(std::string pattern);
SpecificationBuilder& isNull();
SpecificationBuilder& isNotNull();
SpecificationBuilder& andGroup();
SpecificationBuilder& orGroup();
SpecificationBuilder& endGroup();
std::unique_ptr<ISpecificationExpr> build();
private:
template <typename T> void addCondition(ComparisonOp op, T value)
{
if (lastField.empty()) {
throw std::runtime_error("No field specified for condition");
}
auto condition = std::make_unique<Condition>(
lastField, op, ConditionValue(std::move(value)));
current->add(std::move(condition));
lastField.clear();
}
void addCondition(ComparisonOp op)
{
if (lastField.empty()) {
throw std::runtime_error("No field specified for condition");
}
auto condition = std::make_unique<Condition>(lastField, op, std::nullopt);
current->add(std::move(condition));
lastField.clear();
}
void startGroup(LogicalOp opType);
};
// Helper function to create a specification builder
SpecificationBuilder makeSpecification();
} // namespace nxl::helpers

17
cpp17/lib/src/domain/polices/ItemExpirationPolicy.h

@ -1,31 +1,18 @@
#pragma once #pragma once
#include "domain/entities/Item.h" #include "domain/entities/Item.h"
#include "domain/helpers/Specification.h"
#include <chrono> #include <chrono>
namespace nxl::autostore::domain { namespace nxl::autostore::domain {
using ItemExpirationSpec = std::unique_ptr<nxl::helpers::ISpecificationExpr>;
class ItemExpirationPolicy class ItemExpirationPolicy
{ {
public: public:
using TimePoint = std::chrono::system_clock::time_point; bool isExpired(const Item& item,
constexpr static const char* FIELD_EXP_DATE{"expiration_date"}; const std::chrono::system_clock::time_point& currentTime) const
bool isExpired(const Item& item, const TimePoint& currentTime) const
{ {
return item.expirationDate <= currentTime; return item.expirationDate <= currentTime;
} }
ItemExpirationSpec getExpiredSpecification(const TimePoint& currentTime) const
{
return nxl::helpers::SpecificationBuilder()
.field(FIELD_EXP_DATE)
.lessOrEqual(currentTime)
.build();
}
}; };
} // namespace nxl::autostore::domain } // namespace nxl::autostore::domain

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,15 +0,0 @@
#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

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

@ -1,96 +0,0 @@
#include "FileJwtAuthService.h"
#include <jwt-cpp/jwt.h>
#include <fstream>
#include <nlohmann/json.hpp>
#include <picosha2.h>
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::string passwordHash;
picosha2::hash256_hex_string(password, passwordHash);
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"] == passwordHash) {
return userJson["id"].get<std::string>();
}
}
return std::nullopt;
} catch (const std::exception& e) {
return std::nullopt;
}
}
} // namespace nxl::autostore::infrastructure

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

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

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

@ -14,7 +14,7 @@ std::string Jsend::success(const nlohmann::json& data)
return response.dump(); return response.dump();
} }
std::string Jsend::error(std::string_view message, int code, std::string Jsend::error(const std::string& message, int code,
const nlohmann::json& data) const nlohmann::json& data)
{ {
nlohmann::json response; nlohmann::json response;

2
cpp17/lib/src/infrastructure/helpers/Jsend.h

@ -9,7 +9,7 @@ class Jsend
{ {
public: public:
static std::string success(const nlohmann::json& data = nullptr); static std::string success(const nlohmann::json& data = nullptr);
static std::string error(std::string_view message, int code = 500, static std::string error(const std::string& message, int code = 500,
const nlohmann::json& data = nullptr); const nlohmann::json& data = nullptr);
private: private:

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

@ -5,7 +5,7 @@
#include <type_traits> #include <type_traits>
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
domain::Item JsonItem::fromJson(std::string_view jsonBody) domain::Item JsonItem::fromJson(const std::string& jsonBody)
{ {
auto json = nlohmann::json::parse(jsonBody); auto json = nlohmann::json::parse(jsonBody);
return fromJsonObj(json); return fromJsonObj(json);
@ -17,7 +17,7 @@ domain::Item JsonItem::fromJsonObj(const nlohmann::json& j)
item.id = j.value("id", ""); item.id = j.value("id", "");
item.name = j.value("name", ""); item.name = j.value("name", "");
item.orderUrl = j.value("orderUrl", ""); item.orderUrl = j.value("orderUrl", "");
item.userId = j.value("userId", domain::User::NULL_ID); item.userId = j.value("userId", "default-user");
if (j["expirationDate"].is_number()) { if (j["expirationDate"].is_number()) {
// Handle numeric timestamp // Handle numeric timestamp

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

@ -9,7 +9,7 @@ namespace nxl::autostore::infrastructure {
class JsonItem class JsonItem
{ {
public: public:
static domain::Item fromJson(std::string_view jsonBody); static domain::Item fromJson(const std::string& jsonBody);
static std::string toJson(const domain::Item& item); static std::string toJson(const domain::Item& item);
static nlohmann::json toJsonObj(const domain::Item& item); static nlohmann::json toJsonObj(const domain::Item& item);
static domain::Item fromJsonObj(const nlohmann::json& j); static domain::Item fromJsonObj(const nlohmann::json& j);

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

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

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

@ -1,20 +0,0 @@
#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

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

@ -1,364 +1,36 @@
/**
* HTTP-based order service implementation with retry logic and
* connection pooling
*
* FLOW OVERVIEW:
* 1. orderItem() validates input and enqueues order request
* 2. Background worker thread processes queue sequentially (FIFO)
* 3. Failed requests are retried with exponential backoff (1s, 2s, 4s...)
* 4. HTTP clients are cached per host and auto-cleaned when unused
* 5. Service shuts down gracefully, completing queued orders
*
* IMPORTANT LIMITATIONS:
* - Uses single worker thread - retries of failed requests will block
* processing of new orders until retry delay expires
* - Not suitable for time-critical operations due to sequential processing
* - Designed for fire-and-forget order notifications, not real-time
* transactions
*/
#include "HttpOrderService.h" #include "HttpOrderService.h"
#include "autostore/Version.h"
#include <httplib.h>
#include <stdexcept> #include <stdexcept>
#include <regex> #include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <chrono>
#include <memory>
#include <unordered_map>
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
namespace { HttpOrderService::HttpOrderService(const std::string& baseUrl)
: baseUrl(baseUrl)
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)
{
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;
}
if (path.empty()) {
path = "/";
}
path += query;
return {host, path};
}
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) void HttpOrderService::orderItem(const domain::Item& item)
{ {
if (item.orderUrl.empty()) { if (item.orderUrl.empty()) {
throw std::runtime_error("Order URL is empty for item: " + item.name); throw std::runtime_error("Order URL is empty for item: " + item.name);
} }
if (item.orderUrl.find("://") == std::string::npos) { std::string payload =
throw std::runtime_error("Invalid URL format for item: " + item.name R"({"itemName": ")" + item.name + R"(", "itemId": ")" + item.id + "\"}";
+ " (missing protocol)"); sendPostRequest(item.orderUrl, payload);
} }
void HttpOrderService::sendPostRequest(const std::string& url,
const std::string& payload)
{
// In a real implementation, this would use an HTTP client library
// For now, we'll simulate the HTTP call
std::cout << "POST request to: " << url << std::endl;
std::cout << "Payload: " << payload << std::endl;
std::string payload = createOrderPayload(item); // Simulate HTTP error handling
impl->enqueueOrder(item.orderUrl, std::move(payload)); if (url.find("error") != std::string::npos) {
throw std::runtime_error("Failed to send order request to: " + url);
}
} }
} // namespace nxl::autostore::infrastructure } // namespace nxl::autostore::infrastructure

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

@ -2,23 +2,19 @@
#include "application/interfaces/IOrderService.h" #include "application/interfaces/IOrderService.h"
#include "domain/entities/Item.h" #include "domain/entities/Item.h"
#include "autostore/ILogger.h" #include <string>
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
class HttpOrderService : public application::IOrderService class HttpOrderService : public application::IOrderService
{ {
public: public:
explicit HttpOrderService(ILoggerPtr logger); explicit HttpOrderService(const std::string& baseUrl = "");
virtual ~HttpOrderService();
void orderItem(const domain::Item& item) override; void orderItem(const domain::Item& item) override;
private: private:
ILoggerPtr log; std::string baseUrl;
void sendPostRequest(std::string_view url, std::string_view payload); void sendPostRequest(const std::string& url, const std::string& payload);
class Impl;
std::unique_ptr<Impl> impl;
}; };
} // namespace nxl::autostore::infrastructure } // namespace nxl::autostore::infrastructure

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

@ -1,18 +1,9 @@
#include "infrastructure/http/HttpServer.h" #include "infrastructure/http/HttpServer.h"
#include "infrastructure/http/HttpJwtMiddleware.h"
#include <iostream> #include <iostream>
#include <future>
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
namespace { HttpServer::HttpServer() {}
constexpr std::chrono::seconds defaultStartupTimeout{5};
}
HttpServer::HttpServer(ILoggerPtr logger,
application::IAuthService& authService)
: log{std::move(logger)}, authService{authService}
{}
HttpServer::~HttpServer() HttpServer::~HttpServer()
{ {
@ -21,75 +12,61 @@ HttpServer::~HttpServer()
} }
} }
bool HttpServer::start(int port, std::string_view host) bool HttpServer::start(int port, const std::string& host)
{ {
if (running) { if (running) {
return true; return true;
} }
log->info("Starting HTTP server on %s:%d...", host.data(), port); // Set up error handler
server.set_error_handler([](const auto& req, auto& res) {
std::promise<bool> startupPromise; auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
std::future<bool> startupFuture = startupPromise.get_future(); char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
serverThread = std::thread([host, port, this, &startupPromise]() { res.set_content(buf, "text/html");
log->v(1, "Server thread started, binding to %s:%d...", host, port); });
try { // Set up exception handler
// Try to bind to the port server.set_exception_handler(
if (!server.bind_to_port(host.data(), port)) { [](const auto& req, auto& res, std::exception_ptr ep) {
startupPromise.set_value(false); auto fmt = "<h1>Error 500</h1><p>%s</p>";
return; char buf[BUFSIZ];
try {
std::rethrow_exception(ep);
} catch (std::exception& e) {
snprintf(buf, sizeof(buf), fmt, e.what());
} catch (...) {
snprintf(buf, sizeof(buf), fmt, "Unknown Exception");
} }
res.set_content(buf, "text/html");
server.set_logger([this](const httplib::Request& req, const auto& res) { res.status = 500;
log->v(1, "Request: %s\n%s", req.path, req.body); });
log->v(1, "Response: %d, %s", res.status, res.body);
}); std::cout << "Starting HTTP server on " << host << ":" << port << std::endl;
// JWT authentication middleware // Start server in a separate thread
server.set_pre_request_handler( serverThread = std::thread([host, port, this]() {
[this](const httplib::Request& req, std::cout << "Server thread started, listening on " << host << ":" << port
httplib::Response& res) -> httplib::Server::HandlerResponse { << std::endl;
return HttpJwtMiddleware::authenticate(req, res, authService, log); bool listenResult = server.listen(host.c_str(), port);
}); std::cout << "Server stopped, listen result: " << listenResult << std::endl;
// Signal that the server has bound to the port
startupPromise.set_value(true);
log->info("Server thread listening on %s:%d", host, port);
bool listenResult = server.listen_after_bind();
log->info("Server stopped, listen result: %d", listenResult);
} catch (const std::exception& e) {
log->error("Server thread exception: %s", e.what());
startupPromise.set_exception(std::current_exception());
} catch (...) {
log->error("Server thread unknown exception");
startupPromise.set_exception(std::current_exception());
}
}); });
// Wait for the server to start (with a timeout) // Give the server a moment to start
if (startupFuture.wait_for(defaultStartupTimeout) std::this_thread::sleep_for(std::chrono::milliseconds(500));
== std::future_status::timeout) {
log->error("Failed to start HTTP server - timeout");
return false;
}
try { // Check if server is listening by trying to bind to the port
// Check if the server bound to the port successfully if (!server.is_running()) {
if (!startupFuture.get()) { std::cerr << "Failed to start HTTP server - server is not running"
log->error("Failed to start HTTP server - could not bind to port"); << std::endl;
return false; if (serverThread.joinable()) {
serverThread.join();
} }
} catch (const std::exception& e) {
log->error("Failed to start HTTP server - %s", e.what());
return false; return false;
} }
running = true; running = true;
log->info("HTTP server is running"); std::cout << "HTTP server is running" << std::endl;
return true; return true;
} }
@ -99,15 +76,14 @@ void HttpServer::stop()
return; return;
} }
log->info("Stopping HTTP server..."); std::cout << "Stopping HTTP server..." << std::endl;
server.stop(); server.stop();
if (serverThread.joinable()) {
log->info("Waiting for server thread to join..."); // Wait for the server to stop
serverThread.join(); std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
running = false; running = false;
log->info("HTTP server stopped"); std::cout << "HTTP server stopped" << std::endl;
} }
bool HttpServer::isRunning() const bool HttpServer::isRunning() const

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

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

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

@ -5,7 +5,6 @@
#include <chrono> #include <chrono>
#include <ctime> #include <ctime>
#include <iterator> #include <iterator>
#include "FileItemRepository.h"
namespace nxl::autostore::infrastructure { namespace nxl::autostore::infrastructure {
@ -60,7 +59,7 @@ domain::Item::Id_t FileItemRepository::save(const domain::Item& item)
return id; return id;
} }
std::optional<domain::Item> FileItemRepository::findById(domain::Item::Id_t id) std::optional<domain::Item> FileItemRepository::findById(std::string_view id)
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
auto it = std::find_if(items.begin(), items.end(), auto it = std::find_if(items.begin(), items.end(),
@ -73,7 +72,7 @@ std::optional<domain::Item> FileItemRepository::findById(domain::Item::Id_t id)
} }
std::vector<domain::Item> std::vector<domain::Item>
FileItemRepository::findByOwner(domain::User::Id_t userId) FileItemRepository::findByUser(std::string_view userId)
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> userItems; std::vector<domain::Item> userItems;
@ -82,25 +81,13 @@ FileItemRepository::findByOwner(domain::User::Id_t userId)
return userItems; return userItems;
} }
std::vector<domain::Item> FileItemRepository::findWhere( std::vector<domain::Item> FileItemRepository::findAll()
std::function<bool(const domain::Item&)> predicate)
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
std::vector<domain::Item> matchedItems; return items;
std::copy_if(items.begin(), items.end(), std::back_inserter(matchedItems),
predicate);
return matchedItems;
}
std::vector<domain::Item>
nxl::autostore::infrastructure::FileItemRepository::findWhere(
const domain::ItemExpirationSpec& spec)
{
// Not implemented since no SQL-like query language is used
throw std::runtime_error("Not implemented");
} }
void FileItemRepository::remove(domain::Item::Id_t id) void FileItemRepository::remove(std::string_view id)
{ {
std::lock_guard<std::mutex> lock(mtx); std::lock_guard<std::mutex> lock(mtx);
items.erase(std::remove_if(items.begin(), items.end(), items.erase(std::remove_if(items.begin(), items.end(),
@ -129,4 +116,4 @@ void FileItemRepository::persist()
} }
} }
} // namespace nxl::autostore::infrastructure } // namespace nxl::autostore::infrastructure

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

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

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

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

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

@ -0,0 +1,30 @@
#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

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

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

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

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

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

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

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

@ -1,86 +0,0 @@
#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

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

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

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

@ -1,40 +1,21 @@
#pragma once #pragma once
#include "webapi/controllers/BaseController.h" #include "DiContainer.h"
#include "application/commands/AddItem.h" #include <httplib.h> // TODO: forward declaration
#include "application/queries/ListItems.h"
#include "application/queries/GetItem.h"
#include "application/commands/DeleteItem.h"
#include "application/interfaces/IAuthService.h"
#include "infrastructure/helpers/JsonItem.h"
#include <httplib.h>
namespace nxl::autostore::webapi { namespace nxl::autostore::webapi {
class StoreController : public BaseController class StoreController
{ {
public: public:
struct Context StoreController(di::DiContainer& diContainer);
{
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: private:
void addItem(const httplib::Request& req, httplib::Response& res); void addItem(const httplib::Request& req, httplib::Response& res);
void listItems(const httplib::Request& req, httplib::Response& res);
void getItem(const httplib::Request& req, httplib::Response& res);
void deleteItem(const httplib::Request& req, httplib::Response& res);
void assertUserId(std::optional<domain::User::Id_t> userId) const; di::DiContainer& diContainer;
}; };
} // namespace nxl::autostore::webapi } // namespace nxl::autostore::webapi

15
cpp17/tests/CMakeLists.txt

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

57
cpp17/tests/helpers/AddItemTestHelpers.h

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

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

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

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

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

19
cpp17/tests/mocks/MockBlocker.h

@ -1,19 +0,0 @@
#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

24
cpp17/tests/mocks/MockItemRepository.h

@ -1,24 +0,0 @@
#pragma once
#include "application/interfaces/IItemRepository.h"
#include <trompeloeil.hpp>
namespace test {
using nxl::autostore::domain::Item;
using nxl::autostore::domain::User;
using nxl::autostore::domain::ItemExpirationSpec;
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(findWhere, std::vector<Item>(const ItemExpirationSpec&), override);
MAKE_MOCK1(remove, void(Item::Id_t), override);
};
} // namespace test

14
cpp17/tests/mocks/MockOrderService.h

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

24
cpp17/tests/mocks/MockThreadManager.h

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

16
cpp17/tests/mocks/MockTimeProvider.h

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

51
cpp17/tests/mocks/TestLogger.h

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

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

@ -1,223 +0,0 @@
#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);
}
}

692
cpp17/tests/unit/Specification.test.cpp

@ -1,692 +0,0 @@
#include "domain/helpers/Specification.h"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <trompeloeil.hpp>
#include <memory>
#include <stdexcept>
#include <chrono>
using trompeloeil::_;
using namespace nxl::helpers;
TEST_CASE("Specification Unit Tests", "[unit][Specification]")
{
const Renderer TEST_RENDERER{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "(",
.groupEnd = ")",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<bool>()) {
return v.as<bool>() ? "TRUE" : "FALSE";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
} else if (v.is<double>()) {
return std::to_string(v.as<double>());
} else if (v.is<std::chrono::system_clock::time_point>()) {
auto time = v.as<std::chrono::system_clock::time_point>();
auto time_t = std::chrono::system_clock::to_time_t(time);
std::string time_str = std::ctime(&time_t);
if (!time_str.empty() && time_str.back() == '\n') {
time_str.pop_back();
}
return "'" + time_str + "'";
}
return "'unknown'";
}};
SECTION("when single equals string condition is added then proper operator "
"is rendered")
{
// Given
auto spec = makeSpecification().field("F").equals("foo").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F = 'foo')");
}
SECTION(
"when condition with chrono time_point is used then it renders correctly")
{
// Given
auto now = std::chrono::system_clock::now();
auto spec = makeSpecification().field("timestamp").greaterThan(now).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result.find("timestamp >") != std::string::npos);
REQUIRE(result.find("'") != std::string::npos);
}
SECTION("when single not equals string condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").notEquals("foo").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F != 'foo')");
}
SECTION(
"when single less than condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").lessThan(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F < 42)");
}
SECTION("when single less than or equal condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").lessOrEqual(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F <= 42)");
}
SECTION("when single greater than condition is added then proper operator is "
"rendered")
{
// Given
auto spec = makeSpecification().field("F").greaterThan(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F > 42)");
}
SECTION("when single greater than or equal condition is added then proper "
"operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").greaterOrEqual(42).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F >= 42)");
}
SECTION(
"when single like condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").like("%foo%").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F LIKE '%foo%')");
}
SECTION(
"when single is null condition is added then proper operator is rendered")
{
// Given
auto spec = makeSpecification().field("F").isNull().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F IS NULL)");
}
SECTION("when single is not null condition is added then proper operator is "
"rendered")
{
// Given
auto spec = makeSpecification().field("F").isNotNull().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(F IS NOT NULL)");
}
SECTION("when condition with double value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("price").equals(19.99).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(price = 19.990000)");
}
SECTION("when condition with boolean value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("active").equals(true).build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(active = TRUE)");
}
SECTION(
"when condition with const char* value is used then it renders correctly")
{
// Given
auto spec = makeSpecification().field("name").equals("test").build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'test')");
}
SECTION("when multiple AND conditions are added then they are rendered with "
"AND operator")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND age > 30 AND active = TRUE)");
}
SECTION("when AND group is created then conditions are grouped properly")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.andGroup()
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND (age > 30 AND active = TRUE))");
}
SECTION(
"when OR group is created then conditions are grouped with OR operator")
{
// Given
auto spec = makeSpecification()
.orGroup()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "((name = 'John' OR age > 30 OR active = TRUE))");
}
SECTION("when nested groups are created then they are rendered properly")
{
// Given
auto spec = makeSpecification()
.field("category")
.equals("electronics")
.andGroup()
.field("price")
.lessThan(1000)
.orGroup()
.field("brand")
.equals("Sony")
.field("brand")
.equals("Samsung")
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(
result
== "(category = 'electronics' AND (price < 1000 AND (brand = 'Sony' "
"OR brand = 'Samsung')))");
}
SECTION("when complex specification with multiple nested groups is created "
"then it renders correctly")
{
// Given
auto spec = makeSpecification()
.orGroup()
.andGroup()
.field("type")
.equals("food")
.field("expiration_date")
.lessOrEqual("2023-01-01")
.endGroup()
.andGroup()
.field("type")
.equals("non-food")
.field("expiration_date")
.lessOrEqual("2023-03-01")
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result
== "(((type = 'food' AND expiration_date <= '2023-01-01') OR (type "
"= 'non-food' AND expiration_date <= '2023-03-01')))");
}
SECTION("when empty group is created then it renders as empty")
{
// Given
auto spec = makeSpecification()
.field("name")
.equals("John")
.andGroup()
.endGroup()
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John' AND )");
}
SECTION("when endGroup is called without matching startGroup then it should "
"handle gracefully")
{
// Given
auto spec =
makeSpecification().field("name").equals("John").endGroup().build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(name = 'John')");
}
SECTION("when condition is added without field then exception is thrown")
{
// Given
auto builder = makeSpecification();
// When/Then
REQUIRE_THROWS_AS(builder.equals("value"), std::runtime_error);
REQUIRE_THROWS_WITH(builder.equals("value"),
"No field specified for condition");
}
SECTION("when field is called multiple times then last field is used")
{
// Given
auto spec = makeSpecification()
.field("field1")
.field("field2")
.equals("value")
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(field2 = 'value')");
}
SECTION("when multiple conditions with same field are added then they are "
"rendered correctly")
{
// Given
auto spec = makeSpecification()
.field("age")
.greaterThan(18)
.field("age")
.lessThan(65)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result == "(age > 18 AND age < 65)");
}
SECTION("when specification has only conditions with no explicit grouping "
"then AND is used")
{
// Given
auto spec = makeSpecification()
.field("field1")
.equals("value1")
.field("field2")
.equals("value2")
.field("field3")
.equals("value3")
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(
result
== "(field1 = 'value1' AND field2 = 'value2' AND field3 = 'value3')");
}
SECTION("when mixed data types are used then they render correctly")
{
// Given
auto now = std::chrono::system_clock::now();
auto spec = makeSpecification()
.field("name")
.equals("John")
.field("age")
.greaterThan(30)
.field("salary")
.greaterOrEqual(50000.50)
.field("active")
.equals(true)
.field("created_at")
.lessThan(now)
.build();
// When
auto result = spec->render(TEST_RENDERER);
// Then
REQUIRE(result.find("(name = 'John' AND age > 30 AND salary >= "
"50000.500000 AND active = TRUE AND created_at < '")
!= std::string::npos);
REQUIRE(result.find("'") != std::string::npos);
}
SECTION("when ConditionValue is created with string then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(std::string("test"));
// Then
REQUIRE(value.is<std::string>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<std::string>() == "test");
}
SECTION("when ConditionValue is created with int then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(42);
// Then
REQUIRE(value.is<int>());
REQUIRE_FALSE(value.is<std::string>());
REQUIRE(value.as<int>() == 42);
}
SECTION("when ConditionValue is created with double then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(3.14);
// Then
REQUIRE(value.is<double>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<double>() == 3.14);
}
SECTION("when ConditionValue is created with bool then it stores and "
"retrieves correctly")
{
// Given
ConditionValue value(true);
// Then
REQUIRE(value.is<bool>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<bool>() == true);
}
SECTION("when ConditionValue is created with time_point then it stores and "
"retrieves correctly")
{
// Given
auto now = std::chrono::system_clock::now();
ConditionValue value(now);
// Then
REQUIRE(value.is<std::chrono::system_clock::time_point>());
REQUIRE_FALSE(value.is<int>());
REQUIRE(value.as<std::chrono::system_clock::time_point>() == now);
}
SECTION(
"when renderer has custom formatValue function then it is used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "[",
.groupEnd = "]",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "\"" + v.as<std::string>() + "\"";
} else if (v.is<const char*>()) {
return "\"" + std::string(v.as<const char*>()) + "\"";
} else if (v.is<int>()) {
return "INT:" + std::to_string(v.as<int>());
}
return "CUSTOM";
}};
auto spec = makeSpecification()
.field("name")
.equals(std::string("John"))
.field("age")
.greaterThan(30)
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "[name = \"John\" AND age > INT:30]");
}
SECTION(
"when renderer has custom grouping symbols then they are used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "AND",
.opOr = "OR",
.groupStart = "{",
.groupEnd = "}",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
}
return "'unknown'";
}};
auto spec = makeSpecification()
.field("name")
.equals(std::string("John"))
.andGroup()
.field("age")
.greaterThan(30)
.endGroup()
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "{name = 'John' AND {age > 30}}");
}
SECTION(
"when renderer has custom logical operators then they are used correctly")
{
// Given
Renderer customRenderer{
.opEq = "=",
.opNe = "!=",
.opLt = "<",
.opLe = "<=",
.opGt = ">",
.opGe = ">=",
.opLike = "LIKE",
.opIsNull = "IS NULL",
.opIsNotNull = "IS NOT NULL",
.opAnd = "&&",
.opOr = "||",
.groupStart = "(",
.groupEnd = ")",
.formatValue = [](const ConditionValue& v) -> std::string {
if (v.is<std::string>()) {
return "'" + v.as<std::string>() + "'";
} else if (v.is<const char*>()) {
return "'" + std::string(v.as<const char*>()) + "'";
} else if (v.is<int>()) {
return std::to_string(v.as<int>());
} else if (v.is<bool>()) {
return v.as<bool>() ? "TRUE" : "FALSE";
}
return "'unknown'";
}};
auto spec = makeSpecification()
.orGroup()
.field("name")
.equals(std::string("John"))
.orGroup()
.field("age")
.greaterThan(30)
.field("active")
.equals(true)
.endGroup()
.endGroup()
.build();
// When
auto result = spec->render(customRenderer);
// Then
REQUIRE(result == "((name = 'John' || (age > 30 || active = TRUE)))");
}
SECTION(
"when ConditionGroup has no children then render returns empty string")
{
// Given
ConditionGroup group(LogicalOp::AND);
// When
auto result = group.render(TEST_RENDERER);
// Then
REQUIRE(result == "");
}
SECTION("when makeSpecification helper is used then it creates valid builder")
{
// Given
auto builder = makeSpecification();
// When
auto spec = builder.field("test").equals("value").build();
// Then
REQUIRE(spec != nullptr);
auto result = spec->render(TEST_RENDERER);
REQUIRE(result == "(test = 'value')");
}
}

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

@ -1,436 +0,0 @@
#include "application/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::application::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_unique<test::MockTimeProvider>();
auto threadMgr = std::make_unique<test::MockThreadManager>();
auto blocker = std::make_unique<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,
std::move(blocker));
// When
scheduler.start();
}
SECTION("when scheduler is created then it is not running")
{
// Given - recreate blocker for this test since it was moved in previous
// section
auto testBlocker = std::make_unique<test::MockBlocker>();
// When
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// 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;
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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(*testBlocker, blockFor(_));
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
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);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow
);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
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);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
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 - recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// 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);
}
SECTION("when invalid mode combination is used then exception is thrown")
{
// Given - recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// 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);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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(*testBlocker, blockFor(_));
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
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);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
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);
}
SECTION("when task is scheduled with Forever and OnStart mode then it "
"executes on start and at scheduled time only")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
std::vector<std::chrono::system_clock::time_point> executionTimes;
auto currentTime = test::TIMEPOINT_NOW; // 2020-01-01 12:00:00
// Schedule task for 12:00:05 (5 seconds after current time)
auto scheduledTimeDelta = std::chrono::seconds{5};
auto scheduledTime = currentTime + scheduledTimeDelta;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// 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);
// Also add a timeout mechanism in case the scheduler doesn't execute as
// expected
auto timeoutTime = test::TIMEPOINT_NOW + std::chrono::minutes(2);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(
// Advance time by the blocked amount
currentTime += _1;);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
// Record the current time when this execution happens
executionTimes.push_back(currentTime);
// Stop after 2 executions (the expected behavior)
if (executionTimes.size() >= 2) {
scheduler.stop();
}
};
// When - schedule task with both Forever and OnStart modes
// Set time to 12:00:05 (5 seconds after our test TIMEPOINT_NOW)
scheduler.schedule(taskFunction, 12, 0, 5,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then - task should have executed exactly twice:
// 1. Once immediately due to OnStart (at TIMEPOINT_NOW)
// 2. Once at the scheduled time (12:00:05)
// But NOT more than that (which would indicate the bug)
// With the bug, executionTimes will have many entries due to infinite loop
// Without the bug, we should have exactly 2 entries
REQUIRE(executionTimes.size() == 2);
// First execution should be at the initial time (OnStart)
REQUIRE(executionTimes[0] == test::TIMEPOINT_NOW);
// Second execution should be at or after the scheduled time
REQUIRE(executionTimes[1] >= scheduledTime);
// Verify that time has advanced appropriately
// Current time should be at or after the scheduled time
REQUIRE(currentTime >= scheduledTime);
}
}

10
cpp17/vcpkg.json

@ -4,11 +4,7 @@
"dependencies": [ "dependencies": [
"cpp-httplib", "cpp-httplib",
"nlohmann-json", "nlohmann-json",
"jwt-cpp", "dingo",
"spdlog", "catch2"
"picosha2", ]
"catch2",
"trompeloeil"
],
"builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0"
} }

31
golang/.devcontainer/Dockerfile

@ -1,31 +0,0 @@
FROM golang:1.25.1-alpine3.22
WORKDIR /usr/src/app
# Install system dependencies
RUN apk add --no-cache \
git \
bash \
curl \
sudo
# Configure user and group IDs (default: 1000:1000)
ARG USER_ID=1000
ARG GROUP_ID=1000
# Create a group and user with specific UID/GID
RUN addgroup -g ${GROUP_ID} developer \
&& adduser -D -u ${USER_ID} -G developer -s /bin/bash developer \
&& echo "developer ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/developer \
&& chmod 0440 /etc/sudoers.d/developer
RUN chown -R ${USER_ID}:${GROUP_ID} /usr/src/app
USER developer
# Install Go tools
RUN go install github.com/go-delve/delve/cmd/dlv@latest
EXPOSE 3000
CMD ["go", "run", "main.go"]

25
golang/.devcontainer/devcontainer.json

@ -1,25 +0,0 @@
{
"name": "Go dev container",
"dockerComposeFile": "./docker-compose.yml",
"service": "app",
"workspaceFolder": "/usr/src/app",
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.defaultProfile.linux": "bash",
"go.useLanguageServer": true,
"go.gopath": "/go",
"go.goroot": "/usr/local/go"
},
"extensions": [
"golang.go",
"ms-vscode.go-tools",
"ms-vscode.vscode-go",
"ms-vscode.vscode-docker"
]
}
},
"forwardPorts": [3000],
"remoteUser": "developer",
"postCreateCommand": "sudo chown -R developer:1000 /usr/src/app && go mod tidy"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save