From 7024d2ffefe82a39d7430c58ace4d9106d5f125d Mon Sep 17 00:00:00 2001 From: Juan Esteban Nieto Date: Wed, 10 Nov 2021 15:06:30 -0500 Subject: [PATCH 1/2] add models --- app/models/item.rb | 7 +++++ app/models/todo.rb | 7 +++++ db/migrate/20211110194428_create_todos.rb | 10 +++++++ db/migrate/20211110194855_create_items.rb | 11 ++++++++ db/schema.rb | 32 +++++++++++++++++++++++ spec/models/item_spec.rb | 10 +++++++ spec/models/todo_spec.rb | 11 ++++++++ 7 files changed, 88 insertions(+) create mode 100644 app/models/item.rb create mode 100644 app/models/todo.rb create mode 100644 db/migrate/20211110194428_create_todos.rb create mode 100644 db/migrate/20211110194855_create_items.rb create mode 100644 db/schema.rb create mode 100644 spec/models/item_spec.rb create mode 100644 spec/models/todo_spec.rb diff --git a/app/models/item.rb b/app/models/item.rb new file mode 100644 index 0000000..e33eb18 --- /dev/null +++ b/app/models/item.rb @@ -0,0 +1,7 @@ +class Item < ApplicationRecord + # model association + belongs_to :todo + + # validation + validates_presence_of :name +end diff --git a/app/models/todo.rb b/app/models/todo.rb new file mode 100644 index 0000000..6b6956c --- /dev/null +++ b/app/models/todo.rb @@ -0,0 +1,7 @@ +class Todo < ApplicationRecord + # model association + has_many :items, dependent: :destroy + + # validations + validates_presence_of :title, :created_by +end diff --git a/db/migrate/20211110194428_create_todos.rb b/db/migrate/20211110194428_create_todos.rb new file mode 100644 index 0000000..be1f3b1 --- /dev/null +++ b/db/migrate/20211110194428_create_todos.rb @@ -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 diff --git a/db/migrate/20211110194855_create_items.rb b/db/migrate/20211110194855_create_items.rb new file mode 100644 index 0000000..53fc718 --- /dev/null +++ b/db/migrate/20211110194855_create_items.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..4635d0c --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,32 @@ +# 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.define(version: 2021_11_10_194855) do + + create_table "items", force: :cascade do |t| + t.string "name" + t.boolean "done" + t.integer "todo_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["todo_id"], name: "index_items_on_todo_id" + end + + create_table "todos", force: :cascade do |t| + t.string "title" + t.string "created_by" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + add_foreign_key "items", "todos" +end diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb new file mode 100644 index 0000000..7ecafdf --- /dev/null +++ b/spec/models/item_spec.rb @@ -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 diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb new file mode 100644 index 0000000..be399c4 --- /dev/null +++ b/spec/models/todo_spec.rb @@ -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 From 76570a034ab91da4813ae7a7acf82604eb2a01b9 Mon Sep 17 00:00:00 2001 From: Juan Esteban Nieto Date: Wed, 10 Nov 2021 18:38:27 -0500 Subject: [PATCH 2/2] add Controlers and tests --- Gemfile | 1 + app/controllers/application_controller.rb | 2 + app/controllers/concerns/exception_handler.rb | 14 ++ app/controllers/concerns/response.rb | 5 + app/controllers/items_controller.rb | 47 +++++++ app/controllers/todos_controller.rb | 43 ++++++ config/routes.rb | 3 + spec/factories/items.rb | 7 + spec/factories/todos.rb | 6 + spec/rails_helper.rb | 3 + spec/requests/items_spec.rb | 127 ++++++++++++++++++ spec/requests/todos_spec.rb | 108 +++++++++++++++ spec/support/request_spec_helper.rb | 6 + 13 files changed, 372 insertions(+) create mode 100644 app/controllers/concerns/exception_handler.rb create mode 100644 app/controllers/concerns/response.rb create mode 100644 app/controllers/items_controller.rb create mode 100644 app/controllers/todos_controller.rb create mode 100644 spec/factories/items.rb create mode 100644 spec/factories/todos.rb create mode 100644 spec/requests/items_spec.rb create mode 100644 spec/requests/todos_spec.rb create mode 100644 spec/support/request_spec_helper.rb diff --git a/Gemfile b/Gemfile index 889ccab..d8808e2 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ac8823..45e34ea 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,4 @@ class ApplicationController < ActionController::API + include Response + include ExceptionHandler end diff --git a/app/controllers/concerns/exception_handler.rb b/app/controllers/concerns/exception_handler.rb new file mode 100644 index 0000000..48dd18b --- /dev/null +++ b/app/controllers/concerns/exception_handler.rb @@ -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 diff --git a/app/controllers/concerns/response.rb b/app/controllers/concerns/response.rb new file mode 100644 index 0000000..dc37dca --- /dev/null +++ b/app/controllers/concerns/response.rb @@ -0,0 +1,5 @@ +module Response + def json_response(object, status = :ok) + render json: object, status: status + end +end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb new file mode 100644 index 0000000..c2eab7a --- /dev/null +++ b/app/controllers/items_controller.rb @@ -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 + \ No newline at end of file diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb new file mode 100644 index 0000000..fd25b7f --- /dev/null +++ b/app/controllers/todos_controller.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index c06383a..45187e3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/spec/factories/items.rb b/spec/factories/items.rb new file mode 100644 index 0000000..330ff5e --- /dev/null +++ b/spec/factories/items.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :item do + name { Faker::Movies::StarWars.character } + done { false } + todo_id { nil } + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb new file mode 100644 index 0000000..66bb5cc --- /dev/null +++ b/spec/factories/todos.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :todo do + title { Faker::Lorem.word } + created_by { Faker::Number.number(digits: 10) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d74f46f..0d55f9a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -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 @@ -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) diff --git a/spec/requests/items_spec.rb b/spec/requests/items_spec.rb new file mode 100644 index 0000000..3d81245 --- /dev/null +++ b/spec/requests/items_spec.rb @@ -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 diff --git a/spec/requests/todos_spec.rb b/spec/requests/todos_spec.rb new file mode 100644 index 0000000..9e58f9d --- /dev/null +++ b/spec/requests/todos_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' + +RSpec.describe 'Todos API', type: :request do + # initialize test data + let!(:todos) { create_list(:todo, 10) } + let(:todo_id) { todos.first.id } + + # Test suite for GET /todos + describe 'GET /todos' do + # make HTTP get request before each example + before { get '/todos' } + + it 'returns todos' do + # Note `json` is a custom helper to parse JSON responses + expect(json).not_to be_empty + expect(json.size).to eq(10) + end + + it 'returns status code 200' do + expect(response).to have_http_status(200) + end + end + + # Test suite for GET /todos/:id + describe 'GET /todos/:id' do + before { get "/todos/#{todo_id}" } + + context 'when the record exists' do + it 'returns the todo' do + expect(json).not_to be_empty + expect(json['id']).to eq(todo_id) + end + + it 'returns status code 200' do + expect(response).to have_http_status(200) + end + end + + context 'when the record does not exist' do + let(:todo_id) { 100 } + + 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 POST /todos + describe 'POST /todos' do + # valid payload + let(:valid_attributes) { { title: 'Learn Elm', created_by: '1' } } + + context 'when the request is valid' do + before { post '/todos', params: valid_attributes } + + it 'creates a todo' do + expect(json['title']).to eq('Learn Elm') + end + + it 'returns status code 201' do + expect(response).to have_http_status(201) + end + end + + context 'when the request is invalid' do + before { post '/todos', params: { title: 'Foobar' } } + + it 'returns status code 422' do + expect(response).to have_http_status(422) + end + + it 'returns a validation failure message' do + expect(response.body) + .to match(/Validation failed: Created by can't be blank/) + end + end + end + + # Test suite for PUT /todos/:id + describe 'PUT /todos/:id' do + let(:valid_attributes) { { title: 'Shopping' } } + + context 'when the record exists' do + before { put "/todos/#{todo_id}", params: valid_attributes } + + it 'updates the record' do + expect(response.body).to be_empty + end + + it 'returns status code 204' do + expect(response).to have_http_status(204) + end + end + end + + # Test suite for DELETE /todos/:id + describe 'DELETE /todos/:id' do + before { delete "/todos/#{todo_id}" } + + it 'returns status code 204' do + expect(response).to have_http_status(204) + end + end +end diff --git a/spec/support/request_spec_helper.rb b/spec/support/request_spec_helper.rb new file mode 100644 index 0000000..4c2b8ad --- /dev/null +++ b/spec/support/request_spec_helper.rb @@ -0,0 +1,6 @@ +module RequestSpecHelper + # Parse JSON response to ruby hash + def json + JSON.parse(response.body) + end +end