Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ end
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

group :development, :test do
gem 'faker'
gem 'rspec-rails', '~> 5.0.0'
end

Expand Down
2 changes: 2 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
end
14 changes: 14 additions & 0 deletions app/controllers/concerns/exception_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module ExceptionHandler
# provides the more graceful `included` method
extend ActiveSupport::Concern

included do
rescue_from ActiveRecord::RecordNotFound do |e|
json_response({ message: e.message }, :not_found)
end

rescue_from ActiveRecord::RecordInvalid do |e|
json_response({ message: e.message }, :unprocessable_entity)
end
end
end
5 changes: 5 additions & 0 deletions app/controllers/concerns/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
47 changes: 47 additions & 0 deletions app/controllers/items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class ItemsController < ApplicationController
before_action :set_todo
before_action :set_todo_item, only: [:show, :update, :destroy]

# GET /todos/:todo_id/items
def index
json_response(@todo.items)
end

# GET /todos/:todo_id/items/:id
def show
json_response(@item)
end

# POST /todos/:todo_id/items
def create
@todo.items.create!(item_params)
json_response(@todo, :created)
end

# PUT /todos/:todo_id/items/:id
def update
@item.update(item_params)
head :no_content
end

# DELETE /todos/:todo_id/items/:id
def destroy
@item.destroy
head :no_content
end

private

def item_params
params.permit(:name, :done)
end

def set_todo
@todo = Todo.find(params[:todo_id])
end

def set_todo_item
@item = @todo.items.find_by!(id: params[:id]) if @todo
end
end

43 changes: 43 additions & 0 deletions app/controllers/todos_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class TodosController < ApplicationController
before_action :set_todo, only: [:show, :update, :destroy]

# GET /todos
def index
@todos = Todo.all
json_response(@todos)
end

# POST /todos
def create
@todo = Todo.create!(todo_params)
json_response(@todo, :created)
end

# GET /todos/:id
def show
json_response(@todo)
end

# PUT /todos/:id
def update
@todo.update(todo_params)
head :no_content
end

# DELETE /todos/:id
def destroy
@todo.destroy
head :no_content
end

private

def todo_params
# whitelist params
params.permit(:title, :created_by)
end

def set_todo
@todo = Todo.find(params[:id])
end
end
7 changes: 7 additions & 0 deletions app/models/item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Item < ApplicationRecord
# model association
belongs_to :todo

# validation
validates_presence_of :name
end
7 changes: 7 additions & 0 deletions app/models/todo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Todo < ApplicationRecord
# model association
has_many :items, dependent: :destroy

# validations
validates_presence_of :title, :created_by
end
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
resources :todos do
resources :items
end
end
10 changes: 10 additions & 0 deletions db/migrate/20211110194428_create_todos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateTodos < ActiveRecord::Migration[6.1]
def change
create_table :todos do |t|
t.string :title
t.string :created_by

t.timestamps
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20211110194855_create_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateItems < ActiveRecord::Migration[6.1]
def change
create_table :items do |t|
t.string :name
t.boolean :done
t.references :todo, null: false, foreign_key: true

t.timestamps
end
end
end
32 changes: 32 additions & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions spec/factories/items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FactoryBot.define do
factory :item do
name { Faker::Movies::StarWars.character }
done { false }
todo_id { nil }
end
end
6 changes: 6 additions & 0 deletions spec/factories/todos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :todo do
title { Faker::Lorem.word }
created_by { Faker::Number.number(digits: 10) }
end
end
10 changes: 10 additions & 0 deletions spec/models/item_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require 'rails_helper'

RSpec.describe Item, type: :model do
# Association test
# ensure an item record belongs to a single todo record
it { should belong_to(:todo) }
# Validation test
# ensure column name is present before saving
it { should validate_presence_of(:name) }
end
11 changes: 11 additions & 0 deletions spec/models/todo_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'rails_helper'

RSpec.describe Todo, type: :model do
# Association test
# ensure Todo model has a 1:m relationship with the Item model
it { should have_many(:items).dependent(:destroy) }
# Validation tests
# ensure columns title and created_by are present before saving
it { should validate_presence_of(:title) }
it { should validate_presence_of(:created_by) }
end
3 changes: 3 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@

# require database cleaner at the top level
require 'database_cleaner'
require 'support/request_spec_helper'

# [...]
# configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails
Expand All @@ -81,6 +82,8 @@
# add `FactoryBot` methods
config.include FactoryBot::Syntax::Methods

config.include RequestSpecHelper, type: :request

# start by truncating all the tables but then use the faster transaction strategy the rest of the time.
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
Expand Down
127 changes: 127 additions & 0 deletions spec/requests/items_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require 'rails_helper'

RSpec.describe 'Items API' do
# Initialize the test data
let!(:todo) { create(:todo) }
let!(:items) { create_list(:item, 20, todo_id: todo.id) }
let(:todo_id) { todo.id }
let(:id) { items.first.id }

# Test suite for GET /todos/:todo_id/items
describe 'GET /todos/:todo_id/items' do
before { get "/todos/#{todo_id}/items" }

context 'when todo exists' do
it 'returns status code 200' do
expect(response).to have_http_status(200)
end

it 'returns all todo items' do
expect(json.size).to eq(20)
end
end

context 'when todo does not exist' do
let(:todo_id) { 0 }

it 'returns status code 404' do
expect(response).to have_http_status(404)
end

it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Todo/)
end
end
end

# Test suite for GET /todos/:todo_id/items/:id
describe 'GET /todos/:todo_id/items/:id' do
before { get "/todos/#{todo_id}/items/#{id}" }

context 'when todo item exists' do
it 'returns status code 200' do
expect(response).to have_http_status(200)
end

it 'returns the item' do
expect(json['id']).to eq(id)
end
end

context 'when todo item does not exist' do
let(:id) { 0 }

it 'returns status code 404' do
expect(response).to have_http_status(404)
end

it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Item/)
end
end
end

# Test suite for PUT /todos/:todo_id/items
describe 'POST /todos/:todo_id/items' do
let(:valid_attributes) { { name: 'Visit Narnia', done: false } }

context 'when request attributes are valid' do
before { post "/todos/#{todo_id}/items", params: valid_attributes }

it 'returns status code 201' do
expect(response).to have_http_status(201)
end
end

context 'when an invalid request' do
before { post "/todos/#{todo_id}/items", params: {} }

it 'returns status code 422' do
expect(response).to have_http_status(422)
end

it 'returns a failure message' do
expect(response.body).to match(/Validation failed: Name can't be blank/)
end
end
end

# Test suite for PUT /todos/:todo_id/items/:id
describe 'PUT /todos/:todo_id/items/:id' do
let(:valid_attributes) { { name: 'Mozart' } }

before { put "/todos/#{todo_id}/items/#{id}", params: valid_attributes }

context 'when item exists' do
it 'returns status code 204' do
expect(response).to have_http_status(204)
end

it 'updates the item' do
updated_item = Item.find(id)
expect(updated_item.name).to match(/Mozart/)
end
end

context 'when the item does not exist' do
let(:id) { 0 }

it 'returns status code 404' do
expect(response).to have_http_status(404)
end

it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Item/)
end
end
end

# Test suite for DELETE /todos/:id
describe 'DELETE /todos/:id' do
before { delete "/todos/#{todo_id}/items/#{id}" }

it 'returns status code 204' do
expect(response).to have_http_status(204)
end
end
end
Loading