@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,704 +0,0 @@
1
- // src/server/services/__tests__/invitations.test.ts
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
3
- import { invitationsService } from '../invitations'
4
- import { ForbiddenError, ConflictError, NotFoundError } from '../../lib/errors'
5
- import type { AuthEventContext } from '../../lib/audit'
6
- import type { ServiceContext } from '../../types'
7
- import { createUserFixture, createAccountFixture } from '../../__tests__/fixtures'
8
-
9
- // Mock email module
10
- vi.mock('../../lib/email', () => ({
11
- sendInvitationEmail: vi.fn().mockResolvedValue(),
12
- }))
13
-
14
- // Mock audit module
15
- vi.mock('../../lib/audit', () => ({
16
- logAuthEvent: vi.fn(),
17
- }))
18
-
19
- import { sendInvitationEmail } from '../../lib/email'
20
- import { logAuthEvent } from '../../lib/audit'
21
-
22
- /**
23
- * Creates a mock Drizzle database instance with chainable methods
24
- */
25
- function createMockDb() {
26
- return {
27
- select: vi.fn().mockReturnValue({
28
- from: vi.fn().mockReturnValue({
29
- innerJoin: vi.fn().mockReturnValue({
30
- where: vi.fn().mockReturnValue({
31
- limit: vi.fn().mockResolvedValue([]),
32
- }),
33
- }),
34
- where: vi.fn().mockReturnValue({
35
- limit: vi.fn().mockResolvedValue([]),
36
- }),
37
- }),
38
- }),
39
- insert: vi.fn().mockReturnValue({
40
- values: vi.fn().mockReturnValue({
41
- returning: vi.fn().mockResolvedValue([
42
- {
43
- id: 'inv-1',
44
- email: 'invite@test.com',
45
- role: 'VIEWER',
46
- expiresAt: '2025-01-07T00:00:00Z',
47
- },
48
- ]),
49
- }),
50
- }),
51
- update: vi.fn().mockReturnValue({
52
- set: vi.fn().mockReturnValue({
53
- where: vi.fn().mockResolvedValue([]),
54
- }),
55
- }),
56
- delete: vi.fn().mockReturnValue({
57
- where: vi.fn().mockResolvedValue(),
58
- }),
59
- } as any
60
- }
61
-
62
- const mockEnv = {
63
- SENDGRID_API_KEY: 'test-key',
64
- SENDGRID_FROM_EMAIL: 'noreply@test.com',
65
- APP_URL: 'http://localhost:3000',
66
- } as any
67
-
68
- /**
69
- * Creates a standard service context for testing
70
- */
71
- function createMockContext(overrides: Partial<ServiceContext> = {}): ServiceContext {
72
- const user = createUserFixture({
73
- id: 'ctx-user-123',
74
- email: 'admin@test.com',
75
- name: 'Admin User',
76
- })
77
-
78
- return {
79
- accountId: 'account-123',
80
- user,
81
- userRole: 'ADMIN',
82
- transactionId: 'tx-123',
83
- ip: '127.0.0.1',
84
- userAgent: 'TestAgent/1.0',
85
- ...overrides,
86
- }
87
- }
88
-
89
- describe('invitationsService', () => {
90
- let mockDb: ReturnType<typeof createMockDb>
91
- let ctx: ServiceContext
92
-
93
- beforeEach(() => {
94
- vi.clearAllMocks()
95
- mockDb = createMockDb()
96
- ctx = createMockContext()
97
- })
98
-
99
- describe('create', () => {
100
- it('should throw ForbiddenError when assigning higher role than own', async () => {
101
- // A VIEWER (level 4) cannot invite an ADMIN (level 0)
102
- const viewerCtx = createMockContext({ userRole: 'VIEWER' })
103
-
104
- await expect(
105
- invitationsService.create(mockDb, mockEnv, viewerCtx, {
106
- email: 'test@test.com',
107
- role: 'ADMIN',
108
- })
109
- ).rejects.toThrow(ForbiddenError)
110
-
111
- await expect(
112
- invitationsService.create(mockDb, mockEnv, viewerCtx, {
113
- email: 'test@test.com',
114
- role: 'ADMIN',
115
- })
116
- ).rejects.toThrow('Cannot assign a role higher than your own')
117
- })
118
-
119
- it('should throw ConflictError when user already in account', async () => {
120
- const existingUser = createUserFixture({
121
- id: 'existing-user-1',
122
- email: 'existing@test.com',
123
- name: 'Existing User',
124
- })
125
-
126
- // Mock finding existing membership
127
- const membershipChain = {
128
- from: vi.fn().mockReturnValue({
129
- innerJoin: vi.fn().mockReturnValue({
130
- where: vi.fn().mockReturnValue({
131
- limit: vi.fn().mockResolvedValue([
132
- {
133
- users: existingUser,
134
- userAccounts: {
135
- userId: existingUser.id,
136
- accountId: ctx.accountId,
137
- role: 'VIEWER',
138
- },
139
- },
140
- ]),
141
- }),
142
- }),
143
- }),
144
- }
145
-
146
- mockDb.select = vi.fn().mockReturnValue(membershipChain)
147
-
148
- await expect(
149
- invitationsService.create(mockDb, mockEnv, ctx, {
150
- email: 'existing@test.com',
151
- role: 'VIEWER',
152
- })
153
- ).rejects.toThrow(ConflictError)
154
-
155
- await expect(
156
- invitationsService.create(mockDb, mockEnv, ctx, {
157
- email: 'existing@test.com',
158
- role: 'VIEWER',
159
- })
160
- ).rejects.toThrow('User is already a member of this account')
161
- })
162
-
163
- it('should link existing user directly if they exist (without sending email)', async () => {
164
- const existingUser = createUserFixture({
165
- id: 'existing-user-2',
166
- email: 'newmember@test.com',
167
- name: 'New Member',
168
- })
169
-
170
- // First call: check membership (empty - user not in account)
171
- const emptyMembershipChain = {
172
- from: vi.fn().mockReturnValue({
173
- innerJoin: vi.fn().mockReturnValue({
174
- where: vi.fn().mockReturnValue({
175
- limit: vi.fn().mockResolvedValue([]),
176
- }),
177
- }),
178
- }),
179
- }
180
-
181
- // Second call: check if user exists in system
182
- const existingUserChain = {
183
- from: vi.fn().mockReturnValue({
184
- where: vi.fn().mockReturnValue({
185
- limit: vi.fn().mockResolvedValue([existingUser]),
186
- }),
187
- }),
188
- }
189
-
190
- let selectCallCount = 0
191
- mockDb.select = vi.fn().mockImplementation(() => {
192
- selectCallCount++
193
- if (selectCallCount === 1) {
194
- return emptyMembershipChain
195
- }
196
- return existingUserChain
197
- })
198
-
199
- // Mock insert for creating user-account relationship
200
- mockDb.insert = vi.fn().mockReturnValue({
201
- values: vi.fn().mockResolvedValue(),
202
- })
203
-
204
- const result = await invitationsService.create(mockDb, mockEnv, ctx, {
205
- email: 'newmember@test.com',
206
- role: 'EDITOR',
207
- })
208
-
209
- expect(result.linked).toBe(true)
210
- expect(result.invited).toBe(false)
211
- expect(result.user).toEqual({
212
- id: existingUser.id,
213
- email: existingUser.email,
214
- name: existingUser.name,
215
- })
216
- expect(result.invitation).toBeUndefined()
217
-
218
- // Verify email was NOT sent
219
- expect(sendInvitationEmail).not.toHaveBeenCalled()
220
-
221
- // Verify insert was called to link user to account
222
- expect(mockDb.insert).toHaveBeenCalled()
223
- })
224
-
225
- it('should create invitation and send email when user does not exist', async () => {
226
- const testAccount = createAccountFixture({
227
- id: ctx.accountId,
228
- name: 'Test Account',
229
- })
230
-
231
- // Call 1: check membership (empty)
232
- const emptyMembershipChain = {
233
- from: vi.fn().mockReturnValue({
234
- innerJoin: vi.fn().mockReturnValue({
235
- where: vi.fn().mockReturnValue({
236
- limit: vi.fn().mockResolvedValue([]),
237
- }),
238
- }),
239
- }),
240
- }
241
-
242
- // Call 2: check if user exists (empty - user doesn't exist)
243
- const emptyUserChain = {
244
- from: vi.fn().mockReturnValue({
245
- where: vi.fn().mockReturnValue({
246
- limit: vi.fn().mockResolvedValue([]),
247
- }),
248
- }),
249
- }
250
-
251
- // Call 3: check for existing pending invitation (empty)
252
- const emptyInvitationChain = {
253
- from: vi.fn().mockReturnValue({
254
- where: vi.fn().mockReturnValue({
255
- limit: vi.fn().mockResolvedValue([]),
256
- }),
257
- }),
258
- }
259
-
260
- // Call 4: get account name
261
- const accountChain = {
262
- from: vi.fn().mockReturnValue({
263
- where: vi.fn().mockReturnValue({
264
- limit: vi.fn().mockResolvedValue([testAccount]),
265
- }),
266
- }),
267
- }
268
-
269
- let selectCallCount = 0
270
- mockDb.select = vi.fn().mockImplementation(() => {
271
- selectCallCount++
272
- if (selectCallCount === 1) {
273
- return emptyMembershipChain
274
- }
275
- if (selectCallCount === 2) {
276
- return emptyUserChain
277
- }
278
- if (selectCallCount === 3) {
279
- return emptyInvitationChain
280
- }
281
- return accountChain
282
- })
283
-
284
- // Mock insert for creating invitation
285
- const createdInvitation = {
286
- id: 'inv-new-1',
287
- email: 'newinvite@test.com',
288
- role: 'VIEWER',
289
- expiresAt: '2025-01-07T00:00:00Z',
290
- }
291
-
292
- mockDb.insert = vi.fn().mockReturnValue({
293
- values: vi.fn().mockReturnValue({
294
- returning: vi.fn().mockResolvedValue([createdInvitation]),
295
- }),
296
- })
297
-
298
- const result = await invitationsService.create(mockDb, mockEnv, ctx, {
299
- email: 'newinvite@test.com',
300
- role: 'VIEWER',
301
- })
302
-
303
- expect(result.linked).toBe(false)
304
- expect(result.invited).toBe(true)
305
- expect(result.invitation).toEqual({
306
- id: createdInvitation.id,
307
- email: createdInvitation.email,
308
- role: createdInvitation.role,
309
- expiresAt: createdInvitation.expiresAt,
310
- })
311
- expect(result.user).toBeUndefined()
312
-
313
- // Verify email WAS sent
314
- expect(sendInvitationEmail).toHaveBeenCalledTimes(1)
315
- expect(sendInvitationEmail).toHaveBeenCalledWith(
316
- mockEnv,
317
- 'newinvite@test.com',
318
- ctx.user.name,
319
- testAccount.name,
320
- expect.stringContaining(`${mockEnv.APP_URL}/auth/invite/`)
321
- )
322
-
323
- // Verify insert was called to create invitation
324
- expect(mockDb.insert).toHaveBeenCalled()
325
- })
326
-
327
- it('should throw ConflictError when pending invitation already exists', async () => {
328
- // Helper to create mocked select chains
329
- const createSelectMock = () => {
330
- let selectCallCount = 0
331
-
332
- // Call 1: check membership (empty - user not in account)
333
- const emptyMembershipChain = {
334
- from: vi.fn().mockReturnValue({
335
- innerJoin: vi.fn().mockReturnValue({
336
- where: vi.fn().mockReturnValue({
337
- limit: vi.fn().mockResolvedValue([]),
338
- }),
339
- }),
340
- }),
341
- }
342
-
343
- // Call 2: check if user exists (empty - user doesn't exist)
344
- const emptyUserChain = {
345
- from: vi.fn().mockReturnValue({
346
- where: vi.fn().mockReturnValue({
347
- limit: vi.fn().mockResolvedValue([]),
348
- }),
349
- }),
350
- }
351
-
352
- // Call 3: check for existing pending invitation (returns existing invitation)
353
- const existingInvitationChain = {
354
- from: vi.fn().mockReturnValue({
355
- where: vi.fn().mockReturnValue({
356
- limit: vi.fn().mockResolvedValue([
357
- {
358
- id: 'existing-inv-1',
359
- accountId: ctx.accountId,
360
- email: 'pending@test.com',
361
- role: 'VIEWER',
362
- expiresAt: '2025-12-31T00:00:00Z',
363
- acceptedAt: null,
364
- },
365
- ]),
366
- }),
367
- }),
368
- }
369
-
370
- return vi.fn().mockImplementation(() => {
371
- selectCallCount++
372
- if (selectCallCount === 1) {
373
- return emptyMembershipChain
374
- }
375
- if (selectCallCount === 2) {
376
- return emptyUserChain
377
- }
378
- return existingInvitationChain
379
- })
380
- }
381
-
382
- mockDb.select = createSelectMock()
383
- await expect(
384
- invitationsService.create(mockDb, mockEnv, ctx, {
385
- email: 'pending@test.com',
386
- role: 'VIEWER',
387
- })
388
- ).rejects.toThrow(ConflictError)
389
-
390
- // Reset mock for second assertion
391
- mockDb.select = createSelectMock()
392
- await expect(
393
- invitationsService.create(mockDb, mockEnv, ctx, {
394
- email: 'pending@test.com',
395
- role: 'VIEWER',
396
- })
397
- ).rejects.toThrow('Pending invitation already exists for this email')
398
- })
399
- })
400
-
401
- describe('list', () => {
402
- it('should return pending invitations for account', async () => {
403
- const mockInvitations = [
404
- {
405
- id: 'inv-1',
406
- email: 'invited1@test.com',
407
- role: 'EDITOR',
408
- expiresAt: '2025-12-31T00:00:00Z',
409
- createdAt: '2025-01-01T00:00:00Z',
410
- invitedById: 'user-inviter-1',
411
- inviterName: 'John Inviter',
412
- },
413
- {
414
- id: 'inv-2',
415
- email: 'invited2@test.com',
416
- role: 'VIEWER',
417
- expiresAt: '2025-12-31T00:00:00Z',
418
- createdAt: '2025-01-02T00:00:00Z',
419
- invitedById: 'user-inviter-2',
420
- inviterName: 'Jane Inviter',
421
- },
422
- ]
423
-
424
- // Mock the select chain with innerJoin and orderBy
425
- mockDb.select = vi.fn().mockReturnValue({
426
- from: vi.fn().mockReturnValue({
427
- innerJoin: vi.fn().mockReturnValue({
428
- where: vi.fn().mockReturnValue({
429
- orderBy: vi.fn().mockResolvedValue(mockInvitations),
430
- }),
431
- }),
432
- }),
433
- })
434
-
435
- const result = await invitationsService.list(mockDb, ctx)
436
-
437
- expect(result).toHaveLength(2)
438
- expect(result[0]).toEqual({
439
- id: 'inv-1',
440
- email: 'invited1@test.com',
441
- role: 'EDITOR',
442
- invitedBy: { id: 'user-inviter-1', name: 'John Inviter' },
443
- expiresAt: '2025-12-31T00:00:00Z',
444
- createdAt: '2025-01-01T00:00:00Z',
445
- })
446
- expect(result[1]).toEqual({
447
- id: 'inv-2',
448
- email: 'invited2@test.com',
449
- role: 'VIEWER',
450
- invitedBy: { id: 'user-inviter-2', name: 'Jane Inviter' },
451
- expiresAt: '2025-12-31T00:00:00Z',
452
- createdAt: '2025-01-02T00:00:00Z',
453
- })
454
- })
455
-
456
- it('should return empty array when no pending invitations', async () => {
457
- mockDb.select = vi.fn().mockReturnValue({
458
- from: vi.fn().mockReturnValue({
459
- innerJoin: vi.fn().mockReturnValue({
460
- where: vi.fn().mockReturnValue({
461
- orderBy: vi.fn().mockResolvedValue([]),
462
- }),
463
- }),
464
- }),
465
- })
466
-
467
- const result = await invitationsService.list(mockDb, ctx)
468
-
469
- expect(result).toEqual([])
470
- })
471
- })
472
-
473
- describe('revoke', () => {
474
- it('should throw NotFoundError when invitation not found', async () => {
475
- // Mock select returning empty array (invitation not found)
476
- mockDb.select = vi.fn().mockReturnValue({
477
- from: vi.fn().mockReturnValue({
478
- where: vi.fn().mockReturnValue({
479
- limit: vi.fn().mockResolvedValue([]),
480
- }),
481
- }),
482
- })
483
-
484
- await expect(
485
- invitationsService.revoke(mockDb, ctx, 'non-existent-inv')
486
- ).rejects.toThrow(NotFoundError)
487
-
488
- await expect(
489
- invitationsService.revoke(mockDb, ctx, 'non-existent-inv')
490
- ).rejects.toThrow('Invitation not found')
491
- })
492
-
493
- it('should delete invitation successfully', async () => {
494
- const existingInvitation = {
495
- id: 'inv-to-delete',
496
- accountId: ctx.accountId,
497
- email: 'delete@test.com',
498
- role: 'VIEWER',
499
- acceptedAt: null,
500
- }
501
-
502
- // Mock select returning the invitation
503
- mockDb.select = vi.fn().mockReturnValue({
504
- from: vi.fn().mockReturnValue({
505
- where: vi.fn().mockReturnValue({
506
- limit: vi.fn().mockResolvedValue([existingInvitation]),
507
- }),
508
- }),
509
- })
510
-
511
- // Mock delete
512
- mockDb.delete = vi.fn().mockReturnValue({
513
- where: vi.fn().mockResolvedValue(),
514
- })
515
-
516
- await invitationsService.revoke(mockDb, ctx, 'inv-to-delete')
517
-
518
- expect(mockDb.delete).toHaveBeenCalled()
519
- })
520
- })
521
-
522
- describe('getByToken', () => {
523
- it('should return null when invitation not found', async () => {
524
- // Mock select returning empty array
525
- mockDb.select = vi.fn().mockReturnValue({
526
- from: vi.fn().mockReturnValue({
527
- innerJoin: vi.fn().mockReturnValue({
528
- where: vi.fn().mockReturnValue({
529
- limit: vi.fn().mockResolvedValue([]),
530
- }),
531
- }),
532
- }),
533
- })
534
-
535
- const result = await invitationsService.getByToken(mockDb, 'invalid-token')
536
-
537
- expect(result).toBeNull()
538
- })
539
-
540
- it('should return null when invitation already accepted', async () => {
541
- const acceptedInvitation = {
542
- id: 'inv-accepted',
543
- accountId: 'account-123',
544
- email: 'accepted@test.com',
545
- role: 'VIEWER',
546
- expiresAt: '2025-12-31T00:00:00Z',
547
- acceptedAt: '2025-01-01T00:00:00Z', // Already accepted
548
- accountName: 'Test Account',
549
- }
550
-
551
- mockDb.select = vi.fn().mockReturnValue({
552
- from: vi.fn().mockReturnValue({
553
- innerJoin: vi.fn().mockReturnValue({
554
- where: vi.fn().mockReturnValue({
555
- limit: vi.fn().mockResolvedValue([acceptedInvitation]),
556
- }),
557
- }),
558
- }),
559
- })
560
-
561
- const result = await invitationsService.getByToken(mockDb, 'some-token')
562
-
563
- expect(result).toBeNull()
564
- })
565
-
566
- it('should return null when invitation expired', async () => {
567
- const expiredInvitation = {
568
- id: 'inv-expired',
569
- accountId: 'account-123',
570
- email: 'expired@test.com',
571
- role: 'VIEWER',
572
- expiresAt: '2020-01-01T00:00:00Z', // Expired in the past
573
- acceptedAt: null,
574
- accountName: 'Test Account',
575
- }
576
-
577
- mockDb.select = vi.fn().mockReturnValue({
578
- from: vi.fn().mockReturnValue({
579
- innerJoin: vi.fn().mockReturnValue({
580
- where: vi.fn().mockReturnValue({
581
- limit: vi.fn().mockResolvedValue([expiredInvitation]),
582
- }),
583
- }),
584
- }),
585
- })
586
-
587
- const result = await invitationsService.getByToken(mockDb, 'expired-token')
588
-
589
- expect(result).toBeNull()
590
- })
591
-
592
- it('should return invitation when token valid', async () => {
593
- const validInvitation = {
594
- id: 'inv-valid',
595
- accountId: 'account-123',
596
- email: 'valid@test.com',
597
- role: 'EDITOR',
598
- expiresAt: '2099-12-31T00:00:00Z', // Far in the future
599
- acceptedAt: null,
600
- accountName: 'Valid Account',
601
- }
602
-
603
- mockDb.select = vi.fn().mockReturnValue({
604
- from: vi.fn().mockReturnValue({
605
- innerJoin: vi.fn().mockReturnValue({
606
- where: vi.fn().mockReturnValue({
607
- limit: vi.fn().mockResolvedValue([validInvitation]),
608
- }),
609
- }),
610
- }),
611
- })
612
-
613
- const result = await invitationsService.getByToken(mockDb, 'valid-token')
614
-
615
- expect(result).toEqual({
616
- id: 'inv-valid',
617
- accountId: 'account-123',
618
- email: 'valid@test.com',
619
- role: 'EDITOR',
620
- accountName: 'Valid Account',
621
- })
622
- })
623
- })
624
-
625
- describe('accept', () => {
626
- const authCtx: AuthEventContext = {
627
- transactionId: 'tx-accept-123',
628
- ip: '192.168.1.1',
629
- userAgent: 'TestAgent/2.0',
630
- }
631
-
632
- it('should throw NotFoundError when invitation not found', async () => {
633
- // Mock select returning empty array (invitation not found)
634
- mockDb.select = vi.fn().mockReturnValue({
635
- from: vi.fn().mockReturnValue({
636
- where: vi.fn().mockReturnValue({
637
- limit: vi.fn().mockResolvedValue([]),
638
- }),
639
- }),
640
- })
641
-
642
- await expect(
643
- invitationsService.accept(mockDb, 'non-existent-inv', 'user-123', authCtx)
644
- ).rejects.toThrow(NotFoundError)
645
-
646
- await expect(
647
- invitationsService.accept(mockDb, 'non-existent-inv', 'user-123', authCtx)
648
- ).rejects.toThrow('Invitation not found')
649
- })
650
-
651
- it('should add user to account and mark invitation as accepted', async () => {
652
- const invitation = {
653
- id: 'inv-to-accept',
654
- accountId: 'account-456',
655
- email: 'newuser@test.com',
656
- role: 'EDITOR',
657
- token: 'accept-token',
658
- acceptedAt: null,
659
- }
660
-
661
- // Mock select returning the invitation
662
- mockDb.select = vi.fn().mockReturnValue({
663
- from: vi.fn().mockReturnValue({
664
- where: vi.fn().mockReturnValue({
665
- limit: vi.fn().mockResolvedValue([invitation]),
666
- }),
667
- }),
668
- })
669
-
670
- // Mock insert for userAccounts
671
- mockDb.insert = vi.fn().mockReturnValue({
672
- values: vi.fn().mockResolvedValue(),
673
- })
674
-
675
- // Mock update for marking accepted
676
- mockDb.update = vi.fn().mockReturnValue({
677
- set: vi.fn().mockReturnValue({
678
- where: vi.fn().mockResolvedValue(),
679
- }),
680
- })
681
-
682
- await invitationsService.accept(mockDb, 'inv-to-accept', 'new-user-123', authCtx)
683
-
684
- // Verify insert was called to create user-account relationship
685
- expect(mockDb.insert).toHaveBeenCalled()
686
-
687
- // Verify update was called to mark invitation as accepted
688
- expect(mockDb.update).toHaveBeenCalled()
689
-
690
- // Verify logAuthEvent was called
691
- expect(logAuthEvent).toHaveBeenCalledWith(
692
- mockDb,
693
- authCtx,
694
- 'LOGIN',
695
- 'new-user-123',
696
- {
697
- invitationAccepted: true,
698
- accountId: 'account-456',
699
- role: 'EDITOR',
700
- }
701
- )
702
- })
703
- })
704
- })