kybernus 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/dist/cli/commands/ecommerce.d.ts +3 -0
  2. package/dist/cli/commands/ecommerce.d.ts.map +1 -0
  3. package/dist/cli/commands/ecommerce.js +164 -0
  4. package/dist/cli/commands/ecommerce.js.map +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/ecommerce/.env.example +10 -0
  9. package/templates/ecommerce/.github/workflows/ci.yml +102 -0
  10. package/templates/ecommerce/.github/workflows/deploy.yml +31 -0
  11. package/templates/ecommerce/.prettierrc +9 -0
  12. package/templates/ecommerce/Dockerfile +54 -0
  13. package/templates/ecommerce/README.md +295 -0
  14. package/templates/ecommerce/apps/api/.env.example +59 -0
  15. package/templates/ecommerce/apps/api/jest.config.ts +50 -0
  16. package/templates/ecommerce/apps/api/jest.integration.config.ts +45 -0
  17. package/templates/ecommerce/apps/api/package.json +59 -0
  18. package/templates/ecommerce/apps/api/prisma/migrations/20260306000137_init/migration.sql +184 -0
  19. package/templates/ecommerce/apps/api/prisma/migrations/migration_lock.toml +3 -0
  20. package/templates/ecommerce/apps/api/prisma/schema.prisma +181 -0
  21. package/templates/ecommerce/apps/api/prisma/seed.ts +159 -0
  22. package/templates/ecommerce/apps/api/src/__tests__/app.test.ts +39 -0
  23. package/templates/ecommerce/apps/api/src/__tests__/globalSetup.ts +34 -0
  24. package/templates/ecommerce/apps/api/src/__tests__/globalTeardown.ts +16 -0
  25. package/templates/ecommerce/apps/api/src/__tests__/setup.db.ts +18 -0
  26. package/templates/ecommerce/apps/api/src/__tests__/setup.env.ts +14 -0
  27. package/templates/ecommerce/apps/api/src/app.ts +133 -0
  28. package/templates/ecommerce/apps/api/src/application/admin/admin-user.service.ts +24 -0
  29. package/templates/ecommerce/apps/api/src/application/admin/dashboard.service.ts +102 -0
  30. package/templates/ecommerce/apps/api/src/application/auth/auth.service.ts +185 -0
  31. package/templates/ecommerce/apps/api/src/application/cart/cart.service.ts +151 -0
  32. package/templates/ecommerce/apps/api/src/application/cart/coupon.service.ts +51 -0
  33. package/templates/ecommerce/apps/api/src/application/catalog/catalog.service.ts +168 -0
  34. package/templates/ecommerce/apps/api/src/application/checkout/checkout.service.ts +114 -0
  35. package/templates/ecommerce/apps/api/src/application/orders/order.service.ts +93 -0
  36. package/templates/ecommerce/apps/api/src/application/ports/email.port.ts +3 -0
  37. package/templates/ecommerce/apps/api/src/application/ports/payment.port.ts +24 -0
  38. package/templates/ecommerce/apps/api/src/application/ports/shipping.port.ts +9 -0
  39. package/templates/ecommerce/apps/api/src/application/ports/storage.port.ts +3 -0
  40. package/templates/ecommerce/apps/api/src/application/ports/token-blacklist.port.ts +4 -0
  41. package/templates/ecommerce/apps/api/src/application/ports/token.port.ts +18 -0
  42. package/templates/ecommerce/apps/api/src/application/profile/profile.service.ts +76 -0
  43. package/templates/ecommerce/apps/api/src/domain/auth/user.entity.ts +109 -0
  44. package/templates/ecommerce/apps/api/src/domain/auth/user.repository.ts +11 -0
  45. package/templates/ecommerce/apps/api/src/domain/cart/cart.entity.ts +136 -0
  46. package/templates/ecommerce/apps/api/src/domain/cart/cart.repository.ts +8 -0
  47. package/templates/ecommerce/apps/api/src/domain/cart/coupon.entity.ts +58 -0
  48. package/templates/ecommerce/apps/api/src/domain/cart/coupon.repository.ts +10 -0
  49. package/templates/ecommerce/apps/api/src/domain/catalog/category.entity.ts +51 -0
  50. package/templates/ecommerce/apps/api/src/domain/catalog/category.repository.ts +10 -0
  51. package/templates/ecommerce/apps/api/src/domain/catalog/product.entity.ts +130 -0
  52. package/templates/ecommerce/apps/api/src/domain/catalog/product.repository.ts +28 -0
  53. package/templates/ecommerce/apps/api/src/domain/checkout/order.entity.ts +121 -0
  54. package/templates/ecommerce/apps/api/src/domain/checkout/order.repository.ts +11 -0
  55. package/templates/ecommerce/apps/api/src/domain/shared/AppError.ts +12 -0
  56. package/templates/ecommerce/apps/api/src/infrastructure/cache/redis.ts +16 -0
  57. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/admin.registry.ts +13 -0
  58. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/auth.registry.ts +34 -0
  59. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/cart.registry.ts +49 -0
  60. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/catalog.registry.ts +24 -0
  61. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/checkout.registry.ts +47 -0
  62. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/orders.registry.ts +6 -0
  63. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/profile.registry.ts +4 -0
  64. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/cart.memory.repository.ts +33 -0
  65. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/category.memory.repository.ts +41 -0
  66. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/coupon.memory.repository.ts +55 -0
  67. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/order.memory.repository.ts +75 -0
  68. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/product.memory.repository.ts +100 -0
  69. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/user.memory.repository.ts +54 -0
  70. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/auth/user.prisma.repository.ts +83 -0
  71. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/category.prisma.repository.ts +69 -0
  72. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/product.prisma.repository.ts +185 -0
  73. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/checkout/order.prisma.repository.ts +149 -0
  74. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma-client.ts +17 -0
  75. package/templates/ecommerce/apps/api/src/infrastructure/services/email/email.registry.ts +18 -0
  76. package/templates/ecommerce/apps/api/src/infrastructure/services/email/ethereal.email.service.ts +38 -0
  77. package/templates/ecommerce/apps/api/src/infrastructure/services/email/noop.email.service.ts +12 -0
  78. package/templates/ecommerce/apps/api/src/infrastructure/services/email/smtp.email.service.ts +36 -0
  79. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe-webhook.handler.ts +83 -0
  80. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe.adapter.ts +39 -0
  81. package/templates/ecommerce/apps/api/src/infrastructure/services/shipping/mock.shipping.service.ts +17 -0
  82. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/in-memory.storage.service.ts +11 -0
  83. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/local-disk.storage.service.ts +27 -0
  84. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/s3.storage.service.ts +52 -0
  85. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/storage.registry.ts +19 -0
  86. package/templates/ecommerce/apps/api/src/infrastructure/services/token/redis.token.blacklist.ts +23 -0
  87. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.blacklist.ts +30 -0
  88. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.service.ts +136 -0
  89. package/templates/ecommerce/apps/api/src/modules/admin/__tests__/admin.routes.integration.test.ts +250 -0
  90. package/templates/ecommerce/apps/api/src/modules/admin/admin.controller.ts +116 -0
  91. package/templates/ecommerce/apps/api/src/modules/admin/admin.registry.ts +1 -0
  92. package/templates/ecommerce/apps/api/src/modules/admin/admin.routes.ts +21 -0
  93. package/templates/ecommerce/apps/api/src/modules/admin/admin.service.ts +1 -0
  94. package/templates/ecommerce/apps/api/src/modules/admin/admin.user.service.ts +1 -0
  95. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.logout.redis.test.ts +104 -0
  96. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.routes.integration.test.ts +211 -0
  97. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.service.unit.test.ts +260 -0
  98. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/email.service.unit.test.ts +94 -0
  99. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/token.blacklist.redis.test.ts +65 -0
  100. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.entity.unit.test.ts +79 -0
  101. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.prisma.repository.test.ts +138 -0
  102. package/templates/ecommerce/apps/api/src/modules/auth/auth.controller.ts +148 -0
  103. package/templates/ecommerce/apps/api/src/modules/auth/auth.registry.ts +1 -0
  104. package/templates/ecommerce/apps/api/src/modules/auth/auth.routes.ts +17 -0
  105. package/templates/ecommerce/apps/api/src/modules/auth/auth.service.ts +1 -0
  106. package/templates/ecommerce/apps/api/src/modules/auth/redis.token.blacklist.ts +1 -0
  107. package/templates/ecommerce/apps/api/src/modules/auth/token.blacklist.ts +2 -0
  108. package/templates/ecommerce/apps/api/src/modules/auth/token.service.ts +2 -0
  109. package/templates/ecommerce/apps/api/src/modules/auth/user.entity.ts +1 -0
  110. package/templates/ecommerce/apps/api/src/modules/auth/user.prisma.repository.ts +1 -0
  111. package/templates/ecommerce/apps/api/src/modules/auth/user.repository.ts +2 -0
  112. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.entity.unit.test.ts +144 -0
  113. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.routes.integration.test.ts +242 -0
  114. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.service.unit.test.ts +151 -0
  115. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/coupon.admin.integration.test.ts +136 -0
  116. package/templates/ecommerce/apps/api/src/modules/cart/cart.controller.ts +94 -0
  117. package/templates/ecommerce/apps/api/src/modules/cart/cart.entity.ts +1 -0
  118. package/templates/ecommerce/apps/api/src/modules/cart/cart.registry.ts +1 -0
  119. package/templates/ecommerce/apps/api/src/modules/cart/cart.repository.ts +2 -0
  120. package/templates/ecommerce/apps/api/src/modules/cart/cart.routes.ts +17 -0
  121. package/templates/ecommerce/apps/api/src/modules/cart/cart.service.ts +1 -0
  122. package/templates/ecommerce/apps/api/src/modules/cart/coupon.entity.ts +1 -0
  123. package/templates/ecommerce/apps/api/src/modules/cart/coupon.repository.ts +2 -0
  124. package/templates/ecommerce/apps/api/src/modules/cart/coupon.service.ts +1 -0
  125. package/templates/ecommerce/apps/api/src/modules/cart/shipping.service.ts +2 -0
  126. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.routes.integration.test.ts +275 -0
  127. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.service.unit.test.ts +223 -0
  128. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.image.integration.test.ts +130 -0
  129. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.prisma.repository.test.ts +174 -0
  130. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.controller.ts +176 -0
  131. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.registry.ts +1 -0
  132. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.routes.ts +38 -0
  133. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.service.ts +1 -0
  134. package/templates/ecommerce/apps/api/src/modules/catalog/category.entity.ts +1 -0
  135. package/templates/ecommerce/apps/api/src/modules/catalog/category.prisma.repository.ts +1 -0
  136. package/templates/ecommerce/apps/api/src/modules/catalog/category.repository.ts +2 -0
  137. package/templates/ecommerce/apps/api/src/modules/catalog/product.entity.ts +1 -0
  138. package/templates/ecommerce/apps/api/src/modules/catalog/product.prisma.repository.ts +1 -0
  139. package/templates/ecommerce/apps/api/src/modules/catalog/product.repository.ts +2 -0
  140. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.routes.integration.test.ts +163 -0
  141. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.service.unit.test.ts +191 -0
  142. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/order.prisma.repository.test.ts +150 -0
  143. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.controller.ts +59 -0
  144. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.registry.ts +1 -0
  145. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.routes.ts +18 -0
  146. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.service.ts +1 -0
  147. package/templates/ecommerce/apps/api/src/modules/checkout/order.entity.ts +1 -0
  148. package/templates/ecommerce/apps/api/src/modules/checkout/order.prisma.repository.ts +1 -0
  149. package/templates/ecommerce/apps/api/src/modules/checkout/order.repository.ts +2 -0
  150. package/templates/ecommerce/apps/api/src/modules/checkout/tax.service.ts +9 -0
  151. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.entity.unit.test.ts +68 -0
  152. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.routes.integration.test.ts +254 -0
  153. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.service.email.unit.test.ts +142 -0
  154. package/templates/ecommerce/apps/api/src/modules/orders/order.controller.ts +96 -0
  155. package/templates/ecommerce/apps/api/src/modules/orders/order.registry.ts +1 -0
  156. package/templates/ecommerce/apps/api/src/modules/orders/order.routes.ts +17 -0
  157. package/templates/ecommerce/apps/api/src/modules/orders/order.service.ts +1 -0
  158. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe-webhook.unit.test.ts +330 -0
  159. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe.adapter.unit.test.ts +84 -0
  160. package/templates/ecommerce/apps/api/src/modules/payment/adapters/stripe.adapter.ts +1 -0
  161. package/templates/ecommerce/apps/api/src/modules/payment/payment.port.ts +1 -0
  162. package/templates/ecommerce/apps/api/src/modules/payment/stripe-webhook.handler.ts +1 -0
  163. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.routes.integration.test.ts +180 -0
  164. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.service.unit.test.ts +187 -0
  165. package/templates/ecommerce/apps/api/src/modules/profile/profile.controller.ts +92 -0
  166. package/templates/ecommerce/apps/api/src/modules/profile/profile.registry.ts +1 -0
  167. package/templates/ecommerce/apps/api/src/modules/profile/profile.routes.ts +14 -0
  168. package/templates/ecommerce/apps/api/src/modules/profile/profile.service.ts +1 -0
  169. package/templates/ecommerce/apps/api/src/presentation/middlewares/authenticate.ts +37 -0
  170. package/templates/ecommerce/apps/api/src/presentation/middlewares/authorize.ts +23 -0
  171. package/templates/ecommerce/apps/api/src/presentation/middlewares/errorHandler.ts +48 -0
  172. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.controller.ts +116 -0
  173. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.routes.ts +21 -0
  174. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.controller.ts +147 -0
  175. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.routes.ts +17 -0
  176. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.controller.ts +94 -0
  177. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.routes.ts +17 -0
  178. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.controller.ts +176 -0
  179. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.routes.ts +38 -0
  180. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.controller.ts +59 -0
  181. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.routes.ts +18 -0
  182. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.controller.ts +96 -0
  183. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.routes.ts +17 -0
  184. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.controller.ts +92 -0
  185. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.routes.ts +14 -0
  186. package/templates/ecommerce/apps/api/src/presentation/validators/uuidParam.ts +20 -0
  187. package/templates/ecommerce/apps/api/src/server.ts +47 -0
  188. package/templates/ecommerce/apps/api/src/shared/__tests__/uuid.validation.test.ts +111 -0
  189. package/templates/ecommerce/apps/api/src/shared/errors/AppError.ts +1 -0
  190. package/templates/ecommerce/apps/api/src/shared/infra/email/EtherealEmailService.ts +1 -0
  191. package/templates/ecommerce/apps/api/src/shared/infra/email/IEmailService.ts +1 -0
  192. package/templates/ecommerce/apps/api/src/shared/infra/email/NoopEmailService.ts +1 -0
  193. package/templates/ecommerce/apps/api/src/shared/infra/email/SmtpEmailService.ts +1 -0
  194. package/templates/ecommerce/apps/api/src/shared/infra/email/__tests__/ethereal.email.integration.test.ts +32 -0
  195. package/templates/ecommerce/apps/api/src/shared/infra/email/email.registry.ts +1 -0
  196. package/templates/ecommerce/apps/api/src/shared/infra/prisma.ts +1 -0
  197. package/templates/ecommerce/apps/api/src/shared/infra/redis.ts +1 -0
  198. package/templates/ecommerce/apps/api/src/shared/infra/storage/IStorageService.ts +1 -0
  199. package/templates/ecommerce/apps/api/src/shared/infra/storage/InMemoryStorageService.ts +1 -0
  200. package/templates/ecommerce/apps/api/src/shared/infra/storage/LocalDiskStorageService.ts +1 -0
  201. package/templates/ecommerce/apps/api/src/shared/infra/storage/S3StorageService.ts +1 -0
  202. package/templates/ecommerce/apps/api/src/shared/infra/storage/__tests__/s3.storage.unit.test.ts +73 -0
  203. package/templates/ecommerce/apps/api/src/shared/infra/storage/storage.registry.ts +1 -0
  204. package/templates/ecommerce/apps/api/src/shared/middlewares/authenticate.ts +1 -0
  205. package/templates/ecommerce/apps/api/src/shared/middlewares/authorize.ts +1 -0
  206. package/templates/ecommerce/apps/api/src/shared/middlewares/errorHandler.ts +1 -0
  207. package/templates/ecommerce/apps/api/src/shared/validators/uuidParam.ts +1 -0
  208. package/templates/ecommerce/apps/api/tsconfig.json +15 -0
  209. package/templates/ecommerce/apps/web/.env.example +8 -0
  210. package/templates/ecommerce/apps/web/index.html +19 -0
  211. package/templates/ecommerce/apps/web/jest.config.ts +45 -0
  212. package/templates/ecommerce/apps/web/package.json +38 -0
  213. package/templates/ecommerce/apps/web/src/App.tsx +133 -0
  214. package/templates/ecommerce/apps/web/src/__mocks__/fileMock.ts +1 -0
  215. package/templates/ecommerce/apps/web/src/__mocks__/styleMock.ts +1 -0
  216. package/templates/ecommerce/apps/web/src/index.css +159 -0
  217. package/templates/ecommerce/apps/web/src/main.tsx +13 -0
  218. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/CouponsAdminPage.test.tsx +134 -0
  219. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/DashboardPage.test.tsx +65 -0
  220. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/OrdersAdminPage.test.tsx +79 -0
  221. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/ProductsAdminPage.test.tsx +84 -0
  222. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/UsersAdminPage.test.tsx +85 -0
  223. package/templates/ecommerce/apps/web/src/modules/admin/pages/CouponsAdminPage.tsx +179 -0
  224. package/templates/ecommerce/apps/web/src/modules/admin/pages/DashboardPage.tsx +58 -0
  225. package/templates/ecommerce/apps/web/src/modules/admin/pages/OrdersAdminPage.tsx +178 -0
  226. package/templates/ecommerce/apps/web/src/modules/admin/pages/ProductsAdminPage.tsx +444 -0
  227. package/templates/ecommerce/apps/web/src/modules/admin/pages/UsersAdminPage.tsx +87 -0
  228. package/templates/ecommerce/apps/web/src/modules/auth/LoginForm.tsx +91 -0
  229. package/templates/ecommerce/apps/web/src/modules/auth/RegisterForm.tsx +109 -0
  230. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ForgotPasswordPage.test.tsx +42 -0
  231. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/LoginForm.test.tsx +76 -0
  232. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/RegisterForm.test.tsx +62 -0
  233. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ResetPasswordPage.test.tsx +66 -0
  234. package/templates/ecommerce/apps/web/src/modules/auth/pages/ForgotPasswordPage.tsx +100 -0
  235. package/templates/ecommerce/apps/web/src/modules/auth/pages/LoginPage.tsx +39 -0
  236. package/templates/ecommerce/apps/web/src/modules/auth/pages/RegisterPage.tsx +39 -0
  237. package/templates/ecommerce/apps/web/src/modules/auth/pages/ResetPasswordPage.tsx +110 -0
  238. package/templates/ecommerce/apps/web/src/modules/auth/useAuthStore.ts +141 -0
  239. package/templates/ecommerce/apps/web/src/modules/cart/__tests__/CartPage.test.tsx +111 -0
  240. package/templates/ecommerce/apps/web/src/modules/cart/pages/CartPage.tsx +313 -0
  241. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductCard.test.tsx +59 -0
  242. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductFilters.test.tsx +56 -0
  243. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductCard.tsx +78 -0
  244. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductFilters.tsx +104 -0
  245. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductDetailPage.tsx +179 -0
  246. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductListPage.tsx +100 -0
  247. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/CheckoutPage.test.tsx +159 -0
  248. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/StripePaymentForm.test.tsx +79 -0
  249. package/templates/ecommerce/apps/web/src/modules/checkout/components/StripePaymentForm.tsx +55 -0
  250. package/templates/ecommerce/apps/web/src/modules/checkout/hooks/useCheckout.ts +56 -0
  251. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutPage.tsx +344 -0
  252. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutSuccessPage.tsx +12 -0
  253. package/templates/ecommerce/apps/web/src/modules/legal/pages/PrivacyPolicyPage.tsx +207 -0
  254. package/templates/ecommerce/apps/web/src/modules/legal/pages/TermsOfServicePage.tsx +175 -0
  255. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderDetailPage.test.tsx +75 -0
  256. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderHistoryPage.test.tsx +87 -0
  257. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderDetailPage.tsx +73 -0
  258. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderHistoryPage.tsx +97 -0
  259. package/templates/ecommerce/apps/web/src/modules/profile/__tests__/ProfilePage.test.tsx +150 -0
  260. package/templates/ecommerce/apps/web/src/modules/profile/pages/ProfilePage.tsx +275 -0
  261. package/templates/ecommerce/apps/web/src/setupTests.ts +10 -0
  262. package/templates/ecommerce/apps/web/src/shared/components/CookieConsent.tsx +108 -0
  263. package/templates/ecommerce/apps/web/src/shared/components/ErrorBoundary.tsx +112 -0
  264. package/templates/ecommerce/apps/web/src/shared/components/Layout.tsx +143 -0
  265. package/templates/ecommerce/apps/web/src/shared/components/ProtectedRoute.tsx +21 -0
  266. package/templates/ecommerce/apps/web/src/shared/config/siteConfig.ts +57 -0
  267. package/templates/ecommerce/apps/web/src/shared/hooks/usePageTitle.ts +16 -0
  268. package/templates/ecommerce/apps/web/src/shared/lib/apiFetch.ts +16 -0
  269. package/templates/ecommerce/apps/web/src/shared/pages/NotFoundPage.tsx +42 -0
  270. package/templates/ecommerce/apps/web/src/shared/theme/ThemeProvider.tsx +45 -0
  271. package/templates/ecommerce/apps/web/src/shared/theme/__tests__/ThemeProvider.test.tsx +78 -0
  272. package/templates/ecommerce/apps/web/src/shared/theme/createTheme.ts +58 -0
  273. package/templates/ecommerce/apps/web/src/shared/theme/tokens.ts +81 -0
  274. package/templates/ecommerce/apps/web/src/vite-env.d.ts +1 -0
  275. package/templates/ecommerce/apps/web/tsconfig.jest.json +12 -0
  276. package/templates/ecommerce/apps/web/tsconfig.json +25 -0
  277. package/templates/ecommerce/apps/web/tsconfig.node.json +11 -0
  278. package/templates/ecommerce/apps/web/vite.config.ts +30 -0
  279. package/templates/ecommerce/docker-compose.yml +85 -0
  280. package/templates/ecommerce/package-lock.json +11255 -0
  281. package/templates/ecommerce/package.json +27 -0
  282. package/templates/ecommerce/packages/shared-types/package.json +13 -0
  283. package/templates/ecommerce/packages/shared-types/src/index.ts +3 -0
  284. package/templates/ecommerce/packages/shared-types/src/theme.ts +44 -0
  285. package/templates/ecommerce/packages/shared-types/tsconfig.json +11 -0
  286. package/templates/ecommerce/scripts/customize.sh +201 -0
  287. package/templates/ecommerce/tsconfig.json +14 -0
