@cosmicdrift/kumiko-bundled-features 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/package.json +90 -0
  2. package/src/audit/__tests__/audit.integration.ts +328 -0
  3. package/src/audit/constants.ts +7 -0
  4. package/src/audit/feature.ts +23 -0
  5. package/src/audit/handlers/list.query.ts +98 -0
  6. package/src/audit/index.ts +2 -0
  7. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +149 -0
  8. package/src/auth-email-password/__tests__/account-lockout.integration.ts +308 -0
  9. package/src/auth-email-password/__tests__/auth-claims.integration.ts +512 -0
  10. package/src/auth-email-password/__tests__/auth.integration.ts +610 -0
  11. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +67 -0
  12. package/src/auth-email-password/__tests__/email-templates.test.ts +106 -0
  13. package/src/auth-email-password/__tests__/email-verification.integration.ts +327 -0
  14. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +174 -0
  15. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +150 -0
  16. package/src/auth-email-password/__tests__/invite-flow.integration.ts +458 -0
  17. package/src/auth-email-password/__tests__/multi-roles.integration.ts +256 -0
  18. package/src/auth-email-password/__tests__/password-reset.integration.ts +346 -0
  19. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +144 -0
  20. package/src/auth-email-password/__tests__/seed-admin.integration.ts +176 -0
  21. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +310 -0
  22. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +101 -0
  23. package/src/auth-email-password/__tests__/signed-token.test.ts +78 -0
  24. package/src/auth-email-password/__tests__/signup-flow.integration.ts +259 -0
  25. package/src/auth-email-password/auth-user-row.ts +41 -0
  26. package/src/auth-email-password/constants.ts +101 -0
  27. package/src/auth-email-password/email-templates.ts +283 -0
  28. package/src/auth-email-password/feature.ts +140 -0
  29. package/src/auth-email-password/handlers/change-password.write.ts +58 -0
  30. package/src/auth-email-password/handlers/confirm-token-flow.ts +191 -0
  31. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +203 -0
  32. package/src/auth-email-password/handlers/invite-accept.write.ts +189 -0
  33. package/src/auth-email-password/handlers/invite-create.write.ts +145 -0
  34. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +192 -0
  35. package/src/auth-email-password/handlers/login.write.ts +208 -0
  36. package/src/auth-email-password/handlers/logout.write.ts +12 -0
  37. package/src/auth-email-password/handlers/request-email-verification.write.ts +29 -0
  38. package/src/auth-email-password/handlers/request-password-reset.write.ts +31 -0
  39. package/src/auth-email-password/handlers/reset-password.write.ts +61 -0
  40. package/src/auth-email-password/handlers/signup-confirm.write.ts +170 -0
  41. package/src/auth-email-password/handlers/signup-request.write.ts +104 -0
  42. package/src/auth-email-password/handlers/token-request-handler.ts +114 -0
  43. package/src/auth-email-password/handlers/verify-email.write.ts +62 -0
  44. package/src/auth-email-password/i18n.ts +211 -0
  45. package/src/auth-email-password/identity-v3-hash.ts +97 -0
  46. package/src/auth-email-password/index.ts +35 -0
  47. package/src/auth-email-password/invite-token-store.ts +92 -0
  48. package/src/auth-email-password/lockout-store.ts +118 -0
  49. package/src/auth-email-password/password-hashing.ts +43 -0
  50. package/src/auth-email-password/reset-token.ts +28 -0
  51. package/src/auth-email-password/seeding.ts +183 -0
  52. package/src/auth-email-password/signed-token.ts +85 -0
  53. package/src/auth-email-password/signup-token-store.ts +104 -0
  54. package/src/auth-email-password/stream-tenant.ts +31 -0
  55. package/src/auth-email-password/testing.ts +5 -0
  56. package/src/auth-email-password/token-burn-store.ts +57 -0
  57. package/src/auth-email-password/verification-token.ts +27 -0
  58. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +51 -0
  59. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +80 -0
  60. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +94 -0
  61. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +108 -0
  62. package/src/auth-email-password/web/__tests__/session-roles.test.ts +54 -0
  63. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +100 -0
  64. package/src/auth-email-password/web/__tests__/test-utils.tsx +73 -0
  65. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +55 -0
  66. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +59 -0
  67. package/src/auth-email-password/web/auth-client.ts +350 -0
  68. package/src/auth-email-password/web/auth-form-primitives.tsx +70 -0
  69. package/src/auth-email-password/web/auth-gate.tsx +33 -0
  70. package/src/auth-email-password/web/client-plugin.ts +48 -0
  71. package/src/auth-email-password/web/default-topbar-actions.tsx +47 -0
  72. package/src/auth-email-password/web/forgot-password-screen.tsx +110 -0
  73. package/src/auth-email-password/web/index.ts +56 -0
  74. package/src/auth-email-password/web/invite-accept-screen.tsx +220 -0
  75. package/src/auth-email-password/web/login-screen.tsx +150 -0
  76. package/src/auth-email-password/web/reset-password-screen.tsx +152 -0
  77. package/src/auth-email-password/web/session.tsx +171 -0
  78. package/src/auth-email-password/web/signup-complete-screen.tsx +150 -0
  79. package/src/auth-email-password/web/signup-screen.tsx +130 -0
  80. package/src/auth-email-password/web/tenant-switcher.tsx +116 -0
  81. package/src/auth-email-password/web/use-shell-user.ts +34 -0
  82. package/src/auth-email-password/web/user-menu.tsx +89 -0
  83. package/src/auth-email-password/web/verify-email-screen.tsx +102 -0
  84. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +568 -0
  85. package/src/billing-foundation/__tests__/feature.test.ts +110 -0
  86. package/src/billing-foundation/__tests__/webhook-handler.test.ts +199 -0
  87. package/src/billing-foundation/aggregate-id.ts +21 -0
  88. package/src/billing-foundation/constants.ts +70 -0
  89. package/src/billing-foundation/entities.ts +50 -0
  90. package/src/billing-foundation/events.ts +71 -0
  91. package/src/billing-foundation/feature.ts +122 -0
  92. package/src/billing-foundation/get-subscription-for-tenant.ts +39 -0
  93. package/src/billing-foundation/handlers/create-checkout-session.write.ts +79 -0
  94. package/src/billing-foundation/handlers/create-portal-session.write.ts +73 -0
  95. package/src/billing-foundation/handlers/list-subscriptions.query.ts +20 -0
  96. package/src/billing-foundation/handlers/process-event.write.ts +160 -0
  97. package/src/billing-foundation/index.ts +42 -0
  98. package/src/billing-foundation/projection.ts +135 -0
  99. package/src/billing-foundation/types.ts +157 -0
  100. package/src/billing-foundation/webhook-handler.ts +184 -0
  101. package/src/cap-counter/__tests__/cap-counter.integration.ts +566 -0
  102. package/src/cap-counter/__tests__/enforce-cap.test.ts +422 -0
  103. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +265 -0
  104. package/src/cap-counter/aggregate-id.ts +61 -0
  105. package/src/cap-counter/constants.ts +32 -0
  106. package/src/cap-counter/enforce-cap.ts +404 -0
  107. package/src/cap-counter/entity.ts +48 -0
  108. package/src/cap-counter/feature.ts +90 -0
  109. package/src/cap-counter/handlers/get-counter.query.ts +43 -0
  110. package/src/cap-counter/handlers/increment-rolling.write.ts +79 -0
  111. package/src/cap-counter/handlers/increment.write.ts +92 -0
  112. package/src/cap-counter/handlers/mark-soft-warned.write.ts +57 -0
  113. package/src/cap-counter/index.ts +34 -0
  114. package/src/cap-counter/with-cap-enforcement.ts +179 -0
  115. package/src/channel-email/email-channel.ts +48 -0
  116. package/src/channel-email/feature.ts +15 -0
  117. package/src/channel-email/index.ts +4 -0
  118. package/src/channel-email/smtp-transport.ts +65 -0
  119. package/src/channel-email/types.ts +34 -0
  120. package/src/channel-in-app/constants.ts +11 -0
  121. package/src/channel-in-app/feature.ts +30 -0
  122. package/src/channel-in-app/handlers/inbox.query.ts +28 -0
  123. package/src/channel-in-app/handlers/mark-all-read.write.ts +21 -0
  124. package/src/channel-in-app/handlers/mark-read.write.ts +32 -0
  125. package/src/channel-in-app/handlers/unread-count.query.ts +20 -0
  126. package/src/channel-in-app/in-app-channel.ts +44 -0
  127. package/src/channel-in-app/index.ts +4 -0
  128. package/src/channel-in-app/tables.ts +22 -0
  129. package/src/channel-push/feature.ts +15 -0
  130. package/src/channel-push/index.ts +3 -0
  131. package/src/channel-push/push-channel.ts +33 -0
  132. package/src/channel-push/types.ts +22 -0
  133. package/src/config/__tests__/app-overrides.test.ts +118 -0
  134. package/src/config/__tests__/config.integration.ts +1246 -0
  135. package/src/config/constants.ts +23 -0
  136. package/src/config/feature.ts +117 -0
  137. package/src/config/handlers/__tests__/prepare-config-write.test.ts +209 -0
  138. package/src/config/handlers/reset.write.ts +45 -0
  139. package/src/config/handlers/schema.query.ts +22 -0
  140. package/src/config/handlers/set.write.ts +93 -0
  141. package/src/config/handlers/values.query.ts +43 -0
  142. package/src/config/index.ts +15 -0
  143. package/src/config/resolver.ts +283 -0
  144. package/src/config/table.ts +35 -0
  145. package/src/config/write-helpers.ts +268 -0
  146. package/src/delivery/__tests__/delivery-events.integration.ts +166 -0
  147. package/src/delivery/__tests__/delivery.integration.ts +1405 -0
  148. package/src/delivery/constants.ts +33 -0
  149. package/src/delivery/delivery-service.ts +489 -0
  150. package/src/delivery/events.ts +18 -0
  151. package/src/delivery/feature.ts +70 -0
  152. package/src/delivery/handlers/log.query.ts +21 -0
  153. package/src/delivery/handlers/preferences.query.ts +18 -0
  154. package/src/delivery/handlers/set-preference.write.ts +28 -0
  155. package/src/delivery/index.ts +35 -0
  156. package/src/delivery/tables.ts +74 -0
  157. package/src/delivery/testing.ts +47 -0
  158. package/src/delivery/types.ts +71 -0
  159. package/src/delivery/unsubscribe.ts +99 -0
  160. package/src/delivery/upsert-preference.ts +145 -0
  161. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +687 -0
  162. package/src/feature-toggles/constants.ts +20 -0
  163. package/src/feature-toggles/events.ts +18 -0
  164. package/src/feature-toggles/feature.ts +98 -0
  165. package/src/feature-toggles/global-feature-state-table.ts +28 -0
  166. package/src/feature-toggles/handlers/list.query.ts +26 -0
  167. package/src/feature-toggles/handlers/registered.query.ts +56 -0
  168. package/src/feature-toggles/handlers/set.write.ts +158 -0
  169. package/src/feature-toggles/index.ts +9 -0
  170. package/src/feature-toggles/toggle-runtime.ts +73 -0
  171. package/src/file-foundation/__tests__/feature.test.ts +35 -0
  172. package/src/file-foundation/__tests__/file-foundation.integration.ts +235 -0
  173. package/src/file-foundation/feature.ts +123 -0
  174. package/src/file-foundation/index.ts +7 -0
  175. package/src/file-provider-inmemory/__tests__/feature.test.ts +35 -0
  176. package/src/file-provider-inmemory/feature.ts +73 -0
  177. package/src/file-provider-inmemory/index.ts +3 -0
  178. package/src/file-provider-s3/__tests__/feature.test.ts +54 -0
  179. package/src/file-provider-s3/feature.ts +169 -0
  180. package/src/file-provider-s3/index.ts +3 -0
  181. package/src/files-provider-s3/__tests__/env-helper.test.ts +161 -0
  182. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +134 -0
  183. package/src/files-provider-s3/__tests__/s3-provider.test.ts +36 -0
  184. package/src/files-provider-s3/env-helper.ts +49 -0
  185. package/src/files-provider-s3/index.ts +3 -0
  186. package/src/files-provider-s3/s3-provider.ts +114 -0
  187. package/src/foundation-shared/config-helpers.ts +67 -0
  188. package/src/foundation-shared/index.ts +4 -0
  189. package/src/jobs/__tests__/job-system-user.integration.ts +194 -0
  190. package/src/jobs/__tests__/jobs-events.integration.ts +143 -0
  191. package/src/jobs/__tests__/jobs-feature.integration.ts +342 -0
  192. package/src/jobs/constants.ts +21 -0
  193. package/src/jobs/events.ts +39 -0
  194. package/src/jobs/feature.ts +150 -0
  195. package/src/jobs/handlers/detail.query.ts +30 -0
  196. package/src/jobs/handlers/list.query.ts +36 -0
  197. package/src/jobs/handlers/retry.write.ts +69 -0
  198. package/src/jobs/handlers/trigger.write.ts +39 -0
  199. package/src/jobs/index.ts +5 -0
  200. package/src/jobs/job-run-logger.ts +213 -0
  201. package/src/jobs/job-run-table.ts +55 -0
  202. package/src/legal-pages/README.md +195 -0
  203. package/src/legal-pages/__tests__/legal-pages.integration.ts +361 -0
  204. package/src/legal-pages/constants.ts +36 -0
  205. package/src/legal-pages/feature.ts +187 -0
  206. package/src/legal-pages/index.ts +13 -0
  207. package/src/legal-pages/markdown.ts +69 -0
  208. package/src/mail-foundation/__tests__/feature.test.ts +46 -0
  209. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +247 -0
  210. package/src/mail-foundation/feature.ts +160 -0
  211. package/src/mail-foundation/index.ts +14 -0
  212. package/src/mail-transport-inmemory/__tests__/feature.test.ts +37 -0
  213. package/src/mail-transport-inmemory/feature.ts +90 -0
  214. package/src/mail-transport-inmemory/index.ts +3 -0
  215. package/src/mail-transport-smtp/__tests__/feature.test.ts +61 -0
  216. package/src/mail-transport-smtp/feature.ts +182 -0
  217. package/src/mail-transport-smtp/index.ts +3 -0
  218. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +84 -0
  219. package/src/rate-limiting/constants.ts +9 -0
  220. package/src/rate-limiting/feature.ts +16 -0
  221. package/src/rate-limiting/handlers/status.query.ts +52 -0
  222. package/src/rate-limiting/index.ts +2 -0
  223. package/src/renderer-simple/__tests__/simple-renderer.test.ts +97 -0
  224. package/src/renderer-simple/feature.ts +12 -0
  225. package/src/renderer-simple/index.ts +2 -0
  226. package/src/renderer-simple/simple-renderer.ts +72 -0
  227. package/src/secrets/__tests__/rotate.integration.ts +176 -0
  228. package/src/secrets/__tests__/secrets-events.integration.ts +125 -0
  229. package/src/secrets/__tests__/secrets.integration.ts +118 -0
  230. package/src/secrets/feature.ts +84 -0
  231. package/src/secrets/handlers/delete.write.ts +20 -0
  232. package/src/secrets/handlers/list.query.ts +38 -0
  233. package/src/secrets/handlers/rotate.job.ts +193 -0
  234. package/src/secrets/handlers/set.write.ts +50 -0
  235. package/src/secrets/index.ts +16 -0
  236. package/src/secrets/secrets-context.ts +296 -0
  237. package/src/secrets/table.ts +68 -0
  238. package/src/sessions/__tests__/cleanup.integration.ts +175 -0
  239. package/src/sessions/__tests__/password-auto-revoke.integration.ts +202 -0
  240. package/src/sessions/__tests__/sessions.integration.ts +472 -0
  241. package/src/sessions/__tests__/test-helpers.ts +66 -0
  242. package/src/sessions/constants.ts +43 -0
  243. package/src/sessions/feature.ts +84 -0
  244. package/src/sessions/handlers/cleanup.job.ts +109 -0
  245. package/src/sessions/handlers/list.query.ts +35 -0
  246. package/src/sessions/handlers/mine.query.ts +37 -0
  247. package/src/sessions/handlers/revoke-all-others.write.ts +42 -0
  248. package/src/sessions/handlers/revoke.write.ts +76 -0
  249. package/src/sessions/index.ts +17 -0
  250. package/src/sessions/schema/index.ts +5 -0
  251. package/src/sessions/schema/user-session.ts +67 -0
  252. package/src/sessions/session-callbacks.ts +110 -0
  253. package/src/sessions/testing.ts +42 -0
  254. package/src/subscription-mollie/__tests__/feature.test.ts +106 -0
  255. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +421 -0
  256. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +388 -0
  257. package/src/subscription-mollie/constants.ts +33 -0
  258. package/src/subscription-mollie/feature.ts +144 -0
  259. package/src/subscription-mollie/index.ts +13 -0
  260. package/src/subscription-mollie/plugin-methods.ts +79 -0
  261. package/src/subscription-mollie/verify-webhook.ts +244 -0
  262. package/src/subscription-stripe/__tests__/feature.test.ts +98 -0
  263. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +161 -0
  264. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +315 -0
  265. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +306 -0
  266. package/src/subscription-stripe/constants.ts +20 -0
  267. package/src/subscription-stripe/feature.ts +120 -0
  268. package/src/subscription-stripe/index.ts +14 -0
  269. package/src/subscription-stripe/plugin-methods.ts +91 -0
  270. package/src/subscription-stripe/verify-webhook.ts +235 -0
  271. package/src/tenant/__tests__/multi-tenant.integration.ts +278 -0
  272. package/src/tenant/__tests__/seed-testing.integration.ts +229 -0
  273. package/src/tenant/__tests__/tenant.integration.ts +347 -0
  274. package/src/tenant/command-schemas.ts +37 -0
  275. package/src/tenant/constants.ts +37 -0
  276. package/src/tenant/feature.ts +109 -0
  277. package/src/tenant/handlers/active-tenant-ids.query.ts +19 -0
  278. package/src/tenant/handlers/add-member.write.ts +53 -0
  279. package/src/tenant/handlers/cancel-invitation.write.ts +87 -0
  280. package/src/tenant/handlers/create.write.ts +21 -0
  281. package/src/tenant/handlers/disable.write.ts +18 -0
  282. package/src/tenant/handlers/invitations.query.ts +31 -0
  283. package/src/tenant/handlers/list.query.ts +17 -0
  284. package/src/tenant/handlers/me.query.ts +17 -0
  285. package/src/tenant/handlers/members.query.ts +22 -0
  286. package/src/tenant/handlers/memberships.query.ts +24 -0
  287. package/src/tenant/handlers/remove-member.write.ts +40 -0
  288. package/src/tenant/handlers/resolve-user-ids.query.ts +43 -0
  289. package/src/tenant/handlers/update-member-roles.write.ts +54 -0
  290. package/src/tenant/handlers/update.write.ts +20 -0
  291. package/src/tenant/index.ts +12 -0
  292. package/src/tenant/invitation-table.ts +93 -0
  293. package/src/tenant/membership-table.ts +35 -0
  294. package/src/tenant/schema/index.ts +5 -0
  295. package/src/tenant/schema/tenant.ts +27 -0
  296. package/src/tenant/seeding.ts +155 -0
  297. package/src/tenant/testing.ts +8 -0
  298. package/src/text-content/README.md +190 -0
  299. package/src/text-content/__tests__/text-content.integration.ts +415 -0
  300. package/src/text-content/api.ts +92 -0
  301. package/src/text-content/constants.ts +19 -0
  302. package/src/text-content/feature.ts +29 -0
  303. package/src/text-content/handlers/by-slug.query.ts +55 -0
  304. package/src/text-content/handlers/set.write.ts +118 -0
  305. package/src/text-content/index.ts +14 -0
  306. package/src/text-content/seeding.ts +91 -0
  307. package/src/text-content/table.ts +45 -0
  308. package/src/tier-engine/__tests__/compose-app.test.ts +182 -0
  309. package/src/tier-engine/__tests__/drift.test.ts +42 -0
  310. package/src/tier-engine/__tests__/tier-engine.integration.ts +241 -0
  311. package/src/tier-engine/aggregate-id.ts +27 -0
  312. package/src/tier-engine/compose-app.ts +150 -0
  313. package/src/tier-engine/constants.ts +15 -0
  314. package/src/tier-engine/entity.ts +30 -0
  315. package/src/tier-engine/feature.ts +72 -0
  316. package/src/tier-engine/handlers/active-tier.query.ts +23 -0
  317. package/src/tier-engine/index.ts +22 -0
  318. package/src/user/__tests__/seed-testing.integration.ts +127 -0
  319. package/src/user/__tests__/user.integration.ts +198 -0
  320. package/src/user/command-schemas.ts +15 -0
  321. package/src/user/constants.ts +23 -0
  322. package/src/user/feature.ts +32 -0
  323. package/src/user/handlers/create.write.ts +54 -0
  324. package/src/user/handlers/detail.query.ts +9 -0
  325. package/src/user/handlers/find-for-auth.query.ts +38 -0
  326. package/src/user/handlers/list.query.ts +8 -0
  327. package/src/user/handlers/me.query.ts +15 -0
  328. package/src/user/handlers/update.write.ts +54 -0
  329. package/src/user/index.ts +4 -0
  330. package/src/user/schema/index.ts +5 -0
  331. package/src/user/schema/user.ts +69 -0
  332. package/src/user/seeding.ts +93 -0
  333. package/src/user/testing.ts +5 -0
