diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7dc2a5a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: "CI" +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:12-alpine + ports: + - "5432:5432" + env: + POSTGRES_DB: rails_test + POSTGRES_USER: rails + POSTGRES_PASSWORD: password + env: + RAILS_ENV: test + DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test" + steps: + - name: Checkout code + uses: actions/checkout@v3 + # Add or replace dependency steps here + - name: Install Ruby and gems + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + # Add or replace database setup steps here + - name: Set up database schema + run: bin/rails db:schema:load + # Add or replace test runners here + - name: Run specs + run: bin/rspec + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Ruby and gems + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + # Add or replace any other lints here + - name: Security audit application code + run: bin/brakeman + - name: Lint Ruby files + run: bin/rubocop --parallel diff --git a/.gitignore b/.gitignore index 7061489..f84af9b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ # Ignore master key for decrypting credentials and more. /config/master.key + +.byebug_history diff --git a/.rubocop.yml b/.rubocop.yml index 984146f..732deec 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ require: - rubocop-capybara + - rubocop-factory_bot - rubocop-performance - rubocop-rspec - rubocop-rails @@ -14,5 +15,8 @@ AllCops: - lib/tasks/* NewCops: enable +Rails/I18nLocaleTexts: + Enabled: false + Style/Documentation: Enabled: false \ No newline at end of file diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 72b3400..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -ruby-3.2.1 diff --git a/Gemfile b/Gemfile index d108fa6..4feeba3 100644 --- a/Gemfile +++ b/Gemfile @@ -18,12 +18,6 @@ gem 'tzinfo-data', platforms: %i[windows jruby] group :development, :test do gem 'brakeman' gem 'byebug', platforms: %i[mri mingw x64_mingw] - gem 'rspec-rails' - gem 'rubocop', require: false - gem 'rubocop-capybara', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false end group :development do @@ -32,5 +26,13 @@ end group :test do gem 'capybara' - gem 'selenium-webdriver' + gem 'factory_bot_rails' + gem 'rspec-rails' + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-factory_bot', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + gem 'webdrivers' end diff --git a/Gemfile.lock b/Gemfile.lock index 7e4570d..e60a901 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -131,6 +131,11 @@ GEM drb (2.2.0) ruby2_keywords erubi (1.12.0) + factory_bot (6.4.4) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.2) + factory_bot (~> 6.4) + railties (>= 5.0.0) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.1) @@ -262,7 +267,7 @@ GEM ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - selenium-webdriver (4.16.0) + selenium-webdriver (4.10.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -290,6 +295,10 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webdrivers (5.3.1) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0, < 4.11) webrick (1.8.1) websocket (1.2.10) websocket-driver (0.7.6) @@ -309,6 +318,7 @@ DEPENDENCIES brakeman byebug capybara + factory_bot_rails importmap-rails jbuilder pg (~> 1.1) @@ -317,15 +327,16 @@ DEPENDENCIES rspec-rails rubocop rubocop-capybara + rubocop-factory_bot rubocop-performance rubocop-rails rubocop-rspec - selenium-webdriver sprockets-rails stimulus-rails turbo-rails tzinfo-data web-console + webdrivers RUBY VERSION ruby 3.2.2p53 diff --git a/README.md b/README.md index 3fd16a6..fe69ce6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # TransactionCategorization Rails App A simple [Rails](https://rubyonrails.org/) application that categorizes banking transaction via Machine Learning (AI). +![Build Status](https://github.com/agilous/transcat/actions/workflows/ci.yml/badge.svg) + ## Dependencies * [asdf](https://asdf-vm.com/#/) * [Ruby](https://www.ruby-lang.org/en/) 3.2.2 diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb new file mode 100644 index 0000000..b5d6f0e --- /dev/null +++ b/app/controllers/categories_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class CategoriesController < ApplicationController + before_action :set_category, only: %i[show edit update destroy] + + # GET /categories or /categories.json + def index + @categories = Category.all + end + + # GET /categories/1 or /categories/1.json + def show; end + + # GET /categories/new + def new + @category = Category.new + end + + # GET /categories/1/edit + def edit; end + + # POST /categories or /categories.json + def create + @category = Category.new(category_params) + + respond_to do |format| + if @category.save + format.html { redirect_to category_url(@category), notice: 'Category was successfully created.' } + format.json { render :show, status: :created, location: @category } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @category.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /categories/1 or /categories/1.json + def update + respond_to do |format| + if @category.update(category_params) + format.html { redirect_to category_url(@category), notice: 'Category was successfully updated.' } + format.json { render :show, status: :ok, location: @category } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @category.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /categories/1 or /categories/1.json + def destroy + @category.destroy! + + respond_to do |format| + format.html { redirect_to categories_url, notice: 'Category was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_category + @category = Category.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def category_params + params.require(:category).permit(:name) + end +end diff --git a/app/helpers/categories_helper.rb b/app/helpers/categories_helper.rb new file mode 100644 index 0000000..46512be --- /dev/null +++ b/app/helpers/categories_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module CategoriesHelper +end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000..edb6971 --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Category < ApplicationRecord + validates :name, uniqueness: true, presence: true +end diff --git a/app/views/categories/_category.html.erb b/app/views/categories/_category.html.erb new file mode 100644 index 0000000..8769e08 --- /dev/null +++ b/app/views/categories/_category.html.erb @@ -0,0 +1,7 @@ +
+

