diff --git a/README.md b/README.md index 3c87f73..2ac60d8 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ To test Stripe payments, use the following test card details: 1. Go to the [Stripe Dashboard](https://dashboard.stripe.com) and create a new webhook for your production environment. 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`). +3. Select the events you want to listen for (e.g., `checkout.session.completed`, `customer.created`, `customer.deleted`). ## GitHub Actions, Linting and Security Auditing @@ -135,6 +135,16 @@ See all options for running specs bin/rspec --help ``` +## Code Coverage + +[Coverage]: https://docs.ruby-lang.org/en/3.3/Coverage.html "API doc for Ruby's Coverage library" +[SimpleCov]: https://github.com/simplecov-ruby/simplecov "A code coverage analysis tool for Ruby" + +[SimpleCov][SimpleCov] is a code coverage analysis tool for Ruby. It uses [Ruby's built-in Coverage][Coverage] library to +gather code coverage data, but makes processing its results much easier by providing a clean API to filter, group, merge, format, +and display those results, giving you a complete code coverage suite that can be set up with just a couple lines of code. +SimpleCov/Coverage track covered ruby code, gathering coverage for common templating solutions like erb, slim and haml is not supported. + 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: diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index a635924..714dd8a 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -59,8 +59,8 @@ def stripe # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/Cyclom end when "customer.created" customer = event.data.object - user = User.find_by!(email: customer.email) - user.update!(stripe_customer_id: customer.id) + user = User.find_by(email: customer.email) + user&.update!(stripe_customer_id: customer.id) when "customer.deleted" customer = event.data.object user = User.find_by(stripe_customer_id: customer.id) diff --git a/spec/models/order_item_spec.rb b/spec/models/order_item_spec.rb new file mode 100644 index 0000000..535e357 --- /dev/null +++ b/spec/models/order_item_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe OrderItem, type: :model do + describe "db_columns" do + it { should have_db_column(:order_id).of_type(:integer).with_options(null: false) } + it { should have_db_column(:product_id).of_type(:integer).with_options(null: false) } + it { should have_db_column(:stock_id).of_type(:integer).with_options(null: false) } + it { should have_db_column(:order_code).of_type(:string).with_options(null: false) } + it { should have_db_column(:product_name).of_type(:string).with_options(null: false) } + it do + should have_db_column(:product_price).of_type(:decimal).with_options( + null: false, + default: 0.0, + precision: 12, + scale: 2 + ) + end + it { should have_db_column(:size).of_type(:string).with_options(null: false) } + it { should have_db_column(:quantity).of_type(:integer).with_options(null: false) } + it { should have_db_column(:subtotal).of_type(:decimal).with_options(null: false, precision: 12, scale: 2) } + end + + describe "db_indexes" do + it { should have_db_index(:order_code) } + it { should have_db_index(:order_id) } + it { should have_db_index(:product_id) } + it { should have_db_index(:stock_id) } + end + + describe "associations" do + describe "belongs_to" do + it { should belong_to(:order).inverse_of(:order_items) } + it { should belong_to(:product).inverse_of(:order_items) } + it { should belong_to(:stock).inverse_of(:order_items) } + end + end + + describe "validations" do + describe "presence" do + it { should validate_presence_of(:order_code) } + it { should validate_presence_of(:product_name) } + it { should validate_presence_of(:size) } + it { should validate_presence_of(:quantity) } + it { should validate_presence_of(:product_price) } + end + + describe "numericality" do + it { should validate_numericality_of(:quantity).is_greater_than(0) } + it { should validate_numericality_of(:product_price).is_greater_than_or_equal_to(0) } + end + end +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb index 464a8ff..4c0397b 100644 --- a/spec/models/role_spec.rb +++ b/spec/models/role_spec.rb @@ -37,14 +37,16 @@ describe "format" do subject { build :role } - it "name accepts a valid value" do - subject.name = "role" - expect(subject).to be_valid - end - - it "name does not accept an invalid format" do - subject.name = "role-1" - expect(subject).to be_invalid + describe "name" do + it "accepts a valid value" do + subject.name = "role" + expect(subject).to be_valid + end + + it "does not accept an invalid format" do + subject.name = "role-1" + expect(subject).to be_invalid + end end end end diff --git a/spec/models/subscriber_spec.rb b/spec/models/subscriber_spec.rb index 44778da..8e0e71c 100644 --- a/spec/models/subscriber_spec.rb +++ b/spec/models/subscriber_spec.rb @@ -31,14 +31,16 @@ describe "format" do subject { build :subscriber } - it "email accepts a valid value" do - subject.email = "subscriber@email.com" - expect(subject).to be_valid - end - - it "email does not accept an invalid format" do - subject.email = "subscriber@" - expect(subject).to be_invalid + describe "email" do + it "accepts a valid value" do + subject.email = "subscriber@email.com" + expect(subject).to be_valid + end + + it "does not accept an invalid format" do + subject.email = "subscriber@" + expect(subject).to be_invalid + end end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 536b5f1..429704b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4,50 +4,47 @@ RSpec.describe User, type: :model do describe "db_columns" do - it { is_expected.to have_db_column(:email).of_type(:string).with_options(null: false, default: "") } - it do - is_expected.to have_db_column(:encrypted_password).of_type(:string).with_options(null: false, default: "") - end - it { is_expected.to have_db_column(:reset_password_token).of_type(:string) } - it { is_expected.to have_db_column(:reset_password_sent_at).of_type(:datetime) } - it { is_expected.to have_db_column(:remember_created_at).of_type(:datetime) } - it { is_expected.to have_db_column(:sign_in_count).of_type(:integer).with_options(null: false, default: 0) } - it { is_expected.to have_db_column(:current_sign_in_at).of_type(:datetime) } - it { is_expected.to have_db_column(:last_sign_in_at).of_type(:datetime) } - it { is_expected.to have_db_column(:current_sign_in_ip).of_type(:string) } - it { is_expected.to have_db_column(:last_sign_in_ip).of_type(:string) } - it { is_expected.to have_db_column(:first_name).of_type(:string).with_options(null: false) } - it { is_expected.to have_db_column(:last_name).of_type(:string).with_options(null: false) } - it { is_expected.to have_db_column(:phone_number).of_type(:string) } - it { is_expected.to have_db_column(:active).of_type(:boolean).with_options(null: false, default: true) } - it { is_expected.to have_db_column(:role_id).of_type(:integer).with_options(null: false) } - it { is_expected.to have_db_column(:gender).of_type(:string).with_options(null: false) } - it { is_expected.to have_db_column(:deleted_at).of_type(:datetime) } - it { is_expected.to have_db_column(:stripe_customer_id).of_type(:string) } + it { should have_db_column(:email).of_type(:string).with_options(null: false, default: "") } + it { should have_db_column(:encrypted_password).of_type(:string).with_options(null: false, default: "") } + it { should have_db_column(:reset_password_token).of_type(:string) } + it { should have_db_column(:reset_password_sent_at).of_type(:datetime) } + it { should have_db_column(:remember_created_at).of_type(:datetime) } + it { should have_db_column(:sign_in_count).of_type(:integer).with_options(null: false, default: 0) } + it { should have_db_column(:current_sign_in_at).of_type(:datetime) } + it { should have_db_column(:last_sign_in_at).of_type(:datetime) } + it { should have_db_column(:current_sign_in_ip).of_type(:string) } + it { should have_db_column(:last_sign_in_ip).of_type(:string) } + it { should have_db_column(:first_name).of_type(:string).with_options(null: false) } + it { should have_db_column(:last_name).of_type(:string).with_options(null: false) } + it { should have_db_column(:phone_number).of_type(:string) } + it { should have_db_column(:active).of_type(:boolean).with_options(null: false, default: true) } + it { should have_db_column(:role_id).of_type(:integer).with_options(null: false) } + it { should have_db_column(:gender).of_type(:string).with_options(null: false) } + it { should have_db_column(:deleted_at).of_type(:datetime) } + it { should have_db_column(:stripe_customer_id).of_type(:string) } end describe "db_indexes" do - it { is_expected.to have_db_index(:active) } - it { is_expected.to have_db_index(:email).unique } - it { is_expected.to have_db_index(:first_name) } - it { is_expected.to have_db_index(:last_name) } - it { is_expected.to have_db_index(:gender) } - it { is_expected.to have_db_index(:role_id) } - it { is_expected.to have_db_index(:reset_password_token).unique } - it { is_expected.to have_db_index(:stripe_customer_id).unique } + it { should have_db_index(:active) } + it { should have_db_index(:email).unique } + it { should have_db_index(:first_name) } + it { should have_db_index(:last_name) } + it { should have_db_index(:gender) } + it { should have_db_index(:role_id) } + it { should have_db_index(:reset_password_token).unique } + it { should have_db_index(:stripe_customer_id).unique } end describe "associations" do describe "belongs_to" do # Use `without_validating_presence` with `belong_to` to prevent the # matcher from checking whether the association disallows nil (Rails 5+ only). - # This can be helpful if you have a custom hook that always sets - # the association to a meaningful value: - it { is_expected.to belong_to(:role).inverse_of(:users).without_validating_presence } + # This can be helpful if you have a custom hook that always sets the association to a meaningful value: + it { should belong_to(:role).inverse_of(:users).without_validating_presence } end describe "has_many" do - it { is_expected.to have_many(:orders).inverse_of(:user).dependent(:restrict_with_exception) } + it { should have_many(:orders).inverse_of(:user).dependent(:restrict_with_exception) } end end @@ -72,7 +69,7 @@ end describe "inclusion" do - it { is_expected.to validate_inclusion_of(:gender).in_array(User.genders.keys) } + it { should validate_inclusion_of(:gender).in_array(User.genders.keys) } end describe "length" do @@ -98,24 +95,28 @@ describe "format" do subject { build :user } - it "first_name accepts a valid value" do - subject.first_name = "John" - expect(subject).to be_valid - end + describe "first_name" do + it "accepts a valid value" do + subject.first_name = "John" + expect(subject).to be_valid + end - it "first_name does not accept an invalid format" do - subject.first_name = "John-1" - expect(subject).to be_invalid + it "does not accept an invalid format" do + subject.first_name = "John-1" + expect(subject).to be_invalid + end end - it "last_name accepts a valid value" do - subject.last_name = "Doe" - expect(subject).to be_valid - end + describe "last_name" do + it "accepts a valid value" do + subject.last_name = "Doe" + expect(subject).to be_valid + end - it "last_name does not accept an invalid format" do - subject.last_name = "Doe-1" - expect(subject).to be_invalid + it "does not accept an invalid format" do + subject.last_name = "Doe-1" + expect(subject).to be_invalid + end end end