diff --git a/Gemfile b/Gemfile index 716a9ba..8b5ba80 100644 --- a/Gemfile +++ b/Gemfile @@ -14,12 +14,15 @@ gem "jbuilder" gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] gem 'slim-rails' gem "jsbundling-rails" +gem 'kaminari' +gem 'active_model_serializers' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mingw x64_mingw ] gem "rspec-rails" gem "pry-rails" + gem "factory_bot_rails" end group :development do @@ -37,6 +40,7 @@ group :test do gem "rspec-its" gem "shoulda" gem "simplecov", require: false + gem "rails-controller-testing" end # Use Redis for Action Cable diff --git a/Gemfile.lock b/Gemfile.lock index 8aaae9a..cb1a04e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_model_serializers (0.10.15) + actionpack (>= 4.1) + activemodel (>= 4.1) + case_transform (>= 0.2) + jsonapi-renderer (>= 0.1.1.beta1, < 0.3) activejob (7.0.8.6) activesupport (= 7.0.8.6) globalid (>= 0.3.6) @@ -68,6 +73,9 @@ GEM tzinfo (~> 2.0) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + airborne (0.0.18) + rest-client (~> 1.7, >= 1.7.2) + rspec (~> 3.1, >= 3.1.0) annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) @@ -83,6 +91,8 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + case_transform (0.2) + activesupport coderay (1.1.3) concurrent-ruby (1.3.4) crass (1.0.6) @@ -94,6 +104,7 @@ GEM reline (>= 0.3.8) diff-lcs (1.5.1) docile (1.4.1) + domain_name (0.6.20240107) erubi (1.13.0) factory_bot (6.5.0) activesupport (>= 5.0.0) @@ -104,6 +115,8 @@ GEM i18n (>= 1.8.11, < 2) globalid (1.2.1) activesupport (>= 6.1) + http-cookie (1.0.8) + domain_name (~> 0.5) i18n (1.14.6) concurrent-ruby (~> 1.0) io-console (0.7.2) @@ -115,6 +128,19 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) + jsonapi-renderer (0.2.2) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) logger (1.6.1) loofah (2.23.1) crass (~> 1.0.2) @@ -127,6 +153,7 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) + mime-types (2.99.3) mini_mime (1.1.5) minitest (5.25.2) net-imap (0.5.1) @@ -138,6 +165,7 @@ GEM timeout net-smtp (0.5.0) net-protocol + netrc (0.11.0) nio4r (2.7.4) nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) @@ -171,6 +199,10 @@ GEM activesupport (= 7.0.8.6) bundler (>= 1.15.0) railties (= 7.0.8.6) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -192,7 +224,15 @@ GEM regexp_parser (2.9.2) reline (0.5.11) io-console (~> 0.5) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rexml (3.3.9) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) @@ -279,9 +319,12 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES + active_model_serializers + airborne annotate capybara cssbundling-rails @@ -290,9 +333,11 @@ DEPENDENCIES faker jbuilder jsbundling-rails + kaminari pry-rails puma (~> 5.0) rails (~> 7.0.1) + rails-controller-testing redis (~> 4.0) rspec-its rspec-rails diff --git a/app/controllers/api/companies_controller.rb b/app/controllers/api/companies_controller.rb new file mode 100644 index 0000000..46fa306 --- /dev/null +++ b/app/controllers/api/companies_controller.rb @@ -0,0 +1,23 @@ +class Api::CompaniesController < ActionController::API + def index + if params[:name].present? + companies = Company.where('LOWER(name) LIKE ?', "%#{params[:name]&.downcase}%").page(params[:page] || 1).per(params[:per_page] || 10) + else + companies = Company.all.page(params[:page] || 1).per(10) + end + + render json: { meta: pagination_meta(companies), data: companies }, status: 200 + end + + private + + def pagination_meta(scope) + { + current_page: scope.current_page, + next_page: scope.next_page, + prev_page: scope.prev_page, + total_pages: scope.total_pages, + total_count: scope.total_count + } + end +end diff --git a/app/controllers/api/people_controller.rb b/app/controllers/api/people_controller.rb new file mode 100644 index 0000000..88c5d36 --- /dev/null +++ b/app/controllers/api/people_controller.rb @@ -0,0 +1,23 @@ +class Api::PeopleController < ActionController::API + def index + if params[:email].present? + people = Person.where(email: params[:email]).page(params[:page] || 1).per(params[:per_page] || 10) + else + people = Person.includes(:company).page(params[:page] || 1).per(10) + end + + render json: { meta: pagination_meta(people), data: people }, status: 200 + end + + private + + def pagination_meta(scope) + { + current_page: scope.current_page, + next_page: scope.next_page, + prev_page: scope.prev_page, + total_pages: scope.total_pages, + total_count: scope.total_count + } + end +end diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb new file mode 100644 index 0000000..1456ca4 --- /dev/null +++ b/app/controllers/companies_controller.rb @@ -0,0 +1,40 @@ +class CompaniesController < ApplicationController + before_action :load_company, only: [:edit, :update] + + def index + @companies = Company.all.page(params[:page] || 1).per(10) + end + + def new + @company = Company.new + end + + def edit; end + + def create + if Company.create(company_attributes) + redirect_to companies_path, notice: 'Successfully created entry' + else + render :create, alert: 'Unsuccessfully created entry' + end + end + + def update + if @company.update(company_attributes) + redirect_to companies_path, notice: 'Successfully updated entry' + else + render :edit, alert: 'Unsuccessfully created entry' + end + end + + private + + def company_attributes + params.require(:company).permit(:name) + end + + def load_company + @company = Company.find(params[:id]) + end +end + diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 6559b9e..4840946 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -1,26 +1,29 @@ class PeopleController < ApplicationController def index - @people = Person.all + @people = Person.includes(:company).page(params[:page] || 1).per(10) end def new @person = Person.new + @companies = Company.all.order(:name) end def create - if Person.create(person_attributes) - redirect_to people_path, notice: 'Successfully created entry' + @person = Person.new(person_attributes) + + if @person.save + redirect_to people_path, notice: 'Person successfully created' else - render :create, alert: 'Unsuccessfully created entry' + @companies = Company.page(params[:page] || 1).per(10) + render :new end end private def person_attributes - params.require(:person).permit(:name, :email, :phone) + params.require(:person).permit(:name, :email, :phone_number, :company_id) end - end diff --git a/app/models/person.rb b/app/models/person.rb index f935e46..fee1c9d 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -12,6 +12,7 @@ # class Person < ApplicationRecord - belongs_to :company, optional: true + + validates :name, :phone_number, :email, presence: true end diff --git a/app/serializers/person_serializer.rb b/app/serializers/person_serializer.rb new file mode 100644 index 0000000..939788a --- /dev/null +++ b/app/serializers/person_serializer.rb @@ -0,0 +1,8 @@ +# app/serializers/person_serializer.rb +class PersonSerializer < ActiveModel::Serializer + attributes :id, :name, :email, :phone_number, :company_name + + def company_name + object.company&.name + end +end diff --git a/app/views/companies/edit.html.slim b/app/views/companies/edit.html.slim new file mode 100644 index 0000000..6f5e495 --- /dev/null +++ b/app/views/companies/edit.html.slim @@ -0,0 +1,8 @@ +h2 Creating an entry + += form_for @company, class: 'row' do |f| + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + .col-auto.mt-4 + = f.submit class: 'btn btn-primary' diff --git a/app/views/companies/index.html.slim b/app/views/companies/index.html.slim new file mode 100644 index 0000000..c3d2cca --- /dev/null +++ b/app/views/companies/index.html.slim @@ -0,0 +1,25 @@ +h2 Viewing companies + +table.table + thead + tr + th[scope="col"] ID + th[scope="col"] Name + th[scope="col"] Created At + th[scope="col"] Actions + tbody + - @companies.each do |company| + tr + th[scope="row"]= company&.id + td= company.try(:name) + td= company.try(:created_at).strftime("%m/%d/%Y") + td= link_to "Edit", edit_company_path(company), class: 'btn btn-primary' + +.row.justify-content-between + .col-4 + = paginate @companies, theme: 'bootstrap3' + = page_entries_info @companies + .col-4 + = link_to "New Company", new_company_path, class: 'btn btn-success' + + diff --git a/app/views/companies/new.html.slim b/app/views/companies/new.html.slim new file mode 100644 index 0000000..6f5e495 --- /dev/null +++ b/app/views/companies/new.html.slim @@ -0,0 +1,8 @@ +h2 Creating an entry + += form_for @company, class: 'row' do |f| + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + .col-auto.mt-4 + = f.submit class: 'btn btn-primary' diff --git a/app/views/kaminari/bootstrap3/_first_page.html.slim b/app/views/kaminari/bootstrap3/_first_page.html.slim new file mode 100644 index 0000000..eefa7cd --- /dev/null +++ b/app/views/kaminari/bootstrap3/_first_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), + url, remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_gap.html.slim b/app/views/kaminari/bootstrap3/_gap.html.slim new file mode 100644 index 0000000..4becbc8 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_gap.html.slim @@ -0,0 +1,2 @@ +li.disabled.page-item + = link_to raw(t 'views.pagination.truncate'), '#', class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_last_page.html.slim b/app/views/kaminari/bootstrap3/_last_page.html.slim new file mode 100644 index 0000000..4907dec --- /dev/null +++ b/app/views/kaminari/bootstrap3/_last_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), + url, remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_next_page.html.slim b/app/views/kaminari/bootstrap3/_next_page.html.slim new file mode 100644 index 0000000..9c1a744 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_next_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.last?, raw(t 'views.pagination.next'), + url, rel: 'next', remote: remote, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_page.html.slim b/app/views/kaminari/bootstrap3/_page.html.slim new file mode 100644 index 0000000..b496069 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_page.html.slim @@ -0,0 +1,3 @@ +li class="#{'active' if page.current?} page-item" + = link_to page, page.current? ? '#' : url, + remote: remote, rel: page.rel, class: "page-link" diff --git a/app/views/kaminari/bootstrap3/_paginator.html.slim b/app/views/kaminari/bootstrap3/_paginator.html.slim new file mode 100644 index 0000000..adff4bd --- /dev/null +++ b/app/views/kaminari/bootstrap3/_paginator.html.slim @@ -0,0 +1,13 @@ += paginator.render do + ul.pagination + == first_page_tag unless current_page.first? + == prev_page_tag unless current_page.first? + + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + == page_tag page + - elsif !page.was_truncated? + == gap_tag + + == next_page_tag unless current_page.last? + == last_page_tag unless current_page.last? diff --git a/app/views/kaminari/bootstrap3/_prev_page.html.slim b/app/views/kaminari/bootstrap3/_prev_page.html.slim new file mode 100644 index 0000000..185aa51 --- /dev/null +++ b/app/views/kaminari/bootstrap3/_prev_page.html.slim @@ -0,0 +1,3 @@ +li.page-item + = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), + url, rel: 'prev', remote: remote, class: "page-link" diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 1fbdb01..e5c55a2 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -19,6 +19,8 @@ html[lang="en"] a.nav-link[aria-current="page" href="/"]All li.nav-item = link_to('Create', new_person_path, class: 'nav-link') + li.nav-item + = link_to('Companies', companies_path, class: 'nav-link') form.d-flex input.form-control.me-2[type="search" placeholder="Search" aria-label="Search"] button.btn.btn-outline-success[type="submit"] Search diff --git a/app/views/people/_form.html.slim b/app/views/people/_form.html.slim new file mode 100644 index 0000000..7fda90a --- /dev/null +++ b/app/views/people/_form.html.slim @@ -0,0 +1,26 @@ += form_for person, class: 'row' do |f| + - if person.errors.any? + .alert.alert-danger + h4 There were errors with your submission: + ul + - person.errors.full_messages.each do |msg| + li = msg + + .col-auto + = f.label :name, class: 'form-label' + = f.text_field :name, class: 'form-control' + + .col-auto + = f.label :phone_number, class: 'form-label' + = f.text_field :phone_number, class: 'form-control' + + .col-auto + = f.label :email, class: 'form-label' + = f.text_field :email, class: 'form-control' + + .col-auto + = f.label :company_id, 'Select a Company', class: 'form-label' + = f.collection_select :company_id, @companies, :id, :name, prompt: 'Choose a company', class: 'form-select' + + .col-auto.mt-4 + = f.submit person.new_record? ? 'Create Person' : 'Update Person', class: 'btn btn-primary' diff --git a/app/views/people/index.html.slim b/app/views/people/index.html.slim index ddbf52f..f06ea5f 100644 --- a/app/views/people/index.html.slim +++ b/app/views/people/index.html.slim @@ -13,9 +13,11 @@ table.table tr th[scope="row"]= person&.id td= person.try(:name) - td= person.try(:phone) + td= person.try(:phone_number) td= person.try(:email) td= person.try(:company).try(:name) += paginate @people, theme: 'bootstrap3' += page_entries_info @people diff --git a/app/views/people/new.html.slim b/app/views/people/new.html.slim index 40bc446..a262d8b 100644 --- a/app/views/people/new.html.slim +++ b/app/views/people/new.html.slim @@ -1,14 +1,2 @@ h2 Creating an entry - -= form_for @person, class: 'row' do |f| - .col-auto - = f.label :name, class: 'form-label' - = f.text_field :name, class: 'form-control' - .col-auto - = f.label :phone_number, class: 'form-label' - = f.text_field :phone_number, class: 'form-control' - .col-auto - = f.label :email, class: 'form-label' - = f.text_field :email, class: 'form-control' - .col-auto.mt-4 - = f.submit class: 'btn btn-primary' += render 'form', person: @person diff --git a/config/routes.rb b/config/routes.rb index f0685d3..b88d2b5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,11 @@ Rails.application.routes.draw do resources :people, only: [:index, :new, :create] + resources :companies root to: 'people#index' + + namespace :api do + resources :people, only: :index + resources :companies, only: :index + end end diff --git a/spec/controllers/api/companies_controller_spec.rb b/spec/controllers/api/companies_controller_spec.rb new file mode 100644 index 0000000..ea2de40 --- /dev/null +++ b/spec/controllers/api/companies_controller_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe Api::CompaniesController, type: :controller do + describe 'GET /api/companies' do + let!(:companies) { create_list(:company, 25) } + + it 'returns a successful response' do + get :index + expect(response).to have_http_status(:ok) + end + + it 'returns paginated results' do + get :index, params: { page: 1, per_page: 10 } + json = JSON.parse(response.body) + expect(json['meta']['current_page']).to eq(1) + expect(json['meta']['total_pages']).to eq(3) + expect(json['data'].size).to eq(10) + end + + it 'returns filtered results by name' do + create(:company, name: 'Acme Corp') + get :index, params: { name: 'acme' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['name']).to eq('Acme Corp') + end + + it 'handles case-insensitive filtering' do + create(:company, name: 'TestCompany') + get :index, params: { name: 'testcompany' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['name']).to eq('TestCompany') + end + + it 'returns an empty array if no companies match the filter' do + get :index, params: { name: 'nonexistent' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(0) + end + end +end diff --git a/spec/controllers/api/people_controller_spec.rb b/spec/controllers/api/people_controller_spec.rb new file mode 100644 index 0000000..6e72527 --- /dev/null +++ b/spec/controllers/api/people_controller_spec.rb @@ -0,0 +1,32 @@ +RSpec.describe Api::PeopleController, type: :controller do + describe 'GET /api/people' do + let!(:people) { create_list(:person, 25) } + + it 'returns a successful response' do + get :index + expect(response).to have_http_status(:ok) + end + + it 'returns paginated results' do + get :index, params: { page: 1, per_page: 10 } + json = JSON.parse(response.body) + expect(json['meta']['current_page']).to eq(1) + expect(json['meta']['total_pages']).to eq(3) + expect(json['data'].size).to eq(10) + end + + it 'returns filtered results by email' do + create(:person, email: 'test@example.com') + get :index, params: { email: 'test@example.com' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(1) + expect(json['data'][0]['email']).to eq('test@example.com') + end + + it 'returns an empty array if no people match the email filter' do + get :index, params: { email: 'nonexistent@example.com' } + json = JSON.parse(response.body) + expect(json['data'].size).to eq(0) + end + end +end diff --git a/spec/controllers/companies_controller.spec.rb b/spec/controllers/companies_controller.spec.rb new file mode 100644 index 0000000..8cad3eb --- /dev/null +++ b/spec/controllers/companies_controller.spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +RSpec.describe CompaniesController, type: :controller do + let(:valid_attributes) { { name: 'Test Company' } } + let(:invalid_attributes) { { name: '' } } + let!(:company) { create(:company, name: 'Existing Company') } + + describe 'GET #index' do + it 'assigns @companies with paginated results' do + get :index, params: { page: 1 } + expect(assigns(:companies)).to eq([company]) + end + + it 'renders the index template' do + get :index + expect(response).to render_template(:index) + end + end + + describe 'GET #new' do + it 'assigns a new company to @company' do + get :new + expect(assigns(:company)).to be_a_new(Company) + end + + it 'renders the new template' do + get :new + expect(response).to render_template(:new) + end + end + + describe 'GET #edit' do + it 'assigns the requested company to @company' do + get :edit, params: { id: company.id } + expect(assigns(:company)).to eq(company) + end + + it 'renders the edit template' do + get :edit, params: { id: company.id } + expect(response).to render_template(:edit) + end + end + + describe 'POST #create' do + context 'with valid attributes' do + it 'creates a new company' do + expect { + post :create, params: { company: valid_attributes } + }.to change(Company, :count).by(1) + end + + it 'redirects to the index with a success notice' do + post :create, params: { company: valid_attributes } + expect(response).to redirect_to(companies_path) + expect(flash[:notice]).to eq('Successfully created entry') + end + end + + context 'with invalid attributes' do + it 'does not create a new company' do + expect { + post :create, params: { company: invalid_attributes } + }.not_to change(Company, :count) + end + + it 'renders the create template with an alert' do + post :create, params: { company: invalid_attributes } + expect(response).to render_template(:create) + expect(flash[:alert]).to eq('Unsuccessfully created entry') + end + end + end + + describe 'PATCH #update' do + context 'with valid attributes' do + it 'updates the company' do + patch :update, params: { id: company.id, company: valid_attributes } + company.reload + expect(company.name).to eq('Test Company') + end + + it 'redirects to the index with a success notice' do + patch :update, params: { id: company.id, company: valid_attributes } + expect(response).to redirect_to(companies_path) + expect(flash[:notice]).to eq('Successfully updated entry') + end + end + + context 'with invalid attributes' do + it 'does not update the company' do + patch :update, params: { id: company.id, company: invalid_attributes } + company.reload + expect(company.name).not_to eq('') + end + + it 'renders the edit template with an alert' do + patch :update, params: { id: company.id, company: invalid_attributes } + expect(response).to render_template(:edit) + expect(flash[:alert]).to eq('Unsuccessfully created entry') + end + end + end +end diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index 82220e8..c6c7672 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -1,11 +1,53 @@ require 'rails_helper' +def create_people_with_companies(count) + count.times do |i| + company = Company.create!(name: "Company #{i}") + Person.create!(name: "Person #{i}", email: "person#{i}@example.com", phone_number: "123456789#{i}", company: company) + end +end + RSpec.describe PeopleController, type: :controller do subject { response } describe 'GET index' do before { get :index } it { is_expected.to have_http_status(:ok) } + + context "when there are many people to list" do + before do + create_people_with_companies(25) # Creates 25 people with associated companies + end + + it "assigns @people with paginated results" do + get :index, params: { page: 1 } + expect(assigns(:people)).to be_a_kind_of(ActiveRecord::Relation) + expect(assigns(:people).count).to eq(10) # Expect 10 items per page + end + + it "renders the index template" do + get :index, params: { page: 1 } + expect(response).to render_template(:index) + end + + it "paginates correctly for the first page" do + get :index, params: { page: 1 } + expect(assigns(:people).first.name).to eq("Person 0") + expect(assigns(:people).last.name).to eq("Person 9") + end + + it "paginates correctly for the second page" do + get :index, params: { page: 2 } + expect(assigns(:people).first.name).to eq("Person 10") + expect(assigns(:people).last.name).to eq("Person 19") + end + + it "defaults to the first page if no page param is given" do + get :index + expect(assigns(:people).first.name).to eq("Person 0") + expect(assigns(:people).last.name).to eq("Person 9") + end + end end describe 'GET new' do @@ -15,12 +57,34 @@ end describe 'POST create' do - it 'Creates a record' do - expect{ post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo' } } }.to change{ Person.count }.by(1) + let!(:company) { FactoryBot.create(:company) } + + context 'with valid attributes' do + it 'creates a record' do + expect { + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + }.to change { Person.count }.by(1) + end + + it 'associates the person with the correct company' do + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + person = Person.last + expect(person.company).to eq(company) + end + + it 'redirects to the index with a success notice' do + post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo@example.com', company_id: company.id } } + expect(response).to redirect_to(people_path) + expect(flash[:notice]).to eq('Person successfully created') + end end - it 'has status found' do - expect(post :create, params: { person: { name: 'foo', phone_number: '123', email: 'foo' } }).to have_http_status(:found) + context 'with invalid attributes' do + it 'does not create a record' do + expect { + post :create, params: { person: { name: '', phone_number: '', email: '', company_id: nil } } + }.not_to change { Person.count } + end end end end diff --git a/spec/factories/companies.rb b/spec/factories/companies.rb new file mode 100644 index 0000000..f8d2977 --- /dev/null +++ b/spec/factories/companies.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :company do + name { "Company 123" } + end +end diff --git a/spec/factories/people.rb b/spec/factories/people.rb new file mode 100644 index 0000000..a458391 --- /dev/null +++ b/spec/factories/people.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :person do + sequence(:name) { |n| "Person #{n}" } + phone_number { "123456" } + email { "email@gmail.com" } + + association :company + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8b88205..dfca94e 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,4 +1,5 @@ require 'simplecov' + SimpleCov.start require 'spec_helper' @@ -63,6 +64,8 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include FactoryBot::Syntax::Methods end Shoulda::Matchers.configure do |config| config.integrate do |with| diff --git a/spec/views/companies/index.html.slim_spec.rb b/spec/views/companies/index.html.slim_spec.rb new file mode 100644 index 0000000..52af868 --- /dev/null +++ b/spec/views/companies/index.html.slim_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +describe "companies/index.html.slim" do + let!(:company1) { FactoryBot.create(:company, name: "Company One", created_at: Time.zone.parse("2023-01-01")) } + let!(:company2) { FactoryBot.create(:company, name: "Company Two", created_at: Time.zone.parse("2023-01-02")) } + + it "displays the companies in a table" do + assign(:companies, Kaminari.paginate_array([company1, company2]).page(1).per(10)) + + render + + # Check table headers + expect(rendered).to have_selector("table.table thead tr th", text: "ID") + expect(rendered).to have_selector("table.table thead tr th", text: "Name") + expect(rendered).to have_selector("table.table thead tr th", text: "Created At") + expect(rendered).to have_selector("table.table thead tr th", text: "Actions") + + # Check rows for companies + expect(rendered).to have_selector("table.table tbody tr th", text: company1.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: company1.name) + expect(rendered).to have_selector("table.table tbody tr td", text: company1.created_at.strftime("%m/%d/%Y")) + expect(rendered).to have_link("Edit", href: edit_company_path(company1)) + + expect(rendered).to have_selector("table.table tbody tr th", text: company2.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: company2.name) + expect(rendered).to have_selector("table.table tbody tr td", text: company2.created_at.strftime("%m/%d/%Y")) + expect(rendered).to have_link("Edit", href: edit_company_path(company2)) + end + + it "displays a link to create a new company" do + assign(:companies, Kaminari.paginate_array([company1, company2]).page(1).per(10)) + + render + expect(rendered).to have_link("New Company", href: new_company_path) + end +end diff --git a/spec/views/people/index.html.slim_spec.rb b/spec/views/people/index.html.slim_spec.rb index 7e2d362..6f76005 100644 --- a/spec/views/people/index.html.slim_spec.rb +++ b/spec/views/people/index.html.slim_spec.rb @@ -1,5 +1,34 @@ require "rails_helper" describe "people/index.html.slim" do - it "Displays the users" + it "displays the users in a table" do + # Setup test data + company = FactoryBot.create(:company, name: "Test Company") + people = FactoryBot.create_list(:person, 12, company: company) + + # Assign instance variable used in the view + assign(:people, Kaminari.paginate_array(people).page(1).per(10)) + + # Render the view + render + + # Check for table headers + expect(rendered).to have_selector("table.table thead tr th", text: "ID") + expect(rendered).to have_selector("table.table thead tr th", text: "Name") + expect(rendered).to have_selector("table.table thead tr th", text: "Phone number") + expect(rendered).to have_selector("table.table thead tr th", text: "Email address") + expect(rendered).to have_selector("table.table thead tr th", text: "Company") + + # Check for table rows + people.first(10).each do |person| + expect(rendered).to have_selector("table.table tbody tr th", text: person.id.to_s) + expect(rendered).to have_selector("table.table tbody tr td", text: person.name) + expect(rendered).to have_selector("table.table tbody tr td", text: person.phone_number) + expect(rendered).to have_selector("table.table tbody tr td", text: person.email) + expect(rendered).to have_selector("table.table tbody tr td", text: company.name) + end + + # Check for pagination + expect(rendered).to have_selector(".pagination") + end end