diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml deleted file mode 100644 index 8bd7199..0000000 --- a/.github/workflows/brakeman.yml +++ /dev/null @@ -1,58 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow integrates Brakeman with GitHub's Code Scanning feature -# Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications - -name: Brakeman Scan - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '31 4 * * 6' - -permissions: - contents: read - -jobs: - brakeman-scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - name: Brakeman Scan - runs-on: ubuntu-latest - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout - uses: actions/checkout@v3 - - # Customize the ruby version depending on your needs - - name: Setup Ruby - uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4 # v1.127.0 - with: - ruby-version: '2.7' - - - name: Setup Brakeman - env: - BRAKEMAN_VERSION: '4.10' # SARIF support is provided in Brakeman version 4.10+ - run: | - gem install brakeman --version $BRAKEMAN_VERSION - - # Execute Brakeman CLI and generate a SARIF output with the security issues identified during the analysis - - name: Scan - continue-on-error: true - run: | - brakeman -f sarif -o output.sarif.json . - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: output.sarif.json diff --git a/.github/workflows/main_flow.yml b/.github/workflows/main_flow.yml index 205ce45..4e13e88 100644 --- a/.github/workflows/main_flow.yml +++ b/.github/workflows/main_flow.yml @@ -38,4 +38,35 @@ jobs: run: bin/rails db:schema:load # Add or replace test runners here - name: Run tests - run: bin/rake test + run: bin/rake + - name: SonarCloud Scan + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: + -Dsonar.projectKey=lytrungtin_url_shortener + -Dsonar.organization=lytrungtin + -Dsonar.exclusions=/vendor/** + -Dsonar.java.file.suffixes=- + -Dsonar.c.file.suffixes=- + -Dsonar.cpp.file.suffixes=- + -Dsonar.objc.file.suffixes=- + -Dsonar.ruby.coverage.reportPaths=/home/runner/work/url_shortener/url_shortener/coverage/.resultset.json + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Install Ruby and gems + uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4 # v1.127.0 + with: + bundler-cache: true + # Add or replace any other lints here + - name: Security audit dependencies + run: bundle exec bundler-audit update + - name: Security audit application code + run: bundle exe brakeman -q -w2 + - name: Lint Ruby files + run: bundle exec rubocop -A --parallel --require rubocop-rails diff --git a/Gemfile b/Gemfile index 655f62c..290ab92 100644 --- a/Gemfile +++ b/Gemfile @@ -40,13 +40,12 @@ gem 'rack-cors' 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 'dotenv-rails' - gem 'simplecov', require: false, group: :test -end - -group :development do gem 'brakeman' gem 'bundle-audit' + gem 'debug', platforms: %i[mri mingw x64_mingw] + gem 'dotenv-rails' gem 'rubocop' + gem 'rubocop-rails' + gem 'rubocop-rake' + gem 'simplecov', group: :test end diff --git a/Gemfile.lock b/Gemfile.lock index ea45af1..2bfac34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -179,6 +179,12 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.24.0) parser (>= 3.1.1.0) + rubocop-rails (2.17.4) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) ruby-progressbar (1.11.0) simplecov (0.22.0) docile (~> 1.1) @@ -211,6 +217,8 @@ DEPENDENCIES rack-cors rails (~> 7.0.4) rubocop + rubocop-rails + rubocop-rake simplecov tzinfo-data diff --git a/app/controllers/api/v1/url_controller.rb b/app/controllers/api/v1/url_controller.rb index 899e821..2601b81 100644 --- a/app/controllers/api/v1/url_controller.rb +++ b/app/controllers/api/v1/url_controller.rb @@ -2,6 +2,7 @@ module Api module V1 + # /api/v1/url class UrlController < ApplicationController before_action :decode_params, only: [:decode] before_action :encode_params, only: [:encode] diff --git a/app/controllers/redirection_controller.rb b/app/controllers/redirection_controller.rb index 2add0f4..97c4190 100644 --- a/app/controllers/redirection_controller.rb +++ b/app/controllers/redirection_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -class RedirectionController < ActionController::Base +# Handles redirections for All urls already shortened with :slug param +class RedirectionController < ApplicationController before_action :validate_slug def redirect result = Rails.cache.fetch("decode_shortened_url:#{request.original_url}") do @@ -14,7 +15,7 @@ def redirect private def not_found - render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found + render file: Rails.root.join('/public/404.html'), layout: false, status: :not_found end def validate_slug diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d84cb6e..34aa40f 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 08dc537..fe08714 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# app/models/application_record.rb class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/app/models/url.rb b/app/models/url.rb index 1aa5526..c11feb2 100644 --- a/app/models/url.rb +++ b/app/models/url.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true +# Storing and validating URLs input before generate shorten slug class Url < ApplicationRecord - validates_presence_of :original_url, :slug + validates :original_url, :slug, presence: true validates :original_url, url: true - validates_uniqueness_of :slug - validates_length_of :original_url, within: 10..2048 - validates_length_of :slug, within: 4..4 + validates :slug, uniqueness: true + validates :original_url, length: { within: 10..2048 } + validates :slug, length: { within: 4..4 } before_validation :generate_slug before_save do @@ -18,7 +19,7 @@ class Url < ApplicationRecord end def generate_slug - return if !slug.nil? && !slug.empty? + return if !slug.nil? && !slug.strip.nil? slug = SecureRandom.alphanumeric(4) return generate_slug if Url.exists?(slug:) @@ -43,7 +44,7 @@ def self.decode(shortened_url) return ['Shorten URL is not valid'] end - url = Url.find_by_slug(shortened_uri.path.split('/').last) + url = Url.find_by(slug: shortened_uri.path.split('/').last) return url.original_url if url ['Shorten URL is not existed'] diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index b6797eb..dcdd90e 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -2,6 +2,7 @@ require 'net/http' +# URL validator for urls from Model should valid, be able to redirect or response success class UrlValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return if value.blank? diff --git a/config/application.rb b/config/application.rb index 89851a1..b0ce0c6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,7 @@ Dotenv::Railtie.load if %w[development test].include? ENV['RAILS_ENV'] module Shortening + # config/application.rb class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 diff --git a/config/database.yml b/config/database.yml deleted file mode 100644 index e740314..0000000 --- a/config/database.yml +++ /dev/null @@ -1,86 +0,0 @@ -# PostgreSQL. Versions 9.3 and up are supported. -# -# Install the pg driver: -# gem install pg -# On macOS with Homebrew: -# gem install pg -- --with-pg-config=/usr/local/bin/pg_config -# On macOS with MacPorts: -# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config -# On Windows: -# gem install pg -# Choose the win32 build. -# Install PostgreSQL and put its /bin directory on your path. -# -# Configure Using Gemfile -# gem "pg" -# -default: &default - adapter: postgresql - encoding: unicode - # For details on connection pooling, see Rails configuration guide - # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - -development: - <<: *default - database: shortening_development - - # The specified database role being used to connect to postgres. - # To create additional roles in postgres see `$ createuser --help`. - # When left blank, postgres will use the default role. This is - # the same name as the operating system user running Rails. - #username: shortening - - # The password associated with the postgres role (username). - #password: - - # Connect on a TCP socket. Omitted by default since the client uses a - # domain socket that doesn't need configuration. Windows does not have - # domain sockets, so uncomment these lines. - #host: localhost - - # The TCP port the server listens on. Defaults to 5432. - # If your server runs on a different port number, change accordingly. - #port: 5432 - - # Schema search path. The server defaults to $user,public - #schema_search_path: myapp,sharedapp,public - - # Minimum log levels, in increasing order: - # debug5, debug4, debug3, debug2, debug1, - # log, notice, warning, error, fatal, and panic - # Defaults to warning. - #min_messages: notice - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <<: *default - database: shortening_test - -# As with config/credentials.yml, you never want to store sensitive information, -# like your database password, in your source code. If your source code is -# ever seen by anyone, they now have access to your database. -# -# Instead, provide the password or a full connection URL as an environment -# variable when you boot the app. For example: -# -# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" -# -# If the connection URL is provided in the special DATABASE_URL environment -# variable, Rails will automatically merge its configuration values on top of -# the values provided in this file. Alternatively, you can specify a connection -# URL environment variable explicitly: -# -# production: -# url: <%= ENV["MY_APP_DATABASE_URL"] %> -# -# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database -# for a full overview on how database connection configuration can be specified. -# -production: - <<: *default - database: shortening_production - username: shortening - password: <%= ENV["SHORTENING_DATABASE_PASSWORD"] %> diff --git a/db/migrate/20221224115757_create_urls.rb b/db/migrate/20221224115757_create_urls.rb index a8d3e30..d26e2e9 100644 --- a/db/migrate/20221224115757_create_urls.rb +++ b/db/migrate/20221224115757_create_urls.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# db/migrate/20221224115757_create_urls.rb class CreateUrls < ActiveRecord::Migration[7.0] def change create_table :urls do |t| diff --git a/db/migrate/20221227171400_remove_index_to_urls.rb b/db/migrate/20221227171400_remove_index_to_urls.rb index 3ec100a..a0ce544 100644 --- a/db/migrate/20221227171400_remove_index_to_urls.rb +++ b/db/migrate/20221227171400_remove_index_to_urls.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# db/migrate/20221227171400_remove_index_to_urls.rb class RemoveIndexToUrls < ActiveRecord::Migration[7.0] def change remove_index :urls, :original_url, unique: true diff --git a/test/controllers/redirection_controller_test.rb b/test/controllers/redirection_controller_test.rb index 731387a..9e1a110 100644 --- a/test/controllers/redirection_controller_test.rb +++ b/test/controllers/redirection_controller_test.rb @@ -14,6 +14,11 @@ class RedirectionControllerTest < ActionDispatch::IntegrationTest assert_response :not_found end + test 'access with invalid slug format should render to not found' do + get shortened_url(slug: 'te@@st') + assert_response :not_found + end + test 'access with not existed slug should render to not found' do get shortened_url(slug: '1a2b3c') assert_response :not_found diff --git a/test/test_helper.rb b/test/test_helper.rb index 97b71f8..a3a8a9f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,9 +2,16 @@ ENV['RAILS_ENV'] ||= 'test' require 'simplecov' -require "simplecov_json_formatter" -SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter -SimpleCov.start +require 'simplecov_json_formatter' + +# Generate HTML and JSON reports +SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::JSONFormatter + ]) +SimpleCov.start do + enable_coverage :branch +end require_relative '../config/environment' require 'rails/test_help'