From d0afd86a81e735e2cc2277d2cb0ec993faeb3766 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 11:04:39 +0200 Subject: [PATCH 01/25] PC-17: Add and config Devise gem --- Gemfile | 2 + Gemfile.lock | 14 ++ app/views/layouts/application.html.erb | 2 + config/initializers/devise.rb | 313 +++++++++++++++++++++++++ config/locales/devise.en.yml | 65 +++++ 5 files changed, 396 insertions(+) create mode 100644 config/initializers/devise.rb create mode 100644 config/locales/devise.en.yml diff --git a/Gemfile b/Gemfile index cb2594c..bb8d415 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,8 @@ ruby "3.4.1" gem "bootsnap", require: false # Bundle and process CSS [https://github.com/rails/cssbundling-rails] gem "cssbundling-rails" +# Flexible authentication solution for Rails with Warden +gem "devise", "~> 4.9" # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" # Build JSON APIs with ease [https://github.com/rails/jbuilder] diff --git a/Gemfile.lock b/Gemfile.lock index b08579b..078e794 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM uri (>= 0.13.1) ast (2.4.2) base64 (0.2.0) + bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) benchmark (0.4.0) bigdecimal (3.1.9) @@ -92,6 +93,12 @@ GEM debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) dotenv (3.1.7) drb (2.2.1) ed25519 (1.3.0) @@ -168,6 +175,7 @@ GEM racc (~> 1.4) nokogiri (1.18.1-x86_64-linux-musl) racc (~> 1.4) + orm_adapter (0.5.0) ostruct (0.6.1) parallel (1.26.3) parser (3.3.6.0) @@ -230,6 +238,9 @@ GEM regexp_parser (2.10.0) reline (0.6.0) io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) rubocop (1.70.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -301,6 +312,8 @@ GEM unicode-emoji (4.0.4) uri (1.0.2) useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -328,6 +341,7 @@ DEPENDENCIES brakeman cssbundling-rails debug + devise (~> 4.9) dotenv jbuilder jsbundling-rails diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d9d081d..1d43726 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -24,6 +24,8 @@ +

<%= notice %>

+

<%= alert %>