@@ -0,0 +1,211 @@
1
+ import request from 'supertest';
2
+ import app from '../../../app';
3
+
4
+ /**
5
+ * Integration tests for Auth routes.
6
+ * These tests run against the full Express app (no DB — repository is mocked
7
+ * at the module level so we don't need Docker/Testcontainers for Phase 1 unit
8
+ * integration tests; Prisma integration tests will be added when the DB
9
+ * migration is applied in the real test environment).
10
+ */
11
+
12
+ // ── Helpers ──────────────────────────────────────────────────────────────────
13
+ const VALID_REGISTER = {
14
+ name: 'João Silva',
15
+ email: 'joao@email.com',
16
+ password: 'senha1234',
17
+ };
18
+
19
+ // ── Tests ────────────────────────────────────────────────────────────────────
20
+ describe('Auth Routes — Integration', () => {
21
+ describe('POST /api/auth/register', () => {
22
+ it('201: deve registrar usuário com dados válidos', async () => {
23
+ const res = await request(app).post('/api/auth/register').send(VALID_REGISTER);
24
+
25
+ expect(res.status).toBe(201);
26
+ expect(res.body.user).toMatchObject({ email: VALID_REGISTER.email });
27
+ expect((res.body.user as Record<string, unknown>)['passwordHash']).toBeUndefined();
28
+ expect(res.body.accessToken).toBeTruthy();
29
+ });
30
+
31
+ it('409: deve retornar conflict para email duplicado', async () => {
32
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
33
+ const res = await request(app).post('/api/auth/register').send(VALID_REGISTER);
34
+
35
+ expect(res.status).toBe(409);
36
+ expect(res.body).toHaveProperty('error');
37
+ });
38
+
39
+ it('422: deve retornar erros de validação para email inválido', async () => {
40
+ const res = await request(app).post('/api/auth/register').send({
41
+ name: 'João',
42
+ email: 'not-an-email',
43
+ password: 'senha1234',
44
+ });
45
+
46
+ expect(res.status).toBe(422);
47
+ expect(res.body).toHaveProperty('errors');
48
+ });
49
+
50
+ it('422: deve retornar erro para senha menor que 8 caracteres', async () => {
51
+ const res = await request(app).post('/api/auth/register').send({
52
+ name: 'João',
53
+ email: 'joao2@email.com',
54
+ password: '123',
55
+ });
56
+
57
+ expect(res.status).toBe(422);
58
+ });
59
+
60
+ it('422: deve retornar erro para name vazio', async () => {
61
+ const res = await request(app).post('/api/auth/register').send({
62
+ name: '',
63
+ email: 'joao3@email.com',
64
+ password: 'senha1234',
65
+ });
66
+
67
+ expect(res.status).toBe(422);
68
+ });
69
+ });
70
+
71
+ describe('POST /api/auth/login', () => {
72
+ beforeEach(async () => {
73
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
74
+ });
75
+
76
+ it('200: deve retornar accessToken no body e refreshToken como httpOnly cookie', async () => {
77
+ const res = await request(app).post('/api/auth/login').send({
78
+ email: VALID_REGISTER.email,
79
+ password: VALID_REGISTER.password,
80
+ });
81
+
82
+ expect(res.status).toBe(200);
83
+ expect(res.body.accessToken).toBeTruthy();
84
+
85
+ const cookies: string[] = res.headers['set-cookie'] as unknown as string[];
86
+ expect(cookies).toBeDefined();
87
+ const refreshCookie = cookies.find((c) => c.startsWith('refreshToken='));
88
+ expect(refreshCookie).toBeDefined();
89
+ expect(refreshCookie).toContain('HttpOnly');
90
+ });
91
+
92
+ it('401: deve rejeitar credenciais com senha incorreta', async () => {
93
+ const res = await request(app).post('/api/auth/login').send({
94
+ email: VALID_REGISTER.email,
95
+ password: 'senha_errada',
96
+ });
97
+
98
+ expect(res.status).toBe(401);
99
+ });
100
+
101
+ it('401: deve rejeitar email inexistente', async () => {
102
+ const res = await request(app).post('/api/auth/login').send({
103
+ email: 'naoexiste@email.com',
104
+ password: 'senha1234',
105
+ });
106
+
107
+ expect(res.status).toBe(401);
108
+ });
109
+ });
110
+
111
+ describe('POST /api/auth/refresh', () => {
112
+ it('200: deve retornar novo accessToken com refreshToken válido no cookie', async () => {
113
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
114
+ const loginRes = await request(app)
115
+ .post('/api/auth/login')
116
+ .send({ email: VALID_REGISTER.email, password: VALID_REGISTER.password });
117
+
118
+ const cookies: string[] = loginRes.headers['set-cookie'] as unknown as string[];
119
+ const cookieHeader = cookies.find((c) => c.startsWith('refreshToken=')) ?? '';
120
+
121
+ const res = await request(app)
122
+ .post('/api/auth/refresh')
123
+ .set('Cookie', cookieHeader.split(';')[0] ?? '');
124
+
125
+ expect(res.status).toBe(200);
126
+ expect(res.body.accessToken).toBeTruthy();
127
+ });
128
+
129
+ it('401: deve rejeitar requisição sem cookie de refresh', async () => {
130
+ const res = await request(app).post('/api/auth/refresh');
131
+
132
+ expect(res.status).toBe(401);
133
+ });
134
+ });
135
+
136
+ describe('POST /api/auth/logout', () => {
137
+ it('204: deve invalidar o refreshToken e limpar o cookie', async () => {
138
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
139
+ const loginRes = await request(app)
140
+ .post('/api/auth/login')
141
+ .send({ email: VALID_REGISTER.email, password: VALID_REGISTER.password });
142
+
143
+ const cookies: string[] = loginRes.headers['set-cookie'] as unknown as string[];
144
+ const cookieHeader = cookies.find((c) => c.startsWith('refreshToken=')) ?? '';
145
+ const cookieValue = cookieHeader.split(';')[0] ?? '';
146
+
147
+ const res = await request(app)
148
+ .post('/api/auth/logout')
149
+ .set('Cookie', cookieValue);
150
+
151
+ expect(res.status).toBe(204);
152
+
153
+ // Refresh deve falhar após logout
154
+ const refreshRes = await request(app)
155
+ .post('/api/auth/refresh')
156
+ .set('Cookie', cookieValue);
157
+ expect(refreshRes.status).toBe(401);
158
+ });
159
+ });
160
+
161
+ describe('Middleware: authenticate', () => {
162
+ it('401: deve rejeitar requisição sem Bearer token', async () => {
163
+ const res = await request(app).get('/api/auth/me');
164
+
165
+ expect(res.status).toBe(401);
166
+ });
167
+
168
+ it('401: deve rejeitar token malformado', async () => {
169
+ const res = await request(app)
170
+ .get('/api/auth/me')
171
+ .set('Authorization', 'Bearer not-a-valid-jwt');
172
+
173
+ expect(res.status).toBe(401);
174
+ });
175
+
176
+ it('200: deve retornar dados do usuário para token válido', async () => {
177
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
178
+ const loginRes = await request(app)
179
+ .post('/api/auth/login')
180
+ .send({ email: VALID_REGISTER.email, password: VALID_REGISTER.password });
181
+
182
+ const { accessToken } = loginRes.body as { accessToken: string };
183
+
184
+ const res = await request(app)
185
+ .get('/api/auth/me')
186
+ .set('Authorization', `Bearer ${accessToken}`);
187
+
188
+ expect(res.status).toBe(200);
189
+ expect(res.body.email).toBe(VALID_REGISTER.email);
190
+ expect((res.body as Record<string, unknown>)['passwordHash']).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe('Middleware: authorize (ADMIN only)', () => {
195
+ it('403: deve bloquear usuário CUSTOMER em rota de ADMIN', async () => {
196
+ await request(app).post('/api/auth/register').send(VALID_REGISTER);
197
+ const loginRes = await request(app)
198
+ .post('/api/auth/login')
199
+ .send({ email: VALID_REGISTER.email, password: VALID_REGISTER.password });
200
+
201
+ const { accessToken } = loginRes.body as { accessToken: string };
202
+
203
+ // /api/auth/admin-test é uma rota protegida por authorize('ADMIN')
204
+ const res = await request(app)
205
+ .get('/api/auth/admin-test')
206
+ .set('Authorization', `Bearer ${accessToken}`);
207
+
208
+ expect(res.status).toBe(403);
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,260 @@
1
+ import { AuthService } from '../auth.service';
2
+ import { IUserRepository } from '../user.repository';
3
+ import { ITokenService } from '../token.service';
4
+ import { IEmailService } from '../../../shared/infra/email/IEmailService';
5
+ import { UserEntity } from '../user.entity';
6
+
7
+ // ── Helpers ──────────────────────────────────────────────────────────────────
8
+ const makeUser = (overrides: Partial<Parameters<typeof UserEntity.create>[0]> = {}) =>
9
+ UserEntity.create({
10
+ name: 'Test User',
11
+ email: 'test@email.com',
12
+ passwordHash: '$2a$10$hashedpassword',
13
+ ...overrides,
14
+ });
15
+
16
+ // ── Mocks ────────────────────────────────────────────────────────────────────
17
+ const makeUserRepository = (): jest.Mocked<IUserRepository> => ({
18
+ findByEmail: jest.fn(),
19
+ findById: jest.fn(),
20
+ create: jest.fn(),
21
+ update: jest.fn(),
22
+ delete: jest.fn(),
23
+ findAll: jest.fn(),
24
+ });
25
+
26
+ const makeTokenService = (): jest.Mocked<ITokenService> => ({
27
+ generateAccessToken: jest.fn(),
28
+ generateRefreshToken: jest.fn(),
29
+ verifyAccessToken: jest.fn(),
30
+ verifyRefreshToken: jest.fn(),
31
+ invalidateRefreshToken: jest.fn(),
32
+ isRefreshTokenBlacklisted: jest.fn(),
33
+ generatePasswordResetToken: jest.fn(),
34
+ verifyPasswordResetToken: jest.fn(),
35
+ invalidatePasswordResetToken: jest.fn(),
36
+ });
37
+
38
+ const makeEmailService = (): jest.Mocked<IEmailService> => ({
39
+ send: jest.fn().mockResolvedValue(undefined),
40
+ });
41
+
42
+ // ── Tests ────────────────────────────────────────────────────────────────────
43
+ describe('AuthService — Unit', () => {
44
+ let authService: AuthService;
45
+ let userRepository: jest.Mocked<IUserRepository>;
46
+ let tokenService: jest.Mocked<ITokenService>;
47
+ let emailService: jest.Mocked<IEmailService>;
48
+
49
+ beforeEach(() => {
50
+ userRepository = makeUserRepository();
51
+ tokenService = makeTokenService();
52
+ emailService = makeEmailService();
53
+ authService = new AuthService(userRepository, tokenService, emailService);
54
+ });
55
+
56
+ // ── register ──────────────────────────────────────────────────────────────
57
+ describe('register()', () => {
58
+ it('deve criar usuário com role CUSTOMER por padrão', async () => {
59
+ userRepository.findByEmail.mockResolvedValue(null);
60
+ userRepository.create.mockImplementation(async (user) => user);
61
+
62
+ const result = await authService.register({
63
+ name: 'João Silva',
64
+ email: 'joao@email.com',
65
+ password: 'senha1234',
66
+ });
67
+
68
+ expect(result.role).toBe('CUSTOMER');
69
+ expect(userRepository.create).toHaveBeenCalledTimes(1);
70
+ });
71
+
72
+ it('deve hashear a senha antes de salvar (não persiste senha em texto plano)', async () => {
73
+ userRepository.findByEmail.mockResolvedValue(null);
74
+ userRepository.create.mockImplementation(async (user) => user);
75
+
76
+ await authService.register({
77
+ name: 'João',
78
+ email: 'joao@email.com',
79
+ password: 'senha1234',
80
+ });
81
+
82
+ const persistedUser = userRepository.create.mock.calls[0][0];
83
+ expect(persistedUser.passwordHash).not.toBe('senha1234');
84
+ expect(persistedUser.passwordHash).toMatch(/^\$2[ab]\$/); // bcryptjs format
85
+ });
86
+
87
+ it('deve lançar ConflictError se email já estiver cadastrado', async () => {
88
+ userRepository.findByEmail.mockResolvedValue(makeUser());
89
+
90
+ await expect(
91
+ authService.register({ name: 'João', email: 'test@email.com', password: 'senha1234' }),
92
+ ).rejects.toMatchObject({ statusCode: 409 });
93
+ });
94
+
95
+ it('deve lançar ValidationError se senha tiver menos de 8 caracteres', async () => {
96
+ userRepository.findByEmail.mockResolvedValue(null);
97
+
98
+ await expect(
99
+ authService.register({ name: 'João', email: 'joao@email.com', password: '123' }),
100
+ ).rejects.toMatchObject({ statusCode: 422 });
101
+ });
102
+ });
103
+
104
+ // ── login ────────────────────────────────────────────────────────────────
105
+ describe('login()', () => {
106
+ it('deve retornar accessToken e refreshToken para credenciais válidas', async () => {
107
+ // bcryptjs hash of "senha1234"
108
+ const { hash } = await import('bcryptjs');
109
+ const passwordHash = await hash('senha1234', 10);
110
+ const user = UserEntity.create({ name: 'João', email: 'joao@email.com', passwordHash });
111
+
112
+ userRepository.findByEmail.mockResolvedValue(user);
113
+ tokenService.generateAccessToken.mockReturnValue('access-token');
114
+ tokenService.generateRefreshToken.mockReturnValue('refresh-token');
115
+
116
+ const result = await authService.login({ email: 'joao@email.com', password: 'senha1234' });
117
+
118
+ expect(result.accessToken).toBe('access-token');
119
+ expect(result.refreshToken).toBe('refresh-token');
120
+ });
121
+
122
+ it('deve lançar UnauthorizedError para senha incorreta', async () => {
123
+ const { hash } = await import('bcryptjs');
124
+ const passwordHash = await hash('senha1234', 10);
125
+ const user = UserEntity.create({ name: 'João', email: 'joao@email.com', passwordHash });
126
+
127
+ userRepository.findByEmail.mockResolvedValue(user);
128
+
129
+ await expect(
130
+ authService.login({ email: 'joao@email.com', password: 'senha_errada' }),
131
+ ).rejects.toMatchObject({ statusCode: 401 });
132
+ });
133
+
134
+ it('deve lançar UnauthorizedError para email inexistente', async () => {
135
+ userRepository.findByEmail.mockResolvedValue(null);
136
+
137
+ await expect(
138
+ authService.login({ email: 'naoexiste@email.com', password: 'senha1234' }),
139
+ ).rejects.toMatchObject({ statusCode: 401 });
140
+ });
141
+
142
+ it('NÃO deve expor passwordHash no retorno', async () => {
143
+ const { hash } = await import('bcryptjs');
144
+ const passwordHash = await hash('senha1234', 10);
145
+ const user = UserEntity.create({ name: 'João', email: 'joao@email.com', passwordHash });
146
+
147
+ userRepository.findByEmail.mockResolvedValue(user);
148
+ tokenService.generateAccessToken.mockReturnValue('access-token');
149
+ tokenService.generateRefreshToken.mockReturnValue('refresh-token');
150
+
151
+ const result = await authService.login({ email: 'joao@email.com', password: 'senha1234' });
152
+
153
+ expect((result.user as Record<string, unknown>)['passwordHash']).toBeUndefined();
154
+ });
155
+ });
156
+
157
+ // ── refreshToken ─────────────────────────────────────────────────────────
158
+ describe('refreshToken()', () => {
159
+ it('deve emitir novo accessToken para refreshToken válido', async () => {
160
+ const user = makeUser();
161
+ tokenService.isRefreshTokenBlacklisted.mockResolvedValue(false);
162
+ tokenService.verifyRefreshToken.mockReturnValue({ sub: user.id, email: user.email });
163
+ userRepository.findById.mockResolvedValue(user);
164
+ tokenService.generateAccessToken.mockReturnValue('new-access-token');
165
+
166
+ const result = await authService.refreshToken('valid-refresh-token');
167
+
168
+ expect(result.accessToken).toBe('new-access-token');
169
+ });
170
+
171
+ it('deve rejeitar refreshToken que está na blacklist (logout anterior)', async () => {
172
+ tokenService.isRefreshTokenBlacklisted.mockResolvedValue(true);
173
+
174
+ await expect(authService.refreshToken('blacklisted-token')).rejects.toMatchObject({
175
+ statusCode: 401,
176
+ });
177
+ });
178
+
179
+ it('deve rejeitar se verifyRefreshToken lançar erro (token expirado/inválido)', async () => {
180
+ tokenService.isRefreshTokenBlacklisted.mockResolvedValue(false);
181
+ tokenService.verifyRefreshToken.mockImplementation(() => {
182
+ throw new Error('jwt expired');
183
+ });
184
+
185
+ await expect(authService.refreshToken('expired-token')).rejects.toMatchObject({
186
+ statusCode: 401,
187
+ });
188
+ });
189
+ });
190
+
191
+ // ── logout ───────────────────────────────────────────────────────────────
192
+ describe('logout()', () => {
193
+ it('deve invalidar o refreshToken na blacklist', async () => {
194
+ tokenService.invalidateRefreshToken.mockResolvedValue(undefined);
195
+
196
+ await authService.logout('refresh-token-to-invalidate');
197
+
198
+ expect(tokenService.invalidateRefreshToken).toHaveBeenCalledWith(
199
+ 'refresh-token-to-invalidate',
200
+ );
201
+ });
202
+ });
203
+
204
+ // ── forgotPassword ───────────────────────────────────────────────────────
205
+ describe('forgotPassword()', () => {
206
+ it('deve SEMPRE retornar sucesso mesmo se email não existir (anti-enumeration)', async () => {
207
+ userRepository.findByEmail.mockResolvedValue(null);
208
+
209
+ // Should NOT throw — prevents user enumeration attacks
210
+ await expect(
211
+ authService.forgotPassword({ email: 'inexistente@email.com' }),
212
+ ).resolves.not.toThrow();
213
+ });
214
+
215
+ it('deve gerar token de reset e chamar o serviço de envio de email', async () => {
216
+ const user = makeUser();
217
+ userRepository.findByEmail.mockResolvedValue(user);
218
+ tokenService.generatePasswordResetToken.mockResolvedValue('reset-token');
219
+
220
+ await authService.forgotPassword({ email: 'test@email.com' });
221
+
222
+ expect(tokenService.generatePasswordResetToken).toHaveBeenCalledWith(user.id);
223
+ expect(emailService.send).toHaveBeenCalledTimes(1);
224
+ const [to] = emailService.send.mock.calls[0];
225
+ expect(to).toBe('test@email.com');
226
+ });
227
+ });
228
+
229
+ // ── resetPassword ─────────────────────────────────────────────────────────
230
+ describe('resetPassword()', () => {
231
+ it('deve atualizar senha e invalidar o token de reset', async () => {
232
+ const user = makeUser();
233
+ tokenService.verifyPasswordResetToken.mockResolvedValue(user.id);
234
+ userRepository.findById.mockResolvedValue(user);
235
+ userRepository.update.mockImplementation(async (u) => u);
236
+ tokenService.invalidatePasswordResetToken.mockResolvedValue(undefined);
237
+
238
+ await authService.resetPassword({ token: 'valid-reset-token', newPassword: 'newpassword1' });
239
+
240
+ expect(userRepository.update).toHaveBeenCalledTimes(1);
241
+ const updatedUser = userRepository.update.mock.calls[0][0];
242
+ expect(updatedUser.passwordHash).not.toBe('newpassword1');
243
+ expect(tokenService.invalidatePasswordResetToken).toHaveBeenCalledWith('valid-reset-token');
244
+ });
245
+
246
+ it('deve rejeitar token de reset inválido/expirado', async () => {
247
+ tokenService.verifyPasswordResetToken.mockRejectedValue(new Error('invalid token'));
248
+
249
+ await expect(
250
+ authService.resetPassword({ token: 'bad-token', newPassword: 'newpassword1' }),
251
+ ).rejects.toMatchObject({ statusCode: 400 });
252
+ });
253
+
254
+ it('deve lançar erro se nova senha tiver menos de 8 caracteres', async () => {
255
+ await expect(
256
+ authService.resetPassword({ token: 'valid-token', newPassword: '123' }),
257
+ ).rejects.toMatchObject({ statusCode: 422 });
258
+ });
259
+ });
260
+ });
@@ -0,0 +1,94 @@
1
+ import { AuthService } from '../auth.service';
2
+ import { IUserRepository } from '../user.repository';
3
+ import { ITokenService } from '../token.service';
4
+ import { IEmailService } from '../../../shared/infra/email/IEmailService';
5
+ import { UserEntity } from '../user.entity';
6
+
7
+ // ── Helpers ──────────────────────────────────────────────────────────────────
8
+ const makeUser = () =>
9
+ UserEntity.create({
10
+ name: 'Ana Lima',
11
+ email: 'ana@email.com',
12
+ passwordHash: '$2a$10$hashedpassword',
13
+ });
14
+
15
+ // ── Mocks ────────────────────────────────────────────────────────────────────
16
+ const makeUserRepository = (): jest.Mocked<IUserRepository> => ({
17
+ findByEmail: jest.fn(),
18
+ findById: jest.fn(),
19
+ create: jest.fn(),
20
+ update: jest.fn(),
21
+ delete: jest.fn(),
22
+ findAll: jest.fn(),
23
+ });
24
+
25
+ const makeTokenService = (): jest.Mocked<ITokenService> => ({
26
+ generateAccessToken: jest.fn(),
27
+ generateRefreshToken: jest.fn(),
28
+ verifyAccessToken: jest.fn(),
29
+ verifyRefreshToken: jest.fn(),
30
+ invalidateRefreshToken: jest.fn(),
31
+ isRefreshTokenBlacklisted: jest.fn(),
32
+ generatePasswordResetToken: jest.fn(),
33
+ verifyPasswordResetToken: jest.fn(),
34
+ invalidatePasswordResetToken: jest.fn(),
35
+ });
36
+
37
+ const makeEmailService = (): jest.Mocked<IEmailService> => ({
38
+ send: jest.fn().mockResolvedValue(undefined),
39
+ });
40
+
41
+ // ── Tests ────────────────────────────────────────────────────────────────────
42
+ describe('AuthService.forgotPassword() — com IEmailService mockado', () => {
43
+ let authService: AuthService;
44
+ let userRepository: jest.Mocked<IUserRepository>;
45
+ let tokenService: jest.Mocked<ITokenService>;
46
+ let emailService: jest.Mocked<IEmailService>;
47
+
48
+ beforeEach(() => {
49
+ userRepository = makeUserRepository();
50
+ tokenService = makeTokenService();
51
+ emailService = makeEmailService();
52
+ authService = new AuthService(userRepository, tokenService, emailService);
53
+ });
54
+
55
+ it('deve chamar emailService.send() com o email correto do destinatário', async () => {
56
+ const user = makeUser();
57
+ userRepository.findByEmail.mockResolvedValue(user);
58
+ tokenService.generatePasswordResetToken.mockResolvedValue('tok-abc123');
59
+
60
+ await authService.forgotPassword({ email: 'ana@email.com' });
61
+
62
+ expect(emailService.send).toHaveBeenCalledTimes(1);
63
+ const [to] = emailService.send.mock.calls[0];
64
+ expect(to).toBe('ana@email.com');
65
+ });
66
+
67
+ it('deve incluir o token de reset na URL enviada', async () => {
68
+ const user = makeUser();
69
+ userRepository.findByEmail.mockResolvedValue(user);
70
+ tokenService.generatePasswordResetToken.mockResolvedValue('tok-abc123');
71
+
72
+ await authService.forgotPassword({ email: 'ana@email.com' });
73
+
74
+ const [, , html] = emailService.send.mock.calls[0];
75
+ expect(html).toContain('tok-abc123');
76
+ expect(html).toContain('reset-password');
77
+ });
78
+
79
+ it('deve retornar sucesso mesmo se email não existir (anti-enumeration)', async () => {
80
+ userRepository.findByEmail.mockResolvedValue(null);
81
+
82
+ await expect(
83
+ authService.forgotPassword({ email: 'naoexiste@email.com' }),
84
+ ).resolves.not.toThrow();
85
+ });
86
+
87
+ it('não deve chamar emailService.send() se usuário não existir', async () => {
88
+ userRepository.findByEmail.mockResolvedValue(null);
89
+
90
+ await authService.forgotPassword({ email: 'naoexiste@email.com' });
91
+
92
+ expect(emailService.send).not.toHaveBeenCalled();
93
+ });
94
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Integration tests for RedisTokenBlacklist.
3
+ * Requires Redis running on REDIS_URL (defaults to redis://localhost:6379).
4
+ * Run with: npm run test:db
5
+ */
6
+ import Redis from 'ioredis';
7
+ import { RedisTokenBlacklist } from '../redis.token.blacklist';
8
+
9
+ const redisUrl = process.env['REDIS_URL'] ?? 'redis://localhost:6379';
10
+
11
+ describe('RedisTokenBlacklist', () => {
12
+ let redis: Redis;
13
+ let blacklist: RedisTokenBlacklist;
14
+
15
+ beforeAll(async () => {
16
+ redis = new Redis(redisUrl, { lazyConnect: true, maxRetriesPerRequest: 3 });
17
+ await redis.connect();
18
+ blacklist = new RedisTokenBlacklist(redis);
19
+ });
20
+
21
+ afterAll(async () => {
22
+ await redis.quit();
23
+ });
24
+
25
+ afterEach(async () => {
26
+ // Clean up test keys
27
+ const keys = await redis.keys('blacklist:test-*');
28
+ if (keys.length > 0) await redis.del(...keys);
29
+ });
30
+
31
+ it('deve retornar false para jti desconhecido', async () => {
32
+ const result = await blacklist.has('test-unknown-jti');
33
+ expect(result).toBe(false);
34
+ });
35
+
36
+ it('deve persistir jti no Redis com TTL', async () => {
37
+ const jti = 'test-jti-ttl';
38
+ await blacklist.add(jti, 60);
39
+
40
+ const ttl = await redis.ttl(`blacklist:${jti}`);
41
+ expect(ttl).toBeGreaterThan(0);
42
+ expect(ttl).toBeLessThanOrEqual(60);
43
+ });
44
+
45
+ it('deve retornar true após add()', async () => {
46
+ const jti = 'test-jti-present';
47
+ await blacklist.add(jti, 60);
48
+
49
+ const result = await blacklist.has(jti);
50
+ expect(result).toBe(true);
51
+ });
52
+
53
+ it('deve expirar automaticamente após TTL', async () => {
54
+ const jti = 'test-jti-expire';
55
+ await blacklist.add(jti, 1); // 1 second TTL
56
+
57
+ const beforeExpiry = await blacklist.has(jti);
58
+ expect(beforeExpiry).toBe(true);
59
+
60
+ await new Promise((resolve) => setTimeout(resolve, 1200));
61
+
62
+ const afterExpiry = await blacklist.has(jti);
63
+ expect(afterExpiry).toBe(false);
64
+ }, 5000);
65
+ });