@etus/bhono-app 0.1.5 → 0.1.6

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 (269) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +5 -1
  3. package/templates/base/.husky/pre-push +26 -0
  4. package/templates/base/CLAUDE.md +5 -5
  5. package/templates/base/README.md +31 -20
  6. package/templates/base/docs/app_spec.txt +13 -10
  7. package/templates/base/docs/architecture/README.md +3 -0
  8. package/templates/base/docs/architecture/data-requirements.md +4 -3
  9. package/templates/base/docs/architecture/db-bootstrap.md +39 -0
  10. package/templates/base/docs/architecture/drizzle-migration-plan.md +125 -0
  11. package/templates/base/docs/architecture/erd.md +1 -1
  12. package/templates/base/docs/architecture/sql-standards.md +100 -0
  13. package/templates/base/docs/testing.md +36 -29
  14. package/templates/base/package.json +6 -5
  15. package/templates/base/pnpm-lock.yaml +0 -123
  16. package/templates/base/schema.sql +84 -0
  17. package/templates/base/scripts/init.sh +244 -59
  18. package/templates/base/src/client/hooks/use-auth.ts +5 -0
  19. package/templates/base/src/client/routes/_authenticated/dashboard.tsx +1 -1
  20. package/templates/base/src/client/routes/index.tsx +1 -1
  21. package/templates/base/src/server/db/client.ts +3 -5
  22. package/templates/base/src/server/db/records.ts +81 -0
  23. package/templates/base/src/server/db/seed.ts +3 -2
  24. package/templates/base/src/server/db/sql.ts +96 -0
  25. package/templates/base/src/server/index.ts +16 -2
  26. package/templates/base/src/server/lib/audit.ts +74 -26
  27. package/templates/base/src/server/lib/audited-db.ts +219 -109
  28. package/templates/base/src/server/lib/transaction.ts +10 -16
  29. package/templates/base/src/server/middleware/account.ts +8 -15
  30. package/templates/base/src/server/middleware/auth.ts +102 -38
  31. package/templates/base/src/server/middleware/rate-limit.ts +6 -1
  32. package/templates/base/src/server/routes/accounts/handlers.ts +18 -6
  33. package/templates/base/src/server/routes/audits/handlers.ts +3 -1
  34. package/templates/base/src/server/routes/auth/handlers.ts +14 -9
  35. package/templates/base/src/server/routes/auth/test-login.ts +99 -45
  36. package/templates/base/src/server/routes/health/handlers.ts +4 -4
  37. package/templates/base/src/server/routes/invitations/handlers.ts +6 -3
  38. package/templates/base/src/server/routes/users/handlers.ts +21 -14
  39. package/templates/base/src/server/services/accounts.ts +242 -217
  40. package/templates/base/src/server/services/audits.ts +114 -61
  41. package/templates/base/src/server/services/auth.ts +310 -180
  42. package/templates/base/src/server/services/invitations.ts +282 -222
  43. package/templates/base/src/server/services/users.ts +383 -293
  44. package/templates/base/src/server/types/index.ts +1 -2
  45. package/templates/base/{src/server/__tests__/fixtures.ts → tests/fixtures/server.ts} +3 -3
  46. package/templates/base/{src/client/__tests__/setup-browser.ts → tests/helpers/client-setup-browser.ts} +2 -2
  47. package/templates/base/{src/client/__tests__/setup.ts → tests/helpers/client-setup.ts} +1 -1
  48. package/templates/base/{src/client/__tests__/test-utils.tsx → tests/helpers/client-test-utils.tsx} +2 -2
  49. package/templates/base/{src/server/__tests__/setup.ts → tests/helpers/server.ts} +9 -9
  50. package/templates/base/tests/integration/accounts/crud.test.ts +2 -11
  51. package/templates/base/tests/integration/audits/list.test.ts +2 -11
  52. package/templates/base/tests/integration/auth/auth-service.test.ts +1 -10
  53. package/templates/base/tests/integration/auth/invitation-token.test.ts +2 -11
  54. package/templates/base/tests/integration/auth/logout.test.ts +2 -11
  55. package/templates/base/tests/integration/auth/oauth.test.ts +23 -42
  56. package/templates/base/tests/integration/auth/refresh-token.test.ts +1 -9
  57. package/templates/base/tests/integration/auth/session-expiry.test.ts +1 -9
  58. package/templates/base/tests/integration/auth/session.test.ts +2 -11
  59. package/templates/base/tests/integration/auth/super-admin.test.ts +1 -9
  60. package/templates/base/tests/integration/authorization/analytics-role.test.ts +2 -11
  61. package/templates/base/tests/integration/authorization/billing-role.test.ts +2 -11
  62. package/templates/base/tests/integration/authorization/guards-roles.test.ts +1 -9
  63. package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +2 -11
  64. package/templates/base/tests/integration/authorization/roles.test.ts +2 -11
  65. package/templates/base/tests/integration/config/production-behavior.test.ts +2 -11
  66. package/templates/base/tests/integration/health/health.test.ts +25 -44
  67. package/templates/base/tests/integration/invitations/crud.test.ts +2 -11
  68. package/templates/base/tests/integration/invitations/email.test.ts +1 -9
  69. package/templates/base/tests/integration/middleware/auth.test.ts +3 -12
  70. package/templates/base/tests/integration/middleware/request-logger.test.ts +1 -9
  71. package/templates/base/tests/integration/performance/response-times.test.ts +1 -9
  72. package/templates/base/tests/integration/security/cookie-security.test.ts +2 -11
  73. package/templates/base/tests/integration/security/csrf-protection.test.ts +2 -11
  74. package/templates/base/tests/integration/security/log-sanitization.test.ts +1 -9
  75. package/templates/base/tests/integration/security/rate-limiting.test.ts +1 -9
  76. package/templates/base/tests/integration/security/sql-injection.test.ts +7 -18
  77. package/templates/base/tests/integration/security/xss-prevention.test.ts +2 -11
  78. package/templates/base/tests/integration/setup.ts +13 -90
  79. package/templates/base/tests/integration/smoke.test.ts +3 -2
  80. package/templates/base/tests/integration/storage/upload.test.ts +2 -11
  81. package/templates/base/tests/integration/storage/validation.test.ts +2 -11
  82. package/templates/base/tests/integration/users/crud.test.ts +2 -11
  83. package/templates/base/tests/integration/users/list.test.ts +2 -11
  84. package/templates/base/tests/integration/vitest.config.ts +2 -9
  85. package/templates/base/{src/server/__tests__ → tests}/mocks/db.ts +1 -1
  86. package/templates/base/{src/server/__tests__ → tests}/mocks/index.ts +1 -1
  87. package/templates/base/{src/server/__tests__ → tests}/mocks/kv.ts +1 -1
  88. package/templates/base/{src/server/__tests__ → tests}/mocks/r2.ts +1 -1
  89. package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/sidebar.test.tsx +1 -1
  90. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/avatar.test.tsx +1 -1
  91. package/templates/base/{src/client/__tests__ → tests/unit/client/components/ui}/button.test.tsx +1 -1
  92. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/card.test.tsx +1 -1
  93. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/dialog.test.tsx +1 -1
  94. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/input.test.tsx +1 -1
  95. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/loading-skeleton.test.tsx +1 -1
  96. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/skeleton.test.tsx +1 -1
  97. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/sonner.test.tsx +1 -1
  98. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/tabs.test.tsx +1 -1
  99. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/account.test.tsx +1 -1
  100. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/integrations.test.tsx +1 -1
  101. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/settings.test.tsx +1 -1
  102. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/team.test.tsx +1 -1
  103. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/authenticated-layout.test.tsx +1 -1
  104. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/dashboard.test.tsx +1 -1
  105. package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/invite.test.tsx +1 -1
  106. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/login.test.tsx +1 -1
  107. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/navigation.test.tsx +1 -1
  108. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/root-layout.test.tsx +1 -1
  109. package/templates/base/{src/server/auth/__tests__ → tests/unit/server/auth}/guards.test.ts +2 -2
  110. package/templates/base/{src → tests/unit}/server/auth/permissions.test.ts +1 -1
  111. package/templates/base/{src → tests/unit}/server/auth/roles.test.ts +1 -1
  112. package/templates/base/tests/unit/server/db/sql.test.ts +68 -0
  113. package/templates/base/{src → tests/unit}/server/env.test.ts +1 -1
  114. package/templates/base/tests/unit/server/lib/audited-db.test.ts +78 -0
  115. package/templates/base/{src → tests/unit}/server/lib/email.test.ts +1 -1
  116. package/templates/base/{src → tests/unit}/server/lib/errors.test.ts +1 -1
  117. package/templates/base/{src → tests/unit}/server/lib/oauth.test.ts +1 -1
  118. package/templates/base/{src → tests/unit}/server/lib/pagination.test.ts +1 -1
  119. package/templates/base/{src → tests/unit}/server/lib/password.test.ts +1 -1
  120. package/templates/base/{src → tests/unit}/server/lib/providers.test.ts +1 -1
  121. package/templates/base/{src → tests/unit}/server/lib/r2-storage.test.ts +2 -2
  122. package/templates/base/{src → tests/unit}/server/lib/session.test.ts +2 -2
  123. package/templates/base/{src → tests/unit}/server/lib/tokens.test.ts +1 -1
  124. package/templates/base/{src → tests/unit}/server/lib/transaction.test.ts +5 -14
  125. package/templates/base/{src → tests/unit}/server/middleware/account.test.ts +16 -24
  126. package/templates/base/{src → tests/unit}/server/middleware/auth.test.ts +71 -42
  127. package/templates/base/{src → tests/unit}/server/middleware/cors.test.ts +1 -1
  128. package/templates/base/{src → tests/unit}/server/middleware/error-handler.test.ts +2 -2
  129. package/templates/base/{src → tests/unit}/server/middleware/rate-limit.test.ts +3 -2
  130. package/templates/base/{src → tests/unit}/server/middleware/request-context.test.ts +1 -1
  131. package/templates/base/{src → tests/unit}/server/middleware/request-logger.test.ts +1 -1
  132. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/db.test.ts +1 -1
  133. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/kv.test.ts +1 -1
  134. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/r2.test.ts +1 -1
  135. package/templates/base/{src/server/routes/accounts/__tests__ → tests/unit/server/routes/accounts}/handlers.test.ts +12 -12
  136. package/templates/base/{src/server/routes/audits/__tests__ → tests/unit/server/routes/audits}/handlers.test.ts +11 -11
  137. package/templates/base/{src/server/routes/auth/__tests__ → tests/unit/server/routes/auth}/handlers.test.ts +13 -13
  138. package/templates/base/{src/server/routes/health/__tests__ → tests/unit/server/routes/health}/handlers.test.ts +27 -23
  139. package/templates/base/{src/server/routes/invitations/__tests__ → tests/unit/server/routes/invitations}/handlers.test.ts +14 -17
  140. package/templates/base/{src/server/routes/storage/__tests__ → tests/unit/server/routes/storage}/handlers.test.ts +6 -6
  141. package/templates/base/{src/server/routes/users/__tests__ → tests/unit/server/routes/users}/handlers.test.ts +12 -12
  142. package/templates/base/tests/unit/server/services/accounts.test.ts +258 -0
  143. package/templates/base/tests/unit/server/services/audits.test.ts +141 -0
  144. package/templates/base/tests/unit/server/services/auth.test.ts +179 -0
  145. package/templates/base/tests/unit/server/services/invitations.test.ts +165 -0
  146. package/templates/base/tests/unit/server/services/users.test.ts +351 -0
  147. package/templates/base/tsconfig.json +2 -1
  148. package/templates/base/vitest.config.browser.ts +3 -2
  149. package/templates/base/vitest.config.frontend.ts +3 -2
  150. package/templates/base/vitest.config.ts +7 -14
  151. package/templates/base/.claude/settings.local.json +0 -11
  152. package/templates/base/config/drizzle.config.ts +0 -10
  153. package/templates/base/src/server/db/schema/accounts.ts +0 -20
  154. package/templates/base/src/server/db/schema/audit-logs.ts +0 -26
  155. package/templates/base/src/server/db/schema/index.ts +0 -7
  156. package/templates/base/src/server/db/schema/invitations.ts +0 -30
  157. package/templates/base/src/server/db/schema/refresh-tokens.ts +0 -22
  158. package/templates/base/src/server/db/schema/user-accounts.ts +0 -25
  159. package/templates/base/src/server/db/schema/users.ts +0 -33
  160. package/templates/base/src/server/lib/audited-db.test.ts +0 -107
  161. package/templates/base/src/server/lib/schema-helpers.ts +0 -16
  162. package/templates/base/src/server/services/__tests__/accounts.test.ts +0 -764
  163. package/templates/base/src/server/services/__tests__/audits.test.ts +0 -235
  164. package/templates/base/src/server/services/__tests__/auth.test.ts +0 -765
  165. package/templates/base/src/server/services/__tests__/invitations.test.ts +0 -704
  166. package/templates/base/src/server/services/__tests__/users.test.ts +0 -755
  167. package/templates/base/tests/integration/lib/schema-helpers.test.ts +0 -129
  168. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-can-be-collapsed-by-default-1.png +0 -0
  169. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-expands-when-collapsed-and-expand-button-is-clicked-1.png +0 -0
  170. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
  171. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
  172. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-hides-navigation-labels-when-collapsed-1.png +0 -0
  173. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
  174. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
  175. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-keyboard-shortcut-hint-when-expanded-1.png +0 -0
  176. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-info-when-authenticated-1.png +0 -0
  177. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-initials-in-avatar-fallback-1.png +0 -0
  178. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/error-boundary.test.tsx +0 -0
  179. /package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/error-fallback.test.tsx +0 -0
  180. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-auth.test.tsx +0 -0
  181. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-theme.test.tsx +0 -0
  182. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-dashboard-stats-cards-1.png +0 -0
  183. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-quick-action-cards-1.png +0 -0
  184. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-recent-activity-section-1.png +0 -0
  185. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-first-name-in-welcome-message-1.png +0 -0
  186. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-information-in-sidebar-1.png +0 -0
  187. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-render-dashboard-when-authenticated-1.png +0 -0
  188. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-show-navigation-sidebar-1.png +0 -0
  189. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-unauthenticated-should-redirect-to-login-when-not-authenticated-1.png +0 -0
  190. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-accept-invitation-button-1.png +0 -0
  191. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-decline-button-linking-to-homepage-1.png +0 -0
  192. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-invitation-details--email--workspace--role--1.png +0 -0
  193. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-have-a-logo-link-to-homepage-1.png +0 -0
  194. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-render-invitation-page-with-inviter-name-and-workspace-1.png +0 -0
  195. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-show-terms-of-service-and-privacy-policy-links-1.png +0 -0
  196. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-display-Google-OAuth-login-button-1.png +0 -0
  197. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-have-a-link-back-to-home-page-1.png +0 -0
  198. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-content-without-waiting-for-authentication-1.png +0 -0
  199. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-page-at--login-route-1.png +0 -0
  200. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-show-Terms-of-Service-and-Privacy-Policy-links-1.png +0 -0
  201. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-trigger-OAuth-flow-when-clicking-login-button-1.png +0 -0
  202. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-display-404-text-on-not-found-page-1.png +0 -0
  203. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-have-navigation-options-on-404-page-1.png +0 -0
  204. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-render-404-page-for-unknown-routes-1.png +0 -0
  205. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-account-page-1.png +0 -0
  206. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-integrations-page-1.png +0 -0
  207. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-settings-page-1.png +0 -0
  208. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-team-page-1.png +0 -0
  209. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-display-navigation-links-on-home-page-1.png +0 -0
  210. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-have-correct-link-destinations-on-home-page-1.png +0 -0
  211. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-home-page-without-authentication-1.png +0 -0
  212. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-login-page-without-authentication-1.png +0 -0
  213. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-dashboard-to-login-1.png +0 -0
  214. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-settings-to-login-1.png +0 -0
  215. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-team-to-login-1.png +0 -0
  216. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-Active-Sessions-section-with-session-cards-1.png +0 -0
  217. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-page-with-correct-title--Account--1.png +0 -0
  218. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-API-Access-section-1.png +0 -0
  219. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Connected-Accounts-section-with-Google-connected-1.png +0 -0
  220. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Danger-Zone-section-with-delete-button-1.png +0 -0
  221. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Security-section-with-Two-Factor-Authentication-1.png +0 -0
  222. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-API-documentation-section-should-display-API-documentation-link-section-1.png +0 -0
  223. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-Create-Webhook-Dialog-should-have-Add-Webhook-trigger-button-1.png +0 -0
  224. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-category-filters-should-display-all-category-buttons-1.png +0 -0
  225. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-all-integration-cards-with-names-1.png +0 -0
  226. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-category-badges-on-cards-1.png +0 -0
  227. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Configure-button-for-connected-integrations-1.png +0 -0
  228. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connect-button-for-not-connected-integrations-1.png +0 -0
  229. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connected-badge-for-connected-integrations-1.png +0 -0
  230. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Available-Integrations-section-1.png +0 -0
  231. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Webhooks-section-1.png +0 -0
  232. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-connected-count-1.png +0 -0
  233. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-page-description-1.png +0 -0
  234. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-search-input-1.png +0 -0
  235. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-render-with-correct-title--Integrations--1.png +0 -0
  236. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-filter-integrations-based-on-search-query-1.png +0 -0
  237. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-search-by-description-1.png +0 -0
  238. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-show-no-results-message-when-search-has-no-matches-1.png +0 -0
  239. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-existing-webhook-1.png +0 -0
  240. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-events-1.png +0 -0
  241. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-last-delivery-info-1.png +0 -0
  242. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-success-status-1.png +0 -0
  243. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-have-Add-Webhook-button-1.png +0 -0
  244. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-connected-accounts-section-with-Google-provider-1.png +0 -0
  245. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-sessions-and-danger-zone-sections-1.png +0 -0
  246. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-all-notification-toggle-options-1.png +0 -0
  247. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-email-notifications-section-1.png +0 -0
  248. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-toggle-switches-for-notification-options-1.png +0 -0
  249. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-have-toggles-checked-by-default-1.png +0 -0
  250. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display--Save-Changes--button-1.png +0 -0
  251. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-personal-information-form-1.png +0 -0
  252. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-profile-picture-section-1.png +0 -0
  253. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-email-in-disabled-email-input-1.png +0 -0
  254. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-initials-in-avatar-1.png +0 -0
  255. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-name-in-the-name-input-1.png +0 -0
  256. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-all-three-tabs-1.png +0 -0
  257. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-page-description-1.png +0 -0
  258. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-render-with-correct-title--Settings--1.png +0 -0
  259. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-show-Profile-tab-as-default-active-tab-1.png +0 -0
  260. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Account-tab-when-clicked-1.png +0 -0
  261. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Notifications-tab-when-clicked-1.png +0 -0
  262. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Active-Members-section-with-member-count-1.png +0 -0
  263. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Pending-Invitations-section-with-invitation-details-1.png +0 -0
  264. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-have-invite-member-button-that-can-be-clicked-1.png +0 -0
  265. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-page-with-correct-title-and-description-1.png +0 -0
  266. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-search-input-that-filters-team-members-1.png +0 -0
  267. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-show-current-user-with---you---indicator-and-role-badge-1.png +0 -0
  268. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/error-components.test.tsx +0 -0
  269. /package/templates/base/{src/shared/schemas/__tests__ → tests/unit/shared}/schemas.test.ts +0 -0
