diff --git a/.rubocop.yml b/.rubocop.yml index 662a3e108..a93ce97c1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,9 +28,14 @@ Metrics/MethodLength: Metrics/BlockLength: Max: 105 + ExcludedMethods: [describe] Style/StringLiterals: EnforcedStyle: double_quotes Style/Documentation: Enabled: false + +Style/BracesAroundHashParameters: + Exclude: + - spec/controllers/**/* diff --git a/Gemfile b/Gemfile index 96e197b82..d30c70040 100644 --- a/Gemfile +++ b/Gemfile @@ -92,6 +92,8 @@ group :development, :test do end group :test do + gem "airborne" + gem "api_matchers" gem "capybara", "2.13.0" gem "capybara-screenshot" gem "capybara-webkit", "1.14.0" @@ -101,6 +103,7 @@ group :test do gem "generator_spec" gem "launchy" gem "poltergeist" + gem "rails-controller-testing" gem "rails_best_practices" gem "rspec-rails", "3.6.1" gem "rspec-retry" diff --git a/Gemfile.lock b/Gemfile.lock index 2e4b5dead..c5514cb61 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,6 +40,16 @@ GEM tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) + airborne (0.2.13) + activesupport + rack + rack-test (~> 0.6, >= 0.6.2) + rest-client (>= 1.7.3, < 3.0) + rspec (~> 3.1) + api_matchers (0.6.2) + activesupport (>= 3.2.5) + nokogiri (>= 1.5.2) + rspec (>= 3.1) archive-zip (0.7.0) io-like (~> 0.3.0) arel (8.0.0) @@ -98,6 +108,8 @@ GEM debug_inspector (0.0.3) diff-lcs (1.3) docile (1.1.5) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) erubi (1.7.0) erubis (2.7.0) execjs (2.7.0) @@ -114,6 +126,8 @@ GEM railties (>= 3.0.0) globalid (0.4.1) activesupport (>= 4.2.0) + http-cookie (1.0.3) + domain_name (~> 0.5) i18n (0.9.1) concurrent-ruby (~> 1.0) interception (0.5) @@ -144,6 +158,7 @@ GEM libv8 (~> 5.9) minitest (5.10.3) multi_json (1.12.2) + netrc (0.11.0) nio4r (2.1.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) @@ -192,6 +207,10 @@ GEM bundler (>= 1.3.0) railties (= 5.1.4) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -226,6 +245,14 @@ GEM rainbow (~> 2.2) redis (3.3.3) require_all (1.4.0) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rspec (3.6.0) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) rspec-core (3.6.0) rspec-support (~> 3.6.0) rspec-expectations (3.6.0) @@ -306,6 +333,9 @@ GEM thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.4) unicode-display_width (1.3.0) web-console (3.5.1) actionview (>= 5.0) @@ -327,6 +357,8 @@ PLATFORMS ruby DEPENDENCIES + airborne + api_matchers autoprefixer-rails awesome_print brakeman @@ -355,6 +387,7 @@ DEPENDENCIES pry-stack_explorer puma rails (~> 5) + rails-controller-testing rails-html-sanitizer rails_best_practices rainbow diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb new file mode 100644 index 000000000..817e278f5 --- /dev/null +++ b/spec/controllers/comments_controller_spec.rb @@ -0,0 +1,345 @@ +require "rails_helper" +require "airborne" +require "controllers/shared/comments_factory" + +describe CommentsController do + render_views + + include_examples "Comments Factory", true + + subject(:comment_attrs) do + { author: author, text: text } + end + let(:author) { "Ponkey" } + let(:text) { "Says wheeeee..." } + + describe "GET #index," do + subject(:index_comments) { get :index, format: format } + let(:format) { :html } + + before { index_comments } + + it "assigns @comments with Comments.all in DESC order," do + expect(assigns(:comments)).to match_array(comments_list.reverse) + end + + it "renders the :index template," do + expect(response).to render_template :index + end + + describe "when JSON is requested" do + let(:format) { :json } + + it "returns JSON comments list in DESC order," do + expect_json( + "comments.0", + author: "Iesha", + text: "And a comment response..." + ) + expect_json( + "comments.1", + author: "Bai", + text: "Some comment text..." + ) + end + end + end + + describe "GET #show," do + subject(:show_comment) do + get :show, + params: { id: comment.id }, + format: format + end + let(:format) { :html } + + before { show_comment } + + it "assigns id'ed Comment as @comment," do + expect(assigns(:comment)).to eq(comment) + end + + it "renders the :show template," do + expect(response).to render_template :show + end + + describe "when JSON is requested" do + let(:format) { :json } + + it "returns JSON for @comment," do + expect_json({ author: "Bai", text: "Some comment text..." }) + end + end + end + + describe "GET @new" do + subject(:new_comment) do + get :new + end + + before { new_comment } + + it "assigns a new Comment as @comment," do + expect(assigns(:comment)).to be_a_new(Comment) + end + + it "renders the :new template," do + expect(response).to render_template(:new) + end + end + + describe "GET @edit" do + subject(:edit_comment) do + get :edit, params: { id: comment.id } + end + + before { edit_comment } + + it "assigns the id'ed Comment as @comment," do + expect(assigns(:comment)).to eq(comment) + end + + it "renders the :edit template," do + expect(response).to render_template(:edit) + end + end + + describe "POST #create," do + subject(:create_comment) do + post :create, params: { comment: comment_attrs, format: format }, xhr: true + end + let(:format) { :json } + + context "when creating succeeds," do + it "saves a Comment in the database," do + expect { create_comment }.to change(Comment, :count).by(1) + end + + it "assigns @comment with the created Comment," do + create_comment + expect(assigns(:comment)).to be_a(Comment) + expect(assigns(:comment)).to have_attributes( + author: "Ponkey", + text: "Says wheeeee..." + ) + end + + context "when JSON is requested," do + before { create_comment } + + it "renders the :show template," do + expect(response).to render_template(:show) + end + + it "returns JSON for the created comment," do + expect_json({ author: "Ponkey", text: "Says wheeeee..." }) + end + + it "returns a HTTP status of 201," do + expect(response).to have_http_status(:created) + end + + it "sets the @comments' HTTP location header," do + expect(response.header["Location"]) + .to eq("http://test.host/comments/#{assigns(:comment).id}") + end + end + + context "when HTML is requested," do + before { create_comment } + let(:format) { :html } + + it "returns a HTTP status of 302," do + expect(response).to have_http_status(:found) + end + + it "sets a flash notice," do + expect(flash[:notice]).to eq("Comment was successfully created.") + end + + it "redirects to the @comment," do + expect(response).to redirect_to(assigns(:comment)) + end + end + end + + context "when creating fails," do + let(:comment_attrs) do + { author: "", text: "text" } + end + + it "assigns @comment with new Comment JSON," do + create_comment + expect(assigns(@comment)["comment"]).to be_a_new(Comment) + end + + it "does not save a new Comment to the database," do + expect { create_comment }.not_to change(Comment, :count) + end + + context "when JSON is requested," do + before { create_comment } + + it "returns a HTTP status of 422," do + expect(response).to have_http_status(:unprocessable_entity) + end + + it "renders error information JSON," do + expect_json({ author: ["can't be blank"] }) + end + end + + context "when HTML is requested," do + let(:format) { :html } + + it "renders the :new template," do + create_comment + expect(response).to render_template(:new) + end + end + end + end + + describe "PATCH #update," do + subject(:update_comment) do + patch :update, + params: { id: comment.id, comment: comment_attrs, format: format }, + xhr: true + end + let(:format) { :json } + + before do + update_comment + comment.reload + end + + context "when updating succeeds," do + it "locates a Comment and assigns it to @comment," do + expect(assigns(:comment)).to eq(comment) + end + + it "updates the Comment," do + expect(assigns(:comment)).to have_attributes( + author: "Ponkey", + text: "Says wheeeee..." + ) + end + + context "when JSON is requested," do + it "renders the :show template," do + expect(response).to render_template(:show) + end + + it "returns JSON for the created comment," do + expect_json({ author: "Ponkey", text: "Says wheeeee..." }) + end + + it "returns a HTTP status of 201," do + expect(response).to have_http_status(:ok) + end + + it "sets the @comments' HTTP location header," do + expect(response.header["Location"]) + .to eq("http://test.host/comments/#{assigns(:comment).id}") + end + end + + context "when HTML is requested," do + let(:format) { :html } + + it "returns a HTTP status of 302," do + expect(response).to have_http_status(:found) + end + + it "sets a flash notice," do + expect(flash[:notice]).to eq("Comment was successfully updated.") + end + + it "redirects to the @comment," do + expect(response).to redirect_to(assigns(:comment)) + end + end + end + + context "when updating fails," do + let(:comment_attrs) do + { author: nil, text: "Text present..." } + end + + it "does not update the @comment," do + expect(comment).to have_attributes( + author: "Bai", + text: "Some comment text..." + ) + end + + context "when JSON is requested," do + it "renders error information JSON," do + expect_json({ author: ["can't be blank"] }) + end + + it "returns a HTTP status of 422," do + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context "when HTML is requested," do + before { update_comment } + let(:format) { :html } + + it "renders the :new template," do + expect(response).to render_template(:edit) + end + end + end + end + + describe "DELETE destroy" do + before { comment } + + subject(:destroy_comment) do + delete :destroy, params: { id: comment_id, format: format }, xhr: true + end + let(:comment_id) { comment.id } + let(:format) { :json } + + describe "when an existing Comment is id'ed," do + it "deletes the id'ed @comment" do + expect { destroy_comment }.to change(Comment, :count).by(-1) + end + + describe "when JSON is requested" do + it "returns an HTTP status of 204," do + destroy_comment + expect(response).to have_http_status(:no_content) + end + end + + describe "when HTML is requested" do + let(:format) { :html } + + it "deletes the id'ed @comment" do + expect { destroy_comment }.to change(Comment, :count).by(-1) + end + + it "redirects to comments_url," do + destroy_comment + expect(response).to redirect_to(comments_url) + end + + it "adds a flash:notice message," do + destroy_comment + expect(flash[:notice]).to eq("Comment was successfully destroyed.") + end + end + end + + describe "when the id'ed Comment does not exist," do + let(:comment_id) { 23 } + + it "raises a RecordNotFound error," do + expect { destroy_comment }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/controllers/pages_controller_spec.rb b/spec/controllers/pages_controller_spec.rb new file mode 100644 index 000000000..31742f8ff --- /dev/null +++ b/spec/controllers/pages_controller_spec.rb @@ -0,0 +1,69 @@ +require "rails_helper" +require "controllers/shared/comment_props" +require "controllers/shared/comments_factory" + +describe PagesController do + render_views + + def comments_store + assigns(:registered_stores_defer_render) + end + + def comment_json(i) + props_hash = JSON.parse(comments_store.first[:props]) + props_hash["comments"][i].to_json + end + + include_examples "Comments Factory", true + + describe "GET #index," do + subject(:index_comments) { get :index, format: :html } + + before { index_comments } + + describe "hydrates a redux store," do + it "with the store_name 'routerCommentsStore'," do + expect(comments_store + .first[:store_name]).to eq("routerCommentsStore") + end + + include_examples "Comment Props" + include_examples "Comment 0 Attributes" + include_examples "Comment 1 Attributes" + end + + it "renders the :index template," do + expect(response).to render_template :index + end + end + + describe "GET #no_router," do + subject(:no_router_comments) { get :no_router, format: :html } + + before { no_router_comments } + + describe "hydrates a redux store," do + it "with the store_name 'commentsStore'," do + expect(comments_store + .first[:store_name]).to eq("commentsStore") + end + + include_examples "Comment Props" + include_examples "Comment 0 Attributes" + include_examples "Comment 1 Attributes" + end + + it "renders the :index template," do + expect(response).to render_template :no_router + end + end + + describe "GET #simple," do + subject(:simple_comments) { get :simple, format: :html } + + it "renders the :simple template," do + simple_comments + expect(response).to render_template :simple + end + end +end diff --git a/spec/controllers/shared/comment_props.rb b/spec/controllers/shared/comment_props.rb new file mode 100644 index 000000000..e81114073 --- /dev/null +++ b/spec/controllers/shared/comment_props.rb @@ -0,0 +1,24 @@ +require "rails_helper" + +shared_context "Comment Props" do + it "with props for a collection of Comments," do + expect(comments_store.first[:props]).to have_node(:comments) + end +end + +shared_context "Comment 0 Attributes" do + it "with nodes for Comment 0 attributes," do + expect(comment_json(0)).to have_node(:id) + expect(comment_json(0)).to have_node(:author).with("Iesha") + expect(comment_json(0)).to have_node(:text).with("And a comment response...") + end +end + +shared_context "Comment 1 Attributes" do + it "with nodes for Comment 1 attributes," do + expect(comment_json(1)).to have_node(:id) + expect(comment_json(1)).to have_node(:author).with("Bai") + expect(comment_json(1)).to have_node(:text) + .with("Some comment text...") + end +end diff --git a/spec/controllers/shared/comments_factory.rb b/spec/controllers/shared/comments_factory.rb new file mode 100644 index 000000000..c17b4c287 --- /dev/null +++ b/spec/controllers/shared/comments_factory.rb @@ -0,0 +1,22 @@ +require "rails_helper" + +shared_context "Comments Factory" do + subject(:comment) do + create( + :comment, + author: "Bai", + text: "Some comment text..." + ) + end + subject(:comments_list) do + [ + comment, + create( + :comment, + author: "Iesha", + text: "And a comment response..." + ) + ] + end + before { comments_list } +end diff --git a/spec/model/comment_spec.rb b/spec/model/comment_spec.rb new file mode 100644 index 000000000..4b76551c3 --- /dev/null +++ b/spec/model/comment_spec.rb @@ -0,0 +1,10 @@ +require "rails_helper" + +describe Comment, type: :model do + subject(:comment) { create(:comment) } + + describe "model instance" do + it { should respond_to(:author) } + it { should respond_to(:text) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b1683793d..fcc26aedf 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -33,6 +33,7 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods + config.include APIMatchers::RSpecMatchers # Next line will ensure that assets are built if webpack -w is not running ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config, :requires_webpack_assets) diff --git a/spec/routing/comments_routing_spec.rb b/spec/routing/comments_routing_spec.rb new file mode 100644 index 000000000..75e6c4c9d --- /dev/null +++ b/spec/routing/comments_routing_spec.rb @@ -0,0 +1,35 @@ +require "rails_helper" + +describe CommentsController do + describe "routing" do + it "routes to #index" do + expect(get("/comments")).to route_to("comments#index") + end + + it "has a 'comments_path' GET comments#index route name" do + expect(get: comments_path) + .to route_to(controller: "comments", action: "index") + end + + it "routes to #create" do + expect(post("/comments")).to route_to("comments#create") + end + + it "routes to #update (PATCH)" do + expect(patch("/comments/1")).to route_to("comments#update", id: "1") + end + + it "routes to #update (PUT)" do + expect(put("/comments/1")).to route_to("comments#update", id: "1") + end + + it "routes to #show" do + expect(get("/comments/1")).to route_to("comments#show", id: "1") + end + + it "has a 'comment_path(:id)' GET comments#show(:id) route name" do + expect(get: comment_path(id: "1")) + .to route_to(controller: "comments", action: "show", id: "1") + end + end +end