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,222 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::MultiFactorAuthentication::ManagementTest < ActionDispatch::IntegrationTest
4
+ <%- if sms_second_factor? -%>
5
+ include ActiveJob::TestHelper
6
+
7
+ <%- end -%>
8
+ setup { @user = <%= identity.plural %>(:bob) }
9
+ <%- if sms_second_factor? -%>
10
+
11
+ test "enrolling by text turns MFA on only for a correct code" do
12
+ sign_in_as @user
13
+ get new_<%= identity.settings_helper_prefix %>_mfa_sms_path
14
+ assert_response :success
15
+
16
+ post <%= identity.settings_helper_prefix %>_mfa_sms_path
17
+
18
+ patch <%= identity.settings_helper_prefix %>_mfa_sms_path, params: { code: "000000" }
19
+ assert_not @user.reload.sms_enrolled_at?, "a wrong code must not enable MFA"
20
+
21
+ patch <%= identity.settings_helper_prefix %>_mfa_sms_path, params: { code: issued_code }
22
+ assert @user.reload.sms_enrolled_at?
23
+
24
+ assert_response :success
25
+ assert_equal <%= identity.config_reference %>.second_factor.recovery_code_count,
26
+ response.body.scan(%r{<code>([a-z0-9]+)</code>}).flatten.length
27
+ end
28
+
29
+ test "turning the texted factor off clears the flag and the recovery codes with it" do
30
+ sign_in_as @user
31
+ enable_sms
32
+ assert @user.recovery_codes.any?
33
+
34
+ delete <%= identity.settings_helper_prefix %>_mfa_sms_path
35
+ assert_not @user.reload.sms_enrolled_at?
36
+ assert_empty @user.recovery_codes.reload,
37
+ "codes guarding nothing are live sign-in secrets"
38
+ end
39
+ <%- end -%>
40
+ <%- if totp? -%>
41
+
42
+ test "enrolling turns MFA on only for a correct code" do
43
+ sign_in_as @user
44
+ get new_<%= identity.settings_helper_prefix %>_mfa_authenticator_path
45
+ assert_response :success
46
+
47
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: "000000" }
48
+ assert_not @user.reload.totp_enrolled_at?, "a wrong code must not enable MFA"
49
+
50
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: ROTP::TOTP.new(@user.totp_secret).now }
51
+ assert @user.reload.totp_enrolled_at?
52
+
53
+ assert_response :success
54
+ assert_equal <%= identity.config_reference %>.second_factor.recovery_code_count,
55
+ response.body.scan(%r{<code>([a-z0-9]+)</code>}).flatten.length
56
+ end
57
+ <%- if both_second_factors? && admin_mfa_gate? && password_rotatable? -%>
58
+
59
+ test "TOTP enrollment removes the obsolete instruction before rendering recovery codes" do
60
+ @user = <%= identity.plural %>(:alice)
61
+ @user.update_column(:password_changed_at, (<%= identity.config_reference %>.password.maximum_age + 1.day).ago)
62
+ sign_in_as @user
63
+
64
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path,
65
+ params: { code: ROTP::TOTP.new(@user.totp_secret).now }
66
+
67
+ assert_response :success
68
+ assert_select "h1", "Save your recovery codes"
69
+ assert_nil flash[:alert]
70
+ end
71
+ <%- end -%>
72
+ <%- if both_second_factors? -%>
73
+
74
+ test "enrolling an authenticator leaves recovery codes that already exist alone" do
75
+ sign_in_as @user
76
+ @user.webauthn_credentials.create!(name: "Key", external_id: "ext-1", public_key: "pk"<%= factor_attr(:second_factor) %>)
77
+ before = <%= identity.recovery_code_class %>.regenerate_for(@user)
78
+
79
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: ROTP::TOTP.new(@user.totp_secret).now }
80
+
81
+ assert @user.reload.totp_enrolled_at?
82
+ assert before.all? { |code| <%= identity.recovery_code_class %>.redeem(user: @user, code:) },
83
+ "a set the user is holding must survive enrolling another factor"
84
+ end
85
+ <%- end -%>
86
+ <%- end -%>
87
+
88
+ test "regenerating replaces your recovery codes" do
89
+ sign_in_as @user
90
+ post <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
91
+ before = @user.recovery_codes.pluck(:code_digest).sort
92
+ assert_not_empty before
93
+
94
+ post <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
95
+ assert_not_equal before, @user.recovery_codes.reload.pluck(:code_digest).sort
96
+ end
97
+
98
+ test "the codes are shown once, are real, and are never shown again" do
99
+ sign_in_as @user
100
+ post <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
101
+ assert_response :success
102
+
103
+ codes = response.body.scan(%r{<code>([a-z0-9]+)</code>}).flatten
104
+ assert_equal <%= identity.config_reference %>.second_factor.recovery_code_count, codes.length
105
+
106
+ get <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
107
+ assert_response :success
108
+ assert_no_match codes.first, response.body, "a later visit must not show them again"
109
+ assert_equal codes.length, @user.recovery_codes.count, "looking must not mint a new set"
110
+
111
+ assert <%= identity.recovery_code_class %>.redeem(user: @user, code: codes.first)
112
+ end
113
+
114
+ test "the forms that mint recovery codes opt out of Turbo" do
115
+ sign_in_as @user
116
+ <%- if totp? -%>
117
+
118
+ get new_<%= identity.settings_helper_prefix %>_mfa_authenticator_path
119
+ assert_match 'data-turbo="false"', response.body
120
+ <%- end -%>
121
+ <%- if sms_second_factor? -%>
122
+
123
+ get new_<%= identity.settings_helper_prefix %>_mfa_sms_path
124
+ assert_match 'data-turbo="false"', response.body
125
+ <%- end -%>
126
+
127
+ <%- if security_keys? -%>
128
+ <%- if sudoable? -%>
129
+ <%= test_clear_sudo_body %>
130
+ <%- end -%>
131
+ get new_<%= identity.settings_helper_prefix %>_mfa_security_key_path
132
+ assert_match 'data-turbo="false"', response.body
133
+
134
+ <%- end -%>
135
+ get <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
136
+ assert_match 'data-turbo="false"', response.body
137
+ end
138
+
139
+ test "every URL the codes render at answers GET" do
140
+ sign_in_as @user
141
+ <%- if totp? -%>
142
+
143
+ get <%= identity.settings_helper_prefix %>_mfa_authenticator_path
144
+ assert_response :redirect, "reloading the enrollment result must not 404"
145
+ <%- end -%>
146
+ <%- if sms_second_factor? -%>
147
+
148
+ get <%= identity.settings_helper_prefix %>_mfa_sms_path
149
+ assert_response :redirect, "reloading the enrollment result must not 404"
150
+ <%- end -%>
151
+
152
+ <%- if security_keys? -%>
153
+ get <%= identity.settings_helper_prefix %>_mfa_security_keys_path
154
+ assert_response :success, "reloading the security-key enrollment result must not 404"
155
+
156
+ <%- end -%>
157
+ get <%= identity.settings_helper_prefix %>_mfa_recovery_codes_path
158
+ assert_response :success, "reloading the regenerate result must not 404"
159
+ end
160
+ <%- if totp? -%>
161
+
162
+ test "a code submitted while enrolled checks the app and changes nothing" do
163
+ sign_in_as @user
164
+ enable_mfa
165
+ before = @user.recovery_codes.pluck(:code_digest).sort
166
+
167
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: ROTP::TOTP.new(@user.totp_secret).now }
168
+ assert_redirected_to new_<%= identity.settings_helper_prefix %>_mfa_authenticator_path
169
+ assert_equal "That code is correct.", flash[:notice]
170
+ assert_equal before, @user.recovery_codes.reload.pluck(:code_digest).sort
171
+ assert @user.reload.totp_enrolled_at?
172
+ end
173
+
174
+ test "a wrong code while enrolled says so, and still changes nothing" do
175
+ sign_in_as @user
176
+ enable_mfa
177
+ before = @user.recovery_codes.pluck(:code_digest).sort
178
+
179
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: "000000" }
180
+ assert_redirected_to new_<%= identity.settings_helper_prefix %>_mfa_authenticator_path
181
+ assert_match "didn't match", flash[:alert]
182
+ assert_equal before, @user.recovery_codes.reload.pluck(:code_digest).sort
183
+ assert @user.reload.totp_enrolled_at?, "a failed check must not turn MFA off"
184
+ end
185
+
186
+ test "turning MFA off clears the flag and the recovery codes with it" do
187
+ sign_in_as @user
188
+ enable_mfa
189
+ assert @user.recovery_codes.any?
190
+
191
+ delete <%= identity.settings_helper_prefix %>_mfa_authenticator_path
192
+ assert_not @user.reload.totp_enrolled_at?
193
+ assert_empty @user.recovery_codes.reload,
194
+ "codes guarding nothing are live sign-in secrets"
195
+ end
196
+ <%- end -%>
197
+
198
+ private
199
+
200
+ def sign_in_as(user)
201
+ <%= test_sign_in_body %>
202
+ end
203
+ <%- if totp? -%>
204
+
205
+ def enable_mfa
206
+ post <%= identity.settings_helper_prefix %>_mfa_authenticator_path, params: { code: ROTP::TOTP.new(@user.totp_secret).now }
207
+ @user.reload
208
+ end
209
+ <%- end -%>
210
+ <%- if sms_second_factor? -%>
211
+
212
+ def enable_sms
213
+ post <%= identity.settings_helper_prefix %>_mfa_sms_path
214
+ patch <%= identity.settings_helper_prefix %>_mfa_sms_path, params: { code: issued_code }
215
+ @user.reload
216
+ end
217
+
218
+ def issued_code
219
+ enqueued_jobs.reverse.filter_map { |job| job.to_s[/(\d{6}) is your/, 1] }.first
220
+ end
221
+ <%- end -%>
222
+ end
@@ -0,0 +1,57 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::OmniauthIdentitiesControllerTest < ActionDispatch::IntegrationTest
4
+ setup do
5
+ @user = <%= identity.plural %>(:bob)
6
+ @identity = @user.omniauth_identities.create!(provider: "developer", uid: "bobs-login")
7
+ @other = <%= identity.plural %>(:alice).omniauth_identities.create!(provider: "developer", uid: "someone-elses-login")
8
+ end
9
+
10
+ test "the page lists your connected accounts and nobody else's" do
11
+ sign_in_as @user
12
+ get <%= identity.settings_helper_prefix %>_omniauth_identities_path
13
+
14
+ assert_response :success
15
+ assert_match "Developer", response.body
16
+ end
17
+
18
+ test "a connected account may only be unlinked when another way in survives" do
19
+ sign_in_as @user
20
+
21
+ if @user.last_sign_in_method?(:omniauth)
22
+ assert_no_difference -> { @user.omniauth_identities.count } do
23
+ delete <%= identity.settings_helper_prefix %>_omniauth_identity_path(@identity)
24
+ end
25
+ assert_match "only way into your account", flash[:alert].to_s
26
+ else
27
+ assert_difference -> { @user.omniauth_identities.count }, -1 do
28
+ delete <%= identity.settings_helper_prefix %>_omniauth_identity_path(@identity)
29
+ end
30
+ end
31
+ assert_redirected_to <%= identity.settings_helper_prefix %>_omniauth_identities_path
32
+ end
33
+
34
+ test "a connected account can be unlinked once another one exists" do
35
+ spare = @user.omniauth_identities.create!(provider: "developer", uid: "a-spare-login")
36
+ sign_in_as @user
37
+
38
+ assert_difference -> { @user.omniauth_identities.count }, -1 do
39
+ delete <%= identity.settings_helper_prefix %>_omniauth_identity_path(spare)
40
+ end
41
+ end
42
+
43
+ test "you cannot unlink somebody else's connected account" do
44
+ sign_in_as @user
45
+
46
+ assert_no_difference -> { <%= identity.omniauth_identity_class %>.count } do
47
+ delete <%= identity.settings_helper_prefix %>_omniauth_identity_path(@other)
48
+ end
49
+ assert_response :not_found
50
+ end
51
+
52
+ private
53
+
54
+ def sign_in_as(user)
55
+ <%= test_sign_in_body %>
56
+ end
57
+ end
@@ -0,0 +1,146 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::PhoneVerificationsControllerTest < ActionDispatch::IntegrationTest
4
+ include ActiveJob::TestHelper
5
+
6
+ NEW_NUMBER = "+14158264010".freeze
7
+
8
+ setup do
9
+ @user = <%= identity.plural %>(:bob)
10
+ @original = @user.phone
11
+ sign_in_as @user
12
+ end
13
+
14
+ test "requesting a change texts the new number but does not adopt it yet" do
15
+ request_change_to NEW_NUMBER
16
+
17
+ assert_equal @original, @user.reload.phone, "the number must not move until it's verified"
18
+ assert_equal NEW_NUMBER, @user.pending_phone
19
+ assert @user.sms_challenge?(to: NEW_NUMBER)
20
+ assert_redirected_to new_<%= identity.settings_helper_prefix %>_phone_verification_path
21
+ end
22
+
23
+ test "confirming the code adopts the new number" do
24
+ request_change_to NEW_NUMBER
25
+
26
+ patch <%= identity.settings_helper_prefix %>_phone_verification_path, params: { code: issued_code }
27
+
28
+ assert_equal NEW_NUMBER, @user.reload.phone
29
+ assert_nil @user.pending_phone
30
+ assert_not @user.sms_challenge?(to: NEW_NUMBER), "a redeemed code must not survive"
31
+ assert_redirected_to <%= identity.settings_helper_prefix %>_path
32
+ end
33
+
34
+ test "a wrong code leaves the old number in place" do
35
+ request_change_to NEW_NUMBER
36
+
37
+ patch <%= identity.settings_helper_prefix %>_phone_verification_path, params: { code: "000000" }
38
+
39
+ assert_equal @original, @user.reload.phone
40
+ assert_equal NEW_NUMBER, @user.pending_phone
41
+ assert_response :unprocessable_entity
42
+ end
43
+
44
+ test "a re-send goes to the pending number, not the one on file" do
45
+ request_change_to NEW_NUMBER
46
+ post <%= identity.settings_helper_prefix %>_phone_verification_path
47
+
48
+ assert_equal NEW_NUMBER, texted_numbers.last
49
+ end
50
+
51
+ <%- if coy? -%>
52
+ test "requesting a number that belongs to someone else is answered identically and texts no one" do
53
+ taken = <%= identity.plural %>(:alice).phone
54
+
55
+ assert_no_enqueued_jobs(only: SmsDeliveryJob) do
56
+ request_change_to taken
57
+ end
58
+ assert_equal @original, @user.reload.phone
59
+ assert_equal taken, @user.pending_phone
60
+ taken_status, taken_location = response.status, response.location
61
+ follow_redirect!
62
+ taken_body = response.body.gsub(taken, "SUBMITTED")
63
+
64
+ request_change_to NEW_NUMBER
65
+ assert_equal taken_status, response.status
66
+ assert_equal taken_location, response.location
67
+ follow_redirect!
68
+ assert_equal taken_body, response.body.gsub(NEW_NUMBER, "SUBMITTED"),
69
+ "a taken number and a free one must leave the same page behind, not just the same redirect"
70
+
71
+ get <%= identity.settings_helper_prefix %>_path
72
+ assert_select "a[href=?]", new_<%= identity.settings_helper_prefix %>_phone_verification_path
73
+ end
74
+
75
+ test "a re-send after a rejected number texts nobody" do
76
+ request_change_to <%= identity.plural %>(:alice).phone
77
+
78
+ assert_no_enqueued_jobs(only: SmsDeliveryJob) do
79
+ post <%= identity.settings_helper_prefix %>_phone_verification_path
80
+ end
81
+ end
82
+ <%- else -%>
83
+ test "requesting a number that belongs to someone else is refused" do
84
+ assert_no_enqueued_jobs(only: SmsDeliveryJob) do
85
+ request_change_to <%= identity.plural %>(:alice).phone
86
+ end
87
+
88
+ assert_response :unprocessable_entity
89
+ assert_equal @original, @user.reload.phone
90
+ end
91
+ <%- end -%>
92
+
93
+ test "too many wrong guesses destroy the code" do
94
+ request_change_to NEW_NUMBER
95
+ code = issued_code
96
+
97
+ (<%= identity.config_reference %>.sms.max_attempts + 1).times do
98
+ patch <%= identity.settings_helper_prefix %>_phone_verification_path, params: { code: "000000" }
99
+ end
100
+ patch <%= identity.settings_helper_prefix %>_phone_verification_path, params: { code: code }
101
+
102
+ assert_equal @original, @user.reload.phone, "the real code must stop working once the cap is hit"
103
+ assert_equal NEW_NUMBER, @user.pending_phone
104
+ end
105
+
106
+ test "an expired code is refused" do
107
+ request_change_to NEW_NUMBER
108
+ code = issued_code
109
+ travel <%= identity.config_reference %>.tokens.sms_code + 1.minute
110
+
111
+ patch <%= identity.settings_helper_prefix %>_phone_verification_path, params: { code: code }
112
+
113
+ assert_equal @original, @user.reload.phone
114
+ assert_equal NEW_NUMBER, @user.pending_phone
115
+ end
116
+ <%- if principals.phone.optional? -%>
117
+
118
+ test "clearing your number takes effect immediately" do
119
+ patch <%= identity.settings_helper_prefix %>_phone_path, params: {
120
+ phone: ""<%= settings_credential_pair %>
121
+ }
122
+
123
+ assert_nil @user.reload.phone
124
+ end
125
+ <%- end -%>
126
+
127
+ private
128
+
129
+ def request_change_to(number)
130
+ patch <%= identity.settings_helper_prefix %>_phone_path, params: {
131
+ phone: number<%= settings_credential_pair %>
132
+ }
133
+ end
134
+
135
+ def issued_code
136
+ enqueued_jobs.reverse.filter_map { |job| job.to_s[/(\d{6}) is your/, 1] }.first
137
+ end
138
+
139
+ def texted_numbers
140
+ enqueued_jobs.filter_map { |job| job.to_s[/\+\d{6,}/] }
141
+ end
142
+
143
+ def sign_in_as(user)
144
+ <%= test_sign_in_body %>
145
+ end
146
+ end
@@ -0,0 +1,83 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.nested_module("TeamsControllerTest") %> < ActionDispatch::IntegrationTest
4
+ setup do
5
+ @user = <%= identity.plural %>(:bob)
6
+ @team = <%= identity.teams_table %>(:acme)
7
+ end
8
+
9
+ <%- if team_gated_page -%>
10
+ test "a teamless <%= identity.singular %> is held on the team list" do
11
+ leave_every_team
12
+ sign_in_as @user
13
+ get <%= team_gated_page %>
14
+ assert_redirected_to <%= identity.nested_helper("teams_path") %>
15
+ end
16
+
17
+ <%- end -%>
18
+ test "the team list is reachable while held" do
19
+ leave_every_team
20
+ sign_in_as @user
21
+ get <%= identity.nested_helper("teams_path") %>
22
+ assert_response :success
23
+ end
24
+
25
+ test "creating a team lifts the hold" do
26
+ leave_every_team
27
+ sign_in_as @user
28
+ post <%= identity.nested_helper("teams_path") %>, params: { <%= identity.teams_table.singularize %>: { name: "Initech" } }
29
+ assert_response :redirect
30
+ assert @user.reload.on_a_team?
31
+ end
32
+ <%- if scope_teams? -%>
33
+
34
+ test "a team's pages carry its id" do
35
+ sign_in_as @user
36
+ get <%= team_root_helper %>(team_id: @team)
37
+ assert_response :success
38
+ assert_match @team.name, response.body
39
+ end
40
+
41
+ test "a team you don't belong to is a 404" do
42
+ other = <%= identity.team_class %>.create!(name: "Someone Else's")
43
+ sign_in_as @user
44
+ get <%= team_root_helper %>(team_id: other)
45
+ assert_response :not_found
46
+ end
47
+ <%- end -%>
48
+ <%- if session_carries_team? -%>
49
+
50
+ test "you can switch to a team you belong to" do
51
+ sign_in_as @user
52
+ post <%= identity.nested_helper("active_team_path") %>, params: { team_id: @team.id }
53
+ assert_response :redirect
54
+ end
55
+
56
+ test "losing a membership clears that team from your sessions" do
57
+ sign_in_as @user
58
+ post <%= identity.nested_helper("active_team_path") %>, params: { team_id: @team.id }
59
+ assert_equal @team, @user.sessions.sole.reload.team
60
+
61
+ @user.memberships.find_by(team: @team).destroy!
62
+
63
+ assert_nil @user.sessions.sole.reload.team
64
+ end
65
+
66
+ test "you cannot switch to a team you don't belong to" do
67
+ other = <%= identity.team_class %>.create!(name: "Someone Else's")
68
+ sign_in_as @user
69
+ post <%= identity.nested_helper("active_team_path") %>, params: { team_id: other.id }
70
+ assert_response :not_found
71
+ end
72
+ <%- end -%>
73
+
74
+ private
75
+
76
+ def leave_every_team
77
+ @user.memberships.destroy_all
78
+ end
79
+
80
+ def sign_in_as(user)
81
+ <%= test_sign_in_body %>
82
+ end
83
+ end
@@ -0,0 +1,74 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.module_name %>::EmailVerificationsControllerTest < ActionDispatch::IntegrationTest
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+
6
+ test "opening the link changes nothing until it is confirmed" do
7
+ original = @user.email
8
+ @user.update!(pending_email: "moved@example.com")
9
+ token = @user.generate_token_for(:email_change_verification)
10
+
11
+ get <%= identity.route_scope %>_verify_email_change_path(sid: token)
12
+
13
+ assert_response :success
14
+ assert_equal original, @user.reload.email
15
+ assert_equal "moved@example.com", @user.pending_email
16
+ end
17
+
18
+ test "confirming a change adopts the new address without signing anyone in" do
19
+ @user.update!(pending_email: "moved@example.com")
20
+ token = @user.generate_token_for(:email_change_verification)
21
+
22
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: token)
23
+
24
+ assert_equal "moved@example.com", @user.reload.email
25
+ assert_nil @user.pending_email
26
+ assert_not cookies[:<%= identity.cookie_name %>].present?, "proving an address is not a reason to hand out a session"
27
+ end
28
+
29
+ test "a link is good for exactly one use" do
30
+ @user.update!(pending_email: "moved@example.com")
31
+ token = @user.generate_token_for(:email_change_verification)
32
+
33
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: token)
34
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: token)
35
+
36
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
37
+ assert_equal "That confirmation link is invalid or has expired.", flash[:alert]
38
+ end
39
+
40
+ test "replacing the pending address invalidates its old link" do
41
+ original = @user.email
42
+ @user.update!(pending_email: "first@example.com")
43
+ token = @user.generate_token_for(:email_change_verification)
44
+ @user.update!(pending_email: "second@example.com")
45
+
46
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: token)
47
+
48
+ assert_equal original, @user.reload.email
49
+ assert_equal "second@example.com", @user.pending_email
50
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
51
+ end
52
+
53
+ test "an invalid or expired link is refused" do
54
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: "not-a-real-token")
55
+
56
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
57
+ assert_equal "That confirmation link is invalid or has expired.", flash[:alert]
58
+ end
59
+ <%- if email_gated_registration? -%>
60
+
61
+ test "a sign-up link is refused at the email-change endpoint" do
62
+ registration = <%= identity.pending_registration_class %>.new(email: "fresh@example.com"<%= staged_registration_invitation(%("fresh@example.com")) %>)
63
+ registration.save!(validate: false)
64
+ token = registration.generate_token_for(:registration_email_verification)
65
+
66
+ assert_no_difference -> { <%= identity.class_name %>.count } do
67
+ patch <%= identity.route_scope %>_verify_email_change_path(sid: token)
68
+ end
69
+
70
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
71
+ assert_equal "That confirmation link is invalid or has expired.", flash[:alert]
72
+ end
73
+ <%- end -%>
74
+ end
@@ -0,0 +1,90 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.module_name %>::MagicLinkControllerTest < ActionDispatch::IntegrationTest
4
+ include ActionMailer::TestHelper
5
+
6
+ setup { @user = <%= identity.plural %>(:alice) }
7
+
8
+ test "requesting a link for a known account emails one" do
9
+ assert_enqueued_emails 1 do
10
+ post <%= identity.route_scope %>_magic_link_path, params: { email: @user.email }
11
+ end
12
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
13
+ end
14
+
15
+ <%- if coy? -%>
16
+ test "requesting a link for an unknown email gives nothing away" do
17
+ assert_no_enqueued_emails do
18
+ post <%= identity.route_scope %>_magic_link_path, params: { email: "nobody-#{SecureRandom.hex(4)}@example.com" }
19
+ end
20
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
21
+ end
22
+ <%- else -%>
23
+ test "requesting a link for an unknown email is told no account exists" do
24
+ assert_no_enqueued_emails do
25
+ post <%= identity.route_scope %>_magic_link_path, params: { email: "nobody-#{SecureRandom.hex(4)}@example.com" }
26
+ end
27
+ assert_redirected_to <%= identity.route_scope %>_new_magic_link_path
28
+ end
29
+ <%- end -%>
30
+
31
+ test "opening a magic link signs nobody in, and the button signs in" do
32
+ token = @user.generate_magic_link
33
+
34
+ # GET must not consume the link or create a session because mail scanners follow links.
35
+ get <%= identity.route_scope %>_magic_link_path(sid: token)
36
+ assert_response :success
37
+ assert cookies[:<%= identity.cookie_name %>].blank?
38
+
39
+ patch <%= identity.route_scope %>_magic_link_path(sid: token)
40
+ assert cookies[:<%= identity.cookie_name %>].present?
41
+ end
42
+
43
+ test "a used magic link cannot create another session" do
44
+ token = @user.generate_magic_link
45
+
46
+ patch <%= identity.route_scope %>_magic_link_path(sid: token)
47
+ delete <%= identity.route_scope %>_sign_out_path
48
+ patch <%= identity.route_scope %>_magic_link_path(sid: token)
49
+
50
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
51
+ assert cookies[:<%= identity.cookie_name %>].blank?
52
+ end
53
+
54
+ test "requesting a new link invalidates the old one" do
55
+ first = @user.generate_magic_link
56
+ second = @user.generate_magic_link
57
+ refute_equal first, second
58
+
59
+ patch <%= identity.route_scope %>_magic_link_path(sid: first)
60
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
61
+ assert cookies[:<%= identity.cookie_name %>].blank?
62
+
63
+ patch <%= identity.route_scope %>_magic_link_path(sid: second)
64
+ assert cookies[:<%= identity.cookie_name %>].present?
65
+ end
66
+
67
+ test "a missing cache entry rejects an otherwise valid link" do
68
+ token = @user.generate_magic_link
69
+ Rails.cache.delete(@user.magic_link_nonce_key)
70
+
71
+ get <%= identity.route_scope %>_magic_link_path(sid: token)
72
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
73
+
74
+ patch <%= identity.route_scope %>_magic_link_path(sid: token)
75
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
76
+ assert cookies[:<%= identity.cookie_name %>].blank?
77
+ end
78
+
79
+ test "an expired magic link is refused by both halves and signs nobody in" do
80
+ token = @user.generate_magic_link
81
+ travel <%= identity.config_reference %>.tokens.magic_link + 1.minute
82
+
83
+ get <%= identity.route_scope %>_magic_link_path(sid: token)
84
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
85
+
86
+ patch <%= identity.route_scope %>_magic_link_path(sid: token)
87
+ assert_redirected_to <%= identity.route_scope %>_sign_in_path
88
+ assert cookies[:<%= identity.cookie_name %>].blank?
89
+ end
90
+ end