+ Name: + <%= category.name %> +

+ +
diff --git a/app/views/categories/_category.json.jbuilder b/app/views/categories/_category.json.jbuilder new file mode 100644 index 0000000..b7dac0e --- /dev/null +++ b/app/views/categories/_category.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.extract! category, :id, :name, :created_at, :updated_at +json.url category_url(category, format: :json) diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb new file mode 100644 index 0000000..74632c0 --- /dev/null +++ b/app/views/categories/_form.html.erb @@ -0,0 +1,22 @@ +<%= form_with(model: category) do |form| %> + <% if category.errors.any? %> +
+

<%= pluralize(category.errors.count, "error") %> prohibited this category from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :name, style: "display: block" %> + <%= form.text_field :name %> +
+ +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/categories/edit.html.erb b/app/views/categories/edit.html.erb new file mode 100644 index 0000000..996ba21 --- /dev/null +++ b/app/views/categories/edit.html.erb @@ -0,0 +1,10 @@ +

Editing category

+ +<%= render "form", category: @category %> + +
+ +
+ <%= link_to "Show this category", @category %> | + <%= link_to "Back to categories", categories_path %> +
diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb new file mode 100644 index 0000000..58d8158 --- /dev/null +++ b/app/views/categories/index.html.erb @@ -0,0 +1,14 @@ +

<%= notice %>

+ +

Categories

+ +
+ <% @categories.each do |category| %> + <%= render category %> +

+ <%= link_to "Show this category", category %> +

+ <% end %> +
+ +<%= link_to "New category", new_category_path %> diff --git a/app/views/categories/index.json.jbuilder b/app/views/categories/index.json.jbuilder new file mode 100644 index 0000000..0fd4887 --- /dev/null +++ b/app/views/categories/index.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.array! @categories, partial: 'categories/category', as: :category diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb new file mode 100644 index 0000000..8df9c54 --- /dev/null +++ b/app/views/categories/new.html.erb @@ -0,0 +1,9 @@ +

New category

