Skip to content

Commit

Permalink
Refactor test suites
Browse files Browse the repository at this point in the history
  • Loading branch information
binos30 committed Dec 21, 2024
1 parent 1c7e5bd commit cd0a6da
Show file tree
Hide file tree
Showing 66 changed files with 870 additions and 468 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,5 @@ yarn-debug.log*
.ionide

# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode

coverage
14 changes: 11 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,15 @@ group :development, :test do
# A library for generating fake data such as names, addresses, and phone numbers
gem "faker"

# A fixtures replacement with a straightforward definition syntax, support for multiple build strategies [https://github.com/thoughtbot/factory_bot_rails]
gem "factory_bot_rails"

# Static analysis for security vulnerabilities [https://brakemanscanner.org/]
gem "brakeman", require: false

# Optimize queries [https://github.com/flyerhzm/bullet]
gem "bullet"

## Code Formatting & Linting
# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false
Expand Down Expand Up @@ -119,13 +125,15 @@ group :development do
# A normaliser/beautifier for HTML that also understands embedded Ruby. Ideal for tidying up Rails templates
# [https://github.com/threedaymonk/htmlbeautifier]
gem "htmlbeautifier", require: false

# Optimize queries
gem "bullet"
end

group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"

gem "shoulda-matchers", "~> 6.0"

# A code coverage analysis tool for Ruby [https://github.com/simplecov-ruby/simplecov]
gem "simplecov", require: false
end
19 changes: 18 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ GEM
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (6.2.1)
brakeman (6.2.2)
racc
builder (3.3.0)
bullet (7.2.0)
Expand Down Expand Up @@ -121,13 +121,19 @@ GEM
responders
warden (~> 1.2.3)
diff-lcs (1.5.1)
docile (1.4.1)
dotenv (3.1.4)
dotenv-rails (3.1.4)
dotenv (= 3.1.4)
railties (>= 6.1)
drb (2.2.1)
e2mmap (0.1.0)
erubi (1.13.0)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (2.12.0)
Expand Down Expand Up @@ -349,6 +355,14 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
slim (5.2.1)
temple (~> 0.10.0)
tilt (>= 2.1.0)
Expand Down Expand Up @@ -456,6 +470,7 @@ DEPENDENCIES
debug
devise
dotenv-rails
factory_bot_rails
faker
friendly_id
htmlbeautifier
Expand All @@ -473,6 +488,8 @@ DEPENDENCIES
rubocop-rspec
rubocop-rspec_rails
selenium-webdriver
shoulda-matchers (~> 6.0)
simplecov
slim-rails
slim_lint
solargraph
Expand Down
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ To test Stripe payments, use the following test card details:
2. Set the endpoint URL to your production route (e.g., `https://yourdomain.com/stripe_webhooks`).
3. Select the events you want to listen for (e.g., `checkout.session.completed`, `customer.created`).

## GitHub Actions, Linting and Security Auditing

GitHub actions are setup to lint and test the application on pushes to **main** and **feature** branches. It's also setup to deploy the application on pushes to **main**

You can also run these actions locally before pushing to see if your run is likely to fail. See the following gems / commands for more info.

