diff --git a/Gemfile b/Gemfile index 6ebb7fc..d7d0f34 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,9 @@ gem 'jquery-rails' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' +# a library for generating fake data. +gem 'faker' + # Use Capistrano for deployment # gem 'capistrano-rails', group: :development @@ -44,18 +47,20 @@ group :development, :test do # rspec gem 'rspec-rails', '~> 3.5' - gem 'shoulda-matchers' # RSpec::JsonExpectations gem 'rspec-json_expectations' end group :test do - # for build strategies - gem 'factory_girl_rails' - - # database cleaner - gem 'database_rewinder' + # a library for setting up Ruby objects as test data. + gem 'factory_girl_rails', '~> 4.0' + # shoulda-matcher provides Rspec and Minitest compatible one liners that test common Rails functionality. + gem 'shoulda-matchers', '~> 3.1' + # strategies for cleaning database in Ruby. + gem 'database_cleaner' + # an IRB alternative and runtime developer console. + gem 'pry' end group :development do @@ -70,7 +75,7 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -# json genarator -gem 'active_model_serializers' +# ActiveModel::Serializer implementation and Rails hooks. +gem 'active_model_serializers', '~> 0.10.0' gem 'foreman' diff --git a/Gemfile.lock b/Gemfile.lock index f87d02e..1d81c95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,8 +48,9 @@ GEM byebug (9.0.6) case_transform (0.2) activesupport + coderay (1.1.1) concurrent-ruby (1.0.4) - database_rewinder (0.8.0) + database_cleaner (1.5.3) debug_inspector (0.0.2) diff-lcs (1.3) erubis (2.7.0) @@ -59,6 +60,8 @@ GEM factory_girl_rails (4.8.0) factory_girl (~> 4.8.0) railties (>= 3.0.0) + faker (1.7.3) + i18n (~> 0.5) ffi (1.9.17) foreman (0.83.0) thor (~> 0.19.1) @@ -90,6 +93,10 @@ GEM nio4r (1.2.1) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) puma (3.7.0) rack (2.0.1) rack-cors (0.4.1) @@ -149,6 +156,7 @@ GEM tilt (>= 1.1, < 3) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) + slop (3.6.0) spring (2.0.1) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -182,20 +190,22 @@ PLATFORMS ruby DEPENDENCIES - active_model_serializers + active_model_serializers (~> 0.10.0) byebug - database_rewinder - factory_girl_rails + database_cleaner + factory_girl_rails (~> 4.0) + faker foreman jquery-rails listen (~> 3.0.5) + pry puma (~> 3.0) rack-cors rails (~> 5.0.1) rspec-json_expectations rspec-rails (~> 3.5) sass-rails (~> 5.0) - shoulda-matchers + shoulda-matchers (~> 3.1) spring spring-watcher-listen (~> 2.0.0) sqlite3 @@ -204,4 +214,4 @@ DEPENDENCIES web-console (>= 3.3.0) BUNDLED WITH - 1.14.3 + 1.14.6 diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb index e930bf9..d725680 100644 --- a/app/controllers/api/api_controller.rb +++ b/app/controllers/api/api_controller.rb @@ -1,4 +1,6 @@ module Api class ApiController < ActionController::API + include Response + include ExceptionHandler end end diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb index 50e6208..adbd579 100644 --- a/app/controllers/api/messages_controller.rb +++ b/app/controllers/api/messages_controller.rb @@ -2,41 +2,33 @@ module Api class MessagesController < ApiController before_action :set_message, only: [:show, :update, :destroy] - # GET /messages + # GET / messages def index @messages = Message.order(created_at: :asc).all - - render json: @messages + json_response(@messages) end - # GET /messages/1 + # GET / messages / :id def show - render json: @message + json_response(@message) end - # POST /messages + # POST / messages def create - @message = Message.new(message_params) - - if @message.save - render json: @message, status: :created, location: [:admin, @message] - else - render json: @message.errors, status: :unprocessable_entity - end + @message = Message.create!(message_params) + json_response(@message, :created) end - # PATCH/PUT /messages/1 + # PUT / messages / :id def update - if @message.update(message_params) - render json: @message - else - render json: @message.errors, status: :unprocessable_entity - end + @message.update(message_params) + head :no_content end - # DELETE /messages/1 + # DELETE / messages / :id def destroy @message.destroy + head :no_content end private @@ -47,7 +39,7 @@ def set_message # Only allow a trusted parameter "white list" through. def message_params - params.require(:message).permit(:text) + params.permit(:text) end end end diff --git a/app/controllers/concerns/exception_handler.rb b/app/controllers/concerns/exception_handler.rb new file mode 100644 index 0000000..28d8dd9 --- /dev/null +++ b/app/controllers/concerns/exception_handler.rb @@ -0,0 +1,13 @@ +module ExceptionHandler + 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..cfb0caa --- /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/models/message.rb b/app/models/message.rb index b2d5fe6..af8dcf5 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,3 +1,6 @@ class Message < ApplicationRecord + # model association has_many :reactions, dependent: :destroy + # validation + validates_presence_of :text end diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb new file mode 100644 index 0000000..ec3decd --- /dev/null +++ b/spec/factories/messages.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :message do + text { Faker::Lorem.sentences } + end +end diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 57eadc6..ea5439f 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -1,5 +1,10 @@ require 'rails_helper' RSpec.describe Message, type: :model do - #pending "add some examples to (or delete) #{__FILE__}" + # association test + # ensure Message model has a 1:m relationship with the Reaction model + it { should have_many(:reactions).dependent(:destroy) } + # validation tests + # ensure columns text is present before saving + it { should validate_presence_of(:text) } end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 6f1ab14..4f24d6d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -5,7 +5,16 @@ abort("The Rails environment is running in production mode!") if Rails.env.production? require 'spec_helper' require 'rspec/rails' -# Add additional requires below this line. Rails is not loaded until this point! +# require database cleaner at the top level +require 'database_cleaner' + +# configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -20,7 +29,7 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } # Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. @@ -30,11 +39,28 @@ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.include RequestSpecHelper, type: :request # 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 + # add `FactoryGirl` methods + config.include FactoryGirl::Syntax::Methods + + # 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) + DatabaseCleaner.strategy = :transaction + end + + # start the transaction strategy as examples are run + config.around(:each) do |example| + DatabaseCleaner.cleaning do + example.run + end + end + # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and # `post` in specs under `spec/controllers`. diff --git a/spec/requests/messages_spec.rb b/spec/requests/messages_spec.rb index b32d9ab..4b19c1b 100644 --- a/spec/requests/messages_spec.rb +++ b/spec/requests/messages_spec.rb @@ -1,10 +1,105 @@ require 'rails_helper' -RSpec.describe "Messages", type: :request do - describe "GET /messages" do - it "works! (now write some real specs)" do - get api_messages_path +RSpec.describe "Messages API", type: :request do + # initialize tests data + let!(:messages) { create_list(:message, 10) } + let(:message_id) { messages.first.id } + + # test suite for GET / messages + describe "GET / messages" do + before { get "/api/messages" } + + it "returns status code 200" do expect(response).to have_http_status(200) end + + it "returns messages" do + expect(json).not_to be_empty + expect(json.size).to eq(10) + end + end + + # test suite for GET / messages / :id + describe "GET / messages / :id" do + before { get "/api/messages/#{message_id}" } + + context "when the record exists" do + it "returns status code 200" do + expect(response).to have_http_status(200) + end + + it "returns the message" do + expect(json).not_to be_empty + expect(json['id']).to eq(message_id) + end + end + + context "when the record does not exist" do + let(:message_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 Message/) + end + end + end + + # test suite for POST / messages + describe "POST / messages" do + let(:valid_attributes) { { text: "Text" } } + let(:invalid_attributes) { {} } + + context "when the request is valid" do + before { post "/api/messages", params: valid_attributes } + + it "returns status code 201" do + expect(response).to have_http_status(201) + end + + it "creates a message" do + expect(json['text']).to eq("Text") + end + end + + describe "when the request is invalid" do + before { post "/api/messages", params: invalid_attributes } + + 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: Text can't be blank/) + end + end + end + + # test suite for PUT / messages / :id + describe "PUT / messages / :id" do + let(:valid_attributes) { { text: "Text" } } + + context "when the record exists" do + before { put "/api/messages/#{message_id}", params: valid_attributes } + + it "returns status code 204" do + expect(response).to have_http_status(204) + end + + it "updates the record" do + expect(response.body).to be_empty + end + end + end + + # test suite for DELETE / messages / :id + describe "DELETE / messages / :id" do + before { delete "/api/messages/#{message_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..6d493ba --- /dev/null +++ b/spec/support/request_spec_helper.rb @@ -0,0 +1,5 @@ +module RequestSpecHelper + def json + JSON.parse(response.body) + end +end