diff --git a/Gemfile b/Gemfile index 7a44fe3..cb05d20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,44 +1,44 @@ -source 'https://rubygems.org' +source "https://rubygems.org" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem 'rails', '~> 7.2.0', '>= 7.2.0' +gem "rails", "~> 7.2.0", ">= 7.2.0" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem 'sprockets-rails' +gem "sprockets-rails" # Use sqlite3 as the database for Active Record -gem 'sqlite3', '~> 1.4' +gem "sqlite3", "~> 1.4" # Use the Puma web server [https://github.com/puma/puma] -gem 'puma', '>= 5.0' +gem "puma", ">= 5.0" # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem 'importmap-rails' +gem "importmap-rails" # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem 'turbo-rails' +gem "turbo-rails" # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] -gem 'stimulus-rails' +gem "stimulus-rails" # Use Tailwind CSS [https://github.com/rails/tailwindcss-rails] -gem 'tailwindcss-rails' +gem "tailwindcss-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem 'jbuilder' +gem "jbuilder" # Use Redis adapter to run Action Cable in production -gem 'redis', '>= 4.0.1' +gem "redis", ">= 4.0.1" # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] -# gem "kredis" +gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: %i[windows jruby] +gem "tzinfo-data", platforms: %i[windows jruby] # Reduces boot times through caching; required in config/boot.rb -gem 'bootsnap', require: false +gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -gem 'image_processing', '~> 1.2' +gem "image_processing", "~> 1.2" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem 'debug', platforms: %i[mri windows], require: "debug/prelude" + gem "debug", platforms: %i[mri windows], require: "debug/prelude" # Static analysis for security vulnerabilities [https://brakemanscanner.org/] gem "brakeman", require: false @@ -47,12 +47,12 @@ group :development, :test do gem "rubocop-rails-omakase", require: false # Usefull to seed some meaningful entries - gem 'faker', '~> 3.4' + gem "faker", "~> 3.4" end group :development do # Use console on exceptions pages [https://github.com/rails/web-console] - gem 'web-console' + gem "web-console" # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] # gem "rack-mini-profiler" @@ -65,12 +65,17 @@ end group :test do # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] - gem 'capybara' - gem 'selenium-webdriver' + gem "capybara" + gem "selenium-webdriver" end # Used to display svg files inline (helper icon) -gem 'inline_svg', '~> 1.9' +gem "inline_svg", "~> 1.9" # Used by pdf_analyzer for extracting pageformats -gem 'pdf-reader', '~> 2.12' +gem "pdf-reader", "~> 2.12" + +gem "authentication-zero", "~> 3.0" + +# Use Pwned to check if a password has been found in any of the huge data breaches [https://github.com/philnash/pwned] +gem "pwned" diff --git a/Gemfile.lock b/Gemfile.lock index 1e6dcf0..03965f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,9 @@ GEM public_suffix (>= 2.0.2, < 7.0) afm (0.2.2) ast (2.4.2) + authentication-zero (3.0.2) base64 (0.2.0) + bcrypt (3.1.20) bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.18.4) @@ -135,6 +137,10 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.7.2) + kredis (1.7.0) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) + redis (>= 4.2, < 6) language_server-protocol (3.17.0.3) launchy (3.0.1) addressable (~> 2.8) @@ -193,6 +199,7 @@ GEM public_suffix (6.0.1) puma (6.4.2) nio4r (~> 2.0) + pwned (2.4.1) racc (1.8.1) rack (3.1.7) rack-session (2.0.0) @@ -350,6 +357,8 @@ PLATFORMS x86_64-linux DEPENDENCIES + authentication-zero (~> 3.0) + bcrypt (~> 3.1.7) bootsnap brakeman capybara @@ -359,9 +368,11 @@ DEPENDENCIES importmap-rails inline_svg (~> 1.9) jbuilder + kredis letter_opener pdf-reader (~> 2.12) puma (>= 5.0) + pwned rails (~> 7.2.0, >= 7.2.0) redis (>= 4.0.1) rubocop-rails-omakase @@ -375,4 +386,4 @@ DEPENDENCIES web-console BUNDLED WITH - 2.5.16 + 2.5.17 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db2..154e651 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,32 @@ class ApplicationController < ActionController::Base - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. - allow_browser versions: :modern + before_action :set_current_request_details + before_action :authenticate_user! + + private + def current_user + Current.user || authenticate_user_from_session + end + helper_method :current_user + + def authenticate_user_from_session + session_record = Session.find_by_id(cookies.signed[:session_token]) + Current.session = session_record + Current.user + end + + def user_signed_in? + current_user.present? + end + helper_method :user_signed_in? + + def authenticate_user! + unless user_signed_in? + redirect_to sign_in_path + end + end + + def set_current_request_details + Current.user_agent = request.user_agent + Current.ip_address = request.ip + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..95f2992 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController + def index + end +end diff --git a/app/controllers/identity/email_verifications_controller.rb b/app/controllers/identity/email_verifications_controller.rb new file mode 100644 index 0000000..c2ce605 --- /dev/null +++ b/app/controllers/identity/email_verifications_controller.rb @@ -0,0 +1,26 @@ +class Identity::EmailVerificationsController < ApplicationController + skip_before_action :authenticate_user!, only: :show + + before_action :set_user, only: :show + + def show + @user.update! verified: true + redirect_to root_path, notice: "Thank you for verifying your email address" + end + + def create + send_email_verification + redirect_to root_path, notice: "We sent a verification email to your email address" + end + + private + def set_user + @user = User.find_by_token_for!(:email_verification, params[:sid]) + rescue StandardError + redirect_to edit_identity_email_path, alert: "That email verification link is invalid" + end + + def send_email_verification + UserMailer.with(user: Current.user).email_verification.deliver_later + end +end diff --git a/app/controllers/identity/emails_controller.rb b/app/controllers/identity/emails_controller.rb new file mode 100644 index 0000000..407e388 --- /dev/null +++ b/app/controllers/identity/emails_controller.rb @@ -0,0 +1,36 @@ +class Identity::EmailsController < ApplicationController + before_action :set_user + + def edit + end + + def update + if @user.update(user_params) + redirect_to_root + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = Current.user + end + + def user_params + params.permit(:email, :password_challenge).with_defaults(password_challenge: "") + end + + def redirect_to_root + if @user.email_previously_changed? + resend_email_verification + redirect_to root_path, notice: "Your email has been changed" + else + redirect_to root_path + end + end + + def resend_email_verification + UserMailer.with(user: @user).email_verification.deliver_later + end +end diff --git a/app/controllers/identity/password_resets_controller.rb b/app/controllers/identity/password_resets_controller.rb new file mode 100644 index 0000000..23ad9dc --- /dev/null +++ b/app/controllers/identity/password_resets_controller.rb @@ -0,0 +1,43 @@ +class Identity::PasswordResetsController < ApplicationController + skip_before_action :authenticate_user! + + before_action :set_user, only: %i[ edit update ] + + def new + end + + def edit + end + + def create + if @user = User.find_by(email: params[:email], verified: true) + send_password_reset_email + redirect_to sign_in_path, notice: "Check your email for reset instructions" + else + redirect_to new_identity_password_reset_path, alert: "You can't reset your password until you verify your email" + end + end + + def update + if @user.update(user_params) + redirect_to sign_in_path, notice: "Your password was reset successfully. Please sign in" + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = User.find_by_token_for!(:password_reset, params[:sid]) + rescue StandardError + redirect_to new_identity_password_reset_path, alert: "That password reset link is invalid" + end + + def user_params + params.permit(:password, :password_confirmation) + end + + def send_password_reset_email + UserMailer.with(user: @user).password_reset.deliver_later + end +end diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb index 763ac44..0244c23 100644 --- a/app/controllers/jobs_controller.rb +++ b/app/controllers/jobs_controller.rb @@ -1,4 +1,5 @@ class JobsController < ApplicationController + skip_before_action :authenticate_user!, only: :index # GET /jobs or /jobs.json def index @jobs = Job.currently_working_on diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..1956a8b --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,23 @@ +class PasswordsController < ApplicationController + before_action :set_user + + def edit + end + + def update + if @user.update(user_params) + redirect_to root_path, notice: "Your password has been changed" + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = Current.user + end + + def user_params + params.permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "") + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..a80b849 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,30 @@ +class RegistrationsController < ApplicationController + skip_before_action :authenticate_user! + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + + if @user.save + session_record = @user.sessions.create! + cookies.signed.permanent[:session_token] = { value: session_record.id, httponly: true } + + send_email_verification + redirect_to root_path, notice: "Welcome! You have signed up successfully" + else + render :new, status: :unprocessable_entity + end + end + + private + def user_params + params.permit(:email, :password, :password_confirmation) + end + + def send_email_verification + UserMailer.with(user: @user).email_verification.deliver_later + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..e11b583 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,32 @@ +class SessionsController < ApplicationController + skip_before_action :authenticate_user!, only: %i[ new create ] + + before_action :set_session, only: :destroy + + def index + @sessions = Current.user.sessions.order(created_at: :desc) + end + + def new + end + + def create + if user = User.authenticate_by(email: params[:email], password: params[:password]) + @session = user.sessions.create! + cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true } + + redirect_to root_path, notice: "Signed in successfully" + else + redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect" + end + end + + def destroy + @session.destroy; redirect_to(sessions_path, notice: "That session has been logged out") + end + + private + def set_session + @session = Current.user.sessions.find(params[:id]) + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..98c2045 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,15 @@ +class UserMailer < ApplicationMailer + def password_reset + @user = params[:user] + @signed_id = @user.generate_token_for(:password_reset) + + mail to: @user.email, subject: "Reset your password" + end + + def email_verification + @user = params[:user] + @signed_id = @user.generate_token_for(:email_verification) + + mail to: @user.email, subject: "Verify your email" + end +end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..0d0863f --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,6 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + attribute :user_agent, :ip_address + + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/job.rb b/app/models/job.rb index b800e55..20bdf78 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -17,7 +17,9 @@ class Job < ApplicationRecord before_save :set_cost_qm before_save :calc_cost, if: :printed_pages_changes? - # TODO: works only when job is created. Should move analyzer to activestorage :https://discuss.rubyonrails.org/t/active-storage-in-production-lessons-learned-and-in-depth-look-at-how-it-works/83289 + # TODO: works only when job is created. Should move analyzer to activestorage : + # https://discuss.rubyonrails.org/t/active-storage-in-production-lessons-learned-and-in-depth-look-at-how-it-works/83289 + # https://redgreen.no/2021/01/24/custom-analyzer-for-activestorage.html after_create_commit :analyze_pdf # NOTE: Multiple status if paing before brinting? diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..8e94aa8 --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,8 @@ +class Session < ApplicationRecord + belongs_to :user + + before_create do + self.user_agent = Current.user_agent + self.ip_address = Current.ip_address + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 93e7e81..4c137d0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,35 @@ class User < ApplicationRecord + has_secure_password has_many :jobs_as_costumer, foreign_key: :costumer_id, class_name: "Job" has_many :jobs_as_operator, foreign_key: :operator_id, class_name: "Job" + + generates_token_for :email_verification, expires_in: 2.days do + email + end + generates_token_for :password_reset, expires_in: 20.minutes do + password_salt.last(10) + end + + + has_many :sessions, dependent: :destroy + + validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :password, allow_nil: true, length: { minimum: 12 } + validates :password, not_pwned: { message: "might easily be guessed" } + + normalizes :email, with: -> { _1.strip.downcase } + + enum :role, { + user: 0, + operator: 1, + admin: 2 + } + + before_validation if: :email_changed?, on: :update do + self.verified = false + end + + after_update if: :password_digest_previously_changed? do + sessions.where.not(id: Current.session).delete_all + end end diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..677883d --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,23 @@ +
<%= notice %>
+ +Signed as <%= Current.user.email %>
+ +<%= alert %>
+ +<% if Current.user.verified? %> +We sent a verification email to the address below. Check that email and follow those instructions to confirm it's your email address.
+<%= button_to "Re-send verification email", identity_email_verification_path %>
+<% end %> + +<%= form_with(url: identity_email_path, method: :patch) do |form| %> + <% if @user.errors.any? %> +<%= alert %>
+ +<%= alert %>
+ +<%= notice %>
+ ++ User Agent: + <%= session.user_agent %> +
+ ++ Ip Address: + <%= session.ip_address %> +
+ ++ Created at: + <%= session.created_at %> +
+ ++ <%= button_to "Log out", session, method: :delete %> +
+ <% end %> +<%= notice %>
+<%= alert %>
+ +Hey there,
+ +This is to confirm that <%= @user.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.
+ +You must hit the link below to confirm that you received this email.
+ +<%= link_to "Yes, use this email for my account", identity_email_verification_url(sid: @signed_id) %>
+ +Have questions or need help? Just reply to this email and our support team will help you sort it out.
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000..c570a0a --- /dev/null +++ b/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,11 @@ +Hey there,
+ +Can't remember your password for <%= @user.email %>? That's OK, it happens. Just hit the link below to set a new one.
+ +<%= link_to "Reset my password", edit_identity_password_reset_url(sid: @signed_id) %>
+ +If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.
+ +Have questions or need help? Just reply to this email and our support team will help you sort it out.
diff --git a/config/environments/test.rb b/config/environments/test.rb index 0c616a1..f62240e 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -6,6 +6,7 @@ require "active_support/core_ext/integer/time" # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/config/routes.rb b/config/routes.rb index 5a50506..664e467 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,15 @@ Rails.application.routes.draw do + get "sign_in", to: "sessions#new" + post "sign_in", to: "sessions#create" + get "sign_up", to: "registrations#new" + post "sign_up", to: "registrations#create" + resources :sessions, only: [ :index, :show, :destroy ] + resource :password, only: [ :edit, :update ] + namespace :identity do + resource :email, only: [ :edit, :update ] + resource :email_verification, only: [ :show, :create ] + resource :password_reset, only: [ :new, :edit, :create, :update ] + end resources :jobs do member do patch "cancel" @@ -10,8 +21,8 @@ Rails.application.routes.draw do patch "increment_page", :din patch "decrement_page", :din end - end - end + end + end # 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/20240730214152_create_users.rb b/db/migrate/20240730214152_create_users.rb deleted file mode 100644 index 81b5a9f..0000000 --- a/db/migrate/20240730214152_create_users.rb +++ /dev/null @@ -1,13 +0,0 @@ -class CreateUsers < ActiveRecord::Migration[7.1] - def change - create_table :users do |t| - t.string :firstname - t.string :lastname - - t.timestamps - end - - add_foreign_key :jobs, :users, column: :operator_id - add_foreign_key :jobs, :users, column: :costumer_id - end -end diff --git a/db/migrate/20240826144015_create_users.rb b/db/migrate/20240826144015_create_users.rb new file mode 100644 index 0000000..c5391d3 --- /dev/null +++ b/db/migrate/20240826144015_create_users.rb @@ -0,0 +1,18 @@ +class CreateUsers < ActiveRecord::Migration[7.2] + def change + create_table :users do |t| + t.string :email, null: false, index: { unique: true } + t.string :password_digest, null: false + t.string :firstname + t.string :lastname + + t.integer :role, default: 0, index: true + + t.boolean :verified, null: false, default: false + + t.timestamps + end + add_foreign_key :jobs, :users, column: :operator_id + add_foreign_key :jobs, :users, column: :costumer_id + end +end diff --git a/db/migrate/20240826144016_create_sessions.rb b/db/migrate/20240826144016_create_sessions.rb new file mode 100644 index 0000000..a828909 --- /dev/null +++ b/db/migrate/20240826144016_create_sessions.rb @@ -0,0 +1,11 @@ +class CreateSessions < ActiveRecord::Migration[7.2] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + t.string :user_agent + t.string :ip_address + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 056dc55..d751713 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_08_01_153403) do +ActiveRecord::Schema[7.2].define(version: 2024_08_26_144016) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -69,15 +69,31 @@ ActiveRecord::Schema[7.2].define(version: 2024_08_01_153403) do t.index ["status"], name: "index_jobs_on_status" end - create_table "users", force: :cascade do |t| - t.string "firstname" - t.string "lastname" + create_table "sessions", force: :cascade do |t| + t.integer "user_id", null: false + t.string "user_agent" + t.string "ip_address" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_sessions_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "email", null: false + t.string "password_digest", null: false + t.string "firstname" + t.string "lastname" + t.integer "role", default: 0 + t.boolean "verified", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["role"], name: "index_users_on_role" end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "jobs", "users", column: "costumer_id" add_foreign_key "jobs", "users", column: "operator_id" + add_foreign_key "sessions", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index 724779e..681e3b6 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,28 +10,28 @@ Faker::Config.locale = :de -10.times do - User.new(firstname: Faker::Name.unique.first_name, lastname: Faker::Name.unique.last_name).save +# Admin +User.create!(email: "david.boehm@hs-rm.de", firstname: "David", lastname: "Böhm", role: :admin, password_digest: BCrypt::Password.create("admin"), verified: true) + +# Students +students = [] +5.times do + firstname = Faker::Name.unique.first_name + lastname = Faker::Name.unique.last_name + students << User.new(email: firstname + "." + lastname + "@student.hs-rm.de", firstname: firstname, lastname: lastname, password_digest: BCrypt::Password.create("password"), verified: true) + students.last.save! end [ 'GanzWichtig.pdf', 'IchBinIn5MinDran.pdf', 'DerPlanDerImmerProblemeMacht.pdf', 'DieFarbenGefallenMirNicht.pdf', 'MachHinIchHabsEilig.pdf', 'WarumDauertDasSoLange.pdf', 'DenPlanBezahleIchNicht.pdf', 'IchWarAlsErstesDran.pdf', 'WarumIstDerPlotterDefekt.pdf', 'DasNächsteMalGeheIchWoAndersHin.pdf' ].shuffle.each do |pdf| - # a0 = rand(0...7) - # a1 = rand(0...7) - # a2 = rand(0...7) - # a3 = rand(0...7) - # a0.zero? || a1 = 0 && a2 = 0 && a3 = 0 - # a1.zero? || a2 = 0 && a3 = 0 - # a2.zero? || a3 = 0 - status = %i[open open open open open - printing pickup paid canceled].sample + status = %i[open open open open open printing pickup paid canceled].sample job = Job.new(costumer_firstname: Faker::Name.unique.first_name, costumer_lastname: Faker::Name.unique.last_name, + costumer_id: students[rand(0...4)].id, # number_of_plans_a0: a0, number_of_plans_a1: a1, number_of_plans_a2: a2, number_of_plans_a3: a3, status:, privacy_policy_accepted: true) job.pdf = File.open(Rails.root.join('db/pdfs/', pdf)) job.save! - # sleep 1 # for testing broadcasting end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d19212a..f3db1f4 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,5 +1,15 @@ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] + + def sign_in_as(user) + visit sign_in_url + fill_in :email, with: user.email + fill_in :password, with: "Secret1*3*5*" + click_on "Sign in" + + assert_current_path root_url + user + end end diff --git a/test/controllers/identity/email_verifications_controller_test.rb b/test/controllers/identity/email_verifications_controller_test.rb new file mode 100644 index 0000000..90d8513 --- /dev/null +++ b/test/controllers/identity/email_verifications_controller_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class Identity::EmailVerificationsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + @user.update! verified: false + end + + test "should send a verification email" do + assert_enqueued_email_with UserMailer, :email_verification, params: { user: @user } do + post identity_email_verification_url + end + + assert_redirected_to root_url + end + + test "should verify email" do + sid = @user.generate_token_for(:email_verification) + + get identity_email_verification_url(sid: sid, email: @user.email) + assert_redirected_to root_url + end + + test "should not verify email with expired token" do + sid = @user.generate_token_for(:email_verification) + + travel 3.days + + get identity_email_verification_url(sid: sid, email: @user.email) + + assert_redirected_to edit_identity_email_url + assert_equal "That email verification link is invalid", flash[:alert] + end +end diff --git a/test/controllers/identity/emails_controller_test.rb b/test/controllers/identity/emails_controller_test.rb new file mode 100644 index 0000000..dc8ecc4 --- /dev/null +++ b/test/controllers/identity/emails_controller_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class Identity::EmailsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "should get edit" do + get edit_identity_email_url + assert_response :success + end + + test "should update email" do + patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "Secret1*3*5*" } + assert_redirected_to root_url + end + + + test "should not update email with wrong password challenge" do + patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "SecretWrong1*3" } + + assert_response :unprocessable_entity + assert_select "li", /Password challenge is invalid/ + end +end diff --git a/test/controllers/identity/password_resets_controller_test.rb b/test/controllers/identity/password_resets_controller_test.rb new file mode 100644 index 0000000..5b807c5 --- /dev/null +++ b/test/controllers/identity/password_resets_controller_test.rb @@ -0,0 +1,65 @@ +require "test_helper" + +class Identity::PasswordResetsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:lazaro_nixon) + end + + test "should get new" do + get new_identity_password_reset_url + assert_response :success + end + + test "should get edit" do + sid = @user.generate_token_for(:password_reset) + + get edit_identity_password_reset_url(sid: sid) + assert_response :success + end + + test "should send a password reset email" do + assert_enqueued_email_with UserMailer, :password_reset, params: { user: @user } do + post identity_password_reset_url, params: { email: @user.email } + end + + assert_redirected_to sign_in_url + end + + test "should not send a password reset email to a nonexistent email" do + assert_no_enqueued_emails do + post identity_password_reset_url, params: { email: "invalid_email@hey.com" } + end + + assert_redirected_to new_identity_password_reset_url + assert_equal "You can't reset your password until you verify your email", flash[:alert] + end + + test "should not send a password reset email to a unverified email" do + @user.update! verified: false + + assert_no_enqueued_emails do + post identity_password_reset_url, params: { email: @user.email } + end + + assert_redirected_to new_identity_password_reset_url + assert_equal "You can't reset your password until you verify your email", flash[:alert] + end + + test "should update password" do + sid = @user.generate_token_for(:password_reset) + + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + assert_redirected_to sign_in_url + end + + test "should not update password with expired token" do + sid = @user.generate_token_for(:password_reset) + + travel 30.minutes + + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + assert_redirected_to new_identity_password_reset_url + assert_equal "That password reset link is invalid", flash[:alert] + end +end diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb new file mode 100644 index 0000000..430f1a1 --- /dev/null +++ b/test/controllers/passwords_controller_test.rb @@ -0,0 +1,24 @@ +require "test_helper" + +class PasswordsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "should get edit" do + get edit_password_url + assert_response :success + end + + test "should update password" do + patch password_url, params: { password_challenge: "Secret1*3*5*", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + assert_redirected_to root_url + end + + test "should not update password with wrong password challenge" do + patch password_url, params: { password_challenge: "SecretWrong1*3", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + assert_response :unprocessable_entity + assert_select "li", /Password challenge is invalid/ + end +end diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb new file mode 100644 index 0000000..1636fd8 --- /dev/null +++ b/test/controllers/registrations_controller_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class RegistrationsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get sign_up_url + assert_response :success + end + + test "should sign up" do + assert_difference("User.count") do + post sign_up_url, params: { email: "lazaronixon@hey.com", password: "Secret1*3*5*", password_confirmation: "Secret1*3*5*" } + end + + assert_redirected_to root_url + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000..975c1ca --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:lazaro_nixon) + end + + test "should get index" do + sign_in_as @user + + get sessions_url + assert_response :success + end + + test "should get new" do + get sign_in_url + assert_response :success + end + + test "should sign in" do + post sign_in_url, params: { email: @user.email, password: "Secret1*3*5*" } + assert_redirected_to root_url + + get root_url + assert_response :success + end + + test "should not sign in with wrong credentials" do + post sign_in_url, params: { email: @user.email, password: "SecretWrong1*3" } + assert_redirected_to sign_in_url(email_hint: @user.email) + assert_equal "That email or password is incorrect", flash[:alert] + + get root_url + assert_redirected_to sign_in_url + end + + test "should sign out" do + sign_in_as @user + + delete session_url(@user.sessions.last) + assert_redirected_to sessions_url + + follow_redirect! + assert_redirected_to sign_in_url + end +end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index e7ec08b..4c5f5d7 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,9 +1,6 @@ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -one: - firstname: MyString - lastname: MyString - -two: - firstname: MyString - lastname: MyString +lazaro_nixon: + email: lazaronixon@hotmail.com + password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %> + verified: true diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb new file mode 100644 index 0000000..e74b76e --- /dev/null +++ b/test/mailers/user_mailer_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class UserMailerTest < ActionMailer::TestCase + setup do + @user = users(:lazaro_nixon) + end + + test "password_reset" do + mail = UserMailer.with(user: @user).password_reset + assert_equal "Reset your password", mail.subject + assert_equal [@user.email], mail.to + end + + test "email_verification" do + mail = UserMailer.with(user: @user).email_verification + assert_equal "Verify your email", mail.subject + assert_equal [@user.email], mail.to + end +end diff --git a/test/system/identity/emails_test.rb b/test/system/identity/emails_test.rb new file mode 100644 index 0000000..c681a77 --- /dev/null +++ b/test/system/identity/emails_test.rb @@ -0,0 +1,26 @@ +require "application_system_test_case" + +class Identity::EmailsTest < ApplicationSystemTestCase + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "updating the email" do + click_on "Change email address" + + fill_in "New email", with: "new_email@hey.com" + fill_in "Password challenge", with: "Secret1*3*5*" + click_on "Save changes" + + assert_text "Your email has been changed" + end + + test "sending a verification email" do + @user.update! verified: false + + click_on "Change email address" + click_on "Re-send verification email" + + assert_text "We sent a verification email to your email address" + end +end diff --git a/test/system/identity/password_resets_test.rb b/test/system/identity/password_resets_test.rb new file mode 100644 index 0000000..1c254ab --- /dev/null +++ b/test/system/identity/password_resets_test.rb @@ -0,0 +1,28 @@ +require "application_system_test_case" + +class Identity::PasswordResetsTest < ApplicationSystemTestCase + setup do + @user = users(:lazaro_nixon) + @sid = @user.generate_token_for(:password_reset) + end + + test "sending a password reset email" do + visit sign_in_url + click_on "Forgot your password?" + + fill_in "Email", with: @user.email + click_on "Send password reset email" + + assert_text "Check your email for reset instructions" + end + + test "updating password" do + visit edit_identity_password_reset_url(sid: @sid) + + fill_in "New password", with: "Secret6*4*2*" + fill_in "Confirm new password", with: "Secret6*4*2*" + click_on "Save changes" + + assert_text "Your password was reset successfully. Please sign in" + end +end diff --git a/test/system/passwords_test.rb b/test/system/passwords_test.rb new file mode 100644 index 0000000..486fdf9 --- /dev/null +++ b/test/system/passwords_test.rb @@ -0,0 +1,18 @@ +require "application_system_test_case" + +class PasswordsTest < ApplicationSystemTestCase + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "updating the password" do + click_on "Change password" + + fill_in "Password challenge", with: "Secret1*3*5*" + fill_in "New password", with: "Secret6*4*2*" + fill_in "Confirm new password", with: "Secret6*4*2*" + click_on "Save changes" + + assert_text "Your password has been changed" + end +end diff --git a/test/system/registrations_test.rb b/test/system/registrations_test.rb new file mode 100644 index 0000000..da7c0db --- /dev/null +++ b/test/system/registrations_test.rb @@ -0,0 +1,14 @@ +require "application_system_test_case" + +class RegistrationsTest < ApplicationSystemTestCase + test "signing up" do + visit sign_up_url + + fill_in "Email", with: "lazaronixon@hey.com" + fill_in "Password", with: "Secret6*4*2*" + fill_in "Password confirmation", with: "Secret6*4*2*" + click_on "Sign up" + + assert_text "Welcome! You have signed up successfully" + end +end diff --git a/test/system/sessions_test.rb b/test/system/sessions_test.rb new file mode 100644 index 0000000..640c626 --- /dev/null +++ b/test/system/sessions_test.rb @@ -0,0 +1,30 @@ +require "application_system_test_case" + +class SessionsTest < ApplicationSystemTestCase + setup do + @user = users(:lazaro_nixon) + end + + test "visiting the index" do + sign_in_as @user + + click_on "Devices & Sessions" + assert_selector "h1", text: "Sessions" + end + + test "signing in" do + visit sign_in_url + fill_in "Email", with: @user.email + fill_in "Password", with: "Secret1*3*5*" + click_on "Sign in" + + assert_text "Signed in successfully" + end + + test "signing out" do + sign_in_as @user + + click_on "Log out" + assert_text "That session has been logged out" + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c22470..3631664 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,14 +2,15 @@ ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" -module ActiveSupport - class TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all - # Add more helper methods to be used by all tests here... + # Add more helper methods to be used by all tests here... + def sign_in_as(user) + post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }); user end end