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,102 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.module_name %>::SessionLifecycleTest < ActionDispatch::IntegrationTest
4
+ <%- if timeoutable? -%>
5
+ include ActiveSupport::Testing::TimeHelpers
6
+
7
+ <%- end -%>
8
+ setup { @user = <%= identity.plural %>(:bob) }
9
+ <%- if timeoutable? -%>
10
+
11
+ test "an idle session is destroyed and the owner is bounced to sign in" do
12
+ sign_in_as @user
13
+ get <%= identity.settings_helper_prefix %>_sessions_path
14
+ assert_response :success
15
+
16
+ travel <%= identity.config_reference %>.session.idle_timeout + 1.second do
17
+ get <%= identity.settings_helper_prefix %>_sessions_path
18
+ assert_response :redirect
19
+ end
20
+
21
+ assert_empty @user.sessions.reload, "the timed-out session should be destroyed"
22
+ end
23
+ <%- end -%>
24
+ <%- if last_seenable? && test_drivable_door? -%>
25
+
26
+ test "signing in stamps last_seen_at" do
27
+ assert_nil @user.last_seen_at
28
+ sign_in_as @user
29
+ assert_not_nil @user.reload.last_seen_at
30
+ end
31
+ <%- end -%>
32
+ <%- if rememberable? && password? -%>
33
+
34
+ test "'remember me' gives the session cookie an explicit lifetime" do
35
+ sign_in_as @user, remember: true
36
+ assert_match(/expires=/i, session_cookie_header,
37
+ "a remembered session cookie should persist past the browser session")
38
+ end
39
+
40
+ test "without 'remember me' the session cookie lasts only for the browser session" do
41
+ sign_in_as @user
42
+ assert_no_match(/expires=/i, session_cookie_header)
43
+ end
44
+ <%- end -%>
45
+ <%- if max_sessions_evict? -%>
46
+
47
+ test "a sign-in past the concurrent-session cap signs out the least-recently-active session" do
48
+ limit = <%= identity.config_reference %>.session.concurrent_limit
49
+ oldest = @user.sessions.create!
50
+ oldest.update_column(:updated_at, 1.year.ago)
51
+ (limit - 1).times { @user.sessions.create! }
52
+ assert_equal limit, @user.sessions.count
53
+
54
+ sign_in_as @user
55
+
56
+ assert_equal limit, @user.sessions.count, "the cap should still hold after a new sign-in"
57
+ assert_not <%= identity.session_class %>.exists?(oldest.id),
58
+ "the least-recently-active session should have been signed out"
59
+ end
60
+ <%- end -%>
61
+ <%- if max_sessions_prompt? -%>
62
+
63
+ test "a sign-in past the concurrent-session cap holds the user until they sign one out" do
64
+ limit = <%= identity.config_reference %>.session.concurrent_limit
65
+ limit.times { @user.sessions.create! }
66
+ sign_in_as @user
67
+ assert_not @user.within_session_limit?
68
+
69
+ get <%= identity.settings_helper_prefix %>_sessions_path
70
+ assert_response :success
71
+ get <%= identity.settings_helper_prefix %>_path
72
+ assert_redirected_to <%= identity.settings_helper_prefix %>_sessions_path
73
+
74
+ @user.sessions.order(:created_at).first.destroy!
75
+ assert @user.within_session_limit?
76
+ get <%= identity.settings_helper_prefix %>_path
77
+ assert_response :success, "released once back at the cap"
78
+ end
79
+ <%- end -%>
80
+
81
+ private
82
+
83
+ <%- if password? -%>
84
+ def sign_in_as(user, remember: false)
85
+ post <%= identity.route_scope %>_sign_in_path,
86
+ params: { <%= sign_in_field %>: user.<%= login_field %>, password: "quilted-lantern-moss-97", remember_me: (remember ? "1" : "0") }
87
+ end
88
+ <%- else -%>
89
+ def sign_in_as(user)
90
+ <%= test_sign_in_body %>
91
+ end
92
+ <%- end -%>
93
+ <%- if rememberable? && password? -%>
94
+
95
+ def session_cookie_header
96
+ # Rack may return Set-Cookie as one string or an array, so normalize both forms.
97
+ Array(response.headers["Set-Cookie"])
98
+ .flat_map { |line| line.to_s.split("\n") }
99
+ .find { |line| line.start_with?("<%= identity.cookie_name %>=") }.to_s
100
+ end
101
+ <%- end -%>
102
+ end
@@ -0,0 +1,61 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::ApiTokensControllerTest < ActionDispatch::IntegrationTest
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+
6
+ test "shows the secret once and stores only its digest" do
7
+ sign_in_as @user
8
+
9
+ assert_difference "@user.api_tokens.count", 1 do
10
+ post <%= identity.settings_helper_prefix %>_api_tokens_path,
11
+ params: { name: "Deployment"<%= sudo_test_params_pair %> }
12
+ end
13
+ assert_response :created
14
+ assert_includes response.headers["Cache-Control"], "no-store"
15
+ token = @user.api_tokens.last
16
+ secret = response.body[/ae11_[A-Za-z0-9_-]{43}/]
17
+ assert secret
18
+ assert_equal token, <%= identity.api_token_class %>.authenticate(secret)
19
+ refute_includes token.attributes.values, secret
20
+
21
+ get <%= identity.settings_helper_prefix %>_api_tokens_path
22
+ assert_response :success
23
+ refute_includes response.body, secret
24
+ assert_includes response.body, "Deployment"
25
+ end
26
+
27
+ test "an unauthenticated visitor cannot create a token" do
28
+ assert_no_difference "<%= identity.api_token_class %>.count" do
29
+ post <%= identity.settings_helper_prefix %>_api_tokens_path, params: { name: "Unexpected" }
30
+ end
31
+ assert_response :redirect
32
+ end
33
+
34
+ test "revoking a token invalidates it" do
35
+ sign_in_as @user
36
+ token, secret = <%= identity.api_token_class %>.issue!(user: @user, name: "Old")
37
+
38
+ assert_difference "@user.api_tokens.count", -1 do
39
+ delete <%= identity.settings_helper_prefix %>_api_token_path(token)
40
+ end
41
+ assert_redirected_to <%= identity.settings_helper_prefix %>_api_tokens_path
42
+ assert_nil <%= identity.api_token_class %>.authenticate(secret)
43
+ end
44
+
45
+ test "cannot revoke someone else's token" do
46
+ sign_in_as @user
47
+ stranger = <%= identity.plural %>(:alice)
48
+ token, = <%= identity.api_token_class %>.issue!(user: stranger, name: "Other")
49
+
50
+ assert_no_difference "<%= identity.api_token_class %>.count" do
51
+ delete <%= identity.settings_helper_prefix %>_api_token_path(token)
52
+ end
53
+ assert_response :not_found
54
+ end
55
+
56
+ private
57
+
58
+ def sign_in_as(user)
59
+ <%= test_sign_in_body %>
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::Authentications::EventsControllerTest < ActionDispatch::IntegrationTest
4
+ setup { @user = <%= identity.plural %>(:bob) }
5
+
6
+ test "you can see your own authentication events" do
7
+ sign_in_as @user
8
+ get <%= identity.settings_helper_prefix %>_authentications_events_path
9
+ assert_response :success
10
+ end
11
+
12
+ private
13
+
14
+ def sign_in_as(user)
15
+ <%= test_sign_in_body %>
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::EmailVerificationsControllerTest < ActionDispatch::IntegrationTest
4
+ include ActionMailer::TestHelper
5
+
6
+ setup { @user = <%= identity.plural %>(:bob) }
7
+
8
+ test "a resend goes to the address being verified, not the one on file" do
9
+ @user.update!(pending_email: "moved@example.com")
10
+ sign_in_as @user
11
+
12
+ assert_emails 1 do
13
+ post <%= identity.settings_helper_prefix %>_email_verification_path
14
+ end
15
+
16
+ assert_equal [ "moved@example.com" ], ActionMailer::Base.deliveries.last.to
17
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_email_path
18
+ end
19
+
20
+ test "a resend with nothing outstanding sends nothing" do
21
+ sign_in_as @user
22
+
23
+ assert_no_emails do
24
+ post <%= identity.settings_helper_prefix %>_email_verification_path
25
+ end
26
+
27
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_email_path
28
+ <%- if coy? -%>
29
+ assert_equal "We sent you another link.", flash[:notice],
30
+ "a build with nothing outstanding must answer exactly as one with a link in flight"
31
+ <%- end -%>
32
+ end
33
+
34
+ private
35
+
36
+ def sign_in_as(user)
37
+ <%= test_sign_in_body %>
38
+ end
39
+ end
@@ -0,0 +1,157 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::InvitationsControllerTest < ActionDispatch::IntegrationTest
4
+ include ActionMailer::TestHelper
5
+ include ActiveJob::TestHelper
6
+
7
+ setup { @user = <%= identity.plural %>(:bob) }
8
+ <%- if teams? -%>
9
+
10
+ test "choosing a team attaches the invitation to it" do
11
+ sign_in_as @user
12
+ post <%= identity.settings_helper_prefix %>_invitation_path,
13
+ params: { sent_to: <%= email_invitable? ? %("invitee-#{SecureRandom.hex(4)}@example.com") : %("+1415555#{rand(1000..9999)}") %>, team_id: <%= identity.teams_table %>(:acme).id }
14
+
15
+ assert_equal <%= identity.teams_table %>(:acme), <%= identity.invitation_class %>.last.team
16
+ end
17
+
18
+ test "choosing no team invites to the app itself" do
19
+ sign_in_as @user
20
+ post <%= identity.settings_helper_prefix %>_invitation_path,
21
+ params: { sent_to: <%= email_invitable? ? %("invitee-#{SecureRandom.hex(4)}@example.com") : %("+1415555#{rand(1000..9999)}") %>, team_id: "" }
22
+
23
+ assert_nil <%= identity.invitation_class %>.last.team
24
+ end
25
+
26
+ test "an existing account can be invited to a team" do
27
+ sign_in_as @user
28
+ existing = <%= identity.plural %>(:alice)
29
+ team = <%= identity.team_class %>.create!(name: "Beta")
30
+ team.memberships.create!(<%= identity.singular %>: @user)
31
+
32
+ post <%= identity.settings_helper_prefix %>_invitation_path,
33
+ params: { sent_to: existing.<%= email_invitable? ? "email" : "phone" %>, team_id: team.id }
34
+
35
+ assert <%= identity.invitation_class %>.exists?(sent_to: existing.<%= email_invitable? ? "email" : "phone" %>, team:)
36
+ end
37
+ <%- unless coy? -%>
38
+
39
+ test "you cannot invite someone already on the team" do
40
+ sign_in_as @user
41
+ existing = <%= identity.plural %>(:alice)
42
+
43
+ post <%= identity.settings_helper_prefix %>_invitation_path,
44
+ params: { sent_to: existing.<%= email_invitable? ? "email" : "phone" %>, team_id: <%= identity.teams_table %>(:acme).id }
45
+
46
+ assert_response :unprocessable_entity
47
+ assert_not <%= identity.invitation_class %>.exists?(sent_to: existing.<%= email_invitable? ? "email" : "phone" %>)
48
+ end
49
+ <%- end -%>
50
+
51
+ test "you cannot invite into a team you don't belong to" do
52
+ sign_in_as @user
53
+ post <%= identity.settings_helper_prefix %>_invitation_path,
54
+ params: { sent_to: <%= email_invitable? ? %("invitee-#{SecureRandom.hex(4)}@example.com") : %("+1415555#{rand(1000..9999)}") %>,
55
+ team_id: <%= identity.team_class %>.create!(name: "Someone Else's").id }
56
+
57
+ assert_response :not_found
58
+ end
59
+ <%- end -%>
60
+ <%- if email_invitable? -%>
61
+
62
+ test "sending an email invitation emails the invitee" do
63
+ sign_in_as @user
64
+ address = " Invitee-#{SecureRandom.hex(4)}@EXAMPLE.COM "
65
+ <%- if phone_invitable? -%>
66
+ assert_no_enqueued_jobs only: SmsDeliveryJob do
67
+ <%- end -%>
68
+ assert_enqueued_emails 1 do
69
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: address }
70
+ end
71
+ <%- if phone_invitable? -%>
72
+ end
73
+ <%- end -%>
74
+ assert_equal address.strip.downcase, <%= identity.invitation_class %>.last.sent_to
75
+ end
76
+ <%- end -%>
77
+ <%- if phone_invitable? -%>
78
+
79
+ test "sending a phone invitation texts the invitee" do
80
+ sign_in_as @user
81
+ suffix = format("%04d", rand(10_000))
82
+ submitted = "(415) 555-#{suffix}"
83
+ number = "+1415555#{suffix}"
84
+
85
+ invitation_delivery = ->(args) {
86
+ args.one? && args.first[:to] == number && args.first[:body].include?("invited you")
87
+ }
88
+ assert_no_enqueued_emails do
89
+ assert_enqueued_with(job: SmsDeliveryJob, args: invitation_delivery) do
90
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: submitted }
91
+ end
92
+ end
93
+ end
94
+ <%- end -%>
95
+ <%- if coy? -%>
96
+
97
+ test "the invite form answers identically for a destination on file and one that isn't" do
98
+ sign_in_as @user
99
+ <%- if phone_invitable? -%>
100
+
101
+ stranger = "+1415555#{rand(1000..9999)}"
102
+ assert_enqueued_with(job: SmsDeliveryJob) do
103
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: stranger }
104
+ end
105
+ stranger_notice = flash[:notice].sub(stranger, "ADDRESS")
106
+
107
+ assert_no_enqueued_jobs only: SmsDeliveryJob do
108
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: @user.phone }
109
+ end
110
+ on_file_notice = flash[:notice].sub(@user.phone, "ADDRESS")
111
+ <%- else -%>
112
+
113
+ stranger = "invitee-#{SecureRandom.hex(4)}@example.com"
114
+ assert_enqueued_emails 1 do
115
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: stranger }
116
+ end
117
+ stranger_notice = flash[:notice].sub(stranger, "ADDRESS")
118
+
119
+ assert_no_enqueued_emails do
120
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: @user.email }
121
+ end
122
+ on_file_notice = flash[:notice].sub(@user.email, "ADDRESS")
123
+ <%- end -%>
124
+
125
+ assert_equal stranger_notice, on_file_notice,
126
+ "the invite form must not reveal whether a destination already has an account"
127
+ end
128
+ <%- end -%>
129
+ <%- unless coy? -%>
130
+
131
+ test "you cannot invite an address that already has an account" do
132
+ sign_in_as @user
133
+ existing = <%= identity.plural %>(:alice)
134
+
135
+ post <%= identity.settings_helper_prefix %>_invitation_path, params: { sent_to: existing.<%= email_invitable? ? "email" : "phone" %> }
136
+
137
+ assert_response :unprocessable_entity
138
+ assert_not <%= identity.invitation_class %>.exists?(sent_to: existing.<%= email_invitable? ? "email" : "phone" %>)
139
+ end
140
+ <%- end -%>
141
+
142
+ test "deleting an account deletes the invitations it sent and the one it accepted" do
143
+ @user.sent_invitations.create!(sent_to: <%= email_invitable? ? %("sent-#{SecureRandom.hex(4)}@example.com") : %("+1415555#{rand(1000..4999)}") %>)
144
+ accepted = <%= identity.plural %>(:alice).sent_invitations.create!(sent_to: <%= email_invitable? ? %("accepted-#{SecureRandom.hex(4)}@example.com") : %("+1415555#{rand(5000..9999)}") %>)
145
+ accepted.update_column(:invitee_id, @user.id)
146
+
147
+ assert_difference -> { <%= identity.invitation_class %>.count }, -2 do
148
+ @user.destroy
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def sign_in_as(user)
155
+ <%= test_sign_in_body %>
156
+ end
157
+ end
@@ -0,0 +1,256 @@
1
+ require "test_helper"
2
+
3
+ class <%= identity.settings_module %>::ManagementTest < ActionDispatch::IntegrationTest
4
+ include ActionMailer::TestHelper
5
+
6
+ setup { @user = <%= identity.plural %>(:bob) }
7
+
8
+ test "authentication is required unless allowed" do
9
+ get <%= identity.settings_helper_prefix %>_path
10
+ assert_response :redirect
11
+
12
+ follow_redirect!
13
+ assert_response :success
14
+ end
15
+
16
+ test "you can sign one of your sessions out" do
17
+ sign_in_as @user
18
+ session = @user.sessions.reload.last
19
+ assert_difference -> { @user.sessions.count }, -1 do
20
+ delete <%= identity.settings_helper_prefix %>_session_path(session)
21
+ end
22
+ end
23
+ <%- if password? -%>
24
+
25
+ test "you can change your password" do
26
+ sign_in_as @user
27
+ patch <%= identity.settings_helper_prefix %>_password_path,
28
+ params: { password_challenge: "quilted-lantern-moss-97", password: "heron-basalt-drift-41", password_confirmation: "heron-basalt-drift-41" }
29
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_password_path
30
+ assert @user.reload.authenticate("heron-basalt-drift-41"), "the new password should verify"
31
+ end
32
+ <%- end -%>
33
+
34
+ test "deleting your account has a page of its own" do
35
+ sign_in_as @user
36
+ get <%= identity.settings_helper_prefix %>_user_path
37
+ assert_response :success
38
+ end
39
+ <%- if password? && !sudoable? -%>
40
+
41
+ test "deleting your account requires the current password" do
42
+ sign_in_as @user
43
+ assert_no_difference -> { <%= identity.class_name %>.count } do
44
+ delete <%= identity.settings_helper_prefix %>_user_path,
45
+ params: { confirmation: "DELETE ACCOUNT", password_challenge: "wrong-password" }
46
+ end
47
+ end
48
+ <%- end -%>
49
+
50
+ test "deleting your account takes the phrase, typed exactly" do
51
+ sign_in_as @user
52
+ assert_no_difference -> { <%= identity.class_name %>.count } do
53
+ delete <%= identity.settings_helper_prefix %>_user_path, params: { confirmation: "delete account"<%= settings_credential_pair %> }
54
+ end
55
+ assert_redirected_to <%= identity.settings_helper_prefix %>_user_url
56
+ end
57
+
58
+ test "you can delete your account" do
59
+ sign_in_as @user
60
+ assert_difference -> { <%= identity.class_name %>.count }, -1 do
61
+ delete <%= identity.settings_helper_prefix %>_user_path, params: { confirmation: "DELETE ACCOUNT"<%= settings_credential_pair %> }
62
+ end
63
+ end
64
+ <%- if email_changeable? -%>
65
+
66
+ test "you can change your email" do
67
+ sign_in_as @user
68
+ address = "changed-#{SecureRandom.hex(4)}@example.com"
69
+ patch <%= identity.settings_helper_prefix %>_email_path,
70
+ params: { email: address<%= settings_credential_pair %> }
71
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_email_path
72
+ <%- if email_verifiable? -%>
73
+ assert_not_equal address, @user.reload.email
74
+ assert_equal address, @user.pending_email
75
+ <%- else -%>
76
+ assert_equal address, @user.reload.email
77
+ <%- end -%>
78
+ end
79
+ <%- if email_verifiable? -%>
80
+
81
+ test "re-submitting the address already on file stages nothing" do
82
+ sign_in_as @user
83
+ assert_no_enqueued_emails do
84
+ patch <%= identity.settings_helper_prefix %>_email_path,
85
+ params: { email: <%= identity.plural %>(:bob).email<%= settings_credential_pair %> }
86
+ end
87
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_email_path
88
+ assert_nil @user.reload.pending_email
89
+ end
90
+ <%- end -%>
91
+ <%- if principals.email.required? || contactable_validation? -%>
92
+
93
+ test "a removal this account can't afford is refused in words" do
94
+ sign_in_as @user
95
+ <%- if contactable_validation? -%>
96
+ @user.update!(phone: nil)
97
+ <%- end -%>
98
+ patch <%= identity.settings_helper_prefix %>_email_path, params: { email: ""<%= settings_credential_pair %> }
99
+ assert_response :unprocessable_entity
100
+ <%- if contactable_validation? -%>
101
+ assert_includes response.parsed_body.text, "Add <%= channel_keys_article_phrase %>."
102
+ <%- else -%>
103
+ assert_includes response.parsed_body.text, "Email can't be blank"
104
+ <%- end -%>
105
+ assert_equal <%= identity.plural %>(:bob).email, @user.reload.email
106
+ end
107
+ <%- end -%>
108
+
109
+ <%- if coy? -%>
110
+ test "an already-registered email is answered exactly like a free one" do
111
+ sign_in_as @user
112
+ taken = <%= identity.plural %>(:alice).email
113
+ free = "free-#{SecureRandom.hex(4)}@example.com"
114
+
115
+ patch <%= identity.settings_helper_prefix %>_email_path,
116
+ params: { email: taken<%= settings_credential_pair %> }
117
+ taken_status, taken_location = response.status, response.location
118
+ follow_redirect!
119
+ taken_body = response.body.gsub(taken, "submitted@example.com")
120
+
121
+ patch <%= identity.settings_helper_prefix %>_email_path,
122
+ params: { email: free<%= settings_credential_pair %> }
123
+ assert_equal taken_status, response.status
124
+ assert_equal taken_location, response.location
125
+ follow_redirect!
126
+ assert_equal taken_body, response.body.gsub(free, "submitted@example.com"),
127
+ "a taken address and a free one must leave the same page behind, not just the same redirect"
128
+ end
129
+ <%- else -%>
130
+ test "changing to an already-registered email is called out" do
131
+ sign_in_as @user
132
+ patch <%= identity.settings_helper_prefix %>_email_path,
133
+ params: { email: <%= identity.plural %>(:alice).email<%= settings_credential_pair %> }
134
+ assert_response :unprocessable_entity
135
+ assert_equal <%= identity.plural %>(:bob).email, @user.reload.email
136
+ end
137
+ <%- end -%>
138
+ <%- end -%>
139
+ <%- principals.select { |p| p.channel? && p.permanent? }.each do |principal| -%>
140
+
141
+ test "there is no way to change a permanent <%= principal.column %>" do
142
+ sign_in_as @user
143
+ patch "#{<%= identity.settings_helper_prefix %>_path}/<%= principal.column %>", params: { <%= principal.column %>: "<%= principal.type == :email ? "moved@example.com" : "+14158264010" %>" }
144
+ assert_response :not_found
145
+ assert_equal <%= identity.plural %>(:bob).<%= principal.column %>, @user.reload.<%= principal.column %>
146
+ end
147
+ <%- end -%>
148
+ <%- if phone_changeable? -%>
149
+
150
+ test "you can change your phone" do
151
+ sign_in_as @user
152
+ patch <%= identity.settings_helper_prefix %>_phone_path,
153
+ params: { phone: "+1 415 826 4010"<%= settings_credential_pair %> }
154
+ <%- if phone_verifiable? -%>
155
+ assert_redirected_to new_<%= identity.settings_helper_prefix %>_phone_verification_path
156
+ assert_equal "+14158264010", @user.reload.pending_phone
157
+ assert @user.sms_challenge?(to: "+14158264010")
158
+ <%- else -%>
159
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_phone_path
160
+ assert_equal "+14158264010", @user.reload.phone
161
+ <%- end -%>
162
+ end
163
+ <%- if phone_verifiable? -%>
164
+
165
+ test "re-submitting the number already on file stages nothing" do
166
+ sign_in_as @user
167
+ patch <%= identity.settings_helper_prefix %>_phone_path,
168
+ params: { phone: <%= identity.plural %>(:bob).phone<%= settings_credential_pair %> }
169
+ assert_redirected_to edit_<%= identity.settings_helper_prefix %>_phone_path
170
+ assert_nil @user.reload.pending_phone
171
+ assert_not @user.sms_challenge?
172
+ end
173
+ <%- end -%>
174
+ <%- if contactable_validation? -%>
175
+
176
+ test "a phone removal this account can't afford is refused in words" do
177
+ sign_in_as @user
178
+ @user.update!(email: nil)
179
+ patch <%= identity.settings_helper_prefix %>_phone_path, params: { phone: ""<%= settings_credential_pair %> }
180
+ assert_response :unprocessable_entity
181
+ assert_includes response.parsed_body.text, "Add <%= channel_keys_article_phrase %>."
182
+ assert_equal <%= identity.plural %>(:bob).phone, @user.reload.phone
183
+ end
184
+ <%- end -%>
185
+
186
+ test "changing to an already-registered phone <%= coy? ? "reveals nothing" : "is called out" %>" do
187
+ sign_in_as @user
188
+ patch <%= identity.settings_helper_prefix %>_phone_path,
189
+ params: { phone: <%= identity.plural %>(:alice).phone<%= settings_credential_pair %> }
190
+ <%- if coy? -%>
191
+ assert_redirected_to new_<%= identity.settings_helper_prefix %>_phone_verification_path
192
+ assert_equal <%= identity.plural %>(:alice).phone, @user.reload.pending_phone
193
+ assert_not @user.sms_challenge?(to: <%= identity.plural %>(:alice).phone)
194
+ <%- else -%>
195
+ assert_response :unprocessable_entity
196
+ <%- end -%>
197
+ assert_not_equal <%= identity.plural %>(:alice).phone, @user.reload.phone
198
+ end
199
+ <%- end -%>
200
+
201
+ <%- if password_removable? -%>
202
+ <%- if (omniauth? || passkey?) && !magic_link? && !sms_code? -%>
203
+ test "the last way into the account is not removable" do
204
+ sign_in_as @user
205
+ assert_equal [ :password ], @user.sign_in_methods, "fixture should hold only a password"
206
+
207
+ assert_no_changes -> { @user.reload.password_digest } do
208
+ delete <%= identity.settings_helper_prefix %>_password_path
209
+ end
210
+ assert_match "only way into your account", flash[:alert].to_s
211
+ end
212
+ <%- end -%>
213
+
214
+ test "a password can be given up once something else can let you in" do
215
+ <%= password_optional_second_door_setup %>
216
+ sign_in_as @user
217
+
218
+ delete <%= identity.settings_helper_prefix %>_password_path
219
+
220
+ assert_nil @user.reload.password_digest
221
+ assert_not @user.authenticate("quilted-lantern-moss-97"), "a removed password must stop working"
222
+ end
223
+
224
+ test "a password can be set again afterwards, with no current password to pose" do
225
+ <%= password_optional_second_door_setup %>
226
+ sign_in_as @user
227
+ @user.update_columns(password_digest: nil<%= password_rotatable? ? ", password_changed_at: nil" : "" %>)
228
+ <%- if webauthn_credentials? -%>
229
+ @user.webauthn_credentials.create!(name: "Lost key", external_id: "ext-lost-key", public_key: "pk"<%= factor_attr(:first_factor) %>)
230
+ <%- end -%>
231
+
232
+ post <%= identity.settings_helper_prefix %>_password_path,
233
+ params: { password: "heron-basalt-drift-41", password_confirmation: "heron-basalt-drift-41" }
234
+
235
+ assert @user.reload.authenticate("heron-basalt-drift-41"), "the new password should verify"
236
+ end
237
+ <%- end -%>
238
+ <%- if password_nullable? -%>
239
+
240
+ test "a rejected first password is asked for again, not challenged" do
241
+ sign_in_as @user
242
+ @user.update_columns(password_digest: nil<%= password_rotatable? ? ", password_changed_at: nil" : "" %>)
243
+
244
+ post <%= identity.settings_helper_prefix %>_password_path, params: { password: "short", password_confirmation: "short" }
245
+
246
+ assert_response :unprocessable_entity
247
+ assert_no_match(/Current password/, response.parsed_body.text)
248
+ assert_nil @user.reload.password_digest
249
+ end
250
+ <%- end -%>
251
+ private
252
+
253
+ def sign_in_as(user)
254
+ <%= test_sign_in_body %>
255
+ end
256
+ end