@@ -1,765 +0,0 @@
1
- // src/server/services/__tests__/auth.test.ts
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
3
- import { authService } from '../auth'
4
- import type { GoogleUserInfo } from '../../types/auth'
5
- import type { AuthEventContext } from '../../lib/audit'
6
-
7
- // Mock dependencies
8
- vi.mock('../../lib/tokens', () => ({
9
- createAccessToken: vi.fn().mockResolvedValue('mock-access-token'),
10
- generateRefreshToken: vi.fn().mockReturnValue('mock-refresh-token'),
11
- hashToken: vi.fn().mockResolvedValue('hashed-token'),
12
- getRefreshTokenExpiry: vi.fn().mockReturnValue(new Date('2025-01-07T00:00:00Z')),
13
- }))
14
-
15
- vi.mock('../../lib/audit', () => ({
16
- logAuthEvent: vi.fn().mockResolvedValue(),
17
- }))
18
-
19
- import { createAccessToken, generateRefreshToken, hashToken, getRefreshTokenExpiry } from '../../lib/tokens'
20
- import { logAuthEvent } from '../../lib/audit'
21
-
22
- /**
23
- * Creates a mock Drizzle database instance with chainable methods for auth tests
24
- */
25
- function createMockDb(existingUser: any = null) {
26
- let selectCallCount = 0
27
- let insertCallCount = 0
28
-
29
- const createSelectChain = (result: any[]) => ({
30
- from: vi.fn().mockReturnValue({
31
- where: vi.fn().mockReturnValue({
32
- limit: vi.fn().mockResolvedValue(result),
33
- }),
34
- }),
35
- })
36
-
37
- // For new user flow, inserts are in order: users, accounts, userAccounts, refreshTokens
38
- const newUserRecord = {
39
- id: 'new-user-id',
40
- googleId: 'google-123',
41
- email: 'testuser@gmail.com',
42
- name: 'Test User',
43
- avatarUrl: 'https://example.com/avatar.jpg',
44
- status: 'active',
45
- providerIds: ['google'],
46
- isSuperAdmin: false,
47
- createdAt: new Date().toISOString(),
48
- updatedAt: new Date().toISOString(),
49
- deletedAt: null,
50
- }
51
-
52
- const newAccountRecord = {
53
- id: 'new-account-id',
54
- name: "Test User's Account",
55
- }
56
-
57
- const db = {
58
- select: vi.fn().mockImplementation(() => {
59
- selectCallCount++
60
- // First select is for finding user by googleId
61
- if (selectCallCount === 1) {
62
- return createSelectChain(existingUser ? [existingUser] : [])
63
- }
64
- // Subsequent selects return empty
65
- return createSelectChain([])
66
- }),
67
- insert: vi.fn().mockImplementation(() => {
68
- insertCallCount++
69
- const currentInsert = insertCallCount
70
-
71
- return {
72
- values: vi.fn().mockImplementation((values: any) => {
73
- return {
74
- returning: vi.fn().mockImplementation(() => {
75
- // For new user flow: 1=users, 2=accounts, 3=userAccounts, 4=refreshTokens
76
- if (currentInsert === 1) {
77
- // Users insert - merge values with defaults
78
- return Promise.resolve([{
79
- ...newUserRecord,
80
- googleId: values.googleId || newUserRecord.googleId,
81
- email: values.email || newUserRecord.email,
82
- name: values.name || newUserRecord.name,
83
- avatarUrl: values.avatarUrl !== undefined ? values.avatarUrl : newUserRecord.avatarUrl,
84
- status: values.status || newUserRecord.status,
85
- }])
86
- }
87
- if (currentInsert === 2) {
88
- // Accounts insert
89
- return Promise.resolve([{
90
- ...newAccountRecord,
91
- name: values.name || newAccountRecord.name,
92
- }])
93
- }
94
- // For userAccounts and refreshTokens, just return empty/values
95
- return Promise.resolve([values])
96
- }),
97
- }
98
- }),
99
- }
100
- }),
101
- update: vi.fn().mockImplementation(() => ({
102
- set: vi.fn().mockReturnValue({
103
- where: vi.fn().mockReturnValue({
104
- returning: vi.fn().mockResolvedValue([]),
105
- }),
106
- }),
107
- })),
108
- // Expose internals for assertions
109
- _selectCallCount: () => selectCallCount,
110
- _insertCallCount: () => insertCallCount,
111
- }
112
-
113
- return db as any
114
- }
115
-
116
- /**
117
- * Creates a mock environment
118
- */
119
- function createMockEnv() {
120
- return {
121
- JWT_SECRET: 'test-jwt-secret',
122
- JWT_EXPIRY_MINUTES: 15,
123
- REFRESH_TOKEN_EXPIRY_DAYS: 30,
124
- }
125
- }
126
-
127
- /**
128
- * Creates an auth event context for testing
129
- */
130
- function createMockAuthEventContext(overrides: Partial<AuthEventContext> = {}): AuthEventContext {
131
- return {
132
- transactionId: 'tx-123',
133
- ip: '127.0.0.1',
134
- userAgent: 'TestAgent/1.0',
135
- ...overrides,
136
- }
137
- }
138
-
139
- /**
140
- * Creates a mock Google user info
141
- */
142
- function createGoogleUserInfo(overrides: Partial<GoogleUserInfo> = {}): GoogleUserInfo {
143
- return {
144
- sub: 'google-123',
145
- email: 'testuser@gmail.com',
146
- email_verified: true,
147
- name: 'Test User',
148
- picture: 'https://example.com/avatar.jpg',
149
- given_name: 'Test',
150
- family_name: 'User',
151
- ...overrides,
152
- }
153
- }
154
-
155
- /**
156
- * Creates a mock existing user record (as returned from DB)
157
- */
158
- function createExistingUserRecord(overrides: any = {}) {
159
- return {
160
- id: 'existing-user-id',
161
- googleId: 'google-123',
162
- email: 'testuser@gmail.com',
163
- name: 'Test User',
164
- avatarUrl: 'https://example.com/avatar.jpg',
165
- status: 'active',
166
- providerIds: ['google'],
167
- isSuperAdmin: false,
168
- createdAt: '2024-01-01T00:00:00Z',
169
- updatedAt: '2024-01-01T00:00:00Z',
170
- deletedAt: null,
171
- ...overrides,
172
- }
173
- }
174
-
175
- describe('authService', () => {
176
- let mockEnv: ReturnType<typeof createMockEnv>
177
- let mockCtx: AuthEventContext
178
-
179
- beforeEach(() => {
180
- vi.clearAllMocks()
181
- mockEnv = createMockEnv()
182
- mockCtx = createMockAuthEventContext()
183
- })
184
-
185
- describe('findOrCreateUser', () => {
186
- it('should create new user when not found (isNewUser: true)', async () => {
187
- // Arrange
188
- const mockDb = createMockDb(null) // No existing user
189
- const googleUser = createGoogleUserInfo()
190
-
191
- // Act
192
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
193
-
194
- // Assert
195
- expect(result.isNewUser).toBe(true)
196
- expect(result.user.email).toBe(googleUser.email)
197
- expect(result.user.name).toBe(googleUser.name)
198
- expect(result.user.id).toBe('new-user-id')
199
-
200
- // Verify user was inserted
201
- expect(mockDb.insert).toHaveBeenCalled()
202
-
203
- // Verify signup event was logged (for new users)
204
- expect(logAuthEvent).toHaveBeenCalledWith(
205
- mockDb,
206
- mockCtx,
207
- 'SIGNUP',
208
- 'new-user-id',
209
- expect.objectContaining({
210
- email: googleUser.email,
211
- provider: 'google',
212
- accountId: 'new-account-id',
213
- })
214
- )
215
-
216
- // Verify login event was logged
217
- expect(logAuthEvent).toHaveBeenCalledWith(
218
- mockDb,
219
- mockCtx,
220
- 'LOGIN',
221
- 'new-user-id',
222
- expect.objectContaining({
223
- email: googleUser.email,
224
- provider: 'google',
225
- isNewUser: true,
226
- })
227
- )
228
- })
229
-
230
- it('should return existing user when found by googleId (isNewUser: false)', async () => {
231
- // Arrange
232
- const existingUser = createExistingUserRecord()
233
- const mockDb = createMockDb(existingUser)
234
- const googleUser = createGoogleUserInfo()
235
-
236
- // Act
237
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
238
-
239
- // Assert
240
- expect(result.isNewUser).toBe(false)
241
- expect(result.user.id).toBe('existing-user-id')
242
- expect(result.user.email).toBe(existingUser.email)
243
- expect(result.user.name).toBe(existingUser.name)
244
-
245
- // Verify no SIGNUP event was logged (only LOGIN)
246
- expect(logAuthEvent).not.toHaveBeenCalledWith(
247
- expect.anything(),
248
- expect.anything(),
249
- 'SIGNUP',
250
- expect.anything(),
251
- expect.anything()
252
- )
253
-
254
- // Verify login event was logged with isNewUser: false
255
- expect(logAuthEvent).toHaveBeenCalledWith(
256
- mockDb,
257
- mockCtx,
258
- 'LOGIN',
259
- 'existing-user-id',
260
- expect.objectContaining({
261
- email: existingUser.email,
262
- provider: 'google',
263
- isNewUser: false,
264
- })
265
- )
266
- })
267
-
268
- it('should update user profile when info changed (email, name, avatarUrl)', async () => {
269
- // Arrange - existing user has different info than google provides
270
- const existingUser = createExistingUserRecord({
271
- email: 'old-email@gmail.com',
272
- name: 'Old Name',
273
- avatarUrl: 'https://example.com/old-avatar.jpg',
274
- })
275
-
276
- const updatedUser = {
277
- ...existingUser,
278
- email: 'new-email@gmail.com',
279
- name: 'New Name',
280
- avatarUrl: 'https://example.com/new-avatar.jpg',
281
- }
282
-
283
- const mockDb = createMockDb(existingUser)
284
-
285
- // Override update to return updated user
286
- mockDb.update = vi.fn().mockReturnValue({
287
- set: vi.fn().mockReturnValue({
288
- where: vi.fn().mockReturnValue({
289
- returning: vi.fn().mockResolvedValue([updatedUser]),
290
- }),
291
- }),
292
- })
293
-
294
- const googleUser = createGoogleUserInfo({
295
- email: 'new-email@gmail.com',
296
- name: 'New Name',
297
- picture: 'https://example.com/new-avatar.jpg',
298
- })
299
-
300
- // Act
301
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
302
-
303
- // Assert
304
- expect(result.isNewUser).toBe(false)
305
- expect(mockDb.update).toHaveBeenCalled()
306
- expect(result.user.email).toBe('new-email@gmail.com')
307
- expect(result.user.name).toBe('New Name')
308
- })
309
-
310
- it('should not update user when profile info is unchanged', async () => {
311
- // Arrange - existing user has same info as google provides
312
- const existingUser = createExistingUserRecord()
313
- const mockDb = createMockDb(existingUser)
314
-
315
- const googleUser = createGoogleUserInfo({
316
- sub: existingUser.googleId,
317
- email: existingUser.email,
318
- name: existingUser.name,
319
- picture: existingUser.avatarUrl,
320
- })
321
-
322
- // Act
323
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
324
-
325
- // Assert
326
- expect(result.isNewUser).toBe(false)
327
- expect(mockDb.update).not.toHaveBeenCalled()
328
- })
329
-
330
- it('should generate access and refresh tokens', async () => {
331
- // Arrange
332
- const existingUser = createExistingUserRecord()
333
- const mockDb = createMockDb(existingUser)
334
- const googleUser = createGoogleUserInfo()
335
-
336
- // Act
337
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
338
-
339
- // Assert
340
- expect(result.tokens.accessToken).toBe('mock-access-token')
341
- expect(result.refreshToken).toBe('mock-refresh-token')
342
- expect(result.tokens.expiresIn).toBe(60 * 15) // 15 minutes in seconds
343
-
344
- // Verify token functions were called
345
- expect(createAccessToken).toHaveBeenCalledWith(
346
- mockEnv,
347
- existingUser.id,
348
- existingUser.email
349
- )
350
- expect(generateRefreshToken).toHaveBeenCalled()
351
- expect(hashToken).toHaveBeenCalledWith('mock-refresh-token')
352
- expect(getRefreshTokenExpiry).toHaveBeenCalledWith(mockEnv)
353
- })
354
-
355
- it('should store refresh token in database', async () => {
356
- // Arrange
357
- const existingUser = createExistingUserRecord()
358
- const mockDb = createMockDb(existingUser)
359
- const googleUser = createGoogleUserInfo()
360
-
361
- // Track refreshTokens insert values
362
- let refreshTokenValues: any = null
363
- const originalInsert = mockDb.insert
364
- mockDb.insert = vi.fn().mockImplementation((table: any) => {
365
- const result = originalInsert(table)
366
- // Capture the values for refreshTokens table
367
- return {
368
- values: vi.fn().mockImplementation((values: any) => {
369
- // For existing user, the only insert is refreshTokens
370
- refreshTokenValues = values
371
- return { returning: vi.fn().mockResolvedValue([values]) }
372
- }),
373
- }
374
- })
375
-
376
- // Act
377
- await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
378
-
379
- // Assert - verify refresh token insert was called with correct values
380
- expect(mockDb.insert).toHaveBeenCalled()
381
- expect(refreshTokenValues).toEqual(expect.objectContaining({
382
- userId: existingUser.id,
383
- tokenHash: 'hashed-token',
384
- expiresAt: new Date('2025-01-07T00:00:00Z'),
385
- }))
386
- })
387
-
388
- it('should create personal account for new user', async () => {
389
- // Arrange
390
- const mockDb = createMockDb(null) // No existing user
391
- const googleUser = createGoogleUserInfo({ name: 'John Doe' })
392
-
393
- // Act
394
- await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
395
-
396
- // Assert - verify account was created with user's name
397
- // Insert should be called for: users, accounts, userAccounts, refreshTokens
398
- expect(mockDb.insert).toHaveBeenCalledTimes(4)
399
- })
400
-
401
- it('should link new user to account with EDITOR role', async () => {
402
- // Arrange
403
- const mockDb = createMockDb(null) // No existing user
404
- const googleUser = createGoogleUserInfo()
405
-
406
- // Track userAccounts insert
407
- let userAccountsValues: any = null
408
- let insertCallCount = 0
409
- const originalInsert = mockDb.insert
410
- mockDb.insert = vi.fn().mockImplementation((table: any) => {
411
- insertCallCount++
412
- const result = originalInsert(table)
413
- // Third insert is userAccounts (1=users, 2=accounts, 3=userAccounts, 4=refreshTokens)
414
- if (insertCallCount === 3) {
415
- return {
416
- values: vi.fn().mockImplementation((values: any) => {
417
- userAccountsValues = values
418
- return { returning: vi.fn().mockResolvedValue([values]) }
419
- }),
420
- }
421
- }
422
- return result
423
- })
424
-
425
- // Act
426
- await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
427
-
428
- // Assert - verify at least 4 inserts happened (user, account, userAccount, refreshToken)
429
- expect(mockDb.insert).toHaveBeenCalledTimes(4)
430
-
431
- // Assert - verify userAccounts was created with EDITOR role
432
- expect(userAccountsValues).toEqual(expect.objectContaining({
433
- role: 'EDITOR',
434
- }))
435
- })
436
-
437
- it('should handle user with no avatar (picture undefined)', async () => {
438
- // Arrange
439
- const mockDb = createMockDb(null)
440
- const googleUser = createGoogleUserInfo({ picture: undefined })
441
-
442
- // Act
443
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
444
-
445
- // Assert
446
- expect(result.user).toBeDefined()
447
- expect(result.isNewUser).toBe(true)
448
- })
449
-
450
- it('should return correct user structure', async () => {
451
- // Arrange
452
- const existingUser = createExistingUserRecord()
453
- const mockDb = createMockDb(existingUser)
454
- const googleUser = createGoogleUserInfo()
455
-
456
- // Act
457
- const result = await authService.findOrCreateUser(mockDb, mockEnv, googleUser, mockCtx)
458
-
459
- // Assert - verify user object has expected shape
460
- expect(result.user).toEqual(expect.objectContaining({
461
- id: expect.any(String),
462
- email: expect.any(String),
463
- name: expect.any(String),
464
- status: expect.any(String),
465
- providerIds: expect.any(Array),
466
- isSuperAdmin: expect.any(Boolean),
467
- createdAt: expect.any(String),
468
- updatedAt: expect.any(String),
469
- }))
470
- })
471
- })
472
-
473
- describe('refreshAccessToken', () => {
474
- it('should throw UnauthorizedError when refresh token not found', async () => {
475
- // Arrange
476
- const mockDb = createMockDb()
477
- mockDb.select = vi.fn().mockReturnValue({
478
- from: vi.fn().mockReturnValue({
479
- where: vi.fn().mockReturnValue({
480
- limit: vi.fn().mockResolvedValue([]),
481
- }),
482
- }),
483
- })
484
-
485
- // Act & Assert
486
- await expect(
487
- authService.refreshAccessToken(mockDb, mockEnv, 'invalid-token', mockCtx)
488
- ).rejects.toThrow('Invalid or expired refresh token')
489
- })
490
-
491
- it('should throw UnauthorizedError when token is expired', async () => {
492
- // Arrange - Token query returns empty because WHERE clause filters expired tokens
493
- const mockDb = createMockDb()
494
- mockDb.select = vi.fn().mockReturnValue({
495
- from: vi.fn().mockReturnValue({
496
- where: vi.fn().mockReturnValue({
497
- limit: vi.fn().mockResolvedValue([]), // Expired tokens filtered out by query
498
- }),
499
- }),
500
- })
501
-
502
- // Act & Assert - Expired tokens are handled by the WHERE clause (gt expiresAt > new Date())
503
- // so they return empty result, same error as not found
504
- await expect(
505
- authService.refreshAccessToken(mockDb, mockEnv, 'expired-token', mockCtx)
506
- ).rejects.toThrow('Invalid or expired refresh token')
507
- })
508
-
509
- it('should throw UnauthorizedError when token is revoked', async () => {
510
- // Arrange - Token query returns empty because WHERE clause filters revoked tokens
511
- const mockDb = createMockDb()
512
- mockDb.select = vi.fn().mockReturnValue({
513
- from: vi.fn().mockReturnValue({
514
- where: vi.fn().mockReturnValue({
515
- limit: vi.fn().mockResolvedValue([]), // Revoked tokens filtered out by query
516
- }),
517
- }),
518
- })
519
-
520
- // Act & Assert - Revoked tokens are handled by the WHERE clause (isNull revokedAt)
521
- // so they return empty result, same error as not found
522
- await expect(
523
- authService.refreshAccessToken(mockDb, mockEnv, 'revoked-token', mockCtx)
524
- ).rejects.toThrow('Invalid or expired refresh token')
525
- })
526
-
527
- it('should throw UnauthorizedError when user not found', async () => {
528
- // Arrange
529
- const mockDb = createMockDb()
530
- let selectCallCount = 0
531
- mockDb.select = vi.fn().mockImplementation(() => {
532
- selectCallCount++
533
- return {
534
- from: vi.fn().mockReturnValue({
535
- where: vi.fn().mockReturnValue({
536
- limit: vi.fn().mockResolvedValue(
537
- selectCallCount === 1
538
- ? [{ id: 'token-id', userId: 'user-id', tokenHash: 'hashed-token' }] // Token found
539
- : [] // User not found
540
- ),
541
- }),
542
- }),
543
- }
544
- })
545
-
546
- // Act & Assert
547
- await expect(
548
- authService.refreshAccessToken(mockDb, mockEnv, 'valid-token', mockCtx)
549
- ).rejects.toThrow('User not found or inactive')
550
- })
551
-
552
- it('should throw UnauthorizedError when user is inactive', async () => {
553
- // Arrange
554
- const inactiveUser = createExistingUserRecord({ status: 'suspended' })
555
- const mockDb = createMockDb()
556
- let selectCallCount = 0
557
- mockDb.select = vi.fn().mockImplementation(() => {
558
- selectCallCount++
559
- return {
560
- from: vi.fn().mockReturnValue({
561
- where: vi.fn().mockReturnValue({
562
- limit: vi.fn().mockResolvedValue(
563
- selectCallCount === 1
564
- ? [{ id: 'token-id', userId: inactiveUser.id, tokenHash: 'hashed-token' }] // Token found
565
- : [inactiveUser] // User found but inactive
566
- ),
567
- }),
568
- }),
569
- }
570
- })
571
-
572
- // Act & Assert
573
- await expect(
574
- authService.refreshAccessToken(mockDb, mockEnv, 'valid-token', mockCtx)
575
- ).rejects.toThrow('User not found or inactive')
576
- })
577
-
578
- it('should return new tokens when refresh token is valid', async () => {
579
- // Arrange
580
- const existingUser = createExistingUserRecord()
581
- const mockDb = createMockDb()
582
- let selectCallCount = 0
583
- mockDb.select = vi.fn().mockImplementation(() => {
584
- selectCallCount++
585
- return {
586
- from: vi.fn().mockReturnValue({
587
- where: vi.fn().mockReturnValue({
588
- limit: vi.fn().mockResolvedValue(
589
- selectCallCount === 1
590
- ? [{ id: 'token-id', userId: existingUser.id, tokenHash: 'hashed-token' }] // Token found
591
- : [existingUser] // User found
592
- ),
593
- }),
594
- }),
595
- }
596
- })
597
-
598
- // Act
599
- const result = await authService.refreshAccessToken(mockDb, mockEnv, 'valid-refresh-token', mockCtx)
600
-
601
- // Assert
602
- expect(result.accessToken).toBe('mock-access-token')
603
- expect(result.expiresIn).toBe(60 * 15) // 15 minutes in seconds
604
-
605
- // Verify token functions were called
606
- expect(hashToken).toHaveBeenCalledWith('valid-refresh-token')
607
- expect(createAccessToken).toHaveBeenCalledWith(
608
- mockEnv,
609
- existingUser.id,
610
- existingUser.email
611
- )
612
-
613
- // Verify TOKEN_REFRESH event was logged
614
- expect(logAuthEvent).toHaveBeenCalledWith(
615
- mockDb,
616
- mockCtx,
617
- 'TOKEN_REFRESH',
618
- existingUser.id,
619
- expect.objectContaining({
620
- email: existingUser.email,
621
- })
622
- )
623
- })
624
- })
625
-
626
- describe('revokeAllUserTokens', () => {
627
- it('should revoke all refresh tokens for user by marking revokedAt', async () => {
628
- // Arrange
629
- const mockDb = createMockDb()
630
- const userId = 'user-id-to-revoke'
631
-
632
- // Act
633
- await authService.revokeAllUserTokens(mockDb, userId)
634
-
635
- // Assert - verify update was called
636
- expect(mockDb.update).toHaveBeenCalled()
637
- })
638
- })
639
-
640
- describe('revokeRefreshToken', () => {
641
- it('should revoke single refresh token by hash', async () => {
642
- // Arrange
643
- const mockDb = createMockDb()
644
- const refreshToken = 'token-to-revoke'
645
-
646
- // Act
647
- await authService.revokeRefreshToken(mockDb, refreshToken, mockCtx, null)
648
-
649
- // Assert
650
- expect(mockDb.update).toHaveBeenCalled()
651
- expect(hashToken).toHaveBeenCalledWith(refreshToken)
652
- })
653
-
654
- it('should log logout event when userId is provided', async () => {
655
- // Arrange
656
- const mockDb = createMockDb()
657
- const refreshToken = 'token-to-revoke'
658
- const userId = 'user-123'
659
-
660
- // Act
661
- await authService.revokeRefreshToken(mockDb, refreshToken, mockCtx, userId)
662
-
663
- // Assert
664
- expect(mockDb.update).toHaveBeenCalled()
665
- expect(logAuthEvent).toHaveBeenCalledWith(
666
- mockDb,
667
- mockCtx,
668
- 'LOGOUT',
669
- userId,
670
- {}
671
- )
672
- })
673
-
674
- it('should not log logout event when userId is null', async () => {
675
- // Arrange
676
- const mockDb = createMockDb()
677
- vi.mocked(logAuthEvent).mockClear() // Clear previous calls
678
- const refreshToken = 'token-to-revoke'
679
-
680
- // Act
681
- await authService.revokeRefreshToken(mockDb, refreshToken, mockCtx, null)
682
-
683
- // Assert
684
- expect(logAuthEvent).not.toHaveBeenCalled()
685
- })
686
- })
687
-
688
- describe('getCurrentUser', () => {
689
- it('should return user when found', async () => {
690
- // Arrange
691
- const existingUser = createExistingUserRecord()
692
- const mockDb = createMockDb(existingUser)
693
-
694
- // Override select to return user for getCurrentUser
695
- mockDb.select = vi.fn().mockReturnValue({
696
- from: vi.fn().mockReturnValue({
697
- where: vi.fn().mockReturnValue({
698
- limit: vi.fn().mockResolvedValue([existingUser]),
699
- }),
700
- }),
701
- })
702
-
703
- // Act
704
- const result = await authService.getCurrentUser(mockDb, existingUser.id)
705
-
706
- // Assert
707
- expect(result).toEqual(expect.objectContaining({
708
- id: existingUser.id,
709
- email: existingUser.email,
710
- name: existingUser.name,
711
- status: existingUser.status,
712
- isSuperAdmin: existingUser.isSuperAdmin,
713
- }))
714
- })
715
-
716
- it('should throw UnauthorizedError when user not found', async () => {
717
- // Arrange
718
- const mockDb = createMockDb()
719
-
720
- // Override select to return empty
721
- mockDb.select = vi.fn().mockReturnValue({
722
- from: vi.fn().mockReturnValue({
723
- where: vi.fn().mockReturnValue({
724
- limit: vi.fn().mockResolvedValue([]),
725
- }),
726
- }),
727
- })
728
-
729
- // Act & Assert
730
- await expect(
731
- authService.getCurrentUser(mockDb, 'non-existent-user-id')
732
- ).rejects.toThrow('User not found')
733
- })
734
-
735
- it('should return user with correct structure', async () => {
736
- // Arrange
737
- const existingUser = createExistingUserRecord({
738
- providerIds: null, // Test null providerIds fallback
739
- })
740
- const mockDb = createMockDb(existingUser)
741
-
742
- mockDb.select = vi.fn().mockReturnValue({
743
- from: vi.fn().mockReturnValue({
744
- where: vi.fn().mockReturnValue({
745
- limit: vi.fn().mockResolvedValue([existingUser]),
746
- }),
747
- }),
748
- })
749
-
750
- // Act
751
- const result = await authService.getCurrentUser(mockDb, existingUser.id)
752
-
753
- // Assert - verify providerIds defaults to empty array
754
- expect(result.providerIds).toEqual([])
755
- expect(result).toHaveProperty('id')
756
- expect(result).toHaveProperty('email')
757
- expect(result).toHaveProperty('name')
758
- expect(result).toHaveProperty('status')
759
- expect(result).toHaveProperty('isSuperAdmin')
760
- expect(result).toHaveProperty('createdAt')
761
- expect(result).toHaveProperty('updatedAt')
762
- expect(result).toHaveProperty('deletedAt')
763
- })
764
- })
765
- })