- [Brakeman](https://brakemanscanner.org/) - Security audit application code

```bash
bin/brakeman -q -w2
```

- [Brakeman: Ignoring False Positives](https://brakemanscanner.org/docs/ignoring_false_positives) - Creating and Managing an Ignore File

```bash
bin/brakeman -I -q -w2
```

- [Rubocop Rails Omakase](https://github.com/rails/rubocop-rails-omakase) - Ruby Linter

```bash
bin/rubocop
```

**Note:** Some linters like `ESLint`, `Prettier`, etc. will automatically run on `pre-commit` git hook.

## Testing

Setup test database
Expand All @@ -69,6 +95,12 @@ Default: Run all spec files (i.e., those matching spec/\*\*/\*\_spec.rb)
bin/rspec
```

or with `--fail-fast` option to stop running the test suite on the first failed test. You may add a parameter to tell RSpec to stop running the test suite after N failed tests, for example: `--fail-fast=3`

```bash
bin/rspec --fail-fast
```

Run all spec files in a single directory (recursively)

```bash
Expand All @@ -81,20 +113,40 @@ Run a single spec file
bin/rspec spec/models/product_spec.rb
```

Use the plain-English descriptions to generate a report of where the application conforms to (or fails to meet) the spec
Run a single example from a spec file (by line number)

```bash
bin/rspec --format documentation spec/models/product_spec.rb
bin/rspec spec/models/product_spec.rb:6
```

Run a single example from a spec file (by line number)
Use the plain-English descriptions to generate a report of where the application conforms to (or fails to meet) the spec

```bash
bin/rspec --format documentation
```

```bash
bin/rspec spec/models/product_spec.rb:8
bin/rspec --format documentation spec/models/product_spec.rb
```

See all options for running specs

```bash
bin/rspec --help
```

After running your tests, open `coverage/index.html` in the browser of your choice. For example, in a Mac Terminal,
run the following command from your application's root directory:

```bash
open coverage/index.html
```

in a debian/ubuntu Terminal,

```bash
xdg-open coverage/index.html
```

**Note:** [This guide](https://dwheeler.com/essays/open-files-urls.html) can help if you're unsure which command your particular
operating system requires.
3 changes: 2 additions & 1 deletion app/models/category.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class Category < ApplicationRecord
has_one_attached :image do |attachable|
attachable.variant :thumb, resize_to_limit: [50, 50]
end
has_many :products, dependent: :destroy

has_many :products, inverse_of: :category, dependent: :destroy

has_rich_text :description

Expand Down
5 changes: 3 additions & 2 deletions app/models/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
class Order < ApplicationRecord
include Filterable

belongs_to :user
belongs_to :user, inverse_of: :orders

has_many :order_items, -> { order(:id) }, dependent: :destroy, inverse_of: :order
has_many :order_items, -> { order(:id) }, inverse_of: :order, dependent: :destroy

broadcasts_refreshes

validates :order_code, presence: true, uniqueness: { case_sensitive: false }
validates :customer_email, :customer_full_name, :customer_address, presence: true
validate :validate_has_one_item

Expand Down
6 changes: 3 additions & 3 deletions app/models/order_item.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# frozen_string_literal: true

class OrderItem < ApplicationRecord
belongs_to :order
belongs_to :product
belongs_to :stock
belongs_to :order, inverse_of: :order_items
belongs_to :product, inverse_of: :order_items
belongs_to :stock, inverse_of: :order_items

validates :order_code, :product_name, :size, presence: true
validates :quantity, presence: true, numericality: { greater_than: 0 }
Expand Down
13 changes: 4 additions & 9 deletions app/models/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,22 @@ class Product < ApplicationRecord
# Set the attribute from which the slug would be generated
slugify :name

belongs_to :category
belongs_to :category, inverse_of: :products

has_many_attached :images do |attachable|
attachable.variant :thumb, resize_to_limit: [50, 50]
attachable.variant :medium, resize_to_limit: [250, 250]
end
has_many :stocks, dependent: :destroy
has_many :order_items, dependent: :restrict_with_exception
has_many :stocks, inverse_of: :product, dependent: :destroy
has_many :order_items, inverse_of: :product, dependent: :restrict_with_exception

has_rich_text :description

broadcasts_refreshes_to :category
broadcasts_refreshes

validates :name, presence: true, uniqueness: { case_sensitive: false }
validates :price,
presence: true,
numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 999_999_999
}
validates :price, presence: true, numericality: { in: 0..999_999_999 }
validates :images, content_type: %i[jpeg jpg png webp], size: { less_than_or_equal_to: 3.megabytes }

scope :available, -> { joins(:stocks).where("quantity > 0").distinct }
Expand Down
5 changes: 3 additions & 2 deletions app/models/role.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# frozen_string_literal: true

class Role < ApplicationRecord
has_many :users, inverse_of: :role, dependent: :restrict_with_exception

validates :name,
presence: true,
uniqueness: {
case_sensitive: false
},
length: {
minimum: 2,
maximum: 40
in: 2..40
},
format: /\A([^\d\W]|-|\s)*\z/
end
11 changes: 4 additions & 7 deletions app/models/stock.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
# frozen_string_literal: true

class Stock < ApplicationRecord
belongs_to :product
belongs_to :product, inverse_of: :stocks

has_many :order_items, inverse_of: :stock, dependent: :restrict_with_exception

broadcasts_refreshes_to :product
broadcasts_refreshes

validates :size, presence: true, uniqueness: { scope: :product_id, case_sensitive: false }
validates :quantity,
presence: true,
numericality: {
greater_than_or_equal_to: 0,
less_than_or_equal_to: 999_999_999
}
validates :quantity, presence: true, numericality: { only_integer: true, in: 0..999_999_999 }
end
16 changes: 12 additions & 4 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ class User < ApplicationRecord
# The column `gender` is supposed to be of type string in the database.
literal_enum :gender, %w[male female]

belongs_to :role
belongs_to :role, inverse_of: :users

has_many :orders, dependent: :restrict_with_exception
has_many :orders, inverse_of: :user, dependent: :restrict_with_exception

validates :gender, presence: true, inclusion: { in: genders }
validates :gender, inclusion: { in: genders.keys }
validates :email, length: { maximum: 255 }
validates :password,
presence: true,
Expand Down Expand Up @@ -52,6 +52,14 @@ class User < ApplicationRecord
end
scope :filter_by_name, ->(name) { where("CONCAT(users.first_name, ' ', users.last_name) ILIKE ?", "%#{name}%") }

# Overwrite the setter to rely on validations instead of [ArgumentError]
# https://github.com/rails/rails/issues/13971#issuecomment-721821257
def gender=(value)
self[:gender] = value
rescue ArgumentError
self[:gender] = nil
end

# instead of deleting, indicate the user requested a delete & timestamp it
def soft_delete!
update!(active: false, deleted_at: Time.current)
Expand Down Expand Up @@ -98,7 +106,7 @@ def new_and_old_password_must_be_different
password_is_same = Devise::Encryptor.compare(User, encrypted_password_was, password)

return unless password_is_same
errors.add(:base, I18n.t("errors.messages.old_password_not_allowed"))
errors.add(:password, I18n.t("devise.passwords.old_password_not_allowed"))
end

def set_role
Expand Down
5 changes: 5 additions & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
config.after_initialize do
Bullet.enable = false # enable Bullet gem, otherwise do nothing
Bullet.bullet_logger = true # log to the Bullet log file (Rails.root/log/bullet.log)
Bullet.raise = true # raise an error if n+1 query occurs
end

# While tests run files are not watched, reloading is not necessary.
config.enable_reloading = false
Expand Down
2 changes: 1 addition & 1 deletion config/locales/devise.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ en:
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully."
old_password_not_allowed: "Old password not allowed"
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully."
Expand Down Expand Up @@ -64,4 +65,3 @@ en:
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
old_password_not_allowed: "Old password not allowed"
8 changes: 8 additions & 0 deletions spec/factories/categories.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

FactoryBot.define do
factory :category do
sequence(:name) { |n| "Category #{n}" }
description { "Description" }
end
end
14 changes: 14 additions & 0 deletions spec/factories/order_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

FactoryBot.define do
factory :order_item do
order
product
stock { association(:stock, product:) }
order_code { order.order_code }
product_name { product.name }
product_price { product.price }
size { stock.size }
quantity { 2 }
end
end
Loading

0 comments on commit cd0a6da

Please sign in to comment.