<%= yield %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..3fd39be --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = 'c3516357e7fe1d66be7607272ef2dc254bd49269715f05e2f0dcc3a70a363ac858ea33fa3865b613cbfd7472d044d1614b1b34b07479be34a8c49d941cd215b0' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '2824fdff80ba660e2ae30865e8a619d65c5771bb1284c1da29018e4b43140afd6539fcbbc56eeaba8776bb9cf448423972b9c332044682b3f825c09acdae090a' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html, :turbo_stream] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Hotwire/Turbo configuration + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found` respectively, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true +end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000..260e1c4 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" From d7618a951473160d2a93487bebe801ff0be2e2f5 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 11:08:46 +0200 Subject: [PATCH 02/25] PC-17: Add user model with devise --- app/models/user.rb | 6 +++ config/routes.rb | 1 + .../20250114090731_devise_create_users.rb | 45 +++++++++++++++++++ db/schema.rb | 29 ++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20250114090731_devise_create_users.rb create mode 100644 db/schema.rb diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..4756799 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/config/routes.rb b/config/routes.rb index 2e9cef3..bf8db4a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + devise_for :users # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20250114090731_devise_create_users.rb b/db/migrate/20250114090731_devise_create_users.rb new file mode 100644 index 0000000..2d56be0 --- /dev/null +++ b/db/migrate/20250114090731_devise_create_users.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class DeviseCreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.string :name + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..712793b --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,29 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.0].define(version: 2025_01_14_090731) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + end +end From b6b4342433ec57af3c2e95d17b59db8a9fdab0d7 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 12:16:34 +0200 Subject: [PATCH 03/25] PC-17: Add name parameter to Devise --- app/controllers/application_controller.rb | 9 ++++++++- app/models/user.rb | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db2..a24024f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,11 @@ class ApplicationController < ActionController::Base - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + before_action :configure_permitted_parameters, if: :devise_controller? allow_browser versions: :modern + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up, keys: %i[name]) + devise_parameter_sanitizer.permit(:account_update, keys: %i[name]) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 4756799..dcb5f12 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,4 +3,6 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validates :name, presence: true end From 61ffaf4e0e15ee395e17af3352b4e5a78f430a26 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 14:36:42 +0200 Subject: [PATCH 04/25] PC-17: Add user model validations --- app/models/user.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index dcb5f12..1d8c3e4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,6 +3,14 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + NAME_FORMAT = /\A[a-zA-Z0-9_]+\z/ + EMAIL_FORMAT = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + PASSWORD_SYMBOL_FORMAT = /\A(?=.*[^\w\s])[^\s]*\z/ + PASSWORD_REPEATED_CHAR_FORMAT = /\A(?!.*(.)\1\1).*\z/ - validates :name, presence: true + + validates :name, presence: true, format: { with: NAME_FORMAT, message: "must be alphanumeric" } + validates :email, presence: true, uniqueness: true, format: { with: EMAIL_FORMAT, message: "must be a valid email format" } + validates :password, length: { minimum: 8 }, format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } + validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } end From 80f87a13a68a56ceeb22a4c5c0337535e7808010 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 15:00:04 +0200 Subject: [PATCH 05/25] PC-17: Configure Devise to use ENV variable --- config/environments/development.rb | 2 ++ config/environments/test.rb | 2 ++ config/initializers/devise.rb | 17 ++++++----------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 4cc21c4..ec52d98 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -69,4 +69,6 @@ # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! + + config.secret_key_base = ENV["DEVISE_SECRET_KEY"] end diff --git a/config/environments/test.rb b/config/environments/test.rb index c2095b1..9c2cd44 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -50,4 +50,6 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + + config.secret_key_base = ENV["DEVISE_SECRET_KEY"] end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 3fd39be..19eafe8 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -9,12 +9,7 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| - # The secret key used by Devise. Devise uses this key to generate - # random tokens. Changing this key will render invalid all existing - # confirmation, reset password and unlock tokens in the database. - # Devise will use the `secret_key_base` as its `secret_key` - # by default. You can change it below and use your own secret key. - # config.secret_key = 'c3516357e7fe1d66be7607272ef2dc254bd49269715f05e2f0dcc3a70a363ac858ea33fa3865b613cbfd7472d044d1614b1b34b07479be34a8c49d941cd215b0' + config.secret_key = ENV["DEVISE_SECRET_KEY"] # ==> Controller configuration # Configure the parent class to the devise controllers. @@ -24,7 +19,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' @@ -36,7 +31,7 @@ # Load and configure the ORM. Supports :active_record (default) and # :mongoid (bson_ext recommended) by default. Other ORMs may be # available as additional gems. - require 'devise/orm/active_record' + require "devise/orm/active_record" # ==> Configuration for any authentication mechanism # Configure which keys are used when authenticating a user. The default is @@ -58,12 +53,12 @@ # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [ :email ] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + config.strip_whitespace_keys = [ :email ] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the @@ -97,7 +92,7 @@ # Notice that if you are skipping storage for all authentication paths, you # may want to disable generating routes to Devise's sessions controller by # passing skip: :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] + config.skip_session_storage = [ :http_auth ] # By default, Devise cleans up the CSRF token on authentication to # avoid CSRF token fixation attacks. This means that, when using AJAX From 185c40eadd944a9f56afd18a95b8b859fa0d7b41 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Tue, 14 Jan 2025 15:36:25 +0200 Subject: [PATCH 06/25] Fix rubocop offenses --- app/models/user.rb | 7 ++++--- config/initializers/devise.rb | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1d8c3e4..8dbe562 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,9 +8,10 @@ class User < ApplicationRecord PASSWORD_SYMBOL_FORMAT = /\A(?=.*[^\w\s])[^\s]*\z/ PASSWORD_REPEATED_CHAR_FORMAT = /\A(?!.*(.)\1\1).*\z/ - validates :name, presence: true, format: { with: NAME_FORMAT, message: "must be alphanumeric" } - validates :email, presence: true, uniqueness: true, format: { with: EMAIL_FORMAT, message: "must be a valid email format" } - validates :password, length: { minimum: 8 }, format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } + validates :email, presence: true, uniqueness: true, + format: { with: EMAIL_FORMAT, message: "must be a valid email format" } + validates :password, length: { minimum: 8 }, + format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 19eafe8..d04f03d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -121,7 +121,9 @@ config.stretches = Rails.env.test? ? 1 : 12 # Set up a pepper to generate the hashed password. - # config.pepper = '2824fdff80ba660e2ae30865e8a619d65c5771bb1284c1da29018e4b43140afd6539fcbbc56eeaba8776bb9cf448423972b9c332044682b3f825c09acdae090a' + # config.pepper = + # '2824fdff80ba660e2ae30865e8a619d65c5771bb1284c1da29018e4b43 + # 140afd6539fcbbc56eeaba8776bb9cf448423972b9c332044682b3f825c09acdae090a' # Send a notification to the original email when the user's email is changed. # config.send_email_changed_notification = false From 8690ab4c7fa6e064bbde93bf76a869ef8818a5af Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 13:25:06 +0200 Subject: [PATCH 07/25] PC-17: Configure RSpec setup --- .rspec | 2 + Gemfile | 11 ++++- Gemfile.lock | 31 ++++++++++++ spec/rails_helper.rb | 71 ++++++++++++++++++++++++++++ spec/spec_helper.rb | 94 +++++++++++++++++++++++++++++++++++++ spec/support/factory_bot.rb | 3 ++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 .rspec create mode 100644 spec/rails_helper.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/factory_bot.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..775c62b --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--require spec_helper +--format documentation \ No newline at end of file diff --git a/Gemfile b/Gemfile index 8122b33..5841cc6 100644 --- a/Gemfile +++ b/Gemfile @@ -44,11 +44,20 @@ group :development, :test do gem "brakeman", require: false # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" - + # Loads environment variables from '.env' gem "dotenv" + # Provides fixtures replacement for easier test object creation + gem "factory_bot_rails" + # Generates fake data for testing + gem "faker" + # Testing framework for Rails applications, providing tools for writing and running tests + gem "rspec-rails" + # Rubocop Ruby on Rails Style gem "rubocop-rails", require: false + # Provides simple and clean one-liner tests for Rails models, controllers, and other components. + gem "shoulda-matchers" end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 31e58df..4b8ca01 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,12 +99,20 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) + diff-lcs (1.5.1) dotenv (3.1.7) drb (2.2.1) ed25519 (1.3.0) erubi (1.13.1) et-orbi (1.2.11) tzinfo + factory_bot (6.5.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) + railties (>= 5.0.0) + faker (3.5.1) + i18n (>= 1.8.11, < 2) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -243,6 +251,23 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (7.1.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.2) rubocop (1.70.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -262,6 +287,8 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) securerandom (0.4.1) + shoulda-matchers (6.4.0) + activesupport (>= 5.2.0) solid_cable (3.0.5) actioncable (>= 7.2) activejob (>= 7.2) @@ -336,6 +363,8 @@ DEPENDENCIES debug devise (~> 4.9) dotenv + factory_bot_rails + faker jbuilder jsbundling-rails kamal @@ -343,7 +372,9 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.0.1) + rspec-rails rubocop-rails + shoulda-matchers solid_cable solid_cache solid_queue diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..d9a5380 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,71 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file +# that will avoid rails generators crashing because migrations haven't been run yet +# return unless Rails.env.test? +require 'rspec/rails' +require_relative 'support/factory_bot' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails uses metadata to mix in different behaviours to your tests, + # for example enabling you to call `get` and `post` in request specs. e.g.: + # + # RSpec.describe UsersController, type: :request do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/7-0/rspec-rails + # + # You can also this infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..327b58e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..c7890e4 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end From 0a048b36b79e9d2ae87ca70ba5a6805d91b46216 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 13:46:40 +0200 Subject: [PATCH 08/25] PC-17: Add Shoulda Matchers configuration --- spec/rails_helper.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d9a5380..f73180f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -69,3 +69,15 @@ # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end + +RSpec.configure do |config| + config.include Devise::Test::IntegrationHelpers, type: :request + config.include Devise::Test::IntegrationHelpers, type: :feature +end From 4ebfabe3f345d29c75930772bc2823cbe66c0ebe Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 14:40:15 +0200 Subject: [PATCH 09/25] PC-17: Add specs for User model --- spec/factories/users.rb | 8 ++++++++ spec/models/user_spec.rb | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 spec/factories/users.rb create mode 100644 spec/models/user_spec.rb diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..a028154 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :user do + email { Faker::Internet.unique.email } + name { Faker::Internet.username.gsub(/[^a-zA-Z0-9_]/, '') } + password { '@password' } + password_confirmation { '@password' } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..f0adbf5 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe User, type: :model do + let(:user) { create(:user) } + + it 'is expect to have valid factory' do + expect(user).to be_valid + end + + context 'validations' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } + it { is_expected.not_to allow_value('bob').for(:email).with_message('must be a valid email format') } + it { is_expected.not_to allow_value('bob@bob').for(:email).with_message('must be a valid email format') } + it { is_expected.not_to allow_value('bob@gmail,com').for(:email).with_message('must be a valid email format') } + it { is_expected.not_to allow_value('bob@.com').for(:email).with_message('must be a valid email format') } + it { is_expected.not_to allow_value('bob bob@gmail.com').for(:email).with_message('must be a valid email format') } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + it { is_expected.not_to allow_value('@bob@').for(:name).with_message('must be alphanumeric') } + + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + + it 'does not allow duplicate emails' do + create(:user, email: 'duplicate@example.com') + duplicate_user = build(:user, email: 'duplicate@example.com') + expect(duplicate_user).not_to be_valid + expect(duplicate_user.errors[:email]).to include('has already been taken') + end + end +end From ab8f92c82dc1bf0b80eb4069ad0dfbd2355af806 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 15 Jan 2025 14:49:39 +0200 Subject: [PATCH 10/25] PC-17: Update .rubocop.yml and add gem rubocop-rspec_rails --- .rubocop.yml | 11 ++++++++++- Gemfile | 2 ++ Gemfile.lock | 6 ++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 36254ea..baeb8e9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ # Add rules from rubocop-rails require: - rubocop-rails + - rubocop-rspec_rails # Overwrite or add rules to create your own house style # @@ -30,4 +31,12 @@ Layout/SpaceInsidePercentLiteralDelimiters: Enabled: false Bundler/OrderedGems: - Enabled: true \ No newline at end of file + Enabled: true + +Metrics/MethodLength: + Exclude: + - db/migrate/*.rb + +Metrics/BlockLength: + Exclude: + - spec/**/* \ No newline at end of file diff --git a/Gemfile b/Gemfile index 5841cc6..a9a4d0e 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,8 @@ group :development, :test do # Rubocop Ruby on Rails Style gem "rubocop-rails", require: false + gem "rubocop-rspec_rails", require: false + # Provides simple and clean one-liner tests for Rails models, controllers, and other components. gem "shoulda-matchers" end diff --git a/Gemfile.lock b/Gemfile.lock index 4b8ca01..e320bfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -285,6 +285,11 @@ GEM rack (>= 1.1) rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rspec (3.3.0) + rubocop (~> 1.61) + rubocop-rspec_rails (2.30.0) + rubocop (~> 1.61) + rubocop-rspec (~> 3, >= 3.0.1) ruby-progressbar (1.13.0) securerandom (0.4.1) shoulda-matchers (6.4.0) @@ -374,6 +379,7 @@ DEPENDENCIES rails (~> 8.0.1) rspec-rails rubocop-rails + rubocop-rspec_rails shoulda-matchers solid_cable solid_cache From 919f5ad58ff76415f9a882e8d19756864195c831 Mon Sep 17 00:00:00 2001 From: IvanRuskevych Date: Wed, 15 Jan 2025 13:25:10 +0000 Subject: [PATCH 11/25] Add: validation for the password (maximum 128) and test for it. Add: CDN link for simple styles. --- app/models/user.rb | 4 ++-- app/views/home/app.html.erb | 2 +- app/views/layouts/application.html.erb | 3 +++ spec/models/user_spec.rb | 5 +++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 8dbe562..c6a5ed7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,7 +11,7 @@ class User < ApplicationRecord validates :name, presence: true, format: { with: NAME_FORMAT, message: "must be alphanumeric" } validates :email, presence: true, uniqueness: true, format: { with: EMAIL_FORMAT, message: "must be a valid email format" } - validates :password, length: { minimum: 8 }, + validates :password, length: { minimum: 8, maximum: 128 }, format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } -end +end \ No newline at end of file diff --git a/app/views/home/app.html.erb b/app/views/home/app.html.erb index a398a64..62851c5 100644 --- a/app/views/home/app.html.erb +++ b/app/views/home/app.html.erb @@ -1 +1 @@ -Hello Clever Calculator! \ No newline at end of file +

Hello Clever Calculator!

\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 1d43726..0b538a5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -21,6 +21,9 @@ <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + + <%#= TODO: Delete this styles link when the markup for Clever Calculator is ready %> + <%= stylesheet_link_tag "http://cdn.simplecss.org/simple.css" %> diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f0adbf5..c33bae2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -31,6 +31,11 @@ is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password).with_message("is too long (maximum is 128 characters)") + end + it 'does not allow duplicate emails' do create(:user, email: 'duplicate@example.com') duplicate_user = build(:user, email: 'duplicate@example.com') From 63f83cd94f6e582e56bdbd2a5f84652b7c7d4918 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 16:19:14 +0200 Subject: [PATCH 12/25] PC-17: Fixed lint errors --- app/models/user.rb | 2 +- spec/models/user_spec.rb | 3 ++- spec/spec_helper.rb | 31 +++++++++++++++---------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index c6a5ed7..23e2e55 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,4 +14,4 @@ class User < ApplicationRecord validates :password, length: { minimum: 8, maximum: 128 }, format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } -end \ No newline at end of file +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c33bae2..a2ce02f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,7 +33,8 @@ it 'does not allow a password longer than 128 characters' do long_password = 'a' * 129 - is_expected.not_to allow_value(long_password).for(:password).with_message("is too long (maximum is 128 characters)") + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') end it 'does not allow duplicate emails' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58e..500862a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,51 +44,50 @@ # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # This allows you to limit a spec run to individual examples or groups # you care about by tagging them with `:focus` metadata. When nothing # is tagged with `:focus`, all examples get run. RSpec also provides # aliases for `it`, `describe`, and `context` that include `:focus` # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus + # config.filter_run_when_matching :focus # Allows RSpec to persist some state between runs in order to support # the `--only-failures` and `--next-failure` CLI options. We recommend # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" + # config.example_status_persistence_file_path = "spec/examples.txt" # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ - config.disable_monkey_patching! + # config.disable_monkey_patching! # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end + # if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. - config.profile_examples = 10 + # config.profile_examples = 10 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 - config.order = :random + # config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. - Kernel.srand config.seed -=end + # Kernel.srand config.seed end From 84e08062b4374be656bb218cb1cc6b7cf047d6bb Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 16:52:23 +0200 Subject: [PATCH 13/25] PC-17: set mailer sender to use environment variable --- config/initializers/devise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d04f03d..9e15013 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -19,7 +19,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" + config.mailer_sender = ENV['DEFAULT_FROM_ADDRESS'] # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' From 8501157dfc41b38224efaf492374598a6823897a Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Wed, 15 Jan 2025 22:46:32 +0200 Subject: [PATCH 14/25] PC-17: fixed email and name validation --- app/models/user.rb | 6 ++---- spec/models/user_spec.rb | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 23e2e55..f563aa3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,14 +3,12 @@ class User < ApplicationRecord # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable - NAME_FORMAT = /\A[a-zA-Z0-9_]+\z/ - EMAIL_FORMAT = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i PASSWORD_SYMBOL_FORMAT = /\A(?=.*[^\w\s])[^\s]*\z/ PASSWORD_REPEATED_CHAR_FORMAT = /\A(?!.*(.)\1\1).*\z/ - validates :name, presence: true, format: { with: NAME_FORMAT, message: "must be alphanumeric" } + validates :name, presence: true validates :email, presence: true, uniqueness: true, - format: { with: EMAIL_FORMAT, message: "must be a valid email format" } + format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email format" } validates :password, length: { minimum: 8, maximum: 128 }, format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a2ce02f..e2e999d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -11,14 +11,12 @@ it { is_expected.to validate_presence_of(:email) } it { is_expected.to allow_value('bob@example.com').for(:email) } it { is_expected.not_to allow_value('bob').for(:email).with_message('must be a valid email format') } - it { is_expected.not_to allow_value('bob@bob').for(:email).with_message('must be a valid email format') } it { is_expected.not_to allow_value('bob@gmail,com').for(:email).with_message('must be a valid email format') } it { is_expected.not_to allow_value('bob@.com').for(:email).with_message('must be a valid email format') } it { is_expected.not_to allow_value('bob bob@gmail.com').for(:email).with_message('must be a valid email format') } it { is_expected.to validate_presence_of(:name) } it { is_expected.to allow_value('bob').for(:name) } - it { is_expected.not_to allow_value('@bob@').for(:name).with_message('must be alphanumeric') } it { is_expected.to validate_presence_of(:password) } it { is_expected.to validate_length_of(:password).is_at_least(8) } From ca22fe60c49bf04103de3a0e782904a4c19ef1da Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Thu, 16 Jan 2025 10:15:05 +0200 Subject: [PATCH 15/25] PC-17: Remove :registerable User Devise configuration --- app/models/user.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index f563aa3..da3630c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,7 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :validatable + devise :database_authenticatable, :recoverable, :rememberable, :validatable PASSWORD_SYMBOL_FORMAT = /\A(?=.*[^\w\s])[^\s]*\z/ PASSWORD_REPEATED_CHAR_FORMAT = /\A(?!.*(.)\1\1).*\z/ From 2d0dacb905246ab673bc3bf9e7dcec8d288d8175 Mon Sep 17 00:00:00 2001 From: Volodymyr Pivtoranis Date: Thu, 16 Jan 2025 10:22:56 +0200 Subject: [PATCH 16/25] PC-17: Remove unused configure_permitted_parameters --- app/controllers/application_controller.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a24024f..d002aeb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,11 +1,3 @@ class ApplicationController < ActionController::Base - before_action :configure_permitted_parameters, if: :devise_controller? allow_browser versions: :modern - - protected - - def configure_permitted_parameters - devise_parameter_sanitizer.permit(:sign_up, keys: %i[name]) - devise_parameter_sanitizer.permit(:account_update, keys: %i[name]) - end end From 78ab0959fe2ef2d8391c4cd2b99c18783d797a25 Mon Sep 17 00:00:00 2001 From: IvanRuskevych Date: Thu, 16 Jan 2025 08:55:57 +0000 Subject: [PATCH 17/25] create AdminUser model and Add validations --- app/models/admin_user.rb | 16 +++++++ config/routes.rb | 2 + ...0250116084347_devise_create_admin_users.rb | 45 +++++++++++++++++++ db/schema.rb | 15 ++++++- spec/factories/admin_users.rb | 5 +++ spec/models/admin_user_spec.rb | 5 +++ 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 app/models/admin_user.rb create mode 100644 db/migrate/20250116084347_devise_create_admin_users.rb create mode 100644 spec/factories/admin_users.rb create mode 100644 spec/models/admin_user_spec.rb diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb new file mode 100644 index 0000000..57ff5ea --- /dev/null +++ b/app/models/admin_user.rb @@ -0,0 +1,16 @@ +class AdminUser < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :recoverable, + :rememberable, :validatable + + PASSWORD_SYMBOL_FORMAT = /\A(?=.*[^\w\s])[^\s]*\z/ + PASSWORD_REPEATED_CHAR_FORMAT = /\A(?!.*(.)\1\1).*\z/ + + validates :name, presence: true + validates :email, presence: true, uniqueness: true, + format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email format" } + validates :password, length: { minimum: 8, maximum: 128 }, + format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } + validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } +end diff --git a/config/routes.rb b/config/routes.rb index bf8db4a..26ddc74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do devise_for :users + devise_for :admin_users + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20250116084347_devise_create_admin_users.rb b/db/migrate/20250116084347_devise_create_admin_users.rb new file mode 100644 index 0000000..2fa7f15 --- /dev/null +++ b/db/migrate/20250116084347_devise_create_admin_users.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class DeviseCreateAdminUsers < ActiveRecord::Migration[8.0] + def change + create_table :admin_users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + # t.integer :sign_in_count, default: 0, null: false + # t.datetime :current_sign_in_at + # t.datetime :last_sign_in_at + # t.string :current_sign_in_ip + # t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.string :name + + t.timestamps null: false + end + + add_index :admin_users, :email, unique: true + add_index :admin_users, :reset_password_token, unique: true + # add_index :admin_users, :confirmation_token, unique: true + # add_index :admin_users, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 712793b..4b386bd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,23 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_01_14_090731) do +ActiveRecord::Schema[8.0].define(version: 2025_01_16_084347) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + create_table "admin_users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_admin_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true + end + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false diff --git a/spec/factories/admin_users.rb b/spec/factories/admin_users.rb new file mode 100644 index 0000000..e14039b --- /dev/null +++ b/spec/factories/admin_users.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :admin_user do + + end +end diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb new file mode 100644 index 0000000..aec79f9 --- /dev/null +++ b/spec/models/admin_user_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AdminUser, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 03bdf6a4ba4832617bc240207befc9b75aca9f3e Mon Sep 17 00:00:00 2001 From: IvanRuskevych Date: Thu, 16 Jan 2025 09:49:43 +0000 Subject: [PATCH 18/25] PC-20: fix lint errors --- app/models/admin_user.rb | 4 ++-- spec/factories/admin_users.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb index 57ff5ea..6b40d42 100644 --- a/app/models/admin_user.rb +++ b/app/models/admin_user.rb @@ -9,8 +9,8 @@ class AdminUser < ApplicationRecord validates :name, presence: true validates :email, presence: true, uniqueness: true, - format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email format" } + format: { with: URI::MailTo::EMAIL_REGEXP, message: "must be a valid email format" } validates :password, length: { minimum: 8, maximum: 128 }, - format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } + format: { with: PASSWORD_SYMBOL_FORMAT, message: "must contain at least one symbol" } validates :password, format: { with: PASSWORD_REPEATED_CHAR_FORMAT, message: "must not contain repeated characters" } end diff --git a/spec/factories/admin_users.rb b/spec/factories/admin_users.rb index e14039b..410ea52 100644 --- a/spec/factories/admin_users.rb +++ b/spec/factories/admin_users.rb @@ -1,5 +1,4 @@ FactoryBot.define do factory :admin_user do - end end From a0ee7e1debae75c24c559b318c2b508bc0222f4b Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:13:45 +0200 Subject: [PATCH 19/25] Refactor model tests: User and AdminUser - Added shared examples for testing common validations across models. - Refactored tests for User and AdminUser models: - Moved common validation tests to shared examples ("validatable user"). - Reduced code duplication. - Updated password tests for better readability and accuracy. --- spec/factories/admin_users.rb | 4 ++ spec/models/admin_user_spec.rb | 8 +++- spec/models/user_spec.rb | 40 ++-------------- spec/rails_helper.rb | 2 + .../shared_examples/validatable_user.rb | 48 +++++++++++++++++++ 5 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 spec/support/shared_examples/validatable_user.rb diff --git a/spec/factories/admin_users.rb b/spec/factories/admin_users.rb index 410ea52..c1b00ad 100644 --- a/spec/factories/admin_users.rb +++ b/spec/factories/admin_users.rb @@ -1,4 +1,8 @@ FactoryBot.define do factory :admin_user do + email { Faker::Internet.unique.email } + name { Faker::Internet.username.gsub(/[^a-zA-Z0-9_]/, '') } + password { '@password' } + password_confirmation { '@password' } end end diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb index aec79f9..ad6187b 100644 --- a/spec/models/admin_user_spec.rb +++ b/spec/models/admin_user_spec.rb @@ -1,5 +1,11 @@ require 'rails_helper' RSpec.describe AdminUser, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + subject { build(:admin_user) } + + it 'is expect to have valid factory' do + expect(subject).to be_valid + end + + it_behaves_like 'validatable user' end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e2e999d..6d75897 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,45 +1,11 @@ require 'rails_helper' RSpec.describe User, type: :model do - let(:user) { create(:user) } + subject { build(:user) } it 'is expect to have valid factory' do - expect(user).to be_valid + expect(subject).to be_valid end - context 'validations' do - it { is_expected.to validate_presence_of(:email) } - it { is_expected.to allow_value('bob@example.com').for(:email) } - it { is_expected.not_to allow_value('bob').for(:email).with_message('must be a valid email format') } - it { is_expected.not_to allow_value('bob@gmail,com').for(:email).with_message('must be a valid email format') } - it { is_expected.not_to allow_value('bob@.com').for(:email).with_message('must be a valid email format') } - it { is_expected.not_to allow_value('bob bob@gmail.com').for(:email).with_message('must be a valid email format') } - - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to allow_value('bob').for(:name) } - - it { is_expected.to validate_presence_of(:password) } - it { is_expected.to validate_length_of(:password).is_at_least(8) } - it { is_expected.to allow_value('@password').for(:password) } - it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } - it { - is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') - } - it { - is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') - } - - it 'does not allow a password longer than 128 characters' do - long_password = 'a' * 129 - is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') - end - - it 'does not allow duplicate emails' do - create(:user, email: 'duplicate@example.com') - duplicate_user = build(:user, email: 'duplicate@example.com') - expect(duplicate_user).not_to be_valid - expect(duplicate_user.errors[:email]).to include('has already been taken') - end - end + it_behaves_like 'validatable user' end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index f73180f..299fb40 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -81,3 +81,5 @@ config.include Devise::Test::IntegrationHelpers, type: :request config.include Devise::Test::IntegrationHelpers, type: :feature end + +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } diff --git a/spec/support/shared_examples/validatable_user.rb b/spec/support/shared_examples/validatable_user.rb new file mode 100644 index 0000000..1ccc1fe --- /dev/null +++ b/spec/support/shared_examples/validatable_user.rb @@ -0,0 +1,48 @@ +RSpec.shared_examples 'validatable user' do + context 'validations for name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + end + + context 'validations for email' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } + + # Testing a list of invalid email addresses (example as different from the following 4) + invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] + invalid_emails.each do |email| + it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + end + # it { is_expected.not_to allow_value('bob').for(:email).with_message('must be a valid email format') } + # it { is_expected.not_to allow_value('bob@gmail,com').for(:email).with_message('must be a valid email format') } + # it { is_expected.not_to allow_value('bob@.com').for(:email).with_message('must be a valid email format') } + # it { is_expected.not_to allow_value('bob bob@gmail.com').for(:email).with_message('must be a valid email format') } + + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } # example for does not allow duplicate emails + + # it 'does not allow duplicate emails' do + # create(:user, email: 'duplicate@example.com') + # duplicate_user = build(:user, email: 'duplicate@example.com') + # expect(duplicate_user).not_to be_valid + # expect(duplicate_user.errors[:email]).to include('has already been taken') + # end + end + + context 'validations for password' do + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') + end + end +end From 6bcab77b57a4746776328d0f69e86254685182d9 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:29:00 +0200 Subject: [PATCH 20/25] udpate tests --- spec/support/shared_examples/validatable_user.rb | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/spec/support/shared_examples/validatable_user.rb b/spec/support/shared_examples/validatable_user.rb index 1ccc1fe..909cf69 100644 --- a/spec/support/shared_examples/validatable_user.rb +++ b/spec/support/shared_examples/validatable_user.rb @@ -8,24 +8,12 @@ it { is_expected.to validate_presence_of(:email) } it { is_expected.to allow_value('bob@example.com').for(:email) } - # Testing a list of invalid email addresses (example as different from the following 4) invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] invalid_emails.each do |email| it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } end - # it { is_expected.not_to allow_value('bob').for(:email).with_message('must be a valid email format') } - # it { is_expected.not_to allow_value('bob@gmail,com').for(:email).with_message('must be a valid email format') } - # it { is_expected.not_to allow_value('bob@.com').for(:email).with_message('must be a valid email format') } - # it { is_expected.not_to allow_value('bob bob@gmail.com').for(:email).with_message('must be a valid email format') } - it { is_expected.to validate_uniqueness_of(:email).case_insensitive } # example for does not allow duplicate emails - - # it 'does not allow duplicate emails' do - # create(:user, email: 'duplicate@example.com') - # duplicate_user = build(:user, email: 'duplicate@example.com') - # expect(duplicate_user).not_to be_valid - # expect(duplicate_user.errors[:email]).to include('has already been taken') - # end + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } end context 'validations for password' do From 6475233a9ecce838e7ba87981a8ec4d537b5e313 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:32:29 +0200 Subject: [PATCH 21/25] Upgraded tests --- spec/factories/admin_users.rb | 2 +- spec/factories/users.rb | 2 +- spec/models/admin_user_spec.rb | 35 +++++++++++++++++++++++++++++++++- spec/models/user_spec.rb | 35 +++++++++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/spec/factories/admin_users.rb b/spec/factories/admin_users.rb index c1b00ad..c67a4c5 100644 --- a/spec/factories/admin_users.rb +++ b/spec/factories/admin_users.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :admin_user do email { Faker::Internet.unique.email } - name { Faker::Internet.username.gsub(/[^a-zA-Z0-9_]/, '') } + name { Faker::Name.name } password { '@password' } password_confirmation { '@password' } end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a028154..635f021 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :user do email { Faker::Internet.unique.email } - name { Faker::Internet.username.gsub(/[^a-zA-Z0-9_]/, '') } + name { Faker::Name.name } password { '@password' } password_confirmation { '@password' } end diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb index ad6187b..1d2f3dd 100644 --- a/spec/models/admin_user_spec.rb +++ b/spec/models/admin_user_spec.rb @@ -7,5 +7,38 @@ expect(subject).to be_valid end - it_behaves_like 'validatable user' + context 'validations for name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + end + + context 'validations for email' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } + + invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] + invalid_emails.each do |email| + it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + end + + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } + end + + context 'validations for password' do + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d75897..7d6335e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -7,5 +7,38 @@ expect(subject).to be_valid end - it_behaves_like 'validatable user' + context 'validations for name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + end + + context 'validations for email' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } + + invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] + invalid_emails.each do |email| + it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + end + + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } + end + + context 'validations for password' do + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') + end + end end From 33c57bff7b02642de1f27267ab871068989bdd78 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:35:44 +0200 Subject: [PATCH 22/25] Deleted validatable_user --- .../shared_examples/validatable_user.rb | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 spec/support/shared_examples/validatable_user.rb diff --git a/spec/support/shared_examples/validatable_user.rb b/spec/support/shared_examples/validatable_user.rb deleted file mode 100644 index 909cf69..0000000 --- a/spec/support/shared_examples/validatable_user.rb +++ /dev/null @@ -1,36 +0,0 @@ -RSpec.shared_examples 'validatable user' do - context 'validations for name' do - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to allow_value('bob').for(:name) } - end - - context 'validations for email' do - it { is_expected.to validate_presence_of(:email) } - it { is_expected.to allow_value('bob@example.com').for(:email) } - - invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] - invalid_emails.each do |email| - it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } - end - - it { is_expected.to validate_uniqueness_of(:email).case_insensitive } - end - - context 'validations for password' do - it { is_expected.to validate_presence_of(:password) } - it { is_expected.to validate_length_of(:password).is_at_least(8) } - it { is_expected.to allow_value('@password').for(:password) } - it { - is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') - } - it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } - it { - is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') - } - it 'does not allow a password longer than 128 characters' do - long_password = 'a' * 129 - is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') - end - end -end From 9839dca1b025759fd5feb8004172330ef4383414 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:59:18 +0200 Subject: [PATCH 23/25] updated tests styles --- spec/models/admin_user_spec.rb | 66 ++++++++++++++++++---------------- spec/models/user_spec.rb | 65 +++++++++++++++++---------------- 2 files changed, 69 insertions(+), 62 deletions(-) diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb index 1d2f3dd..dafb7fe 100644 --- a/spec/models/admin_user_spec.rb +++ b/spec/models/admin_user_spec.rb @@ -3,42 +3,46 @@ RSpec.describe AdminUser, type: :model do subject { build(:admin_user) } - it 'is expect to have valid factory' do - expect(subject).to be_valid - end + + context 'Validations' do + describe 'factory' do + it 'is expect to have valid factory' do + expect(subject).to be_valid + end + end - context 'validations for name' do - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to allow_value('bob').for(:name) } - end + describe 'name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + end - context 'validations for email' do - it { is_expected.to validate_presence_of(:email) } - it { is_expected.to allow_value('bob@example.com').for(:email) } + describe 'email' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } - invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] - invalid_emails.each do |email| - it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] + invalid_emails.each do |email| + it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + end + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } end - it { is_expected.to validate_uniqueness_of(:email).case_insensitive } - end - - context 'validations for password' do - it { is_expected.to validate_presence_of(:password) } - it { is_expected.to validate_length_of(:password).is_at_least(8) } - it { is_expected.to allow_value('@password').for(:password) } - it { - is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') - } - it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } - it { - is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') - } - it 'does not allow a password longer than 128 characters' do - long_password = 'a' * 129 - is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') + describe 'password' do + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') + end end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7d6335e..e3fa59e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3,42 +3,45 @@ RSpec.describe User, type: :model do subject { build(:user) } - it 'is expect to have valid factory' do - expect(subject).to be_valid - end + context 'Validations' do + describe 'factory' do + it 'is expect to have valid factory' do + expect(subject).to be_valid + end + end - context 'validations for name' do - it { is_expected.to validate_presence_of(:name) } - it { is_expected.to allow_value('bob').for(:name) } - end + describe 'name' do + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value('bob').for(:name) } + end - context 'validations for email' do - it { is_expected.to validate_presence_of(:email) } - it { is_expected.to allow_value('bob@example.com').for(:email) } + describe 'email' do + it { is_expected.to validate_presence_of(:email) } + it { is_expected.to allow_value('bob@example.com').for(:email) } - invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] - invalid_emails.each do |email| - it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + invalid_emails = ['bob', 'bob@gmail,com', 'bob@.com', 'bob bob@gmail.com'] + invalid_emails.each do |email| + it { is_expected.not_to allow_value(email).for(:email).with_message('must be a valid email format') } + end + it { is_expected.to validate_uniqueness_of(:email).case_insensitive } end - it { is_expected.to validate_uniqueness_of(:email).case_insensitive } - end - - context 'validations for password' do - it { is_expected.to validate_presence_of(:password) } - it { is_expected.to validate_length_of(:password).is_at_least(8) } - it { is_expected.to allow_value('@password').for(:password) } - it { - is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') - } - it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } - it { - is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') - } - it 'does not allow a password longer than 128 characters' do - long_password = 'a' * 129 - is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') + describe 'password' do + it { is_expected.to validate_presence_of(:password) } + it { is_expected.to validate_length_of(:password).is_at_least(8) } + it { is_expected.to allow_value('@password').for(:password) } + it { + is_expected.not_to allow_value('passsword!').for(:password).with_message('must not contain repeated characters') + } + it { is_expected.not_to allow_value('password').for(:password).with_message('must contain at least one symbol') } + it { + is_expected.not_to allow_value('!23456').for(:password).with_message('is too short (minimum is 8 characters)') + } + it 'does not allow a password longer than 128 characters' do + long_password = 'a' * 129 + is_expected.not_to allow_value(long_password).for(:password) + .with_message('is too long (maximum is 128 characters)') + end end end end From 8c6e53af7115a1aaa054edcd733ec1b9fe1a8b06 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:14:39 +0200 Subject: [PATCH 24/25] fix lint --- spec/models/admin_user_spec.rb | 5 ++--- spec/models/user_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb index dafb7fe..9dab6e8 100644 --- a/spec/models/admin_user_spec.rb +++ b/spec/models/admin_user_spec.rb @@ -3,8 +3,7 @@ RSpec.describe AdminUser, type: :model do subject { build(:admin_user) } - - context 'Validations' do + context 'Validations' do describe 'factory' do it 'is expect to have valid factory' do expect(subject).to be_valid @@ -41,7 +40,7 @@ it 'does not allow a password longer than 128 characters' do long_password = 'a' * 129 is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') + .with_message('is too long (maximum is 128 characters)') end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e3fa59e..eef43e8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3,7 +3,7 @@ RSpec.describe User, type: :model do subject { build(:user) } - context 'Validations' do + context 'Validations' do describe 'factory' do it 'is expect to have valid factory' do expect(subject).to be_valid @@ -40,7 +40,7 @@ it 'does not allow a password longer than 128 characters' do long_password = 'a' * 129 is_expected.not_to allow_value(long_password).for(:password) - .with_message('is too long (maximum is 128 characters)') + .with_message('is too long (maximum is 128 characters)') end end end From f86435d80ce11eb063107737a02ce04c839876b0 Mon Sep 17 00:00:00 2001 From: AndrewRiabets <79791263+AndrewRiabets@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:28:20 +0200 Subject: [PATCH 25/25] update tests --- spec/models/admin_user_spec.rb | 10 +++++----- spec/models/user_spec.rb | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/models/admin_user_spec.rb b/spec/models/admin_user_spec.rb index 9dab6e8..8bc1fa3 100644 --- a/spec/models/admin_user_spec.rb +++ b/spec/models/admin_user_spec.rb @@ -3,13 +3,13 @@ RSpec.describe AdminUser, type: :model do subject { build(:admin_user) } - context 'Validations' do - describe 'factory' do - it 'is expect to have valid factory' do - expect(subject).to be_valid - end + context 'factory' do + it 'is expect to have valid factory' do + expect(subject).to be_valid end + end + context 'Validations' do describe 'name' do it { is_expected.to validate_presence_of(:name) } it { is_expected.to allow_value('bob').for(:name) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index eef43e8..d57a761 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3,13 +3,13 @@ RSpec.describe User, type: :model do subject { build(:user) } - context 'Validations' do - describe 'factory' do - it 'is expect to have valid factory' do - expect(subject).to be_valid - end + context 'factory' do + it 'is expect to have valid factory' do + expect(subject).to be_valid end + end + context 'Validations' do describe 'name' do it { is_expected.to validate_presence_of(:name) } it { is_expected.to allow_value('bob').for(:name) }