bundle add rspec-rails
rails generate rspec:install
Create a User model via Devise and add appropriate configurations✅
bundle add devise
rails generate devise install
rails generate devise User
rails db:migrate
Add code to config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
Replace code in config/initializers/devise.rb
# find this line:
config.sign_out_via = :delete
# and replace it with this:
config.sign_out_via = :get
Create registrations and sessions controllers to handle sign ups and logins
rails generate devise:controllers users -c registrations sessions
Replace code in app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
def create
build_resource(sign_up_params)
resource.save
sign_in(resource_name, resource)
render json: resource
end
end
Replace code in app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts = {})
render json: resource
end
def respond_to_on_destroy
render json: { message: "Logged out." }
end
end
Update devise routes: config/routes.rb
Rails.application.routes.draw do
devise_for :users,
path: '',
path_names: {
sign_in: 'login',
sign_out: 'logout',
registration: 'signup'
},
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
end
Update config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3001'
resource '*',
headers: ["Authorization"],
expose: ["Authorization"],
methods: [:get, :post, :put, :patch, :delete, :options, :head],
max_age: 600
end
end
Uncomment this from Gemfile
Add JWT dependencies and configurations✅
Run this command to open the window to add the new secret key
EDITOR="code --wait" bin/rails credentials:edit
Add the secret key below the secret key base using this code
jwt_secret_key: <newly-created secret key>
In the terminal hit 'control + c' to save the file
If you get an error saying it didn't save. Manually save the file with 'command + s' in the VScode window
Add this to config/initializers/devise.rb
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.jwt_special_key
jwt.dispatch_requests = [
['POST', %r{^/login$}],
]
jwt.revocation_requests = [
['DELETE', %r{^/logout$}]
]
jwt.expiration_time = 5.minutes.to_i
end
rails generate model jwt_denylist
Add this code to the migration: db/migrate/
def change
create_table :jwt_denylist do |t|
t.string :jti, null: false
t.datetime :exp, null: false
end
add_index :jwt_denylist, :jti
end
Replace this in the app/models/user.rb
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
Generate model in the terminal
rails generate resource Apartment street:string unit:string city:string state:string square_footage:integer price:string bedrooms:integer bathrooms:float pets:string image:text user_id:integer
Define the relationship between Apartment and the User in app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
has_many :apartments
end
Define the relationship between Apartment and the User in app/models/apartment.rb
class Apartment < ApplicationRecord
belongs_to :user
end
Add a global variable for user to allow for easier testing in spec/models/apartment_spec.rb
require 'rails_helper'
RSpec.describe Apartment, type: :model do
let(:user) { User.create(
email: 'test@example.com',
password: 'password',
password_confirmation: 'password'
)
}
Add first test to test for valid attributes
it 'is valid with valid attributes' do
apartment = user.apartments.create(
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
)
expect(apartment).to be_valid
end
Add test for validating presence of attributes repeat for each data column making sure to update the it statement and the expect statement
it 'is not valid without a street attribute' do
apartment = user.apartments.create(
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
)
expect(apartment).not_to be_valid
expect(apartment.errors[:street].first).to eq("can't be blank")
end
end
Add validation to app/models/apartment.rb
class Apartment < ApplicationRecord
belongs_to :user
validates :street, :unit, :city, :state, :square_footage, :price, :bedrooms, :pets, :bathrooms, :image, presence: true
end
Add a global user variable for the test file
RSpec.describe "Apartments", type: :request do
let(:user) { User.create(
email: 'test@example.com',
password: 'password',
password_confirmation: 'password'
)
}
Add a mock GET request for index
describe 'GET #index' do
it 'returns a list of apartments and http success' do
apartment = user.apartments.create(
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg'
)
get apartments_path
expect(response).to have_http_status(200)
expect(apartment).to be_valid
end
Add a mock POST request for create
describe 'POST #create' do
it 'creates a valid apartment with http success' do
post apartments_path, params: {
apartment: {
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
user_id: user.id
}
}
apartment = Apartment.where(street: 'Test Street').first
expect(response).to have_http_status(200)
expect(apartment).to be_valid
end
it 'creates a invalid apartment' do
post apartments_path, params: {
apartment: {
street: nil,
unit: nil,
city: nil,
state: nil,
square_footage: nil,
price: nil,
bedrooms: nil,
bathrooms: nil,
pets: nil,
image: nil,
user_id: nil
}
}
apartment = Apartment.where(street: nil).first
expect(response).to have_http_status(422)
expect(apartment).to eq(nil)
end
end
Add a mock PATCH request for update
describe 'Patch #update' do
it 'updates a valid apartment with http success' do
post apartments_path, params: {
apartment: {
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
user_id: user.id
}
}
apartment = Apartment.where(street: 'Test Street').first
patch apartment_path(apartment), params: {
apartment: {
street: 'Test Street for patch',
unit: 'Test Unit for patch',
city: 'Test City for patch',
state: 'Test State for patch',
square_footage: 1001,
price: '$1001',
bedrooms: 2,
bathrooms: 1.5,
pets: 'Test Pets for patch',
image: 'https://images.unsplash.com/photo-1515263487990-61b07816b324?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
user_id: user.id
}
}
apartment = Apartment.where(street: 'Test Street for patch').first
expect(apartment.street).to eq('Test Street for patch')
expect(apartment.unit).to eq('Test Unit for patch')
expect(apartment.city).to eq('Test City for patch')
expect(apartment.state).to eq('Test State for patch')
expect(apartment.square_footage).to eq(1001)
expect(apartment.price).to eq('$1001')
expect(apartment.bedrooms).to eq(2)
expect(apartment.bathrooms).to eq(1.5)
expect(apartment.pets).to eq('Test Pets for patch')
expect(apartment.image).to eq('https://images.unsplash.com/photo-1515263487990-61b07816b324?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')
expect(response).to have_http_status(200)
end
it 'updates a invalid apartment' do
post apartments_path, params: {
apartment: {
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
user_id: user.id
}
}
apartment = Apartment.where(street: 'Test Street').first
patch apartment_path(apartment), params: {
apartment: {
street: nil,
unit: nil,
city: nil,
state: nil,
square_footage: nil,
price: nil,
bedrooms: nil,
bathrooms: nil,
pets: nil,
image: nil,
user_id: nil
}
}
apartment = Apartment.where(street: nil).first
expect(response).to have_http_status(422)
expect(apartment).to eq(nil)
end
end
Add a mock DESTORY request for delete
describe 'DELETE #destroy' do
it 'deletes an apartment' do
apartment = user.apartments.create(
apartment: {
street: 'Test Street',
unit: 'Test Unit',
city: 'Test City',
state: 'Test State',
square_footage: 1000,
price: '$1000',
bedrooms: 1,
bathrooms: 1.0,
pets: 'Test Pets',
image: 'https://c8.alamy.com/comp/B0RJGE/small-bungalow-home-with-pathway-in-addlestone-surrey-uk-B0RJGE.jpg',
user_id: user.id
}
)
delete apartment_path(apartment)
apartment = Apartment.where(street: 'Test Street').first
expect(apartment).to eq(nil)
end
end
test for 422 http error on failed apartment
it 'creates a invalid apartment' do
post apartments_path, params: {
apartment: {
street: nil,
unit: nil,
city: nil,
state: nil,
square_footage: nil,
price: nil,
bedrooms: nil,
bathrooms: nil,
pets: nil,
image: nil,
user_id: nil
}
}
apartment = Apartment.where(street: nil).first
expect(response).to have_http_status(422)
expect(apartment).to eq(nil)
end
end
end
Add index method to app/controllers/apartments_controller.rb
class ApartmentsController < ApplicationController
def index
apartments = Apartment.all
render json: apartments
end
Add create method to app/controllers/apartments_controller.rb
def create
apartment = Apartment.create(apartment_params)
if apartment.valid?
render json: apartment
else
render json: apartment.errors, status: 422
end
end
Add update method to app/controllers/apartments_controller.rb
def update
apartment = Apartment.find(params[:id])
apartment.update(apartment_params)
if apartment.valid?
render json: apartment
else
render json: apartment.errors, status: 422
end
end
Add destory method to app/controllers/apartments_controller.rb
def destroy
apartment = Apartment.find(params[:id])
if apartment.destroy
render json: {}, status: 204
end
end
Add params method in private
private
def apartment_params
params.require(:apartment).permit(:street, :unit, :city, :state, :square_footage, :price, :bedrooms, :bathrooms, :pets, :image, :user_id)
end
end