@@ -0,0 +1,54 @@
1
+ // computeActiveRoles — client-side merge muss byte-identisch zum server-
2
+ // side merge in auth-routes.ts (switch-tenant) + login.write.ts sein.
3
+ // Sonst sieht client andere roles als server → role-gating divergiert
4
+ // (entweder UI zeigt was nicht erlaubt ist, oder umgekehrt).
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import type { CurrentUserProfile, TenantSummary } from "../auth-client";
8
+ import { computeActiveRoles } from "../session";
9
+
10
+ const user = (globalRoles: readonly string[]): CurrentUserProfile => ({
11
+ id: "u1",
12
+ email: "u@e.com",
13
+ displayName: "U",
14
+ globalRoles,
15
+ });
16
+
17
+ const t = (id: string, roles: readonly string[]): TenantSummary => ({ tenantId: id, roles });
18
+
19
+ describe("computeActiveRoles", () => {
20
+ test("user=null → []", () => {
21
+ expect(computeActiveRoles(null, "tenant-1", [t("tenant-1", ["Admin"])])).toEqual([]);
22
+ });
23
+
24
+ test("globalRoles + active tenant membership → merged + dedupe", () => {
25
+ const result = computeActiveRoles(user(["SystemAdmin"]), "tenant-1", [
26
+ t("tenant-1", ["Admin"]),
27
+ ]);
28
+ expect([...result].sort()).toEqual(["Admin", "SystemAdmin"]);
29
+ });
30
+
31
+ test("dedupe: gleiche Rolle in global + membership → einmal", () => {
32
+ const result = computeActiveRoles(user(["Admin", "SystemAdmin"]), "tenant-1", [
33
+ t("tenant-1", ["Admin", "User"]),
34
+ ]);
35
+ expect([...result].sort()).toEqual(["Admin", "SystemAdmin", "User"]);
36
+ });
37
+
38
+ test("kein activeTenantId → nur globalRoles", () => {
39
+ const result = computeActiveRoles(user(["SystemAdmin"]), null, [t("tenant-1", ["Admin"])]);
40
+ expect(result).toEqual(["SystemAdmin"]);
41
+ });
42
+
43
+ test("activeTenantId zeigt auf nicht-vorhandenen tenant → nur globalRoles", () => {
44
+ const result = computeActiveRoles(user(["SystemAdmin"]), "tenant-2", [
45
+ t("tenant-1", ["Admin"]),
46
+ ]);
47
+ expect(result).toEqual(["SystemAdmin"]);
48
+ });
49
+
50
+ test("OHNE globalRoles + active membership → nur membership-roles", () => {
51
+ const result = computeActiveRoles(user([]), "tenant-1", [t("tenant-1", ["Admin"])]);
52
+ expect(result).toEqual(["Admin"]);
53
+ });
54
+ });
@@ -0,0 +1,100 @@
1
+ // @vitest-environment jsdom
2
+ import { screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, test } from "vitest";
5
+ import { TenantSwitcher } from "../tenant-switcher";
6
+ import { makeSessionApi, renderWithProviders } from "./test-utils";
7
+
8
+ // Radix-DropdownMenu reagiert auf pointerdown, nicht auf click — daher
9
+ // userEvent statt fireEvent.
10
+
11
+ describe("TenantSwitcher", () => {
12
+ test("renders nothing when user is null", () => {
13
+ const session = makeSessionApi({ status: "unauthenticated", user: null });
14
+ const { container } = renderWithProviders(<TenantSwitcher />, { session });
15
+ expect(container.firstChild).toBeNull();
16
+ });
17
+
18
+ test("renders nothing when user has only one tenant", () => {
19
+ const session = makeSessionApi({
20
+ tenants: [{ tenantId: "t1", roles: ["Admin"] }],
21
+ });
22
+ const { container } = renderWithProviders(<TenantSwitcher />, { session });
23
+ expect(container.firstChild).toBeNull();
24
+ });
25
+
26
+ test("renders trigger when user has multiple tenants", () => {
27
+ const session = makeSessionApi({
28
+ activeTenantId: "tenant-a",
29
+ tenants: [
30
+ { tenantId: "tenant-a", roles: ["Admin"] },
31
+ { tenantId: "tenant-b", roles: ["User"] },
32
+ ],
33
+ });
34
+ renderWithProviders(<TenantSwitcher tenantName={(id) => `Tenant ${id}`} />, { session });
35
+ // tenantName-Resolver liefert "Tenant tenant-a" als Trigger-Label
36
+ expect(screen.getByText("Tenant tenant-a")).toBeTruthy();
37
+ });
38
+
39
+ test("opens dropdown showing all memberships with roles", async () => {
40
+ const user = userEvent.setup();
41
+ const session = makeSessionApi({
42
+ activeTenantId: "tenant-a",
43
+ tenants: [
44
+ { tenantId: "tenant-a", roles: ["Admin"] },
45
+ { tenantId: "tenant-b", roles: ["User", "Billing"] },
46
+ ],
47
+ });
48
+ renderWithProviders(<TenantSwitcher tenantName={(id) => `Tenant ${id}`} />, {
49
+ session,
50
+ });
51
+ await user.click(screen.getByRole("button", { name: /Tenant tenant-a/ }));
52
+ // Trigger zeigt aktiven Tenant ("Tenant tenant-a") + Dropdown-Items
53
+ // listen ALLE Tenants — nutze getAllByText um Mehrdeutigkeit
54
+ // explizit zu erlauben, dann Roles-Strings als eindeutigen Anker.
55
+ expect(screen.getAllByText("Tenant tenant-a").length).toBeGreaterThan(0);
56
+ expect(screen.getByText("Tenant tenant-b")).toBeTruthy();
57
+ expect(screen.getByText("Admin")).toBeTruthy();
58
+ expect(screen.getByText("User, Billing")).toBeTruthy();
59
+ });
60
+
61
+ test("clicking a tenant triggers switchTenant", async () => {
62
+ const user = userEvent.setup();
63
+ const session = makeSessionApi({
64
+ activeTenantId: "tenant-a",
65
+ tenants: [
66
+ { tenantId: "tenant-a", roles: ["Admin"] },
67
+ { tenantId: "tenant-b", roles: ["User"] },
68
+ ],
69
+ });
70
+ renderWithProviders(<TenantSwitcher tenantName={(id) => `Tenant ${id}`} />, {
71
+ session,
72
+ });
73
+ await user.click(screen.getByRole("button", { name: /Tenant tenant-a/ }));
74
+ await user.click(screen.getByText("Tenant tenant-b"));
75
+ expect(session.switchTenant).toHaveBeenCalledWith("tenant-b");
76
+ });
77
+
78
+ test("clicking the active tenant is a no-op (closes menu, no switch call)", async () => {
79
+ const user = userEvent.setup();
80
+ const session = makeSessionApi({
81
+ activeTenantId: "tenant-a",
82
+ tenants: [
83
+ { tenantId: "tenant-a", roles: ["Admin"] },
84
+ { tenantId: "tenant-b", roles: ["User"] },
85
+ ],
86
+ });
87
+ renderWithProviders(<TenantSwitcher tenantName={(id) => `Tenant ${id}`} />, {
88
+ session,
89
+ });
90
+ await user.click(screen.getByRole("button", { name: /Tenant tenant-a/ }));
91
+ // Im Dropdown gibt's einen menuitemcheckbox für tenant-a (Radix-Role
92
+ // bei CheckboxItem) — nicht den Trigger erwischen, sondern den im
93
+ // role="menu".
94
+ const items = screen.getAllByRole("menuitemcheckbox");
95
+ const activeItem = items.find((el) => el.textContent?.includes("Tenant tenant-a"));
96
+ expect(activeItem).toBeDefined();
97
+ if (activeItem) await user.click(activeItem);
98
+ expect(session.switchTenant).not.toHaveBeenCalled();
99
+ });
100
+ });
@@ -0,0 +1,73 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // Shared test setup für die Web-UI-Components. Mountet das Minimum
4
+ // an Provider-Tree den die Components zur Laufzeit voraussetzen
5
+ // (LocaleProvider mit Bundle, SessionContext mit injizierbarem Wert).
6
+
7
+ import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
8
+ import {
9
+ createStaticLocaleResolver,
10
+ LocaleProvider,
11
+ PrimitivesProvider,
12
+ } from "@cosmicdrift/kumiko-renderer";
13
+ import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
14
+ import { render as _render, type RenderResult } from "@testing-library/react";
15
+ import type { ReactElement } from "react";
16
+ import { vi } from "vitest";
17
+ import { defaultTranslations } from "../../i18n";
18
+ import type { SessionApi, SessionState } from "../session";
19
+ import { SessionContext } from "../session";
20
+
21
+ // Stateless Resolver — module-level cached, weil renderWithProviders
22
+ // ihn pro Mount sonst neu konstruiert (~0.5ms × N Tests). Tests die
23
+ // einen *anderen* Locale brauchen, übergeben ihren eigenen Resolver
24
+ // über options.resolver.
25
+ const sharedDeResolver = createStaticLocaleResolver({ locale: "de" });
26
+
27
+ export type MakeSessionApiOptions = Partial<SessionState> & {
28
+ readonly login?: SessionApi["login"];
29
+ readonly logout?: SessionApi["logout"];
30
+ readonly switchTenant?: SessionApi["switchTenant"];
31
+ };
32
+
33
+ export function makeSessionApi(overrides: MakeSessionApiOptions = {}): SessionApi {
34
+ const { login, logout, switchTenant, ...stateOverrides } = overrides;
35
+ const base: SessionState = {
36
+ status: "authenticated",
37
+ user: {
38
+ id: "test-user",
39
+ email: "user@example.com",
40
+ displayName: "Test User",
41
+ globalRoles: [],
42
+ },
43
+ activeTenantId: "tenant-1",
44
+ tenants: [{ tenantId: "tenant-1", roles: ["Admin"] }],
45
+ roles: ["Admin"],
46
+ ...stateOverrides,
47
+ };
48
+ return {
49
+ ...base,
50
+ login: login ?? vi.fn<SessionApi["login"]>(async () => ({ ok: true })),
51
+ logout: logout ?? vi.fn<SessionApi["logout"]>(async () => {}),
52
+ switchTenant: switchTenant ?? vi.fn<SessionApi["switchTenant"]>(async () => {}),
53
+ };
54
+ }
55
+
56
+ export function renderWithProviders(
57
+ ui: ReactElement,
58
+ options: {
59
+ readonly resolver?: LocaleResolver;
60
+ readonly session?: SessionApi;
61
+ } = {},
62
+ ): RenderResult & { readonly session: SessionApi } {
63
+ const resolver = options.resolver ?? sharedDeResolver;
64
+ const session = options.session ?? makeSessionApi();
65
+ const result = _render(
66
+ <PrimitivesProvider value={defaultPrimitives}>
67
+ <LocaleProvider resolver={resolver} fallbackBundles={[defaultTranslations]}>
68
+ <SessionContext.Provider value={session}>{ui}</SessionContext.Provider>
69
+ </LocaleProvider>
70
+ </PrimitivesProvider>,
71
+ );
72
+ return { ...result, session };
73
+ }
@@ -0,0 +1,55 @@
1
+ // @vitest-environment jsdom
2
+ import { screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { describe, expect, test } from "vitest";
5
+ import { UserMenu } from "../user-menu";
6
+ import { makeSessionApi, renderWithProviders } from "./test-utils";
7
+
8
+ // Radix-DropdownMenu reagiert auf pointerdown — fireEvent.click greift
9
+ // dort nicht. userEvent simuliert die volle Pointer-Sequenz und Radix
10
+ // öffnet sauber.
11
+
12
+ describe("UserMenu", () => {
13
+ test("renders nothing when user is null", () => {
14
+ const session = makeSessionApi({ status: "unauthenticated", user: null });
15
+ const { container } = renderWithProviders(<UserMenu />, { session });
16
+ expect(container.firstChild).toBeNull();
17
+ });
18
+
19
+ test("shows displayName + initials when authenticated", () => {
20
+ const session = makeSessionApi({
21
+ user: { id: "u1", email: "alice@example.com", displayName: "Alice Wonder", globalRoles: [] },
22
+ });
23
+ renderWithProviders(<UserMenu />, { session });
24
+ // Avatar = "AW", Display-Name "Alice Wonder"
25
+ expect(screen.getByText("AW")).toBeTruthy();
26
+ expect(screen.getByText("Alice Wonder")).toBeTruthy();
27
+ });
28
+
29
+ test("falls back to email-based initials when displayName empty", () => {
30
+ const session = makeSessionApi({
31
+ user: { id: "u1", email: "bob@example.com", displayName: "", globalRoles: [] },
32
+ });
33
+ renderWithProviders(<UserMenu />, { session });
34
+ // Trim "" → leerer displayName → fallback auf email → erste 2 Chars
35
+ expect(screen.getByText("BO")).toBeTruthy();
36
+ });
37
+
38
+ test("opens dropdown on click and shows logout button", async () => {
39
+ const user = userEvent.setup();
40
+ const session = makeSessionApi();
41
+ renderWithProviders(<UserMenu />, { session });
42
+ await user.click(screen.getByRole("button", { name: /Test User/ }));
43
+ expect(screen.getByText("Abmelden")).toBeTruthy();
44
+ expect(screen.getByText("user@example.com")).toBeTruthy();
45
+ });
46
+
47
+ test("logout-click triggers session.logout", async () => {
48
+ const user = userEvent.setup();
49
+ const session = makeSessionApi();
50
+ renderWithProviders(<UserMenu />, { session });
51
+ await user.click(screen.getByRole("button", { name: /Test User/ }));
52
+ await user.click(screen.getByText("Abmelden"));
53
+ expect(session.logout).toHaveBeenCalledOnce();
54
+ });
55
+ });
@@ -0,0 +1,59 @@
1
+ // @vitest-environment jsdom
2
+ import { screen, waitFor } from "@testing-library/react";
3
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
4
+ import { VerifyEmailScreen } from "../verify-email-screen";
5
+ import { renderWithProviders } from "./test-utils";
6
+
7
+ beforeEach(() => {
8
+ vi.stubGlobal(
9
+ "fetch",
10
+ vi.fn(async () => new Response(null, { status: 200 })),
11
+ );
12
+ });
13
+ afterEach(() => {
14
+ vi.unstubAllGlobals();
15
+ });
16
+
17
+ describe("VerifyEmailScreen", () => {
18
+ test("ohne Token → missing-token-Page", () => {
19
+ renderWithProviders(<VerifyEmailScreen />);
20
+ expect(screen.getByText(/enthält keinen Token/i)).toBeTruthy();
21
+ });
22
+
23
+ test("mit Token + 200 → success-state nach auto-submit", async () => {
24
+ const fetchMock = vi.fn(async () => new Response(null, { status: 200 }));
25
+ vi.stubGlobal("fetch", fetchMock);
26
+
27
+ renderWithProviders(<VerifyEmailScreen token="t-abc" />);
28
+
29
+ await waitFor(() => {
30
+ expect(fetchMock).toHaveBeenCalledWith(
31
+ "/api/auth/verify-email",
32
+ expect.objectContaining({
33
+ method: "POST",
34
+ body: JSON.stringify({ token: "t-abc" }),
35
+ }),
36
+ );
37
+ expect(screen.getByText("E-Mail bestätigt")).toBeTruthy();
38
+ });
39
+ });
40
+
41
+ test("mit Token + 422 → error-state", async () => {
42
+ const errBody = JSON.stringify({
43
+ error: {
44
+ code: "invalid_verification_token",
45
+ details: { reason: "invalid_verification_token" },
46
+ },
47
+ });
48
+ vi.stubGlobal(
49
+ "fetch",
50
+ vi.fn(async () => new Response(errBody, { status: 422 })),
51
+ );
52
+
53
+ renderWithProviders(<VerifyEmailScreen token="bad" />);
54
+
55
+ await waitFor(() => {
56
+ expect(screen.getByText("Bestätigung fehlgeschlagen")).toBeTruthy();
57
+ });
58
+ });
59
+ });