+ +<%= render "form", category: @category %> + +
+ +
+ <%= link_to "Back to categories", categories_path %> +
diff --git a/app/views/categories/show.html.erb b/app/views/categories/show.html.erb new file mode 100644 index 0000000..dd28ade --- /dev/null +++ b/app/views/categories/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @category %> + +
+ <%= link_to "Edit this category", edit_category_path(@category) %> | + <%= link_to "Back to categories", categories_path %> + + <%= button_to "Destroy this category", @category, method: :delete %> +
diff --git a/app/views/categories/show.json.jbuilder b/app/views/categories/show.json.jbuilder new file mode 100644 index 0000000..00fa333 --- /dev/null +++ b/app/views/categories/show.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.partial! 'categories/category', category: @category diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000..b4fe8de --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'brakeman' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/ci b/bin/ci index 21f8824..6dd454c 100755 --- a/bin/ci +++ b/bin/ci @@ -18,10 +18,15 @@ Dir.chdir(APP_ROOT) do ruby_linting_passed = system("bin/rubocop") terminal_message("Done.\n\n") + terminal_message("Running Brakeman Scan...") + brakeman_passed = system("bin/brakeman") + terminal_message("Done.\n\n") + pass_fail(ruby_tests_passed, "Ruby Tests") pass_fail(ruby_linting_passed, "Ruby Linter") + pass_fail(brakeman_passed, "Brakeman Scan") - return 1 unless ruby_tests_passed && ruby_linting_passed + return 1 unless ruby_tests_passed && ruby_linting_passed && brakeman_passed return 0 end diff --git a/config/brakeman.yml b/config/brakeman.yml new file mode 100644 index 0000000..5b45b5d --- /dev/null +++ b/config/brakeman.yml @@ -0,0 +1,8 @@ +--- +:rails3: false +:rails4: false +:rails5: false +:rails6: false +:rails7: true +:quiet: true +:summary_only: :no_summary \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a125ef0..fbd66dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :categories # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. @@ -6,5 +7,5 @@ get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") - # root "posts#index" + root "categories#index" end diff --git a/db/migrate/20231229203335_create_categories.rb b/db/migrate/20231229203335_create_categories.rb new file mode 100644 index 0000000..da6ce09 --- /dev/null +++ b/db/migrate/20231229203335_create_categories.rb @@ -0,0 +1,11 @@ +class CreateCategories < ActiveRecord::Migration[7.2] + def change + create_table :categories do |t| + t.string :name, null: false + + t.timestamps + end + + add_index :categories, [:name], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 83701a2..7875a37 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,15 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 0) do +ActiveRecord::Schema[7.2].define(version: 2023_12_29_203335) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "categories", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name"], name: "index_categories_on_name", unique: true + end + end diff --git a/spec/factories/category_factory.rb b/spec/factories/category_factory.rb new file mode 100644 index 0000000..c4294f9 --- /dev/null +++ b/spec/factories/category_factory.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :category do + name { ['Clothing', 'Dining Out', 'Groceries', 'Rent', 'Transportation'].sample } + end +end diff --git a/spec/features/category_spec.rb b/spec/features/category_spec.rb new file mode 100644 index 0000000..dfd9222 --- /dev/null +++ b/spec/features/category_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Category' do + describe 'creating' do + before do + visit new_category_path + + fill_in 'Name', with: 'Gas' + click_button 'Create Category' + end + + it 'displays a success message' do + expect(page).to have_text 'Category was successfully created.' + end + + it 'displays the new Category' do + expect(page).to have_text 'Name: Gas' + end + + context 'when attempting to create a duplicate category' do + before do + visit new_category_path + + fill_in 'Name', with: Category.last.name + click_button 'Create Category' + end + + it 'displays an error message' do + expect(page).to have_text 'Name has already been taken' + end + + it 'returns to the new view' do + expect(page).to have_current_path(new_category_path) + end + end + end + + describe 'deleting' do + let(:category) { create(:category) } + + before do + visit category_path(category) + + click_button 'Destroy this category' + end + + it 'displays a success message' do + expect(page).to have_text 'Category was successfully destroyed.' + end + + it 'returns to the index view' do + expect(page).to have_current_path(categories_path) + end + end + + describe 'indexing' do + let(:category_names) { %w[Food Gas Utilities Rent] } + let!(:categories) { category_names.map { |name| create(:category, name:) } } + + before do + visit categories_path + end + + it 'lists all the category names' do + expect(categories.pluck(&:name)).to(be_all { |name| !page.text(/Name: #{name}/).nil? }) + end + end + + describe 'showing' do + let(:category) { create(:category) } + + before do + visit category_path(category) + end + + it 'displays the category name' do + expect(page).to have_text "Name: #{category.name}" + end + end + + describe 'updating' do + let(:category) { create(:category) } + let(:new_name) { "#{category.name} and more!" } + + before do + visit edit_category_path(category) + + fill_in 'Name', with: new_name + click_button 'Update Category' + end + + it 'displays a success message' do + expect(page).to have_text 'Category was successfully updated.' + end + + it 'displays the updated Category' do + expect(page).to have_text "Name: #{new_name}" + end + + context 'when attempting to update the name to an existing category name' do + let!(:another_category) { create(:category, name: "#{category.name} and still more!") } + + before do + visit edit_category_path(category) + + fill_in 'Name', with: another_category.name + click_button 'Update Category' + end + + it 'displays an error message' do + expect(page).to have_text 'Name has already been taken' + end + + it 'returns to the new view' do + expect(page).to have_current_path(edit_category_path(category)) + end + end + end +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb new file mode 100644 index 0000000..e627c73 --- /dev/null +++ b/spec/models/category_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Category do + subject(:category) { create(:category) } + + it { is_expected.to be_valid } + + context 'when name is nil' do + it 'raises an exception' do + expect { create(:category, name: nil) }.to raise_exception(ActiveRecord::RecordInvalid) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 254658c..99ebdaf 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -22,7 +22,7 @@ # directory. Alternatively, in the individual `*_spec.rb` files, manually # require only the support files necessary. # -# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } +Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } # Checks for pending migrations and applies them before tests are run. # If you are not using ActiveRecord, you can remove these lines. diff --git a/spec/requests/categories_spec.rb b/spec/requests/categories_spec.rb new file mode 100644 index 0000000..ac375da --- /dev/null +++ b/spec/requests/categories_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe '/categories' do + describe 'GET /index' do + it 'renders a successful response' do + get categories_url + expect(response).to be_successful + end + end + + describe 'GET /show' do + let!(:category) { create(:category) } + + it 'renders a successful response' do + get category_url(category) + expect(response).to be_successful + end + end + + describe 'GET /new' do + it 'renders a successful response' do + get new_category_url + expect(response).to be_successful + end + end + + describe 'GET /edit' do + let!(:category) { create(:category) } + + it 'renders a successful response' do + get edit_category_url(category) + expect(response).to be_successful + end + end + + describe 'POST /create' do + context 'with valid parameters' do + let(:valid_attributes) { { name: 'Food' } } + + it 'creates a new Category' do + expect do + post categories_url, params: { category: valid_attributes } + end.to change(Category, :count).by(1) + end + + it 'redirects to the created category' do + post categories_url, params: { category: valid_attributes } + expect(response).to redirect_to(category_url(Category.last)) + end + end + + context 'with invalid parameters' do + let(:invalid_attributes) { { name: nil } } + + it 'does not create a new Category' do + expect do + post categories_url, params: { category: invalid_attributes } + end.not_to change(Category, :count) + end + + it "renders a response with 422 status (i.e. to display the 'new' template)" do + post categories_url, params: { category: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe 'PATCH /update' do + context 'with valid parameters' do + let!(:category) { create(:category) } + let(:new_attributes) { { name: "#{category.name} and more!" } } + + it 'updates the requested category' do + patch category_url(category), params: { category: new_attributes } + category.reload + expect(category.reload.name).to eq(new_attributes[:name]) + end + + it 'redirects to the category' do + patch category_url(category), params: { category: new_attributes } + category.reload + expect(response).to redirect_to(category_url(category)) + end + end + + context 'with invalid parameters' do + let!(:category) { create(:category) } + let(:invalid_attributes) { { name: nil } } + + it "renders a response with 422 status (i.e. to display the 'edit' template)" do + patch category_url(category), params: { category: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe 'DELETE /destroy' do + let!(:category) { create(:category) } + + it 'destroys the requested category' do + expect do + delete category_url(category) + end.to change(Category, :count).by(-1) + end + + it 'redirects to the categories list' do + delete category_url(category) + expect(response).to redirect_to(categories_url) + end + end +end diff --git a/spec/routing/categories_routing_spec.rb b/spec/routing/categories_routing_spec.rb new file mode 100644 index 0000000..175e2ed --- /dev/null +++ b/spec/routing/categories_routing_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe CategoriesController do + describe 'routing' do + it 'routes to #index' do + expect(get: '/categories').to route_to('categories#index') + end + + it 'routes to #new' do + expect(get: '/categories/new').to route_to('categories#new') + end + + it 'routes to #show' do + expect(get: '/categories/1').to route_to('categories#show', id: '1') + end + + it 'routes to #edit' do + expect(get: '/categories/1/edit').to route_to('categories#edit', id: '1') + end + + it 'routes to #create' do + expect(post: '/categories').to route_to('categories#create') + end + + it 'routes to #update via PUT' do + expect(put: '/categories/1').to route_to('categories#update', id: '1') + end + + it 'routes to #update via PATCH' do + expect(patch: '/categories/1').to route_to('categories#update', id: '1') + end + + it 'routes to #destroy' do + expect(delete: '/categories/1').to route_to('categories#destroy', id: '1') + end + end +end diff --git a/spec/routing/root_routing_spec.rb b/spec/routing/root_routing_spec.rb new file mode 100644 index 0000000..12ec444 --- /dev/null +++ b/spec/routing/root_routing_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'root routing' do + it 'routes to categories#index' do + expect(get: '/').to route_to('categories#index') + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 409c64b..2773164 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -91,4 +91,15 @@ # # test failures related to randomization by passing the same `--seed` value # # as the one that triggered the failure. # Kernel.srand config.seed + + config.before(:each, type: :feature) do |example| + # Available drivers :rack_test, :selenium, :selenium_headless, :selenium_chrome, :selenium_chrome_headless + javascript_driver = example.metadata[:js_driver] || ENV.fetch('JAVASCRIPT_DRIVER', :selenium_headless).to_sym + Capybara.current_driver = javascript_driver + Capybara.server = :puma, { Silent: true } + end + + config.after(:each, type: :feature) do + Capybara.use_default_driver + end end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..2e7665c --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end