authnz_eleven 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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +384 -0
  5. data/lib/authnz_eleven/identity.rb +210 -0
  6. data/lib/authnz_eleven/principal.rb +201 -0
  7. data/lib/authnz_eleven/principals.rb +111 -0
  8. data/lib/authnz_eleven/version.rb +5 -0
  9. data/lib/authnz_eleven/wizard.rb +570 -0
  10. data/lib/authnz_eleven.rb +15 -0
  11. data/lib/generators/authnz_eleven/USAGE +26 -0
  12. data/lib/generators/authnz_eleven/authnz_eleven_generator.rb +3040 -0
  13. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.LICENSE +22 -0
  14. data/lib/generators/authnz_eleven/templates/config/forbidden_usernames.txt +558 -0
  15. data/lib/generators/authnz_eleven/templates/config/initializers/authentication.rb.tt +275 -0
  16. data/lib/generators/authnz_eleven/templates/config/initializers/omniauth.rb.tt +9 -0
  17. data/lib/generators/authnz_eleven/templates/config/initializers/team_slug.rb.tt +24 -0
  18. data/lib/generators/authnz_eleven/templates/config/initializers/webauthn.rb.tt +14 -0
  19. data/lib/generators/authnz_eleven/templates/config/locales/authentication.en.yml.tt +11 -0
  20. data/lib/generators/authnz_eleven/templates/controllers/active_teams_controller.rb.tt +6 -0
  21. data/lib/generators/authnz_eleven/templates/controllers/admin/dashboard_controller.rb.tt +4 -0
  22. data/lib/generators/authnz_eleven/templates/controllers/admin/sessions_controller.rb.tt +17 -0
  23. data/lib/generators/authnz_eleven/templates/controllers/admin/users_controller.rb.tt +91 -0
  24. data/lib/generators/authnz_eleven/templates/controllers/admin_controller.rb.tt +6 -0
  25. data/lib/generators/authnz_eleven/templates/controllers/base_controller.rb.tt +11 -0
  26. data/lib/generators/authnz_eleven/templates/controllers/concerns/api_token_authentication.rb.tt +21 -0
  27. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/easy_dev_login.rb.tt +47 -0
  28. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/registering.rb.tt +25 -0
  29. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/sudoable.rb.tt +117 -0
  30. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication/webauthn_ceremony.rb.tt +51 -0
  31. data/lib/generators/authnz_eleven/templates/controllers/concerns/authentication.rb.tt +421 -0
  32. data/lib/generators/authnz_eleven/templates/controllers/concerns/authorization.rb.tt +48 -0
  33. data/lib/generators/authnz_eleven/templates/controllers/concerns/captchable.rb.tt +11 -0
  34. data/lib/generators/authnz_eleven/templates/controllers/impersonations_controller.rb.tt +8 -0
  35. data/lib/generators/authnz_eleven/templates/controllers/invitations_controller.rb.tt +65 -0
  36. data/lib/generators/authnz_eleven/templates/controllers/settings/api_tokens_controller.rb.tt +54 -0
  37. data/lib/generators/authnz_eleven/templates/controllers/settings/authentications/events_controller.rb.tt +5 -0
  38. data/lib/generators/authnz_eleven/templates/controllers/settings/dashboard_controller.rb.tt +4 -0
  39. data/lib/generators/authnz_eleven/templates/controllers/settings/email_verifications_controller.rb.tt +15 -0
  40. data/lib/generators/authnz_eleven/templates/controllers/settings/emails_controller.rb.tt +93 -0
  41. data/lib/generators/authnz_eleven/templates/controllers/settings/invitations_controller.rb.tt +61 -0
  42. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/authenticators_controller.rb.tt +87 -0
  43. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/recovery_codes_controller.rb.tt +27 -0
  44. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/security_keys_controller.rb.tt +86 -0
  45. data/lib/generators/authnz_eleven/templates/controllers/settings/multi_factor_authentication/sms_controller.rb.tt +84 -0
  46. data/lib/generators/authnz_eleven/templates/controllers/settings/omniauth_identities_controller.rb.tt +27 -0
  47. data/lib/generators/authnz_eleven/templates/controllers/settings/passkeys_controller.rb.tt +70 -0
  48. data/lib/generators/authnz_eleven/templates/controllers/settings/passwords_controller.rb.tt +74 -0
  49. data/lib/generators/authnz_eleven/templates/controllers/settings/phone_verifications_controller.rb.tt +39 -0
  50. data/lib/generators/authnz_eleven/templates/controllers/settings/phones_controller.rb.tt +88 -0
  51. data/lib/generators/authnz_eleven/templates/controllers/settings/sessions_controller.rb.tt +29 -0
  52. data/lib/generators/authnz_eleven/templates/controllers/settings/users_controller.rb.tt +42 -0
  53. data/lib/generators/authnz_eleven/templates/controllers/settings_controller.rb.tt +5 -0
  54. data/lib/generators/authnz_eleven/templates/controllers/teams_controller.rb.tt +38 -0
  55. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passkeys_controller.rb.tt +36 -0
  56. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments/passwords_controller.rb.tt +28 -0
  57. data/lib/generators/authnz_eleven/templates/controllers/users/credential_enrollments_controller.rb.tt +14 -0
  58. data/lib/generators/authnz_eleven/templates/controllers/users/easy_dev_logins_controller.rb.tt +19 -0
  59. data/lib/generators/authnz_eleven/templates/controllers/users/email_verifications_controller.rb.tt +36 -0
  60. data/lib/generators/authnz_eleven/templates/controllers/users/magic_link_controller.rb.tt +53 -0
  61. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/recovery_codes_controller.rb.tt +30 -0
  62. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/security_keys_controller.rb.tt +34 -0
  63. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/sms_controller.rb.tt +45 -0
  64. data/lib/generators/authnz_eleven/templates/controllers/users/multi_factor_authentication/challenge/totps_controller.rb.tt +42 -0
  65. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth/registrations_controller.rb.tt +26 -0
  66. data/lib/generators/authnz_eleven/templates/controllers/users/omniauth_controller.rb.tt +139 -0
  67. data/lib/generators/authnz_eleven/templates/controllers/users/passkeys/sessions_controller.rb.tt +18 -0
  68. data/lib/generators/authnz_eleven/templates/controllers/users/password_resets_controller.rb.tt +64 -0
  69. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/email_verifications_controller.rb.tt +53 -0
  70. data/lib/generators/authnz_eleven/templates/controllers/users/registrations/phone_verifications_controller.rb.tt +37 -0
  71. data/lib/generators/authnz_eleven/templates/controllers/users/registrations_controller.rb.tt +112 -0
  72. data/lib/generators/authnz_eleven/templates/controllers/users/sessions_controller.rb.tt +52 -0
  73. data/lib/generators/authnz_eleven/templates/controllers/users/sms_sessions_controller.rb.tt +53 -0
  74. data/lib/generators/authnz_eleven/templates/controllers/users/sudos_controller.rb.tt +42 -0
  75. data/lib/generators/authnz_eleven/templates/erb/admin/dashboard/index.html.erb.tt +9 -0
  76. data/lib/generators/authnz_eleven/templates/erb/admin/sessions/index.html.erb.tt +27 -0
  77. data/lib/generators/authnz_eleven/templates/erb/admin/users/index.html.erb.tt +32 -0
  78. data/lib/generators/authnz_eleven/templates/erb/admin/users/show.html.erb.tt +70 -0
  79. data/lib/generators/authnz_eleven/templates/erb/invitations/show.html.erb.tt +9 -0
  80. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/create.html.erb.tt +7 -0
  81. data/lib/generators/authnz_eleven/templates/erb/settings/api_tokens/index.html.erb.tt +29 -0
  82. data/lib/generators/authnz_eleven/templates/erb/settings/authentications/events/index.html.erb.tt +18 -0
  83. data/lib/generators/authnz_eleven/templates/erb/settings/dashboard/index.html.erb.tt +75 -0
  84. data/lib/generators/authnz_eleven/templates/erb/settings/emails/edit.html.erb.tt +68 -0
  85. data/lib/generators/authnz_eleven/templates/erb/settings/invitations/new.html.erb.tt +31 -0
  86. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/authenticators/new.html.erb.tt +57 -0
  87. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/create.html.erb.tt +14 -0
  88. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/recovery_codes/index.html.erb.tt +13 -0
  89. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/edit.html.erb.tt +14 -0
  90. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/index.html.erb.tt +20 -0
  91. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/security_keys/new.html.erb.tt +17 -0
  92. data/lib/generators/authnz_eleven/templates/erb/settings/multi_factor_authentication/sms/new.html.erb.tt +35 -0
  93. data/lib/generators/authnz_eleven/templates/erb/settings/omniauth_identities/index.html.erb.tt +28 -0
  94. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/edit.html.erb.tt +14 -0
  95. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/index.html.erb.tt +21 -0
  96. data/lib/generators/authnz_eleven/templates/erb/settings/passkeys/new.html.erb.tt +13 -0
  97. data/lib/generators/authnz_eleven/templates/erb/settings/passwords/edit.html.erb.tt +68 -0
  98. data/lib/generators/authnz_eleven/templates/erb/settings/phone_verifications/new.html.erb.tt +22 -0
  99. data/lib/generators/authnz_eleven/templates/erb/settings/phones/edit.html.erb.tt +58 -0
  100. data/lib/generators/authnz_eleven/templates/erb/settings/sessions/index.html.erb.tt +29 -0
  101. data/lib/generators/authnz_eleven/templates/erb/settings/users/show.html.erb.tt +28 -0
  102. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_nav.html.erb.tt +24 -0
  103. data/lib/generators/authnz_eleven/templates/erb/shared/_authnz_eleven_sudo_challenge.html.erb.tt +43 -0
  104. data/lib/generators/authnz_eleven/templates/erb/shared/_captcha.html.erb.tt +8 -0
  105. data/lib/generators/authnz_eleven/templates/erb/teams/index.html.erb.tt +34 -0
  106. data/lib/generators/authnz_eleven/templates/erb/teams/new.html.erb.tt +22 -0
  107. data/lib/generators/authnz_eleven/templates/erb/teams/show.html.erb.tt +5 -0
  108. data/lib/generators/authnz_eleven/templates/erb/user_mailer/email_change_verification.html.erb.tt +7 -0
  109. data/lib/generators/authnz_eleven/templates/erb/user_mailer/invitation.html.erb.tt +11 -0
  110. data/lib/generators/authnz_eleven/templates/erb/user_mailer/magic_link.html.erb.tt +5 -0
  111. data/lib/generators/authnz_eleven/templates/erb/user_mailer/password_reset.html.erb.tt +6 -0
  112. data/lib/generators/authnz_eleven/templates/erb/user_mailer/registration_email_verification.html.erb.tt +7 -0
  113. data/lib/generators/authnz_eleven/templates/erb/user_mailer/security_notification.html.erb.tt +15 -0
  114. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passkeys/new.html.erb.tt +24 -0
  115. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/passwords/new.html.erb.tt +21 -0
  116. data/lib/generators/authnz_eleven/templates/erb/users/credential_enrollments/show.html.erb.tt +21 -0
  117. data/lib/generators/authnz_eleven/templates/erb/users/email_verifications/show.html.erb.tt +9 -0
  118. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/edit.html.erb.tt +9 -0
  119. data/lib/generators/authnz_eleven/templates/erb/users/magic_link/new.html.erb.tt +22 -0
  120. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/recovery_codes/new.html.erb.tt +20 -0
  121. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/security_keys/new.html.erb.tt +17 -0
  122. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/sms/new.html.erb.tt +25 -0
  123. data/lib/generators/authnz_eleven/templates/erb/users/multi_factor_authentication/challenge/totps/new.html.erb.tt +21 -0
  124. data/lib/generators/authnz_eleven/templates/erb/users/omniauth/registrations/new.html.erb.tt +28 -0
  125. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/edit.html.erb.tt +26 -0
  126. data/lib/generators/authnz_eleven/templates/erb/users/password_resets/new.html.erb.tt +20 -0
  127. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/new.html.erb.tt +11 -0
  128. data/lib/generators/authnz_eleven/templates/erb/users/registrations/email_verifications/show.html.erb.tt +34 -0
  129. data/lib/generators/authnz_eleven/templates/erb/users/registrations/new.html.erb.tt +87 -0
  130. data/lib/generators/authnz_eleven/templates/erb/users/registrations/phone_verifications/new.html.erb.tt +46 -0
  131. data/lib/generators/authnz_eleven/templates/erb/users/sessions/new.html.erb.tt +88 -0
  132. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/edit.html.erb.tt +30 -0
  133. data/lib/generators/authnz_eleven/templates/erb/users/sms_sessions/new.html.erb.tt +22 -0
  134. data/lib/generators/authnz_eleven/templates/erb/users/sudos/new.html.erb.tt +9 -0
  135. data/lib/generators/authnz_eleven/templates/javascript/controllers/webauthn_controller.js.tt +142 -0
  136. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_deferred_job.rb.tt +9 -0
  137. data/lib/generators/authnz_eleven/templates/jobs/authnz_eleven_mail_delivery_job.rb.tt +3 -0
  138. data/lib/generators/authnz_eleven/templates/jobs/sms_delivery_job.rb.tt +11 -0
  139. data/lib/generators/authnz_eleven/templates/lib/captcha/null.rb.tt +9 -0
  140. data/lib/generators/authnz_eleven/templates/lib/captcha/turnstile.rb.tt +39 -0
  141. data/lib/generators/authnz_eleven/templates/lib/captcha.rb.tt +15 -0
  142. data/lib/generators/authnz_eleven/templates/lib/sms/log.rb.tt +8 -0
  143. data/lib/generators/authnz_eleven/templates/lib/sms/twilio.rb.tt +70 -0
  144. data/lib/generators/authnz_eleven/templates/lib/sms.rb.tt +15 -0
  145. data/lib/generators/authnz_eleven/templates/mailers/user_mailer.rb.tt +98 -0
  146. data/lib/generators/authnz_eleven/templates/migrations/add_guest_to_users_migration.rb.tt +6 -0
  147. data/lib/generators/authnz_eleven/templates/migrations/create_api_tokens_migration.rb.tt +14 -0
  148. data/lib/generators/authnz_eleven/templates/migrations/create_events_migration.rb.tt +26 -0
  149. data/lib/generators/authnz_eleven/templates/migrations/create_invitations_migration.rb.tt +20 -0
  150. data/lib/generators/authnz_eleven/templates/migrations/create_memberships_migration.rb.tt +12 -0
  151. data/lib/generators/authnz_eleven/templates/migrations/create_omniauth_identities_migration.rb.tt +16 -0
  152. data/lib/generators/authnz_eleven/templates/migrations/create_password_histories_migration.rb.tt +11 -0
  153. data/lib/generators/authnz_eleven/templates/migrations/create_pending_registrations_migration.rb.tt +31 -0
  154. data/lib/generators/authnz_eleven/templates/migrations/create_recovery_codes_migration.rb.tt +11 -0
  155. data/lib/generators/authnz_eleven/templates/migrations/create_sessions_migration.rb.tt +19 -0
  156. data/lib/generators/authnz_eleven/templates/migrations/create_teams_migration.rb.tt +9 -0
  157. data/lib/generators/authnz_eleven/templates/migrations/create_users_migration.rb.tt +70 -0
  158. data/lib/generators/authnz_eleven/templates/migrations/create_webauthn_credentials_migration.rb.tt +16 -0
  159. data/lib/generators/authnz_eleven/templates/models/api_token.rb.tt +24 -0
  160. data/lib/generators/authnz_eleven/templates/models/concerns/adminable.rb.tt +15 -0
  161. data/lib/generators/authnz_eleven/templates/models/concerns/authenticatable.rb.tt +151 -0
  162. data/lib/generators/authnz_eleven/templates/models/concerns/bannable.rb.tt +24 -0
  163. data/lib/generators/authnz_eleven/templates/models/concerns/deadboltable.rb.tt +39 -0
  164. data/lib/generators/authnz_eleven/templates/models/concerns/email_verifiable.rb.tt +52 -0
  165. data/lib/generators/authnz_eleven/templates/models/concerns/eventable.rb.tt +22 -0
  166. data/lib/generators/authnz_eleven/templates/models/concerns/guestable.rb.tt +30 -0
  167. data/lib/generators/authnz_eleven/templates/models/concerns/invitable.rb.tt +12 -0
  168. data/lib/generators/authnz_eleven/templates/models/concerns/magic_linkable.rb.tt +34 -0
  169. data/lib/generators/authnz_eleven/templates/models/concerns/password_historical.rb.tt +28 -0
  170. data/lib/generators/authnz_eleven/templates/models/concerns/password_policy.rb.tt +38 -0
  171. data/lib/generators/authnz_eleven/templates/models/concerns/password_rotatable.rb.tt +15 -0
  172. data/lib/generators/authnz_eleven/templates/models/concerns/phone_verifiable.rb.tt +50 -0
  173. data/lib/generators/authnz_eleven/templates/models/concerns/principals.rb.tt +67 -0
  174. data/lib/generators/authnz_eleven/templates/models/concerns/recoverable.rb.tt +19 -0
  175. data/lib/generators/authnz_eleven/templates/models/concerns/second_factor.rb.tt +52 -0
  176. data/lib/generators/authnz_eleven/templates/models/concerns/sms_challengeable.rb.tt +38 -0
  177. data/lib/generators/authnz_eleven/templates/models/concerns/team_member.rb.tt +20 -0
  178. data/lib/generators/authnz_eleven/templates/models/current.rb.tt +28 -0
  179. data/lib/generators/authnz_eleven/templates/models/event.rb.tt +32 -0
  180. data/lib/generators/authnz_eleven/templates/models/invitation.rb.tt +122 -0
  181. data/lib/generators/authnz_eleven/templates/models/membership.rb.tt +16 -0
  182. data/lib/generators/authnz_eleven/templates/models/omniauth_identity.rb.tt +12 -0
  183. data/lib/generators/authnz_eleven/templates/models/password_history.rb.tt +3 -0
  184. data/lib/generators/authnz_eleven/templates/models/pending_registration.rb.tt +216 -0
  185. data/lib/generators/authnz_eleven/templates/models/recovery_code.rb.tt +41 -0
  186. data/lib/generators/authnz_eleven/templates/models/session.rb.tt +46 -0
  187. data/lib/generators/authnz_eleven/templates/models/team.rb.tt +12 -0
  188. data/lib/generators/authnz_eleven/templates/models/user.rb.tt +59 -0
  189. data/lib/generators/authnz_eleven/templates/models/webauthn_credential.rb.tt +12 -0
  190. data/lib/generators/authnz_eleven/templates/tasks/authnz_eleven.rake.tt +41 -0
  191. data/lib/generators/authnz_eleven/templates/test_unit/controllers/account_existence_parity_test.rb.tt +158 -0
  192. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/access_test.rb.tt +139 -0
  193. data/lib/generators/authnz_eleven/templates/test_unit/controllers/admin/multi_factor_authentication_test.rb.tt +65 -0
  194. data/lib/generators/authnz_eleven/templates/test_unit/controllers/api_token_authentication_test.rb.tt +45 -0
  195. data/lib/generators/authnz_eleven/templates/test_unit/controllers/credential_enrollment_test.rb.tt +84 -0
  196. data/lib/generators/authnz_eleven/templates/test_unit/controllers/guest_access_test.rb.tt +113 -0
  197. data/lib/generators/authnz_eleven/templates/test_unit/controllers/invitations_controller_test.rb.tt +230 -0
  198. data/lib/generators/authnz_eleven/templates/test_unit/controllers/passkey_ceremony_test.rb.tt +126 -0
  199. data/lib/generators/authnz_eleven/templates/test_unit/controllers/password_rotation_test.rb.tt +67 -0
  200. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_key_ceremony_test.rb.tt +218 -0
  201. data/lib/generators/authnz_eleven/templates/test_unit/controllers/security_notifications_test.rb.tt +39 -0
  202. data/lib/generators/authnz_eleven/templates/test_unit/controllers/session_lifecycle_test.rb.tt +102 -0
  203. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/api_tokens_controller_test.rb.tt +61 -0
  204. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/authentications/events_controller_test.rb.tt +17 -0
  205. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/email_verifications_controller_test.rb.tt +39 -0
  206. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/invitations_controller_test.rb.tt +157 -0
  207. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/management_test.rb.tt +256 -0
  208. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/multi_factor_authentication/management_test.rb.tt +222 -0
  209. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/omniauth_identities_controller_test.rb.tt +57 -0
  210. data/lib/generators/authnz_eleven/templates/test_unit/controllers/settings/phone_verifications_controller_test.rb.tt +146 -0
  211. data/lib/generators/authnz_eleven/templates/test_unit/controllers/teams_controller_test.rb.tt +83 -0
  212. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/email_verifications_controller_test.rb.tt +74 -0
  213. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/magic_link_controller_test.rb.tt +90 -0
  214. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/multi_factor_authentication/challenge_test.rb.tt +143 -0
  215. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth/registrations_controller_test.rb.tt +138 -0
  216. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/omniauth_controller_test.rb.tt +97 -0
  217. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/password_resets_controller_test.rb.tt +58 -0
  218. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/email_verifications_controller_test.rb.tt +102 -0
  219. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations/phone_verifications_controller_test.rb.tt +69 -0
  220. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/registrations_controller_test.rb.tt +180 -0
  221. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sessions_controller_test.rb.tt +124 -0
  222. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sms_sessions_controller_test.rb.tt +111 -0
  223. data/lib/generators/authnz_eleven/templates/test_unit/controllers/users/sudos_controller_test.rb.tt +110 -0
  224. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/memberships.yml.tt +13 -0
  225. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/teams.yml.tt +2 -0
  226. data/lib/generators/authnz_eleven/templates/test_unit/fixtures/users.yml.tt +68 -0
  227. data/lib/generators/authnz_eleven/templates/test_unit/mailers/user_mailer_test.rb.tt +88 -0
  228. data/lib/generators/authnz_eleven/templates/test_unit/models/api_token_test.rb.tt +38 -0
  229. data/lib/generators/authnz_eleven/templates/test_unit/models/password_history_test.rb.tt +51 -0
  230. data/lib/generators/authnz_eleven/templates/test_unit/models/session_test.rb.tt +16 -0
  231. data/lib/generators/authnz_eleven/templates/test_unit/models/sms_challengeable_test.rb.tt +108 -0
  232. data/lib/generators/authnz_eleven/templates/test_unit/models/user/guestable_test.rb.tt +78 -0
  233. data/lib/generators/authnz_eleven/templates/test_unit/models/user/recovery_codes_test.rb.tt +32 -0
  234. data/lib/generators/authnz_eleven/templates/test_unit/models/user/sign_in_methods_test.rb.tt +56 -0
  235. data/lib/generators/authnz_eleven/templates/test_unit/models/user_test.rb.tt +145 -0
  236. data/lib/generators/authnz_eleven/wizard_generator.rb +52 -0
  237. metadata +309 -0
@@ -0,0 +1,88 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.mailer_class %>Test < ActionMailer::TestCase
4
+ setup { @user = <%= identity.plural %>(:alice) }
5
+ <%- if security_notifications? -%>
6
+
7
+ test "security_notification describes the change without including credentials" do
8
+ mail = <%= identity.mailer_class %>.with(
9
+ to: @user.email,
10
+ event: :password_changed,
11
+ occurred_at: Time.zone.local(2026, 1, 2, 3, 4),
12
+ ip_address: "192.0.2.1",
13
+ user_agent: "Example Browser"
14
+ ).security_notification
15
+
16
+ assert_equal [ @user.email ], mail.to
17
+ assert_equal "Your password was changed", mail.subject
18
+ assert_includes mail.body.encoded, "This security change was made to your account."
19
+ assert_includes mail.body.encoded, "192.0.2.1"
20
+ assert_includes mail.body.encoded, "Example Browser"
21
+ end
22
+ <%- end -%>
23
+ <%- if recoverable? -%>
24
+
25
+ test "password_reset links to the reset form" do
26
+ token = @user.generate_token_for(:password_reset)
27
+ mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.email).password_reset
28
+
29
+ assert_equal [ @user.email ], mail.to
30
+ assert_equal "Reset your password", mail.subject
31
+ expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_reset_password_url(sid: token, host: "example.com")
32
+ assert_includes mail.body.encoded, expected_url
33
+ end
34
+ <%- end -%>
35
+ <%- if email_change_verification? -%>
36
+
37
+ test "email_change_verification describes and links the change" do
38
+ @user.update!(pending_email: "moved@example.com")
39
+ token = @user.generate_token_for(:email_change_verification)
40
+ mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.pending_email).email_change_verification
41
+
42
+ assert_equal [ "moved@example.com" ], mail.to
43
+ assert_equal "Verify your new email address", mail.subject
44
+ expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_verify_email_change_url(sid: token, host: "example.com")
45
+ assert_includes mail.body.encoded, expected_url
46
+ end
47
+ <%- end -%>
48
+ <%- if email_gated_registration? -%>
49
+
50
+ test "registration_email_verification describes registration" do
51
+ registration = <%= identity.pending_registration_class %>.new(email: "fresh@example.com"<%= staged_registration_invitation(%("fresh@example.com")) %>)
52
+ registration.save!(validate: false)
53
+ token = registration.generate_token_for(:registration_email_verification)
54
+ mail = <%= identity.mailer_class %>.with(signed_id: token, to: registration.email).registration_email_verification
55
+
56
+ assert_equal [ "fresh@example.com" ], mail.to
57
+ assert_equal "Verify your email to finish signing up", mail.subject
58
+ expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_verify_email_url(sid: token, host: "example.com")
59
+ assert_includes mail.body.encoded, expected_url
60
+ end
61
+ <%- end -%>
62
+ <%- if magic_link? -%>
63
+
64
+ test "magic_link links to the sign in url" do
65
+ token = @user.generate_magic_link
66
+ mail = <%= identity.mailer_class %>.with(signed_id: token, to: @user.email).magic_link
67
+
68
+ assert_equal [ @user.email ], mail.to
69
+ assert_equal "Your sign in link", mail.subject
70
+ expected_url = Rails.application.routes.url_helpers.<%= identity.route_scope %>_magic_link_url(sid: token, host: "example.com")
71
+ assert_includes mail.body.encoded, expected_url
72
+ end
73
+ <%- end -%>
74
+ <%- if email_invitable? -%>
75
+
76
+ test "invitation names the inviter and links to the accept page" do
77
+ invitation = <%= identity.invitation_class %>.create!(sent_to: "invitee@example.com", inviter: @user)
78
+ token = invitation.generate_token_for(:invitation)
79
+ mail = <%= identity.mailer_class %>.with(invitation: invitation, signed_id: token, to: invitation.sent_to).invitation
80
+
81
+ assert_equal [ "invitee@example.com" ], mail.to
82
+ assert_equal "You've been invited to #{<%= identity.config_reference %>.app_name}", mail.subject
83
+ assert_includes mail.body.encoded, @user.<%= principals.display.column %>
84
+ expected_url = Rails.application.routes.url_helpers.<%= identity.namespaced? ? "#{identity.route_scope}_accept_invitation" : "accept_invitation" %>_url(token: token, host: "example.com")
85
+ assert_includes mail.body.encoded, expected_url
86
+ end
87
+ <%- end -%>
88
+ end
@@ -0,0 +1,38 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.api_token_class %>Test < ActiveSupport::TestCase
4
+ test "only the secret authenticates until expiry or revocation" do
5
+ user = <%= identity.plural %>(:bob)
6
+ token, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
7
+
8
+ assert_equal user, <%= identity.api_token_class %>.authenticate(secret).user
9
+ refute_equal secret, token.token_digest
10
+ assert_nil <%= identity.api_token_class %>.authenticate("ae11_#{SecureRandom.urlsafe_base64(32)}")
11
+ assert_nil <%= identity.api_token_class %>.authenticate("not-a-token")
12
+
13
+ token.update!(expires_at: 1.second.ago)
14
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
15
+
16
+ token.update!(expires_at: 1.day.from_now)
17
+ token.destroy!
18
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
19
+ end
20
+
21
+ test "deleting the account deletes its tokens" do
22
+ user = <%= identity.plural %>(:bob)
23
+ _, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
24
+ user.destroy!
25
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
26
+ end
27
+ <%- if bannable? -%>
28
+
29
+ test "a suspended account cannot use a token" do
30
+ user = <%= identity.plural %>(:bob)
31
+ _, secret = <%= identity.api_token_class %>.issue!(user:, name: "Script")
32
+ user.ban!
33
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
34
+ user.unban!
35
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
36
+ end
37
+ <%- end -%>
38
+ end
@@ -0,0 +1,51 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.password_history_class %>ReuseTest < ActiveSupport::TestCase
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+
6
+ test "rejects changing the password to the current one" do
7
+ assert_not @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97")
8
+ assert_includes @user.errors[:password], "was used recently. Please choose a different one"
9
+ end
10
+
11
+ test "rejects reusing a recently-used password" do
12
+ @user.update!(password: "FirstChange1", password_confirmation: "FirstChange1")
13
+
14
+ assert_not @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97")
15
+ assert @user.errors[:password].any?
16
+ end
17
+
18
+ test "accepts a genuinely new password and archives the digest it replaced" do
19
+ assert_difference -> { @user.password_histories.count }, 1 do
20
+ assert @user.update(password: "BrandNew1234", password_confirmation: "BrandNew1234"),
21
+ @user.errors.full_messages.to_sentence
22
+ end
23
+ end
24
+
25
+ test "a later save of the same record does not re-check the new password" do
26
+ @user.update!(password: "BrandNew1234", password_confirmation: "BrandNew1234")
27
+
28
+ assert @user.update(updated_at: Time.current), @user.errors.full_messages.to_sentence
29
+ end
30
+
31
+ test "keeps only the configured depth of history" do
32
+ depth = <%= identity.config_reference %>.password.history_depth
33
+
34
+ (1..depth + 2).each do |i|
35
+ @user.update!(password: "Rotation#{i}pw", password_confirmation: "Rotation#{i}pw")
36
+ end
37
+
38
+ assert_equal depth, @user.password_histories.count
39
+ end
40
+
41
+ test "forgets a password once it falls past the depth window" do
42
+ depth = <%= identity.config_reference %>.password.history_depth
43
+
44
+ (1..depth + 1).each do |i|
45
+ @user.update!(password: "Rotation#{i}pw", password_confirmation: "Rotation#{i}pw")
46
+ end
47
+
48
+ assert @user.update(password: "quilted-lantern-moss-97", password_confirmation: "quilted-lantern-moss-97"),
49
+ @user.errors.full_messages.to_sentence
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.session_class %>Test < ActiveSupport::TestCase
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+
6
+ test "active and timed_out split sessions by the idle timeout" do
7
+ fresh = @user.sessions.create!
8
+ stale = @user.sessions.create!
9
+ stale.update_column(:updated_at, (<%= identity.config_reference %>.session.idle_timeout + 1.hour).ago)
10
+
11
+ assert_includes <%= identity.session_class %>.active, fresh
12
+ assert_includes <%= identity.session_class %>.timed_out, stale
13
+ refute_includes <%= identity.session_class %>.active, stale
14
+ refute_includes <%= identity.session_class %>.timed_out, fresh
15
+ end
16
+ end
@@ -0,0 +1,108 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.class_name %>::SmsChallengeableTest < ActiveSupport::TestCase
4
+ include ActiveJob::TestHelper
5
+
6
+ setup do
7
+ @user = <%= identity.plural %>(:bob)
8
+ Rails.cache.clear
9
+ end
10
+
11
+ test "issuing stores a challenge and texts six digits" do
12
+ assert_enqueued_with(job: SmsDeliveryJob) do
13
+ assert @user.issue_sms_challenge
14
+ end
15
+
16
+ assert @user.sms_challenge?
17
+ assert_match(/\d{6} is your/, enqueued_jobs.last.to_s)
18
+ end
19
+
20
+ test "a new delivery replaces the previous code" do
21
+ @user.issue_sms_challenge
22
+ replace_code("xxxxxx")
23
+
24
+ @user.issue_sms_challenge
25
+
26
+ assert_not @user.verify_sms_challenge("xxxxxx")
27
+ assert @user.verify_sms_challenge(issued_code)
28
+ end
29
+
30
+ test "a correct code works exactly once" do
31
+ @user.issue_sms_challenge
32
+ code = issued_code
33
+
34
+ assert @user.verify_sms_challenge(code)
35
+ assert_not @user.verify_sms_challenge(code)
36
+ end
37
+
38
+ test "a correct code verifies only the number it was texted to" do
39
+ @user.issue_sms_challenge(to: "+14155550100")
40
+ code = issued_code
41
+
42
+ assert_not @user.verify_sms_challenge(code, to: "+14155550199")
43
+ assert @user.verify_sms_challenge(code, to: "+14155550100")
44
+ end
45
+
46
+ test "a code waiting for another number is not a challenge for this one" do
47
+ @user.issue_sms_challenge(to: "+14155550100")
48
+
49
+ assert_not @user.sms_challenge?
50
+ assert @user.sms_challenge?(to: "+14155550100")
51
+ end
52
+
53
+ test "a code expires" do
54
+ @user.issue_sms_challenge
55
+ code = issued_code
56
+ travel <%= identity.config_reference %>.tokens.sms_code + 1.minute
57
+
58
+ assert_not @user.verify_sms_challenge(code)
59
+ end
60
+
61
+ test "wrong codes spend the attempt budget" do
62
+ @user.issue_sms_challenge
63
+ code = issued_code
64
+
65
+ <%= identity.config_reference %>.sms.max_attempts.times do
66
+ assert_not @user.verify_sms_challenge(wrong_code)
67
+ end
68
+
69
+ assert_not @user.verify_sms_challenge(code)
70
+ end
71
+
72
+ test "a new delivery starts a fresh attempt budget" do
73
+ @user.issue_sms_challenge
74
+ <%= identity.config_reference %>.sms.max_attempts.times do
75
+ @user.verify_sms_challenge(wrong_code)
76
+ end
77
+
78
+ @user.issue_sms_challenge
79
+
80
+ assert @user.verify_sms_challenge(issued_code)
81
+ end
82
+
83
+ test "a missing cache fails closed" do
84
+ cache = Rails.cache
85
+ Rails.cache = ActiveSupport::Cache::NullStore.new
86
+
87
+ @user.issue_sms_challenge
88
+
89
+ assert_not @user.verify_sms_challenge(issued_code)
90
+ ensure
91
+ Rails.cache = cache
92
+ end
93
+
94
+ private
95
+
96
+ def issued_code
97
+ enqueued_jobs.reverse.filter_map { |job| job.to_s[/(\d{6}) is your/, 1] }.first
98
+ end
99
+
100
+ def wrong_code
101
+ issued_code == "000000" ? "000001" : "000000"
102
+ end
103
+
104
+ def replace_code(code, to: @user.phone)
105
+ Rails.cache.write(@user.send(:sms_challenge_key, :code), { code: code, to: to },
106
+ expires_in: <%= identity.config_reference %>.tokens.sms_code)
107
+ end
108
+ end
@@ -0,0 +1,78 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.class_name %>::GuestableTest < ActiveSupport::TestCase
4
+ test "create_guest! builds a valid, persisted guest" do
5
+ guest = <%= identity.class_name %>.create_guest!
6
+
7
+ assert guest.persisted?
8
+ assert guest.guest?
9
+ <%- if principals.email -%>
10
+ assert_match(/\Aguest_.+@example\.com\z/, guest.email)
11
+ <%- end -%>
12
+ <%- if principals.username -%>
13
+ assert_match(/\Aguest_[a-zA-Z0-9]+\z/, guest.username)
14
+ <%- end -%>
15
+ end
16
+
17
+ test "create_guest! generates a unique guest each time" do
18
+ assert_not_equal <%= identity.class_name %>.create_guest!.<%= principals.display.column %>, <%= identity.class_name %>.create_guest!.<%= principals.display.column %>
19
+ end
20
+
21
+ test "guests scope returns only guest users" do
22
+ guest = <%= identity.class_name %>.create_guest!
23
+
24
+ assert_includes <%= identity.class_name %>.guests, guest
25
+ assert_not_includes <%= identity.class_name %>.guests, <%= identity.plural %>(:alice)
26
+ end
27
+
28
+ test "regular users are not guests" do
29
+ assert_not <%= identity.plural %>(:alice).guest?
30
+ end
31
+
32
+ test "expired_guests finds guests past config.guest.ttl, and only those" do
33
+ fresh = <%= identity.class_name %>.create_guest!
34
+ stale = <%= identity.class_name %>.create_guest!
35
+ stale.update_column(:created_at, (<%= identity.config_reference %>.guest.ttl + 1.day).ago)
36
+
37
+ assert_includes <%= identity.class_name %>.expired_guests, stale
38
+ assert_not_includes <%= identity.class_name %>.expired_guests, fresh
39
+ end
40
+
41
+ test "expired_guests never collects a real <%= identity.singular %>, however old" do
42
+ <%= identity.plural %>(:alice).update_column(:created_at, (<%= identity.config_reference %>.guest.ttl + 1.year).ago)
43
+
44
+ assert_not_includes <%= identity.class_name %>.expired_guests, <%= identity.plural %>(:alice)
45
+ end
46
+
47
+ test "absorb_guest takes the guest row with it, in one transaction" do
48
+ guest = <%= identity.class_name %>.create_guest!
49
+
50
+ assert_difference -> { <%= identity.class_name %>.guests.count }, -1 do
51
+ <%= identity.plural %>(:alice).absorb_guest(guest)
52
+ end
53
+ assert_nil <%= identity.class_name %>.find_by(id: guest.id)
54
+ end
55
+ <%- if timeoutable? -%>
56
+
57
+ test "<%= identity.session_class %>.timed_out leaves idle guest sessions alone" do
58
+ guest_session = <%= identity.class_name %>.create_guest!.sessions.create!
59
+ <%= identity.singular %>_session = <%= identity.plural %>(:alice).sessions.create!
60
+ idle = (<%= identity.session_class %>.idle_timeout + 1.hour).ago
61
+ [ guest_session, <%= identity.singular %>_session ].each { |s| s.update_column(:updated_at, idle) }
62
+
63
+ assert_includes <%= identity.session_class %>.timed_out, <%= identity.singular %>_session
64
+ assert_not_includes <%= identity.session_class %>.timed_out, guest_session,
65
+ "guests do not time out; config.guest.ttl controls when they are collected"
66
+ end
67
+ <%- end -%>
68
+ <%- if trackable? -%>
69
+
70
+ test "a guest session records no sign-in event" do
71
+ guest = <%= identity.class_name %>.create_guest!
72
+
73
+ assert_no_difference -> { guest.events.count } do
74
+ guest.sessions.create!
75
+ end
76
+ end
77
+ <%- end -%>
78
+ end
@@ -0,0 +1,32 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.class_name %>::RecoveryCodesTest < ActiveSupport::TestCase
4
+ setup do
5
+ @user = <%= identity.plural %>(:bob)
6
+ @user.update!(totp_enrolled_at: Time.current)
7
+ <%= identity.recovery_code_class %>.regenerate_for(@user)
8
+ end
9
+
10
+ test "turning the authenticator off keeps the codes while a security key remains" do
11
+ add_security_key
12
+ @user.update!(totp_enrolled_at: nil)
13
+ @user.discard_orphaned_recovery_codes!
14
+
15
+ assert @user.recovery_codes.reload.any?
16
+ end
17
+
18
+ test "removing the last security key takes the codes with it" do
19
+ add_security_key
20
+ @user.update!(totp_enrolled_at: nil)
21
+ <%= second_factor_keys_expr("@user") %>.destroy_all
22
+ @user.discard_orphaned_recovery_codes!
23
+
24
+ assert_empty @user.recovery_codes.reload
25
+ end
26
+
27
+ private
28
+
29
+ def add_security_key
30
+ <%= second_factor_keys_expr("@user") %>.create!(name: "Key", external_id: "ext-#{@user.id}", public_key: "pk"<%= factor_attr(:second_factor) %>)
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.class_name %>::SignInMethodsTest < ActiveSupport::TestCase
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+ <%- if omniauth? -%>
6
+
7
+ test "a linked provider is a way in" do
8
+ @user.omniauth_identities.create!(provider: "developer", uid: "only")
9
+ assert_includes @user.reload.sign_in_methods, :omniauth
10
+ end
11
+
12
+ test "a spare provider is always another way in" do
13
+ @user.omniauth_identities.create!(provider: "developer", uid: "one")
14
+ @user.omniauth_identities.create!(provider: "developer", uid: "two")
15
+
16
+ assert_not @user.reload.last_sign_in_method?(:omniauth),
17
+ "two linked accounts means removing one leaves the other"
18
+ end
19
+
20
+ test "last_sign_in_method? is true exactly when this kind is all that is left" do
21
+ @user.omniauth_identities.create!(provider: "developer", uid: "only")
22
+ @user.reload
23
+
24
+ assert_equal (@user.sign_in_methods == [ :omniauth ]), @user.last_sign_in_method?(:omniauth)
25
+ end
26
+ <%- end -%>
27
+ <%- if passkey? && omniauth? -%>
28
+
29
+ test "a provider is protected in a passkey build when no passkey was ever enrolled" do
30
+ @user.webauthn_credentials.destroy_all
31
+ @user.omniauth_identities.destroy_all
32
+ @user.omniauth_identities.create!(provider: "developer", uid: "only")
33
+ @user.reload
34
+ <%- if password? || magic_link? -%>
35
+
36
+ assert_not @user.last_sign_in_method?(:omniauth),
37
+ "this build has another sign-in method, so the link is removable"
38
+ <%- else -%>
39
+
40
+ assert @user.last_sign_in_method?(:omniauth),
41
+ "having passkey sign-in enabled does not mean this account has a passkey"
42
+ <%- end -%>
43
+ end
44
+ <%- end -%>
45
+ <%- if passkey? -%>
46
+
47
+ test "an enrolled passkey is a way in, and the last one is protected" do
48
+ @user.webauthn_credentials<%= ".passkeys" if passkey_and_security_keys? %>.destroy_all
49
+ @user.webauthn_credentials.create!(name: "Only key", external_id: "ext-only", public_key: "pk"<%= factor_attr(:first_factor) %>)
50
+ @user.reload
51
+
52
+ assert_includes @user.sign_in_methods, :passkey
53
+ assert_equal (@user.sign_in_methods == [ :passkey ]), @user.last_sign_in_method?(:passkey)
54
+ end
55
+ <%- end -%>
56
+ end
@@ -0,0 +1,145 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.class_name %>Test < ActiveSupport::TestCase
4
+ <%- if principals.email -%>
5
+ test "malformed emails are rejected" do
6
+ user = build_<%= identity.singular %>(email: "person..name@example.com")
7
+ assert_not user.valid?
8
+ assert_includes user.errors[:email], "is invalid"
9
+ end
10
+ <%- end -%>
11
+ <%- if principals.email&.optional? -%>
12
+
13
+ test "email is optional" do
14
+ user = build_<%= identity.singular %>(email: nil)
15
+ assert user.valid?
16
+ end
17
+
18
+ test "a blank email normalizes to nil so the partial unique index applies" do
19
+ user = build_<%= identity.singular %>(email: "")
20
+ assert user.save
21
+ assert_nil user.reload.email
22
+ end
23
+
24
+ test "set emails are still unique (enforced by the DB index)" do
25
+ assert build_<%= identity.singular %>(email: "dup@example.com").save
26
+ assert_raises(ActiveRecord::RecordNotUnique) do
27
+ build_<%= identity.singular %>(email: "dup@example.com").save(validate: false)
28
+ end
29
+ end
30
+ <%- end -%>
31
+ <%- if principals.username -%>
32
+ <%- if principals.email&.optional? -%>
33
+
34
+ <%- end -%>
35
+ test "username is required" do
36
+ user = build_<%= identity.singular %>(username: nil)
37
+ assert_not user.valid?
38
+ assert_includes user.errors[:username], "can't be blank"
39
+ end
40
+
41
+ test "username uniqueness is honest and case-insensitive" do
42
+ taken = <%= identity.plural %>(:alice).username
43
+ user = build_<%= identity.singular %>(username: taken.upcase)
44
+ assert_not user.valid?
45
+ assert_includes user.errors[:username], "has already been taken"
46
+ end
47
+
48
+ test "forbidden usernames are rejected" do
49
+ user = build_<%= identity.singular %>(username: "admin")
50
+ assert_not user.valid?
51
+ assert_includes user.errors[:username], "is forbidden"
52
+ end
53
+
54
+ test "the chosen case is preserved on save" do
55
+ user = build_<%= identity.singular %>(username: "MixedCase")
56
+ assert user.save
57
+ assert_equal "MixedCase", user.reload.username
58
+ end
59
+
60
+ test "username uniqueness is also enforced by the DB index" do
61
+ taken = <%= identity.plural %>(:alice).username
62
+ assert_raises(ActiveRecord::RecordNotUnique) do
63
+ build_<%= identity.singular %>(username: taken.upcase).save(validate: false)
64
+ end
65
+ end
66
+ <%- end -%>
67
+ <%- if principals.phone -%>
68
+ <%- if principals.email&.optional? || principals.username -%>
69
+
70
+ <%- end -%>
71
+ test "a valid phone is accepted and normalized to E.164" do
72
+ user = build_<%= identity.singular %>(phone: "+1 415 826 4001")
73
+ assert user.save
74
+ assert_equal "+14158264001", user.reload.phone
75
+ end
76
+
77
+ test "a number typed without a country code is read in the default country" do
78
+ with_default_country("US") do
79
+ user = build_<%= identity.singular %>(phone: "(415) 826-4002")
80
+ assert user.save
81
+ assert_equal "+14158264002", user.reload.phone
82
+ end
83
+ end
84
+
85
+ test "a bare number that isn't valid in the default country is rejected, not reinterpreted" do
86
+ with_default_country("US") do
87
+ user = build_<%= identity.singular %>(phone: "6494461709")
88
+ assert_not user.valid?
89
+ assert_not_equal "+6494461709", user.phone, "must not be read as a New Zealand number"
90
+ end
91
+ end
92
+
93
+ test "an explicit country code overrides the default country" do
94
+ with_default_country("US") do
95
+ user = build_<%= identity.singular %>(phone: "+44 20 7183 8750")
96
+ assert user.save
97
+ assert_equal "+442071838750", user.reload.phone
98
+ end
99
+ end
100
+
101
+ test "an array of default countries is tried in order" do
102
+ with_default_country(%w[US GB]) do
103
+ user = build_<%= identity.singular %>(phone: "02071838750")
104
+ assert user.save
105
+ assert_equal "+442071838750", user.reload.phone
106
+ end
107
+ end
108
+ <%- if principals.phone.required? -%>
109
+
110
+ test "phone is required" do
111
+ assert_not build_<%= identity.singular %>(phone: nil).valid?
112
+ end
113
+ <%- else -%>
114
+
115
+ test "phone is optional" do
116
+ assert build_<%= identity.singular %>(phone: nil).valid?
117
+ end
118
+ <%- end -%>
119
+
120
+ test "set phones are unique (enforced by the DB index)" do
121
+ assert build_<%= identity.singular %>(phone: "+14158264002").save
122
+ assert_raises(ActiveRecord::RecordNotUnique) do
123
+ build_<%= identity.singular %>(phone: "+14158264002").save(validate: false)
124
+ end
125
+ end
126
+ <%- end -%>
127
+
128
+ private
129
+
130
+ def build_<%= identity.singular %>(**attrs)
131
+ defaults = <%= account_fixture_literal(indent: 4) %>
132
+ <%= identity.class_name %>.new(defaults.merge(attrs))
133
+ end
134
+ <%- if principals.phone -%>
135
+
136
+ def with_default_country(country)
137
+ # Mutate and restore this OrderedOptions value because its method_missing reader cannot be stubbed reliably.
138
+ previous = <%= identity.config_reference %>.phone.default_country
139
+ <%= identity.config_reference %>.phone.default_country = country
140
+ yield
141
+ ensure
142
+ <%= identity.config_reference %>.phone.default_country = previous
143
+ end
144
+ <%- end -%>
145
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require_relative "authnz_eleven_generator"
5
+ require_relative "../../authnz_eleven/wizard"
6
+
7
+ module AuthnzEleven
8
+ # `bin/rails generate authnz_eleven:wizard`
9
+ #
10
+ # Picks the flags interactively, prints the command it assembled, and offers
11
+ # to run it. The printed command is the point: it goes in a README, and
12
+ # re-running it means the same thing.
13
+ class WizardGenerator < Rails::Generators::Base
14
+ desc "Pick authnz_eleven's flags interactively, then run the generator"
15
+
16
+ # Not a gemspec dependency: huh isn't on RubyGems yet (the `huh` name there
17
+ # belongs to an unrelated library, so don't send anyone to `gem install`),
18
+ # and a gemspec can't declare a git source. It is also wanted exactly once.
19
+ INSTALL = <<~MSG
20
+ The wizard needs the `huh` gem for its terminal forms. Add it to your
21
+ Gemfile, then `bundle install`:
22
+
23
+ gem "huh", github: "marcoroth/huh-ruby", group: :development
24
+
25
+ Or skip the wizard and write the flags yourself:
26
+
27
+ bin/rails generate authnz_eleven --help
28
+ MSG
29
+
30
+ def pick_flags
31
+ @flags = wizard.run
32
+ rescue LoadError
33
+ abort INSTALL
34
+ end
35
+
36
+ def report
37
+ say ""
38
+ say " #{wizard.command}", :green
39
+ say ""
40
+ end
41
+
42
+ def generate
43
+ return say("Nothing generated.", :yellow) unless yes?("Run it now? [y/N]")
44
+
45
+ AuthnzElevenGenerator.start(@flags)
46
+ end
47
+
48
+ private
49
+
50
+ def wizard = @wizard ||= Wizard.new
51
+ end
52
+ end